mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Compare commits
125 Commits
feat/updat
...
v2026.0403
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c05de2cf0a | ||
|
|
c5145c2fca | ||
|
|
62bf33c9d1 |
23
.github/workflows/build-artifacts.yml
vendored
23
.github/workflows/build-artifacts.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Cache Android NDK
|
||||
uses: actions/cache@v4
|
||||
@@ -95,6 +98,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Linux desktop
|
||||
run: flutter config --enable-linux-desktop
|
||||
@@ -132,6 +138,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Windows desktop
|
||||
shell: pwsh
|
||||
@@ -174,6 +183,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable macOS desktop
|
||||
run: flutter config --enable-macos-desktop
|
||||
@@ -181,6 +193,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Install macOS native build tools
|
||||
run: brew install automake libtool
|
||||
|
||||
- name: Build macOS release
|
||||
run: flutter build macos --release
|
||||
|
||||
@@ -216,6 +231,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
@@ -272,6 +290,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable web
|
||||
run: flutter config --enable-web
|
||||
@@ -280,7 +301,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build web release
|
||||
run: flutter build web --release --base-href /meshcore-sar/
|
||||
run: flutter build web --wasm --release --base-href /meshcore-sar/
|
||||
|
||||
- name: Upload pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
3
.github/workflows/build-multiplatform.yml
vendored
3
.github/workflows/build-multiplatform.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -28,6 +28,7 @@ migrate_working_dir/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
pubspec_overrides.yaml
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
@@ -78,9 +79,15 @@ play-store/
|
||||
create_feature_graphic.py
|
||||
|
||||
# Tool caches and local state
|
||||
.devenv/
|
||||
.cachebro/
|
||||
.osgrep/
|
||||
third_party/lpcnet_flutter/
|
||||
worker/node_modules/
|
||||
worker/.wrangler/
|
||||
worker/.bun/
|
||||
worker/.astro/
|
||||
worker/dist/
|
||||
|
||||
# Claude Code local settings (permissions, personal config)
|
||||
.claude/settings.local.json
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-15 to 2026-03-22
|
||||
|
||||
- Device connectivity and routing: added Android USB serial companion support, serial compass support, GPS module controls, per-contact path size selection, and favourites support via firmware flags.
|
||||
- Location and map intelligence: added RSSI-based repeater trilateration and contact location estimates, isolated MET history, matched the official advert import/discovery flow, and refreshed map presentation by removing contact trails.
|
||||
- Telemetry and device info: surfaced self telemetry and sensor defaults, opened device info from signal and battery taps, improved signal chip behavior, fixed duplicate telemetry delivery, and restored telemetry refresh behavior.
|
||||
- Contacts and messaging: added meshcore:// contact sharing and repeater neighbour views, supported MeshCore message links, fixed DM retry behavior, corrected room poster naming, and compacted the recipient selector.
|
||||
- UI polish and platform upkeep: improved device settings layout, tightened sensor and contact presentation, expanded localization coverage, and refreshed platform dependencies and build numbers.
|
||||
|
||||
## Key PRs
|
||||
|
||||
- [#26](https://github.com/dz0ny/meshcore-sar/pull/26) Merge pull request #26 from `dz0ny/feat/sensor-telemetry-preview`
|
||||
|
||||
## 2026-03-08 to 2026-03-14
|
||||
|
||||
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.
|
||||
|
||||
3
Makefile
3
Makefile
@@ -135,6 +135,9 @@ clean: ## Clean build artifacts
|
||||
flutter clean
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
build-web: ## Build web with WebAssembly
|
||||
flutter build web --wasm --release
|
||||
|
||||
# Build for all platforms
|
||||
build-all: bump ## Build for Android and iOS
|
||||
flutter build apk --release
|
||||
|
||||
@@ -84,6 +84,8 @@ PODS:
|
||||
- Flutter
|
||||
- vibration (3.0.0):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
@@ -108,6 +110,7 @@ DEPENDENCIES:
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -161,6 +164,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||
@@ -189,6 +194,7 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 131;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>115</string>
|
||||
<string>131</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -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.000729">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.385323">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.008455">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="93.530304">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="224.694344">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="210.562484">
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="170.718455">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1638,5 +1638,252 @@
|
||||
"contactFailedToClearRoute": "Pfad konnte nicht gelöscht werden: {error}",
|
||||
"contactRouteCleared": "Pfad gelöscht",
|
||||
"contactRouteSet": "Pfad gesetzt: {route}",
|
||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}"
|
||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK-Zeitüberschreitung",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Nutzlast",
|
||||
"hops": "Hops",
|
||||
"hashSize": "Hash-Größe",
|
||||
"pathBytes": "Pfad-Bytes",
|
||||
"selectedPath": "Ausgewählter Pfad",
|
||||
"estimatedTx": "Geschätzte Sendung",
|
||||
"senderToReceipt": "Sender bis Empfang",
|
||||
"receivedCopies": "Empfangene Kopien",
|
||||
"retryCause": "Wiederholungsursache",
|
||||
"retryMode": "Wiederholungsmodus",
|
||||
"retryResult": "Wiederholungsergebnis",
|
||||
"lastRetry": "Letzter Versuch",
|
||||
"rxPackets": "RX-Pakete",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Rate",
|
||||
"window": "Fenster",
|
||||
"posttxDelay": "Sendeverzögerung",
|
||||
"bandpass": "Bandpass",
|
||||
"bandpassFilterVoice": "Bandpassfilter Sprache",
|
||||
"frequency": "Frequenz",
|
||||
"australia": "Australien",
|
||||
"australiaNarrow": "Australien (Schmal)",
|
||||
"australiaQld": "Australien: QLD",
|
||||
"australiaSaWa": "Australien: SA, WA",
|
||||
"newZealand": "Neuseeland",
|
||||
"newZealandNarrow": "Neuseeland (Schmal)",
|
||||
"switzerland": "Schweiz",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Tschechien (Schmal)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Langstrecke)",
|
||||
"euukDeprecated": "EU/UK (Veraltet)",
|
||||
"euukNarrow": "EU/UK (Schmal)",
|
||||
"usacanadaRecommended": "USA/Kanada (Empfohlen)",
|
||||
"vietnamDeprecated": "Vietnam (Veraltet)",
|
||||
"vietnamNarrow": "Vietnam (Schmal)",
|
||||
"active": "Aktiv",
|
||||
"addContact": "Kontakt hinzufügen",
|
||||
"all": "Alle",
|
||||
"autoResolve": "Automatisch auflösen",
|
||||
"clearAllLabel": "Alle löschen",
|
||||
"clearRelays": "Relais löschen",
|
||||
"clearFilters": "Filter löschen",
|
||||
"clearRoute": "Route löschen",
|
||||
"clearMessages": "Nachrichten löschen",
|
||||
"clearScale": "Maßstab löschen",
|
||||
"clearDiscoveries": "Entdeckungen löschen",
|
||||
"clearOnlineTraceDatabase": "Online-Trace-Datenbank löschen",
|
||||
"clearAllChannels": "Alle Kanäle löschen",
|
||||
"clearAllContacts": "Alle Kontakte löschen",
|
||||
"clearChannels": "Kanäle löschen",
|
||||
"clearContacts": "Kontakte löschen",
|
||||
"clearPathOnMaxRetry": "Pfad bei max. Wiederholung löschen",
|
||||
"create": "Erstellen",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"defaultValue": "Standard",
|
||||
"duplicate": "Duplizieren",
|
||||
"editName": "Name bearbeiten",
|
||||
"open": "Öffnen",
|
||||
"paste": "Einfügen",
|
||||
"preview": "Vorschau",
|
||||
"remove": "Entfernen",
|
||||
"rename": "Umbenennen",
|
||||
"resolveAll": "Alle auflösen",
|
||||
"send": "Senden",
|
||||
"sendAnyway": "Trotzdem senden",
|
||||
"share": "Teilen",
|
||||
"shareContact": "Kontakt teilen",
|
||||
"trace": "Trace",
|
||||
"use": "Verwenden",
|
||||
"useSelectedFrequency": "Ausgewählte Frequenz verwenden",
|
||||
"discovery": "Erkennung",
|
||||
"discoverRepeaters": "Repeater entdecken",
|
||||
"discoverSensors": "Sensoren entdecken",
|
||||
"repeaterDiscoverySent": "Repeater-Erkennung gesendet",
|
||||
"sensorDiscoverySent": "Sensor-Erkennung gesendet",
|
||||
"clearedPendingDiscoveries": "Ausstehende Entdeckungen gelöscht.",
|
||||
"autoDiscovery": "Automatische Erkennung",
|
||||
"enableAutomaticAdding": "Automatisches Hinzufügen aktivieren",
|
||||
"autoaddRepeaters": "Repeater automatisch hinzufügen",
|
||||
"autoaddRoomServers": "Raumserver automatisch hinzufügen",
|
||||
"autoaddSensors": "Sensoren automatisch hinzufügen",
|
||||
"autoaddUsers": "Benutzer automatisch hinzufügen",
|
||||
"overwriteOldestWhenFull": "Älteste bei vollem Speicher überschreiben",
|
||||
"storage": "Speicher",
|
||||
"dangerZone": "Gefahrenbereich",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Favoriten",
|
||||
"sensors": "Sensoren",
|
||||
"others": "Andere",
|
||||
"gpsModule": "GPS-Modul",
|
||||
"liveTraffic": "Live-Verkehr",
|
||||
"repeatersMap": "Repeater-Karte",
|
||||
"spectrumScan": "Spektrum-Scan",
|
||||
"blePacketLogs": "BLE-Paketprotokolle",
|
||||
"onlineTraceDatabase": "Online-Trace-Datenbank",
|
||||
"routePathByteSize": "Pfad-Bytegröße",
|
||||
"messageNotifications": "Nachrichtenbenachrichtigungen",
|
||||
"sarAlerts": "SAR-Alarme",
|
||||
"discoveryNotifications": "Erkennungsbenachrichtigungen",
|
||||
"updateNotifications": "Updatebenachrichtigungen",
|
||||
"muteWhileAppIsOpen": "Stumm bei geöffneter App",
|
||||
"disableContacts": "Kontakte deaktivieren",
|
||||
"enableSensorsTab": "Sensoren-Tab aktivieren",
|
||||
"enableProfiles": "Profile aktivieren",
|
||||
"autoRouteRotation": "Automatische Routenrotation",
|
||||
"nearestRepeaterFallback": "Nächster Repeater als Fallback",
|
||||
"deleteAllStoredMessageHistory": "Gesamten Nachrichtenverlauf löschen",
|
||||
"messageFontSize": "Nachrichtenschriftgröße",
|
||||
"rotateMapWithHeading": "Karte mit Richtung drehen",
|
||||
"showMapDebugInfo": "Karten-Debug-Info anzeigen",
|
||||
"openMapInFullscreen": "Karte im Vollbild öffnen",
|
||||
"showSarMarkersLabel": "SAR-Markierungen anzeigen",
|
||||
"displaySarMarkersOnTheMainMap": "SAR-Markierungen auf der Hauptkarte anzeigen",
|
||||
"showAllContactTrailsLabel": "Alle Kontaktpfade anzeigen",
|
||||
"hideRepeatersOnMap": "Repeater auf Karte ausblenden",
|
||||
"setMapScale": "Kartenmaßstab einstellen",
|
||||
"customMapScaleSaved": "Benutzerdefinierter Kartenmaßstab gespeichert",
|
||||
"voiceBitrate": "Sprach-Bitrate",
|
||||
"voiceCompressor": "Sprachkompressor",
|
||||
"balancesQuietAndLoudSpeechLevels": "Gleicht leise und laute Sprache aus",
|
||||
"voiceLimiter": "Sprach-Limiter",
|
||||
"preventsClippingPeaksBeforeEncoding": "Verhindert Clipping-Spitzen vor der Kodierung",
|
||||
"micAutoGain": "Mikrofon-Automatik",
|
||||
"letsTheRecorderAdjustInputLevel": "Lässt den Rekorder den Eingangspegel anpassen",
|
||||
"echoCancellation": "Echounterdrückung",
|
||||
"noiseSuppression": "Rauschunterdrückung",
|
||||
"trimSilenceInVoiceMessages": "Stille in Sprachnachrichten trimmen",
|
||||
"compressor": "Kompressor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatische Verstärkung",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Rauschen",
|
||||
"silenceTrim": "Stille",
|
||||
"maxImageSize": "Maximale Bildgröße",
|
||||
"imageCompression": "Bildkomprimierung",
|
||||
"grayscale": "Graustufen",
|
||||
"ultraMode": "Ultra-Modus",
|
||||
"fastPrivateGpsUpdates": "Schnelle private GPS-Updates",
|
||||
"movementThreshold": "Bewegungsschwelle",
|
||||
"fastGpsMovementThreshold": "Schneller GPS-Bewegungsschwellenwert",
|
||||
"fastGpsActiveuseInterval": "Schnelles GPS aktive Nutzung Intervall",
|
||||
"activeuseUpdateInterval": "Aktive Nutzung Updateintervall",
|
||||
"repeatNearbyTraffic": "Nahverkehr wiederholen",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Über Repeater im Mesh weiterleiten",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Nur in der Nähe, ohne Repeater-Flooding",
|
||||
"multihop": "Multi-Hop",
|
||||
"createProfile": "Profil erstellen",
|
||||
"renameProfile": "Profil umbenennen",
|
||||
"newProfile": "Neues Profil",
|
||||
"manageProfiles": "Profile verwalten",
|
||||
"enableProfilesToStartManagingThem": "Aktivieren Sie Profile, um sie zu verwalten.",
|
||||
"openMessage": "Nachricht öffnen",
|
||||
"jumpToTheRelatedSarMessage": "Zur zugehörigen SAR-Nachricht springen",
|
||||
"removeSarMarker": "SAR-Markierung entfernen",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Bitte wählen Sie ein Ziel zum Senden der SAR-Markierung",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-Markierung an öffentlichen Kanal gesendet",
|
||||
"sarMarkerSentToRoom": "SAR-Markierung an Raum gesendet",
|
||||
"loadFromGallery": "Aus Galerie laden",
|
||||
"replaceImage": "Bild ersetzen",
|
||||
"selectFromGallery": "Aus Galerie auswählen",
|
||||
"team": "Team",
|
||||
"found": "Gefunden",
|
||||
"staging": "Sammelpunkt",
|
||||
"object": "Objekt",
|
||||
"quiet": "Ruhig",
|
||||
"moderate": "Mäßig",
|
||||
"busy": "Beschäftigt",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum-Scan hat keine Kandidatenfrequenzen gefunden",
|
||||
"searchMessages": "Nachrichten suchen",
|
||||
"sendImageFromGallery": "Bild aus Galerie senden",
|
||||
"takePhoto": "Foto aufnehmen",
|
||||
"dmOnly": "Nur Direktnachricht",
|
||||
"allMessages": "Alle Nachrichten",
|
||||
"sendToPublicChannel": "An öffentlichen Kanal senden?",
|
||||
"selectMarkerTypeAndDestination": "Markierungstyp und Ziel auswählen",
|
||||
"noDestinationsAvailableLabel": "Keine Ziele verfügbar",
|
||||
"image": "Bild",
|
||||
"format": "Format",
|
||||
"dimensions": "Abmessungen",
|
||||
"segments": "Segmente",
|
||||
"transfers": "Übertragungen",
|
||||
"downloadedBy": "Heruntergeladen von",
|
||||
"saveDiscoverySettings": "Erkennungseinstellungen speichern",
|
||||
"savePublicInfo": "Öffentliche Info speichern",
|
||||
"saveRadioSettings": "Funkeinstellungen speichern",
|
||||
"savePath": "Pfad speichern",
|
||||
"wipeDeviceData": "Gerätedaten löschen",
|
||||
"wipeDevice": "Gerät löschen",
|
||||
"destructiveDeviceActions": "Destruktive Geräteaktionen.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wählen Sie eine Voreinstellung oder passen Sie die Funkeinstellungen an.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wählen Sie den Namen und Standort, den dieses Gerät teilt.",
|
||||
"availableSpaceOnThisDevice": "Verfügbarer Speicherplatz auf diesem Gerät.",
|
||||
"used": "Belegt",
|
||||
"total": "Gesamt",
|
||||
"renameValue": "Wert umbenennen",
|
||||
"customizeFields": "Felder anpassen",
|
||||
"livePreview": "Live-Vorschau",
|
||||
"refreshSchedule": "Aktualisierungsplan",
|
||||
"noResponse": "Keine Antwort",
|
||||
"refreshing": "Wird aktualisiert",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wählen Sie einen Relay oder Knoten zur Beobachtung.",
|
||||
"publicKeyLabel": "Öffentlicher Schlüssel",
|
||||
"alreadyInContacts": "Bereits in Kontakten",
|
||||
"connectToADeviceBeforeAddingContacts": "Verbinden Sie sich mit einem Gerät, bevor Sie Kontakte hinzufügen",
|
||||
"fromContacts": "Aus Kontakten",
|
||||
"onlineOnly": "Nur online",
|
||||
"inBoth": "In beiden",
|
||||
"source": "Quelle",
|
||||
"manualRouteEdit": "Manuelle Routenbearbeitung",
|
||||
"observedMeshRoute": "Beobachtete Mesh-Route",
|
||||
"allMessagesCleared": "Alle Nachrichten gelöscht",
|
||||
"onlineTraceDatabaseCleared": "Online-Trace-Datenbank gelöscht",
|
||||
"packetLogsCleared": "Paketprotokolle gelöscht",
|
||||
"hexDataCopiedToClipboard": "Hex-Daten in die Zwischenablage kopiert",
|
||||
"developerModeEnabled": "Entwicklermodus aktiviert",
|
||||
"developerModeDisabled": "Entwicklermodus deaktiviert",
|
||||
"clipboardIsEmpty": "Zwischenablage ist leer",
|
||||
"contactImported": "Kontakt importiert",
|
||||
"contactLinkCopiedToClipboard": "Kontaktlink in Zwischenablage kopiert",
|
||||
"failedToExportContact": "Kontaktexport fehlgeschlagen",
|
||||
"noLogsToExport": "Keine Protokolle zum Exportieren",
|
||||
"exportAsCsv": "Als CSV exportieren",
|
||||
"exportAsText": "Als Text exportieren",
|
||||
"receivedRfc3339": "Empfangen (RFC3339)",
|
||||
"buildTime": "Build-Zeit",
|
||||
"downloadUrlNotAvailable": "Download-URL nicht verfügbar",
|
||||
"cannotOpenDownloadUrl": "Download-URL kann nicht geöffnet werden",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Update-Prüfung nur auf Android verfügbar",
|
||||
"youAreRunningTheLatestVersion": "Sie verwenden die neueste Version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Update verfügbar, aber Download-URL nicht gefunden",
|
||||
"startTictactoe": "Tic-Tac-Toe starten",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nicht verfügbar",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: Gegner unbekannt",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: Warten auf Start",
|
||||
"acceptsShareLinks": "Akzeptiert freigegebene Links",
|
||||
"supportsRawHex": "Unterstützt rohes Hex",
|
||||
"clipboardfriendly": "Zwischenablage-freundlich",
|
||||
"captured": "Erfasst",
|
||||
"size": "Größe",
|
||||
"noCustomChannelsToClear": "Keine benutzerdefinierten Kanäle zum Löschen.",
|
||||
"noDeviceContactsToClear": "Keine Gerätekontakte zum Löschen."
|
||||
}
|
||||
|
||||
@@ -2303,5 +2303,252 @@
|
||||
"contactFailedToClearRoute": "Αποτυχία εκκαθάρισης διαδρομής: {error}",
|
||||
"contactRouteCleared": "Η διαδρομή εκκαθαρίστηκε",
|
||||
"contactRouteSet": "Ορίστηκε διαδρομή: {route}",
|
||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}"
|
||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Λήξη χρόνου ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ωφέλιμο φορτίο",
|
||||
"hops": "Αναπηδήσεις",
|
||||
"hashSize": "Μέγεθος hash",
|
||||
"pathBytes": "Bytes διαδρομής",
|
||||
"selectedPath": "Επιλεγμένη διαδρομή",
|
||||
"estimatedTx": "Εκτιμώμενη εκπομπή",
|
||||
"senderToReceipt": "Αποστολέας σε απόδειξη",
|
||||
"receivedCopies": "Ληφθέντα αντίγραφα",
|
||||
"retryCause": "Αιτία επανάληψης",
|
||||
"retryMode": "Λειτουργία επανάληψης",
|
||||
"retryResult": "Αποτέλεσμα επανάληψης",
|
||||
"lastRetry": "Τελευταία επανάληψη",
|
||||
"rxPackets": "Πακέτα RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Ρυθμός",
|
||||
"window": "Παράθυρο",
|
||||
"posttxDelay": "Καθυστέρηση μετά-tx",
|
||||
"bandpass": "Ζωνοπερατό",
|
||||
"bandpassFilterVoice": "Ζωνοπερατό φίλτρο φωνής",
|
||||
"frequency": "Συχνότητα",
|
||||
"australia": "Αυστραλία",
|
||||
"australiaNarrow": "Αυστραλία (Στενό)",
|
||||
"australiaQld": "Αυστραλία: QLD",
|
||||
"australiaSaWa": "Αυστραλία: SA, WA",
|
||||
"newZealand": "Νέα Ζηλανδία",
|
||||
"newZealandNarrow": "Νέα Ζηλανδία (Στενό)",
|
||||
"switzerland": "Ελβετία",
|
||||
"portugal433": "Πορτογαλία 433",
|
||||
"portugal868": "Πορτογαλία 868",
|
||||
"czechRepublicNarrow": "Τσεχία (Στενό)",
|
||||
"eu433mhzLongRange": "ΕΕ 433MHz (Μεγάλη Εμβέλεια)",
|
||||
"euukDeprecated": "ΕΕ/ΗΒ (Παρωχημένο)",
|
||||
"euukNarrow": "ΕΕ/ΗΒ (Στενό)",
|
||||
"usacanadaRecommended": "ΗΠΑ/Καναδάς (Προτεινόμενο)",
|
||||
"vietnamDeprecated": "Βιετνάμ (Παρωχημένο)",
|
||||
"vietnamNarrow": "Βιετνάμ (Στενό)",
|
||||
"active": "Ενεργό",
|
||||
"addContact": "Προσθήκη επαφής",
|
||||
"all": "Όλα",
|
||||
"autoResolve": "Αυτόματη επίλυση",
|
||||
"clearAllLabel": "Εκκαθάριση όλων",
|
||||
"clearRelays": "Εκκαθάριση αναμεταδοτών",
|
||||
"clearFilters": "Εκκαθάριση φίλτρων",
|
||||
"clearRoute": "Εκκαθάριση διαδρομής",
|
||||
"clearMessages": "Εκκαθάριση μηνυμάτων",
|
||||
"clearScale": "Εκκαθάριση κλίμακας",
|
||||
"clearDiscoveries": "Εκκαθάριση ανακαλύψεων",
|
||||
"clearOnlineTraceDatabase": "Εκκαθάριση βάσης ιχνών",
|
||||
"clearAllChannels": "Εκκαθάριση όλων των καναλιών",
|
||||
"clearAllContacts": "Εκκαθάριση όλων των επαφών",
|
||||
"clearChannels": "Εκκαθάριση καναλιών",
|
||||
"clearContacts": "Εκκαθάριση επαφών",
|
||||
"clearPathOnMaxRetry": "Εκκαθάριση μονοπατιού στο μέγιστο",
|
||||
"create": "Δημιουργία",
|
||||
"custom": "Προσαρμοσμένο",
|
||||
"defaultValue": "Προεπιλογή",
|
||||
"duplicate": "Αντιγραφή",
|
||||
"editName": "Επεξεργασία ονόματος",
|
||||
"open": "Άνοιγμα",
|
||||
"paste": "Επικόλληση",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"remove": "Αφαίρεση",
|
||||
"rename": "Μετονομασία",
|
||||
"resolveAll": "Επίλυση όλων",
|
||||
"send": "Αποστολή",
|
||||
"sendAnyway": "Αποστολή ούτως ή άλλως",
|
||||
"share": "Κοινοποίηση",
|
||||
"shareContact": "Κοινοποίηση επαφής",
|
||||
"trace": "Ίχνος",
|
||||
"use": "Χρήση",
|
||||
"useSelectedFrequency": "Χρήση επιλεγμένης συχνότητας",
|
||||
"discovery": "Ανακάλυψη",
|
||||
"discoverRepeaters": "Ανακάλυψη αναμεταδοτών",
|
||||
"discoverSensors": "Ανακάλυψη αισθητήρων",
|
||||
"repeaterDiscoverySent": "Αποστολή ανακάλυψης αναμεταδοτών",
|
||||
"sensorDiscoverySent": "Αποστολή ανακάλυψης αισθητήρων",
|
||||
"clearedPendingDiscoveries": "Εκκαθάριση εκκρεμών ανακαλύψεων.",
|
||||
"autoDiscovery": "Αυτόματη ανακάλυψη",
|
||||
"enableAutomaticAdding": "Ενεργοποίηση αυτόματης προσθήκης",
|
||||
"autoaddRepeaters": "Αυτόματη προσθήκη αναμεταδοτών",
|
||||
"autoaddRoomServers": "Αυτόματη προσθήκη διακομιστών δωματίου",
|
||||
"autoaddSensors": "Αυτόματη προσθήκη αισθητήρων",
|
||||
"autoaddUsers": "Αυτόματη προσθήκη χρηστών",
|
||||
"overwriteOldestWhenFull": "Αντικατάσταση παλαιότερων όταν γεμίσει",
|
||||
"storage": "Αποθήκευση",
|
||||
"dangerZone": "Ζώνη κινδύνου",
|
||||
"profiles": "Προφίλ",
|
||||
"favourites": "Αγαπημένα",
|
||||
"sensors": "Αισθητήρες",
|
||||
"others": "Άλλοι",
|
||||
"gpsModule": "Μονάδα GPS",
|
||||
"liveTraffic": "Ζωντανή κίνηση",
|
||||
"repeatersMap": "Χάρτης αναμεταδοτών",
|
||||
"spectrumScan": "Σάρωση φάσματος",
|
||||
"blePacketLogs": "Αρχεία πακέτων BLE",
|
||||
"onlineTraceDatabase": "Βάση δεδομένων ιχνών",
|
||||
"routePathByteSize": "Μέγεθος διαδρομής σε bytes",
|
||||
"messageNotifications": "Ειδοποιήσεις μηνυμάτων",
|
||||
"sarAlerts": "Ειδοποιήσεις SAR",
|
||||
"discoveryNotifications": "Ειδοποιήσεις ανακάλυψης",
|
||||
"updateNotifications": "Ειδοποιήσεις ενημερώσεων",
|
||||
"muteWhileAppIsOpen": "Σίγαση με ανοιχτή εφαρμογή",
|
||||
"disableContacts": "Απενεργοποίηση επαφών",
|
||||
"enableSensorsTab": "Ενεργοποίηση καρτέλας Αισθητήρων",
|
||||
"enableProfiles": "Ενεργοποίηση προφίλ",
|
||||
"autoRouteRotation": "Αυτόματη εναλλαγή διαδρομής",
|
||||
"nearestRepeaterFallback": "Πλησιέστερος αναμεταδότης ως εφεδρεία",
|
||||
"deleteAllStoredMessageHistory": "Διαγραφή όλου του ιστορικού",
|
||||
"messageFontSize": "Μέγεθος γραμματοσειράς μηνυμάτων",
|
||||
"rotateMapWithHeading": "Περιστροφή χάρτη με κατεύθυνση",
|
||||
"showMapDebugInfo": "Εμφάνιση πληροφοριών αποσφαλμάτωσης",
|
||||
"openMapInFullscreen": "Άνοιγμα χάρτη σε πλήρη οθόνη",
|
||||
"showSarMarkersLabel": "Εμφάνιση δεικτών SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Εμφάνιση δεικτών SAR στον κύριο χάρτη",
|
||||
"showAllContactTrailsLabel": "Εμφάνιση όλων των ιχνών επαφών",
|
||||
"hideRepeatersOnMap": "Απόκρυψη αναμεταδοτών στον χάρτη",
|
||||
"setMapScale": "Ρύθμιση κλίμακας χάρτη",
|
||||
"customMapScaleSaved": "Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε",
|
||||
"voiceBitrate": "Ρυθμός bit φωνής",
|
||||
"voiceCompressor": "Συμπιεστής φωνής",
|
||||
"balancesQuietAndLoudSpeechLevels": "Εξισορρόπηση χαμηλών και δυνατών επιπέδων",
|
||||
"voiceLimiter": "Περιοριστής φωνής",
|
||||
"preventsClippingPeaksBeforeEncoding": "Αποτρέπει κοπή κορυφών πριν την κωδικοποίηση",
|
||||
"micAutoGain": "Αυτόματη ενίσχυση μικροφώνου",
|
||||
"letsTheRecorderAdjustInputLevel": "Αφήνει τη συσκευή να ρυθμίσει την ένταση",
|
||||
"echoCancellation": "Ακύρωση ηχούς",
|
||||
"noiseSuppression": "Καταστολή θορύβου",
|
||||
"trimSilenceInVoiceMessages": "Περικοπή σιωπής σε φωνητικά μηνύματα",
|
||||
"compressor": "Συμπιεστής",
|
||||
"limiter": "Περιοριστής",
|
||||
"autoGain": "Αυτόματη ενίσχυση",
|
||||
"echoCancel": "Ηχώ",
|
||||
"noiseSuppress": "Θόρυβος",
|
||||
"silenceTrim": "Σιωπή",
|
||||
"maxImageSize": "Μέγιστο μέγεθος εικόνας",
|
||||
"imageCompression": "Συμπίεση εικόνας",
|
||||
"grayscale": "Κλίμακα του γκρι",
|
||||
"ultraMode": "Λειτουργία ultra",
|
||||
"fastPrivateGpsUpdates": "Γρήγορες ιδιωτικές ενημερώσεις GPS",
|
||||
"movementThreshold": "Κατώφλι κίνησης",
|
||||
"fastGpsMovementThreshold": "Κατώφλι κίνησης γρήγορου GPS",
|
||||
"fastGpsActiveuseInterval": "Διάστημα ενεργής χρήσης γρήγορου GPS",
|
||||
"activeuseUpdateInterval": "Διάστημα ενημέρωσης ενεργής χρήσης",
|
||||
"repeatNearbyTraffic": "Επανάληψη κοντινής κίνησης",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Αναμετάδοση μέσω αναμεταδοτών στο δίκτυο",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Μόνο κοντά, χωρίς πλημμυρισμό",
|
||||
"multihop": "Πολλαπλών αναπηδήσεων",
|
||||
"createProfile": "Δημιουργία προφίλ",
|
||||
"renameProfile": "Μετονομασία προφίλ",
|
||||
"newProfile": "Νέο προφίλ",
|
||||
"manageProfiles": "Διαχείριση προφίλ",
|
||||
"enableProfilesToStartManagingThem": "Ενεργοποιήστε τα προφίλ για να τα διαχειριστείτε.",
|
||||
"openMessage": "Άνοιγμα μηνύματος",
|
||||
"jumpToTheRelatedSarMessage": "Μετάβαση στο σχετικό μήνυμα SAR",
|
||||
"removeSarMarker": "Αφαίρεση δείκτη SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Επιλέξτε προορισμό για αποστολή δείκτη SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Δείκτης SAR εκπεμπόμενος στο δημόσιο κανάλι",
|
||||
"sarMarkerSentToRoom": "Δείκτης SAR εστάλη στο δωμάτιο",
|
||||
"loadFromGallery": "Φόρτωση από τη γκαλερί",
|
||||
"replaceImage": "Αντικατάσταση εικόνας",
|
||||
"selectFromGallery": "Επιλογή από τη γκαλερί",
|
||||
"team": "Ομάδα",
|
||||
"found": "Βρέθηκε",
|
||||
"staging": "Χώρος συγκέντρωσης",
|
||||
"object": "Αντικείμενο",
|
||||
"quiet": "Ήσυχο",
|
||||
"moderate": "Μέτριο",
|
||||
"busy": "Απασχολημένο",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Η σάρωση φάσματος δεν βρήκε υποψήφιες συχνότητες",
|
||||
"searchMessages": "Αναζήτηση μηνυμάτων",
|
||||
"sendImageFromGallery": "Αποστολή εικόνας από τη γκαλερί",
|
||||
"takePhoto": "Λήψη φωτογραφίας",
|
||||
"dmOnly": "Μόνο απευθείας",
|
||||
"allMessages": "Όλα τα μηνύματα",
|
||||
"sendToPublicChannel": "Αποστολή στο δημόσιο κανάλι;",
|
||||
"selectMarkerTypeAndDestination": "Επιλέξτε τύπο δείκτη και προορισμό",
|
||||
"noDestinationsAvailableLabel": "Δεν υπάρχουν διαθέσιμοι προορισμοί",
|
||||
"image": "Εικόνα",
|
||||
"format": "Μορφή",
|
||||
"dimensions": "Διαστάσεις",
|
||||
"segments": "Τμήματα",
|
||||
"transfers": "Μεταφορές",
|
||||
"downloadedBy": "Λήφθηκε από",
|
||||
"saveDiscoverySettings": "Αποθήκευση ρυθμίσεων ανακάλυψης",
|
||||
"savePublicInfo": "Αποθήκευση δημόσιων πληροφοριών",
|
||||
"saveRadioSettings": "Αποθήκευση ρυθμίσεων ραδιοφώνου",
|
||||
"savePath": "Αποθήκευση διαδρομής",
|
||||
"wipeDeviceData": "Διαγραφή δεδομένων συσκευής",
|
||||
"wipeDevice": "Διαγραφή συσκευής",
|
||||
"destructiveDeviceActions": "Καταστροφικές ενέργειες συσκευής.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Επιλέξτε το όνομα και την τοποθεσία που μοιράζεται αυτή η συσκευή.",
|
||||
"availableSpaceOnThisDevice": "Διαθέσιμος χώρος σε αυτή τη συσκευή.",
|
||||
"used": "Χρησιμοποιήθηκε",
|
||||
"total": "Σύνολο",
|
||||
"renameValue": "Μετονομασία τιμής",
|
||||
"customizeFields": "Προσαρμογή πεδίων",
|
||||
"livePreview": "Ζωντανή προεπισκόπηση",
|
||||
"refreshSchedule": "Χρονοδιάγραμμα ανανέωσης",
|
||||
"noResponse": "Χωρίς απάντηση",
|
||||
"refreshing": "Ανανέωση",
|
||||
"unavailable": "Μη διαθέσιμο",
|
||||
"pickARelayOrNodeToWatchInSensors": "Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.",
|
||||
"publicKeyLabel": "Δημόσιο κλειδί",
|
||||
"alreadyInContacts": "Ήδη στις επαφές",
|
||||
"connectToADeviceBeforeAddingContacts": "Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές",
|
||||
"fromContacts": "Από τις επαφές",
|
||||
"onlineOnly": "Μόνο συνδεδεμένοι",
|
||||
"inBoth": "Σε αμφότερα",
|
||||
"source": "Πηγή",
|
||||
"manualRouteEdit": "Χειροκίνητη επεξεργασία διαδρομής",
|
||||
"observedMeshRoute": "Παρατηρούμενη διαδρομή δικτύου",
|
||||
"allMessagesCleared": "Όλα τα μηνύματα διαγράφηκαν",
|
||||
"onlineTraceDatabaseCleared": "Βάση δεδομένων ιχνών εκκαθαρίστηκε",
|
||||
"packetLogsCleared": "Τα αρχεία πακέτων εκκαθαρίστηκαν",
|
||||
"hexDataCopiedToClipboard": "Τα δεδομένα hex αντιγράφηκαν",
|
||||
"developerModeEnabled": "Λειτουργία προγραμματιστή ενεργοποιήθηκε",
|
||||
"developerModeDisabled": "Λειτουργία προγραμματιστή απενεργοποιήθηκε",
|
||||
"clipboardIsEmpty": "Το πρόχειρο είναι κενό",
|
||||
"contactImported": "Επαφή εισήχθη",
|
||||
"contactLinkCopiedToClipboard": "Ο σύνδεσμος επαφής αντιγράφηκε",
|
||||
"failedToExportContact": "Αποτυχία εξαγωγής επαφής",
|
||||
"noLogsToExport": "Δεν υπάρχουν αρχεία για εξαγωγή",
|
||||
"exportAsCsv": "Εξαγωγή ως CSV",
|
||||
"exportAsText": "Εξαγωγή ως κείμενο",
|
||||
"receivedRfc3339": "Ελήφθη (RFC3339)",
|
||||
"buildTime": "Ώρα κατασκευής",
|
||||
"downloadUrlNotAvailable": "Η URL λήψης δεν είναι διαθέσιμη",
|
||||
"cannotOpenDownloadUrl": "Αδυναμία ανοίγματος URL λήψης",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Ο έλεγχος ενημερώσεων είναι διαθέσιμος μόνο σε Android",
|
||||
"youAreRunningTheLatestVersion": "Χρησιμοποιείτε την πιο πρόσφατη έκδοση",
|
||||
"updateAvailableButDownloadUrlNotFound": "Διαθέσιμη ενημέρωση αλλά η URL λήψης δεν βρέθηκε",
|
||||
"startTictactoe": "Εκκίνηση Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe μη διαθέσιμο",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: άγνωστος αντίπαλος",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: αναμονή για εκκίνηση",
|
||||
"acceptsShareLinks": "Αποδέχεται κοινόχρηστους συνδέσμους",
|
||||
"supportsRawHex": "Υποστηρίζει ακατέργαστο hex",
|
||||
"clipboardfriendly": "Φιλικό προς πρόχειρο",
|
||||
"captured": "Καταγράφηκε",
|
||||
"size": "Μέγεθος",
|
||||
"noCustomChannelsToClear": "Δεν υπάρχουν προσαρμοσμένα κανάλια.",
|
||||
"noDeviceContactsToClear": "Δεν υπάρχουν επαφές συσκευής."
|
||||
}
|
||||
|
||||
@@ -660,6 +660,30 @@
|
||||
"@flood": {
|
||||
"description": "Flood routing indicator"
|
||||
},
|
||||
"autoSend": "Auto Send",
|
||||
"@autoSend": {
|
||||
"description": "Auto send mode - uses current path"
|
||||
},
|
||||
"autoSendDescription": "Send via current path.",
|
||||
"@autoSendDescription": {
|
||||
"description": "Description for auto send mode"
|
||||
},
|
||||
"sendDirect": "Send Direct",
|
||||
"@sendDirect": {
|
||||
"description": "Send direct mode - zero-hop to contact"
|
||||
},
|
||||
"sendDirectDescription": "Send directly to this contact.",
|
||||
"@sendDirectDescription": {
|
||||
"description": "Description for send direct mode"
|
||||
},
|
||||
"sendFlood": "Send Flood",
|
||||
"@sendFlood": {
|
||||
"description": "Send flood mode - via all repeaters"
|
||||
},
|
||||
"sendFloodDescription": "Send via all repeaters.",
|
||||
"@sendFloodDescription": {
|
||||
"description": "Description for send flood mode"
|
||||
},
|
||||
"loggedIn": "Logged In",
|
||||
"@loggedIn": {
|
||||
"description": "Logged in status badge"
|
||||
@@ -2228,80 +2252,55 @@
|
||||
"description": "Message when there are no chat contacts to send to"
|
||||
},
|
||||
"reply": "Reply",
|
||||
"@reply": {
|
||||
},
|
||||
"@reply": {},
|
||||
"technicalDetails": "Technical details",
|
||||
"@technicalDetails": {
|
||||
},
|
||||
"@technicalDetails": {},
|
||||
"messageTechnicalDetails": "Message technical details",
|
||||
"@messageTechnicalDetails": {
|
||||
},
|
||||
"@messageTechnicalDetails": {},
|
||||
"linkQuality": "Link quality",
|
||||
"@linkQuality": {
|
||||
},
|
||||
"@linkQuality": {},
|
||||
"delivery": "Delivery",
|
||||
"@delivery": {
|
||||
},
|
||||
"@delivery": {},
|
||||
"status": "Status",
|
||||
"@status": {
|
||||
},
|
||||
"@status": {},
|
||||
"expectedAckTag": "Expected ACK tag",
|
||||
"@expectedAckTag": {
|
||||
},
|
||||
"@expectedAckTag": {},
|
||||
"roundTrip": "Round-trip",
|
||||
"@roundTrip": {
|
||||
},
|
||||
"@roundTrip": {},
|
||||
"retryAttempt": "Retry attempt",
|
||||
"@retryAttempt": {
|
||||
},
|
||||
"@retryAttempt": {},
|
||||
"floodFallback": "Flood fallback",
|
||||
"@floodFallback": {
|
||||
},
|
||||
"@floodFallback": {},
|
||||
"identity": "Identity",
|
||||
"@identity": {
|
||||
},
|
||||
"@identity": {},
|
||||
"messageId": "Message ID",
|
||||
"@messageId": {
|
||||
},
|
||||
"@messageId": {},
|
||||
"sender": "Sender",
|
||||
"@sender": {
|
||||
},
|
||||
"@sender": {},
|
||||
"senderKey": "Sender key",
|
||||
"@senderKey": {
|
||||
},
|
||||
"@senderKey": {},
|
||||
"recipient": "Recipient",
|
||||
"@recipient": {
|
||||
},
|
||||
"@recipient": {},
|
||||
"recipientKey": "Recipient key",
|
||||
"@recipientKey": {
|
||||
},
|
||||
"@recipientKey": {},
|
||||
"voice": "Voice",
|
||||
"@voice": {
|
||||
},
|
||||
"@voice": {},
|
||||
"voiceId": "Voice ID",
|
||||
"@voiceId": {
|
||||
},
|
||||
"@voiceId": {},
|
||||
"envelope": "Envelope",
|
||||
"@envelope": {
|
||||
},
|
||||
"@envelope": {},
|
||||
"sessionProgress": "Session progress",
|
||||
"@sessionProgress": {
|
||||
},
|
||||
"@sessionProgress": {},
|
||||
"complete": "Complete",
|
||||
"@complete": {
|
||||
},
|
||||
"@complete": {},
|
||||
"rawDump": "Raw dump",
|
||||
"@rawDump": {
|
||||
},
|
||||
"@rawDump": {},
|
||||
"cannotRetryMissingRecipient": "Cannot retry: recipient information missing",
|
||||
"@cannotRetryMissingRecipient": {
|
||||
},
|
||||
"@cannotRetryMissingRecipient": {},
|
||||
"voiceUnavailable": "Voice unavailable right now",
|
||||
"@voiceUnavailable": {
|
||||
},
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Requesting voice",
|
||||
"@requestingVoice": {
|
||||
},
|
||||
"@requestingVoice": {},
|
||||
"device": "device",
|
||||
"@device": {
|
||||
"description": "Generic device label"
|
||||
@@ -2310,7 +2309,7 @@
|
||||
"@change": {
|
||||
"description": "Generic change action"
|
||||
},
|
||||
"wizardOverviewDescription": "This app combines MeshCore messaging, SAR field updates, mapping, and device tools in one place.",
|
||||
"wizardOverviewDescription": "This app combines MeshCore messaging, SAR field updates, discovery, mapping, and device tools in one place.",
|
||||
"@wizardOverviewDescription": {
|
||||
"description": "Updated onboarding overview description"
|
||||
},
|
||||
@@ -2322,7 +2321,7 @@
|
||||
"@wizardOverviewFeature2": {
|
||||
"description": "Updated onboarding overview feature 2"
|
||||
},
|
||||
"wizardOverviewFeature3": "Connect over BLE or TCP, then manage the companion radio from inside the app.",
|
||||
"wizardOverviewFeature3": "Connect over BLE, WiFi, or Serial, then manage the companion radio from inside the app.",
|
||||
"@wizardOverviewFeature3": {
|
||||
"description": "Updated onboarding overview feature 3"
|
||||
},
|
||||
@@ -2435,11 +2434,11 @@
|
||||
"@wizardNetworkTitle": {
|
||||
"description": "Updated onboarding network page title"
|
||||
},
|
||||
"wizardNetworkDescription": "The Contacts tab organizes the network you discover and the routes you learn over time.",
|
||||
"wizardNetworkDescription": "The Contacts and Discovery flows help you find nearby nodes, store repeaters, and manage the routes you learn over time.",
|
||||
"@wizardNetworkDescription": {
|
||||
"description": "Updated onboarding network page description"
|
||||
},
|
||||
"wizardNetworkFeature1": "Review team members, repeaters, rooms, channels, and pending adverts in one list.",
|
||||
"wizardNetworkFeature1": "Discover repeaters and sensors, then review team members, rooms, channels, and pending adverts in one list.",
|
||||
"@wizardNetworkFeature1": {
|
||||
"description": "Updated onboarding network feature 1"
|
||||
},
|
||||
@@ -2447,7 +2446,7 @@
|
||||
"@wizardNetworkFeature2": {
|
||||
"description": "Updated onboarding network feature 2"
|
||||
},
|
||||
"wizardNetworkFeature3": "Create channels and manage network destinations without leaving the app.",
|
||||
"wizardNetworkFeature3": "Add new repeaters right after you connect, then create channels and manage network destinations without leaving the app.",
|
||||
"@wizardNetworkFeature3": {
|
||||
"description": "Updated onboarding network feature 3"
|
||||
},
|
||||
@@ -2487,10 +2486,18 @@
|
||||
"@wizardToolsFeature2": {
|
||||
"description": "Updated onboarding tools feature 2"
|
||||
},
|
||||
"wizardToolsFeature3": "Use packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.",
|
||||
"wizardToolsFeature3": "Use live traffic, packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.",
|
||||
"@wizardToolsFeature3": {
|
||||
"description": "Updated onboarding tools feature 3"
|
||||
},
|
||||
"postConnectDiscoveryTitle": "Discover repeaters now?",
|
||||
"@postConnectDiscoveryTitle": {
|
||||
"description": "Title for the prompt shown after a successful device connection"
|
||||
},
|
||||
"postConnectDiscoveryDescription": "Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.",
|
||||
"@postConnectDiscoveryDescription": {
|
||||
"description": "Body text for the prompt shown after a successful device connection"
|
||||
},
|
||||
"contactInSensors": "In Sensors",
|
||||
"@contactInSensors": {
|
||||
"description": "Contact actions label when already in sensors"
|
||||
@@ -2542,5 +2549,277 @@
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rssi": "RSSI",
|
||||
"@rssi": {
|
||||
"description": "Received signal strength indicator"
|
||||
},
|
||||
"snr": "SNR",
|
||||
"@snr": {
|
||||
"description": "Signal-to-noise ratio"
|
||||
},
|
||||
"ackTimeout": "ACK timeout",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Payload",
|
||||
"hops": "Hops",
|
||||
"hashSize": "Hash size",
|
||||
"pathBytes": "Path bytes",
|
||||
"selectedPath": "Selected path",
|
||||
"estimatedTx": "Estimated tx",
|
||||
"senderToReceipt": "Sender to receipt",
|
||||
"receivedCopies": "Received copies",
|
||||
"retryCause": "Retry cause",
|
||||
"retryMode": "Retry mode",
|
||||
"retryResult": "Retry result",
|
||||
"lastRetry": "Last retry",
|
||||
"rxPackets": "RX packets",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Rate",
|
||||
"window": "Window",
|
||||
"posttxDelay": "Post-tx delay",
|
||||
"bandpass": "Band-pass",
|
||||
"bandpassFilterVoice": "Band-pass filter voice",
|
||||
"frequency": "Frequency",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Narrow)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "New Zealand",
|
||||
"newZealandNarrow": "New Zealand (Narrow)",
|
||||
"switzerland": "Switzerland",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Czech Republic (Narrow)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Long Range)",
|
||||
"euukDeprecated": "EU/UK (Deprecated)",
|
||||
"euukNarrow": "EU/UK (Narrow)",
|
||||
"usacanadaRecommended": "USA/Canada (Recommended)",
|
||||
"vietnamDeprecated": "Vietnam (Deprecated)",
|
||||
"vietnamNarrow": "Vietnam (Narrow)",
|
||||
"active": "Active",
|
||||
"addContact": "Add Contact",
|
||||
"all": "All",
|
||||
"autoResolve": "Auto resolve",
|
||||
"clearAllLabel": "Clear all",
|
||||
"clearRelays": "Clear relays",
|
||||
"clearFilters": "Clear filters",
|
||||
"clearRoute": "Clear Route",
|
||||
"clearMessages": "Clear Messages",
|
||||
"clearScale": "Clear scale",
|
||||
"clearDiscoveries": "Clear discoveries",
|
||||
"clearOnlineTraceDatabase": "Clear online trace database",
|
||||
"clearAllChannels": "Clear all channels",
|
||||
"clearAllContacts": "Clear all contacts",
|
||||
"clearChannels": "Clear channels",
|
||||
"clearContacts": "Clear contacts",
|
||||
"clearPathOnMaxRetry": "Clear path on max retry",
|
||||
"create": "Create",
|
||||
"custom": "Custom",
|
||||
"defaultValue": "Default",
|
||||
"duplicate": "Duplicate",
|
||||
"editName": "Edit name",
|
||||
"open": "Open",
|
||||
"paste": "Paste",
|
||||
"preview": "Preview",
|
||||
"remove": "Remove",
|
||||
"rename": "Rename",
|
||||
"resolveAll": "Resolve all",
|
||||
"send": "Send",
|
||||
"sendAnyway": "Send anyway",
|
||||
"share": "Share",
|
||||
"shareContact": "Share Contact",
|
||||
"trace": "Trace",
|
||||
"use": "Use",
|
||||
"useSelectedFrequency": "Use selected frequency",
|
||||
"discovery": "Discovery",
|
||||
"discoverRepeaters": "Discover repeaters",
|
||||
"discoverSensors": "Discover sensors",
|
||||
"repeaterDiscoverySent": "Repeater discovery sent",
|
||||
"sensorDiscoverySent": "Sensor discovery sent",
|
||||
"clearedPendingDiscoveries": "Cleared pending discoveries.",
|
||||
"autoDiscovery": "Auto discovery",
|
||||
"enableAutomaticAdding": "Enable automatic adding",
|
||||
"autoaddRepeaters": "Auto-add repeaters",
|
||||
"autoaddRoomServers": "Auto-add room servers",
|
||||
"autoaddSensors": "Auto-add sensors",
|
||||
"autoaddUsers": "Auto-add users",
|
||||
"overwriteOldestWhenFull": "Overwrite oldest when full",
|
||||
"storage": "Storage",
|
||||
"dangerZone": "Danger zone",
|
||||
"profiles": "Profiles",
|
||||
"favourites": "Favourites",
|
||||
"sensors": "Sensors",
|
||||
"others": "Others",
|
||||
"gpsModule": "GPS Module",
|
||||
"liveTraffic": "Live Traffic",
|
||||
"repeatersMap": "Repeaters Map",
|
||||
"spectrumScan": "Spectrum Scan",
|
||||
"blePacketLogs": "BLE Packet Logs",
|
||||
"onlineTraceDatabase": "Online trace database",
|
||||
"routePathByteSize": "Route path byte size",
|
||||
"messageNotifications": "Message notifications",
|
||||
"sarAlerts": "SAR alerts",
|
||||
"discoveryNotifications": "Discovery notifications",
|
||||
"updateNotifications": "Update notifications",
|
||||
"muteWhileAppIsOpen": "Mute while app is open",
|
||||
"disableContacts": "Disable Contacts",
|
||||
"enableSensorsTab": "Enable Sensors tab",
|
||||
"enableProfiles": "Enable Profiles",
|
||||
"autoRouteRotation": "Auto route rotation",
|
||||
"nearestRepeaterFallback": "Nearest repeater fallback",
|
||||
"deleteAllStoredMessageHistory": "Delete all stored message history",
|
||||
"messageFontSize": "Message font size",
|
||||
"rotateMapWithHeading": "Rotate map with heading",
|
||||
"showMapDebugInfo": "Show map debug info",
|
||||
"openMapInFullscreen": "Open map in fullscreen",
|
||||
"showSarMarkersLabel": "Show SAR markers",
|
||||
"displaySarMarkersOnTheMainMap": "Display SAR markers on the main map",
|
||||
"showAllContactTrailsLabel": "Show all contact trails",
|
||||
"hideRepeatersOnMap": "Hide repeaters on map",
|
||||
"setMapScale": "Set map scale",
|
||||
"customMapScaleSaved": "Custom map scale saved",
|
||||
"voiceBitrate": "Voice bitrate",
|
||||
"voiceCompressor": "Voice compressor",
|
||||
"balancesQuietAndLoudSpeechLevels": "Balances quiet and loud speech levels",
|
||||
"voiceLimiter": "Voice limiter",
|
||||
"preventsClippingPeaksBeforeEncoding": "Prevents clipping peaks before encoding",
|
||||
"micAutoGain": "Mic auto gain",
|
||||
"letsTheRecorderAdjustInputLevel": "Lets the recorder adjust input level",
|
||||
"echoCancellation": "Echo cancellation",
|
||||
"noiseSuppression": "Noise suppression",
|
||||
"trimSilenceInVoiceMessages": "Trim silence in voice messages",
|
||||
"compressor": "Compressor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Auto gain",
|
||||
"echoCancel": "Echo cancel",
|
||||
"noiseSuppress": "Noise suppress",
|
||||
"silenceTrim": "Silence trim",
|
||||
"maxImageSize": "Max image size",
|
||||
"imageCompression": "Image compression",
|
||||
"grayscale": "Grayscale",
|
||||
"ultraMode": "Ultra mode",
|
||||
"fastPrivateGpsUpdates": "Fast private GPS updates",
|
||||
"movementThreshold": "Movement threshold",
|
||||
"fastGpsMovementThreshold": "Fast GPS movement threshold",
|
||||
"fastGpsActiveuseInterval": "Fast GPS active-use interval",
|
||||
"activeuseUpdateInterval": "Active-use update interval",
|
||||
"repeatNearbyTraffic": "Repeat nearby traffic",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Relay through repeaters across the mesh",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Nearby only, without repeater flooding",
|
||||
"multihop": "Multi-hop",
|
||||
"createProfile": "Create Profile",
|
||||
"renameProfile": "Rename Profile",
|
||||
"newProfile": "New Profile",
|
||||
"manageProfiles": "Manage profiles",
|
||||
"enableProfilesToStartManagingThem": "Enable profiles to start managing them.",
|
||||
"openMessage": "Open message",
|
||||
"jumpToTheRelatedSarMessage": "Jump to the related SAR message",
|
||||
"removeSarMarker": "Remove SAR marker",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Please select a destination to send SAR marker",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR marker broadcast to public channel",
|
||||
"sarMarkerSentToRoom": "SAR marker sent to room",
|
||||
"loadFromGallery": "Load from gallery",
|
||||
"replaceImage": "Replace image",
|
||||
"selectFromGallery": "Select from gallery",
|
||||
"team": "Team",
|
||||
"found": "Found",
|
||||
"staging": "Staging",
|
||||
"object": "Object",
|
||||
"quiet": "Quiet",
|
||||
"moderate": "Moderate",
|
||||
"busy": "Busy",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spectrum scan returned no candidate frequencies",
|
||||
"searchMessages": "Search messages",
|
||||
"sendImageFromGallery": "Send image from gallery",
|
||||
"takePhoto": "Take photo",
|
||||
"dmOnly": "DM only",
|
||||
"allMessages": "All messages",
|
||||
"sendToPublicChannel": "Send to Public Channel?",
|
||||
"selectMarkerTypeAndDestination": "Select marker type and destination",
|
||||
"noDestinationsAvailableLabel": "No destinations available",
|
||||
"image": "Image",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimensions",
|
||||
"segments": "Segments",
|
||||
"transfers": "Transfers",
|
||||
"downloadedBy": "Downloaded by",
|
||||
"saveDiscoverySettings": "Save discovery settings",
|
||||
"savePublicInfo": "Save public info",
|
||||
"saveRadioSettings": "Save radio settings",
|
||||
"savePath": "Save Path",
|
||||
"wipeDeviceData": "Wipe device data",
|
||||
"wipeDevice": "Wipe device",
|
||||
"destructiveDeviceActions": "Destructive device actions.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Choose a preset or fine-tune custom radio settings.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Choose the name and location this device shares.",
|
||||
"availableSpaceOnThisDevice": "Available space on this device.",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"renameValue": "Rename value",
|
||||
"customizeFields": "Customize fields",
|
||||
"livePreview": "Live preview",
|
||||
"refreshSchedule": "Refresh schedule",
|
||||
"noResponse": "No response",
|
||||
"refreshing": "Refreshing",
|
||||
"unavailable": "Unavailable",
|
||||
"pickARelayOrNodeToWatchInSensors": "Pick a relay or node to watch in Sensors.",
|
||||
"publicKeyLabel": "Public key",
|
||||
"alreadyInContacts": "Already in contacts",
|
||||
"connectToADeviceBeforeAddingContacts": "Connect to a device before adding contacts",
|
||||
"fromContacts": "From contacts",
|
||||
"onlineOnly": "Online only",
|
||||
"inBoth": "In both",
|
||||
"source": "Source",
|
||||
"manualRouteEdit": "Manual route edit",
|
||||
"observedMeshRoute": "Observed mesh route",
|
||||
"allMessagesCleared": "All messages cleared",
|
||||
"onlineTraceDatabaseCleared": "Online trace database cleared",
|
||||
"packetLogsCleared": "Packet logs cleared",
|
||||
"hexDataCopiedToClipboard": "Hex data copied to clipboard",
|
||||
"developerModeEnabled": "Developer mode enabled",
|
||||
"developerModeDisabled": "Developer mode disabled",
|
||||
"clipboardIsEmpty": "Clipboard is empty",
|
||||
"contactImported": "Contact imported",
|
||||
"contactLinkCopiedToClipboard": "Contact link copied to clipboard",
|
||||
"failedToExportContact": "Failed to export contact",
|
||||
"noLogsToExport": "No logs to export",
|
||||
"exportAsCsv": "Export as CSV",
|
||||
"exportAsText": "Export as Text",
|
||||
"receivedRfc3339": "Received (RFC3339)",
|
||||
"buildTime": "Build Time",
|
||||
"downloadUrlNotAvailable": "Download URL not available",
|
||||
"cannotOpenDownloadUrl": "Cannot open download URL",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Update check is only available on Android",
|
||||
"youAreRunningTheLatestVersion": "You are running the latest version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Update available but download URL not found",
|
||||
"startTictactoe": "Start Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe unavailable",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: opponent unknown",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: waiting for start",
|
||||
"acceptsShareLinks": "Accepts share links",
|
||||
"supportsRawHex": "Supports raw hex",
|
||||
"clipboardfriendly": "Clipboard-friendly",
|
||||
"captured": "Captured",
|
||||
"size": "Size",
|
||||
"noCustomChannelsToClear": "No custom channels to clear.",
|
||||
"noDeviceContactsToClear": "No device contacts to clear.",
|
||||
|
||||
"setRegionScope": "Set region scope",
|
||||
"regionScope": "Region scope",
|
||||
"regionScopeNone": "None (global)",
|
||||
"clearRegionScope": "Clear scope",
|
||||
"regionScopeWarning": "Only repeaters allowing this region will forward.",
|
||||
"discoverRegions": "Discover from repeaters",
|
||||
"discoveringRegions": "Discovering regions...",
|
||||
"enterRegionName": "Region name (e.g. auckland)",
|
||||
"noRegionsFound": "No regions found on this repeater.",
|
||||
"regionScopeSet": "Region scope set to {name}",
|
||||
"@regionScopeSet": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"regionScopeCleared": "Region scope cleared"
|
||||
}
|
||||
|
||||
@@ -1638,5 +1638,252 @@
|
||||
"contactFailedToClearRoute": "No se pudo borrar la ruta: {error}",
|
||||
"contactRouteCleared": "Ruta borrada",
|
||||
"contactRouteSet": "Ruta establecida: {route}",
|
||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}"
|
||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tiempo de espera ACK",
|
||||
"opcode": "Código de operación",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamaño del hash",
|
||||
"pathBytes": "Bytes de ruta",
|
||||
"selectedPath": "Ruta seleccionada",
|
||||
"estimatedTx": "Transmisión estimada",
|
||||
"senderToReceipt": "Del remitente al recibo",
|
||||
"receivedCopies": "Copias recibidas",
|
||||
"retryCause": "Causa del reintento",
|
||||
"retryMode": "Modo de reintento",
|
||||
"retryResult": "Resultado del reintento",
|
||||
"lastRetry": "Último reintento",
|
||||
"rxPackets": "Paquetes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocidad",
|
||||
"window": "Ventana",
|
||||
"posttxDelay": "Retardo post-tx",
|
||||
"bandpass": "Paso de banda",
|
||||
"bandpassFilterVoice": "Filtro de paso de banda de voz",
|
||||
"frequency": "Frecuencia",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Estrecho)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nueva Zelanda",
|
||||
"newZealandNarrow": "Nueva Zelanda (Estrecho)",
|
||||
"switzerland": "Suiza",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estrecho)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Largo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estrecho)",
|
||||
"usacanadaRecommended": "EE.UU./Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietnam (Obsoleto)",
|
||||
"vietnamNarrow": "Vietnam (Estrecho)",
|
||||
"active": "Activo",
|
||||
"addContact": "Añadir contacto",
|
||||
"all": "Todo",
|
||||
"autoResolve": "Resolver automáticamente",
|
||||
"clearAllLabel": "Borrar todo",
|
||||
"clearRelays": "Borrar repetidores",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"clearRoute": "Borrar ruta",
|
||||
"clearMessages": "Borrar mensajes",
|
||||
"clearScale": "Borrar escala",
|
||||
"clearDiscoveries": "Borrar descubrimientos",
|
||||
"clearOnlineTraceDatabase": "Borrar base de datos de trazas",
|
||||
"clearAllChannels": "Borrar todos los canales",
|
||||
"clearAllContacts": "Borrar todos los contactos",
|
||||
"clearChannels": "Borrar canales",
|
||||
"clearContacts": "Borrar contactos",
|
||||
"clearPathOnMaxRetry": "Borrar ruta en máximo reintento",
|
||||
"create": "Crear",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Predeterminado",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nombre",
|
||||
"open": "Abrir",
|
||||
"paste": "Pegar",
|
||||
"preview": "Vista previa",
|
||||
"remove": "Eliminar",
|
||||
"rename": "Renombrar",
|
||||
"resolveAll": "Resolver todo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar de todos modos",
|
||||
"share": "Compartir",
|
||||
"shareContact": "Compartir contacto",
|
||||
"trace": "Traza",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frecuencia seleccionada",
|
||||
"discovery": "Descubrimiento",
|
||||
"discoverRepeaters": "Descubrir repetidores",
|
||||
"discoverSensors": "Descubrir sensores",
|
||||
"repeaterDiscoverySent": "Descubrimiento de repetidores enviado",
|
||||
"sensorDiscoverySent": "Descubrimiento de sensores enviado",
|
||||
"clearedPendingDiscoveries": "Descubrimientos pendientes borrados.",
|
||||
"autoDiscovery": "Descubrimiento automático",
|
||||
"enableAutomaticAdding": "Habilitar adición automática",
|
||||
"autoaddRepeaters": "Añadir repetidores automáticamente",
|
||||
"autoaddRoomServers": "Añadir servidores de sala automáticamente",
|
||||
"autoaddSensors": "Añadir sensores automáticamente",
|
||||
"autoaddUsers": "Añadir usuarios automáticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescribir más antiguos cuando esté lleno",
|
||||
"storage": "Almacenamiento",
|
||||
"dangerZone": "Zona de peligro",
|
||||
"profiles": "Perfiles",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Otros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfico en vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Escaneo de espectro",
|
||||
"blePacketLogs": "Registros de paquetes BLE",
|
||||
"onlineTraceDatabase": "Base de datos de trazas",
|
||||
"routePathByteSize": "Tamaño en bytes de la ruta",
|
||||
"messageNotifications": "Notificaciones de mensajes",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificaciones de descubrimiento",
|
||||
"updateNotifications": "Notificaciones de actualización",
|
||||
"muteWhileAppIsOpen": "Silenciar con la app abierta",
|
||||
"disableContacts": "Desactivar contactos",
|
||||
"enableSensorsTab": "Habilitar pestaña Sensores",
|
||||
"enableProfiles": "Habilitar perfiles",
|
||||
"autoRouteRotation": "Rotación automática de ruta",
|
||||
"nearestRepeaterFallback": "Repetidor más cercano como reserva",
|
||||
"deleteAllStoredMessageHistory": "Eliminar todo el historial de mensajes",
|
||||
"messageFontSize": "Tamaño de fuente de mensajes",
|
||||
"rotateMapWithHeading": "Rotar mapa con rumbo",
|
||||
"showMapDebugInfo": "Mostrar info de depuración del mapa",
|
||||
"openMapInFullscreen": "Abrir mapa en pantalla completa",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR en el mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos los rastros de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores en el mapa",
|
||||
"setMapScale": "Establecer escala del mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Tasa de bits de voz",
|
||||
"voiceCompressor": "Compresor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra niveles de habla suaves y altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene recorte de picos antes de codificar",
|
||||
"micAutoGain": "Ganancia automática del micrófono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite al grabador ajustar el nivel de entrada",
|
||||
"echoCancellation": "Cancelación de eco",
|
||||
"noiseSuppression": "Supresión de ruido",
|
||||
"trimSilenceInVoiceMessages": "Recortar silencio en mensajes de voz",
|
||||
"compressor": "Compresor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganancia automática",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruido",
|
||||
"silenceTrim": "Silencio",
|
||||
"maxImageSize": "Tamaño máximo de imagen",
|
||||
"imageCompression": "Compresión de imagen",
|
||||
"grayscale": "Escala de grises",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Actualizaciones GPS privadas rápidas",
|
||||
"movementThreshold": "Umbral de movimiento",
|
||||
"fastGpsMovementThreshold": "Umbral de movimiento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso activo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de actualización de uso activo",
|
||||
"repeatNearbyTraffic": "Repetir tráfico cercano",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir a través de repetidores en la red",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo cercanos, sin inundación de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Crear perfil",
|
||||
"renameProfile": "Renombrar perfil",
|
||||
"newProfile": "Nuevo perfil",
|
||||
"manageProfiles": "Gestionar perfiles",
|
||||
"enableProfilesToStartManagingThem": "Habilite los perfiles para comenzar a gestionarlos.",
|
||||
"openMessage": "Abrir mensaje",
|
||||
"jumpToTheRelatedSarMessage": "Ir al mensaje SAR relacionado",
|
||||
"removeSarMarker": "Eliminar marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleccione un destino para enviar el marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido al canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado a la sala",
|
||||
"loadFromGallery": "Cargar desde galería",
|
||||
"replaceImage": "Reemplazar imagen",
|
||||
"selectFromGallery": "Seleccionar de galería",
|
||||
"team": "Equipo",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunión",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "El escaneo de espectro no encontró frecuencias candidatas",
|
||||
"searchMessages": "Buscar mensajes",
|
||||
"sendImageFromGallery": "Enviar imagen desde galería",
|
||||
"takePhoto": "Tomar foto",
|
||||
"dmOnly": "Solo mensaje directo",
|
||||
"allMessages": "Todos los mensajes",
|
||||
"sendToPublicChannel": "¿Enviar al canal público?",
|
||||
"selectMarkerTypeAndDestination": "Seleccione tipo de marcador y destino",
|
||||
"noDestinationsAvailableLabel": "No hay destinos disponibles",
|
||||
"image": "Imagen",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensiones",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferencias",
|
||||
"downloadedBy": "Descargado por",
|
||||
"saveDiscoverySettings": "Guardar ajustes de descubrimiento",
|
||||
"savePublicInfo": "Guardar información pública",
|
||||
"saveRadioSettings": "Guardar ajustes de radio",
|
||||
"savePath": "Guardar ruta",
|
||||
"wipeDeviceData": "Borrar datos del dispositivo",
|
||||
"wipeDevice": "Borrar dispositivo",
|
||||
"destructiveDeviceActions": "Acciones destructivas del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Elija un preajuste o ajuste la configuración de radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Elija el nombre y la ubicación que comparte este dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Espacio disponible en este dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renombrar valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Vista previa en vivo",
|
||||
"refreshSchedule": "Programación de actualización",
|
||||
"noResponse": "Sin respuesta",
|
||||
"refreshing": "Actualizando",
|
||||
"unavailable": "No disponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Elija un relay o nodo para observar.",
|
||||
"publicKeyLabel": "Clave pública",
|
||||
"alreadyInContacts": "Ya en contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conéctese a un dispositivo antes de añadir contactos",
|
||||
"fromContacts": "De contactos",
|
||||
"onlineOnly": "Solo en línea",
|
||||
"inBoth": "En ambos",
|
||||
"source": "Fuente",
|
||||
"manualRouteEdit": "Edición manual de ruta",
|
||||
"observedMeshRoute": "Ruta mesh observada",
|
||||
"allMessagesCleared": "Todos los mensajes borrados",
|
||||
"onlineTraceDatabaseCleared": "Base de datos de trazas borrada",
|
||||
"packetLogsCleared": "Registros de paquetes borrados",
|
||||
"hexDataCopiedToClipboard": "Datos hex copiados al portapapeles",
|
||||
"developerModeEnabled": "Modo desarrollador activado",
|
||||
"developerModeDisabled": "Modo desarrollador desactivado",
|
||||
"clipboardIsEmpty": "El portapapeles está vacío",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Enlace de contacto copiado al portapapeles",
|
||||
"failedToExportContact": "Error al exportar el contacto",
|
||||
"noLogsToExport": "No hay registros para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recibido (RFC3339)",
|
||||
"buildTime": "Hora de compilación",
|
||||
"downloadUrlNotAvailable": "URL de descarga no disponible",
|
||||
"cannotOpenDownloadUrl": "No se puede abrir la URL de descarga",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La comprobación de actualizaciones solo está disponible en Android",
|
||||
"youAreRunningTheLatestVersion": "Está ejecutando la última versión",
|
||||
"updateAvailableButDownloadUrlNotFound": "Actualización disponible pero URL de descarga no encontrada",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe no disponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconocido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: esperando inicio",
|
||||
"acceptsShareLinks": "Acepta enlaces compartidos",
|
||||
"supportsRawHex": "Soporta hex sin procesar",
|
||||
"clipboardfriendly": "Compatible con portapapeles",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamaño",
|
||||
"noCustomChannelsToClear": "No hay canales personalizados que borrar.",
|
||||
"noDeviceContactsToClear": "No hay contactos del dispositivo que borrar."
|
||||
}
|
||||
|
||||
@@ -1641,5 +1641,252 @@
|
||||
"contactFailedToClearRoute": "Échec de l'effacement de la route : {error}",
|
||||
"contactRouteCleared": "Route effacée",
|
||||
"contactRouteSet": "Route définie : {route}",
|
||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}"
|
||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Délai ACK expiré",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Charge utile",
|
||||
"hops": "Sauts",
|
||||
"hashSize": "Taille du hash",
|
||||
"pathBytes": "Octets du chemin",
|
||||
"selectedPath": "Chemin sélectionné",
|
||||
"estimatedTx": "Émission estimée",
|
||||
"senderToReceipt": "Émetteur au reçu",
|
||||
"receivedCopies": "Copies reçues",
|
||||
"retryCause": "Cause de la relance",
|
||||
"retryMode": "Mode de relance",
|
||||
"retryResult": "Résultat de la relance",
|
||||
"lastRetry": "Dernière relance",
|
||||
"rxPackets": "Paquets RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Débit",
|
||||
"window": "Fenêtre",
|
||||
"posttxDelay": "Délai post-émission",
|
||||
"bandpass": "Passe-bande",
|
||||
"bandpassFilterVoice": "Filtre passe-bande voix",
|
||||
"frequency": "Fréquence",
|
||||
"australia": "Australie",
|
||||
"australiaNarrow": "Australie (Étroit)",
|
||||
"australiaQld": "Australie: QLD",
|
||||
"australiaSaWa": "Australie: SA, WA",
|
||||
"newZealand": "Nouvelle-Zélande",
|
||||
"newZealandNarrow": "Nouvelle-Zélande (Étroit)",
|
||||
"switzerland": "Suisse",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "République tchèque (Étroit)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longue Portée)",
|
||||
"euukDeprecated": "UE/RU (Obsolète)",
|
||||
"euukNarrow": "UE/RU (Étroit)",
|
||||
"usacanadaRecommended": "USA/Canada (Recommandé)",
|
||||
"vietnamDeprecated": "Vietnam (Obsolète)",
|
||||
"vietnamNarrow": "Vietnam (Étroit)",
|
||||
"active": "Actif",
|
||||
"addContact": "Ajouter un contact",
|
||||
"all": "Tout",
|
||||
"autoResolve": "Résolution automatique",
|
||||
"clearAllLabel": "Tout effacer",
|
||||
"clearRelays": "Effacer les relais",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"clearRoute": "Effacer la route",
|
||||
"clearMessages": "Effacer les messages",
|
||||
"clearScale": "Effacer l'échelle",
|
||||
"clearDiscoveries": "Effacer les découvertes",
|
||||
"clearOnlineTraceDatabase": "Effacer la base de traces",
|
||||
"clearAllChannels": "Effacer tous les canaux",
|
||||
"clearAllContacts": "Effacer tous les contacts",
|
||||
"clearChannels": "Effacer les canaux",
|
||||
"clearContacts": "Effacer les contacts",
|
||||
"clearPathOnMaxRetry": "Effacer le chemin au max de relances",
|
||||
"create": "Créer",
|
||||
"custom": "Personnalisé",
|
||||
"defaultValue": "Par défaut",
|
||||
"duplicate": "Dupliquer",
|
||||
"editName": "Modifier le nom",
|
||||
"open": "Ouvrir",
|
||||
"paste": "Coller",
|
||||
"preview": "Aperçu",
|
||||
"remove": "Supprimer",
|
||||
"rename": "Renommer",
|
||||
"resolveAll": "Résoudre tout",
|
||||
"send": "Envoyer",
|
||||
"sendAnyway": "Envoyer quand même",
|
||||
"share": "Partager",
|
||||
"shareContact": "Partager le contact",
|
||||
"trace": "Trace",
|
||||
"use": "Utiliser",
|
||||
"useSelectedFrequency": "Utiliser la fréquence sélectionnée",
|
||||
"discovery": "Découverte",
|
||||
"discoverRepeaters": "Découvrir les répéteurs",
|
||||
"discoverSensors": "Découvrir les capteurs",
|
||||
"repeaterDiscoverySent": "Découverte de répéteurs envoyée",
|
||||
"sensorDiscoverySent": "Découverte de capteurs envoyée",
|
||||
"clearedPendingDiscoveries": "Découvertes en attente effacées.",
|
||||
"autoDiscovery": "Découverte automatique",
|
||||
"enableAutomaticAdding": "Activer l'ajout automatique",
|
||||
"autoaddRepeaters": "Ajout auto des répéteurs",
|
||||
"autoaddRoomServers": "Ajout auto des serveurs de salle",
|
||||
"autoaddSensors": "Ajout auto des capteurs",
|
||||
"autoaddUsers": "Ajout auto des utilisateurs",
|
||||
"overwriteOldestWhenFull": "Écraser les plus anciens si plein",
|
||||
"storage": "Stockage",
|
||||
"dangerZone": "Zone de danger",
|
||||
"profiles": "Profils",
|
||||
"favourites": "Favoris",
|
||||
"sensors": "Capteurs",
|
||||
"others": "Autres",
|
||||
"gpsModule": "Module GPS",
|
||||
"liveTraffic": "Trafic en direct",
|
||||
"repeatersMap": "Carte des répéteurs",
|
||||
"spectrumScan": "Scan spectral",
|
||||
"blePacketLogs": "Journaux de paquets BLE",
|
||||
"onlineTraceDatabase": "Base de données de traces",
|
||||
"routePathByteSize": "Taille en octets du chemin",
|
||||
"messageNotifications": "Notifications de messages",
|
||||
"sarAlerts": "Alertes SAR",
|
||||
"discoveryNotifications": "Notifications de découverte",
|
||||
"updateNotifications": "Notifications de mise à jour",
|
||||
"muteWhileAppIsOpen": "Muet quand l'appli est ouverte",
|
||||
"disableContacts": "Désactiver les contacts",
|
||||
"enableSensorsTab": "Activer l'onglet Capteurs",
|
||||
"enableProfiles": "Activer les profils",
|
||||
"autoRouteRotation": "Rotation automatique de route",
|
||||
"nearestRepeaterFallback": "Répéteur le plus proche en repli",
|
||||
"deleteAllStoredMessageHistory": "Supprimer tout l'historique",
|
||||
"messageFontSize": "Taille de police des messages",
|
||||
"rotateMapWithHeading": "Tourner la carte avec le cap",
|
||||
"showMapDebugInfo": "Afficher les infos de débogage",
|
||||
"openMapInFullscreen": "Ouvrir la carte en plein écran",
|
||||
"showSarMarkersLabel": "Afficher les marqueurs SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Afficher les marqueurs SAR sur la carte",
|
||||
"showAllContactTrailsLabel": "Afficher toutes les traces de contacts",
|
||||
"hideRepeatersOnMap": "Masquer les répéteurs sur la carte",
|
||||
"setMapScale": "Définir l'échelle de la carte",
|
||||
"customMapScaleSaved": "Échelle de carte personnalisée enregistrée",
|
||||
"voiceBitrate": "Débit vocal",
|
||||
"voiceCompressor": "Compresseur vocal",
|
||||
"balancesQuietAndLoudSpeechLevels": "Équilibre les niveaux faibles et forts",
|
||||
"voiceLimiter": "Limiteur vocal",
|
||||
"preventsClippingPeaksBeforeEncoding": "Empêche l'écrêtage avant l'encodage",
|
||||
"micAutoGain": "Gain auto du micro",
|
||||
"letsTheRecorderAdjustInputLevel": "Laisse l'enregistreur ajuster le niveau d'entrée",
|
||||
"echoCancellation": "Annulation d'écho",
|
||||
"noiseSuppression": "Suppression du bruit",
|
||||
"trimSilenceInVoiceMessages": "Couper le silence dans les messages vocaux",
|
||||
"compressor": "Compresseur",
|
||||
"limiter": "Limiteur",
|
||||
"autoGain": "Gain automatique",
|
||||
"echoCancel": "Écho",
|
||||
"noiseSuppress": "Bruit",
|
||||
"silenceTrim": "Silence",
|
||||
"maxImageSize": "Taille d'image maximale",
|
||||
"imageCompression": "Compression d'image",
|
||||
"grayscale": "Niveaux de gris",
|
||||
"ultraMode": "Mode ultra",
|
||||
"fastPrivateGpsUpdates": "Mises à jour GPS privées rapides",
|
||||
"movementThreshold": "Seuil de mouvement",
|
||||
"fastGpsMovementThreshold": "Seuil de mouvement GPS rapide",
|
||||
"fastGpsActiveuseInterval": "Intervalle d'utilisation active GPS rapide",
|
||||
"activeuseUpdateInterval": "Intervalle de mise à jour en utilisation active",
|
||||
"repeatNearbyTraffic": "Répéter le trafic proche",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Relayer via les répéteurs du réseau",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Proximité seulement, sans inondation",
|
||||
"multihop": "Multi-saut",
|
||||
"createProfile": "Créer un profil",
|
||||
"renameProfile": "Renommer le profil",
|
||||
"newProfile": "Nouveau profil",
|
||||
"manageProfiles": "Gérer les profils",
|
||||
"enableProfilesToStartManagingThem": "Activez les profils pour commencer à les gérer.",
|
||||
"openMessage": "Ouvrir le message",
|
||||
"jumpToTheRelatedSarMessage": "Aller au message SAR associé",
|
||||
"removeSarMarker": "Supprimer le marqueur SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Sélectionnez une destination pour envoyer le marqueur",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marqueur SAR diffusé sur le canal public",
|
||||
"sarMarkerSentToRoom": "Marqueur SAR envoyé à la salle",
|
||||
"loadFromGallery": "Charger depuis la galerie",
|
||||
"replaceImage": "Remplacer l'image",
|
||||
"selectFromGallery": "Sélectionner depuis la galerie",
|
||||
"team": "Équipe",
|
||||
"found": "Trouvé",
|
||||
"staging": "Zone de regroupement",
|
||||
"object": "Objet",
|
||||
"quiet": "Calme",
|
||||
"moderate": "Modéré",
|
||||
"busy": "Occupé",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Le scan spectral n'a trouvé aucune fréquence candidate",
|
||||
"searchMessages": "Rechercher des messages",
|
||||
"sendImageFromGallery": "Envoyer une image de la galerie",
|
||||
"takePhoto": "Prendre une photo",
|
||||
"dmOnly": "Message direct uniquement",
|
||||
"allMessages": "Tous les messages",
|
||||
"sendToPublicChannel": "Envoyer au canal public ?",
|
||||
"selectMarkerTypeAndDestination": "Sélectionnez le type de marqueur et la destination",
|
||||
"noDestinationsAvailableLabel": "Aucune destination disponible",
|
||||
"image": "Image",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimensions",
|
||||
"segments": "Segments",
|
||||
"transfers": "Transferts",
|
||||
"downloadedBy": "Téléchargé par",
|
||||
"saveDiscoverySettings": "Enregistrer les paramètres de découverte",
|
||||
"savePublicInfo": "Enregistrer les infos publiques",
|
||||
"saveRadioSettings": "Enregistrer les paramètres radio",
|
||||
"savePath": "Enregistrer le chemin",
|
||||
"wipeDeviceData": "Effacer les données de l'appareil",
|
||||
"wipeDevice": "Effacer l'appareil",
|
||||
"destructiveDeviceActions": "Actions destructives de l'appareil.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Choisissez un préréglage ou ajustez les paramètres radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Choisissez le nom et l'emplacement partagés par cet appareil.",
|
||||
"availableSpaceOnThisDevice": "Espace disponible sur cet appareil.",
|
||||
"used": "Utilisé",
|
||||
"total": "Total",
|
||||
"renameValue": "Renommer la valeur",
|
||||
"customizeFields": "Personnaliser les champs",
|
||||
"livePreview": "Aperçu en direct",
|
||||
"refreshSchedule": "Planification d'actualisation",
|
||||
"noResponse": "Pas de réponse",
|
||||
"refreshing": "Actualisation",
|
||||
"unavailable": "Indisponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Choisissez un relais ou nœud à surveiller.",
|
||||
"publicKeyLabel": "Clé publique",
|
||||
"alreadyInContacts": "Déjà dans les contacts",
|
||||
"connectToADeviceBeforeAddingContacts": "Connectez-vous à un appareil avant d'ajouter des contacts",
|
||||
"fromContacts": "Des contacts",
|
||||
"onlineOnly": "En ligne uniquement",
|
||||
"inBoth": "Dans les deux",
|
||||
"source": "Source",
|
||||
"manualRouteEdit": "Édition manuelle de route",
|
||||
"observedMeshRoute": "Route mesh observée",
|
||||
"allMessagesCleared": "Tous les messages effacés",
|
||||
"onlineTraceDatabaseCleared": "Base de données de traces effacée",
|
||||
"packetLogsCleared": "Journaux de paquets effacés",
|
||||
"hexDataCopiedToClipboard": "Données hex copiées dans le presse-papiers",
|
||||
"developerModeEnabled": "Mode développeur activé",
|
||||
"developerModeDisabled": "Mode développeur désactivé",
|
||||
"clipboardIsEmpty": "Le presse-papiers est vide",
|
||||
"contactImported": "Contact importé",
|
||||
"contactLinkCopiedToClipboard": "Lien du contact copié dans le presse-papiers",
|
||||
"failedToExportContact": "Échec de l'exportation du contact",
|
||||
"noLogsToExport": "Aucun journal à exporter",
|
||||
"exportAsCsv": "Exporter en CSV",
|
||||
"exportAsText": "Exporter en texte",
|
||||
"receivedRfc3339": "Reçu (RFC3339)",
|
||||
"buildTime": "Heure de build",
|
||||
"downloadUrlNotAvailable": "URL de téléchargement non disponible",
|
||||
"cannotOpenDownloadUrl": "Impossible d'ouvrir l'URL de téléchargement",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La vérification des mises à jour n'est disponible que sur Android",
|
||||
"youAreRunningTheLatestVersion": "Vous utilisez la dernière version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Mise à jour disponible mais URL de téléchargement introuvable",
|
||||
"startTictactoe": "Démarrer Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: adversaire inconnu",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: en attente du début",
|
||||
"acceptsShareLinks": "Accepte les liens partagés",
|
||||
"supportsRawHex": "Supporte le hex brut",
|
||||
"clipboardfriendly": "Compatible avec le presse-papiers",
|
||||
"captured": "Capturé",
|
||||
"size": "Taille",
|
||||
"noCustomChannelsToClear": "Aucun canal personnalisé à effacer.",
|
||||
"noDeviceContactsToClear": "Aucun contact de l'appareil à effacer."
|
||||
}
|
||||
|
||||
@@ -563,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Brisanje rute nije uspjelo: {error}",
|
||||
"contactRouteCleared": "Ruta obrisana",
|
||||
"contactRouteSet": "Ruta postavljena: {route}",
|
||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}"
|
||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK istek vremena",
|
||||
"opcode": "Opkod",
|
||||
"payload": "Sadržaj",
|
||||
"hops": "Skokovi",
|
||||
"hashSize": "Veličina hash-a",
|
||||
"pathBytes": "Bajtovi putanje",
|
||||
"selectedPath": "Odabrana putanja",
|
||||
"estimatedTx": "Procijenjeno slanje",
|
||||
"senderToReceipt": "Od pošiljatelja do primitka",
|
||||
"receivedCopies": "Primljene kopije",
|
||||
"retryCause": "Razlog ponovnog pokušaja",
|
||||
"retryMode": "Način ponovnog pokušaja",
|
||||
"retryResult": "Rezultat ponovnog pokušaja",
|
||||
"lastRetry": "Zadnji pokušaj",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Brzina",
|
||||
"window": "Prozor",
|
||||
"posttxDelay": "Kašnjenje nakon slanja",
|
||||
"bandpass": "Pojasni filtar",
|
||||
"bandpassFilterVoice": "Pojasni filtar glasa",
|
||||
"frequency": "Frekvencija",
|
||||
"australia": "Australija",
|
||||
"australiaNarrow": "Australija (usko)",
|
||||
"australiaQld": "Australija: QLD",
|
||||
"australiaSaWa": "Australija: SA, WA",
|
||||
"newZealand": "Novi Zeland",
|
||||
"newZealandNarrow": "Novi Zeland (usko)",
|
||||
"switzerland": "Švicarska",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Češka (usko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (veliki domet)",
|
||||
"euukDeprecated": "EU/UK (zastarjelo)",
|
||||
"euukNarrow": "EU/UK (usko)",
|
||||
"usacanadaRecommended": "SAD/Kanada (preporučeno)",
|
||||
"vietnamDeprecated": "Vijetnam (zastarjelo)",
|
||||
"vietnamNarrow": "Vijetnam (usko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Sve",
|
||||
"autoResolve": "Automatski razriješi",
|
||||
"clearAllLabel": "Očisti sve",
|
||||
"clearRelays": "Očisti posrednike",
|
||||
"clearFilters": "Očisti filtre",
|
||||
"clearRoute": "Očisti putanju",
|
||||
"clearMessages": "Očisti poruke",
|
||||
"clearScale": "Očisti mjerilo",
|
||||
"clearDiscoveries": "Očisti otkrića",
|
||||
"clearOnlineTraceDatabase": "Očisti bazu mrežnih tragova",
|
||||
"clearAllChannels": "Očisti sve kanale",
|
||||
"clearAllContacts": "Očisti sve kontakte",
|
||||
"clearChannels": "Očisti kanale",
|
||||
"clearContacts": "Očisti kontakte",
|
||||
"clearPathOnMaxRetry": "Očisti putanju kod maks. pokušaja",
|
||||
"create": "Stvori",
|
||||
"custom": "Prilagođeno",
|
||||
"defaultValue": "Zadano",
|
||||
"duplicate": "Dupliciraj",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Otvori",
|
||||
"paste": "Zalijepi",
|
||||
"preview": "Pregled",
|
||||
"remove": "Ukloni",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razriješi sve",
|
||||
"send": "Pošalji",
|
||||
"sendAnyway": "Svejedno pošalji",
|
||||
"share": "Dijeli",
|
||||
"shareContact": "Dijeli kontakt",
|
||||
"trace": "Praćenje",
|
||||
"use": "Koristi",
|
||||
"useSelectedFrequency": "Koristi odabranu frekvenciju",
|
||||
"discovery": "Otkrivanje",
|
||||
"discoverRepeaters": "Otkrij posrednike",
|
||||
"discoverSensors": "Otkrij senzore",
|
||||
"repeaterDiscoverySent": "Otkrivanje posrednika poslano",
|
||||
"sensorDiscoverySent": "Otkrivanje senzora poslano",
|
||||
"clearedPendingDiscoveries": "Očišćena čekajuća otkrića.",
|
||||
"autoDiscovery": "Automatsko otkrivanje",
|
||||
"enableAutomaticAdding": "Omogući automatsko dodavanje",
|
||||
"autoaddRepeaters": "Automatski dodaj posrednike",
|
||||
"autoaddRoomServers": "Automatski dodaj poslužitelje soba",
|
||||
"autoaddSensors": "Automatski dodaj senzore",
|
||||
"autoaddUsers": "Automatski dodaj korisnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarije kad je puno",
|
||||
"storage": "Pohrana",
|
||||
"dangerZone": "Opasna zona",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Favoriti",
|
||||
"sensors": "Senzori",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet uživo",
|
||||
"repeatersMap": "Karta posrednika",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "BLE zapisi paketa",
|
||||
"onlineTraceDatabase": "Baza mrežnih tragova",
|
||||
"routePathByteSize": "Veličina putanje u bajtima",
|
||||
"messageNotifications": "Obavijesti o porukama",
|
||||
"sarAlerts": "SAR upozorenja",
|
||||
"discoveryNotifications": "Obavijesti o otkrivanju",
|
||||
"updateNotifications": "Obavijesti o ažuriranju",
|
||||
"muteWhileAppIsOpen": "Utišaj dok je aplikacija otvorena",
|
||||
"disableContacts": "Onemogući kontakte",
|
||||
"enableSensorsTab": "Omogući karticu Senzori",
|
||||
"enableProfiles": "Omogući profile",
|
||||
"autoRouteRotation": "Automatsko rotiranje putanje",
|
||||
"nearestRepeaterFallback": "Najbliži posrednik kao rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši svu pohranjenu povijest poruka",
|
||||
"messageFontSize": "Veličina fonta poruka",
|
||||
"rotateMapWithHeading": "Rotiraj kartu prema smjeru",
|
||||
"showMapDebugInfo": "Prikaži debug info karte",
|
||||
"openMapInFullscreen": "Otvori kartu na cijelom zaslonu",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnoj karti",
|
||||
"showAllContactTrailsLabel": "Prikaži sve tragove kontakata",
|
||||
"hideRepeatersOnMap": "Sakrij posrednike na karti",
|
||||
"setMapScale": "Postavi mjerilo karte",
|
||||
"customMapScaleSaved": "Prilagođeno mjerilo karte spremljeno",
|
||||
"voiceBitrate": "Bitna brzina glasa",
|
||||
"voiceCompressor": "Kompresor glasa",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe i glasne razine govora",
|
||||
"voiceLimiter": "Ograničivač glasa",
|
||||
"preventsClippingPeaksBeforeEncoding": "Sprječava rezanje vrhova prije kodiranja",
|
||||
"micAutoGain": "Automatsko pojačanje mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Dopušta snimaču prilagodbu razine ulaza",
|
||||
"echoCancellation": "Poništavanje odjeka",
|
||||
"noiseSuppression": "Potiskivanje šuma",
|
||||
"trimSilenceInVoiceMessages": "Izreži tišinu u glasovnim porukama",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Ograničivač",
|
||||
"autoGain": "Automatsko pojačanje",
|
||||
"echoCancel": "Odjek",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Maksimalna veličina slike",
|
||||
"imageCompression": "Kompresija slike",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Brza privatna GPS ažuriranja",
|
||||
"movementThreshold": "Prag kretanja",
|
||||
"fastGpsMovementThreshold": "Prag kretanja za brzi GPS",
|
||||
"fastGpsActiveuseInterval": "Interval aktivnog korištenja brzog GPS-a",
|
||||
"activeuseUpdateInterval": "Interval ažuriranja pri aktivnom korištenju",
|
||||
"repeatNearbyTraffic": "Ponovi obližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Proslijedi kroz posrednike kroz mrežu",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo obližnji, bez preplavljivanja posrednika",
|
||||
"multihop": "Višeskočno",
|
||||
"createProfile": "Stvori profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Novi profil",
|
||||
"manageProfiles": "Upravljaj profilima",
|
||||
"enableProfilesToStartManagingThem": "Omogućite profile da biste ih počeli upravljati.",
|
||||
"openMessage": "Otvori poruku",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezanu SAR poruku",
|
||||
"removeSarMarker": "Ukloni SAR oznaku",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Odaberite odredište za slanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka emitirana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana u sobu",
|
||||
"loadFromGallery": "Učitaj iz galerije",
|
||||
"replaceImage": "Zamijeni sliku",
|
||||
"selectFromGallery": "Odaberi iz galerije",
|
||||
"team": "Tim",
|
||||
"found": "Pronađeno",
|
||||
"staging": "Sabirno mjesto",
|
||||
"object": "Objekt",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Umjereno",
|
||||
"busy": "Zauzeto",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje nije pronašlo prikladne frekvencije",
|
||||
"searchMessages": "Pretraži poruke",
|
||||
"sendImageFromGallery": "Pošalji sliku iz galerije",
|
||||
"takePhoto": "Snimi fotografiju",
|
||||
"dmOnly": "Samo izravno",
|
||||
"allMessages": "Sve poruke",
|
||||
"sendToPublicChannel": "Pošalji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Odaberite vrstu oznake i odredište",
|
||||
"noDestinationsAvailableLabel": "Nema dostupnih odredišta",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prijenosi",
|
||||
"downloadedBy": "Preuzeo",
|
||||
"saveDiscoverySettings": "Spremi postavke otkrivanja",
|
||||
"savePublicInfo": "Spremi javne podatke",
|
||||
"saveRadioSettings": "Spremi radijske postavke",
|
||||
"savePath": "Spremi putanju",
|
||||
"wipeDeviceData": "Obriši podatke uređaja",
|
||||
"wipeDevice": "Obriši uređaj",
|
||||
"destructiveDeviceActions": "Destruktivne radnje na uređaju.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Odaberite predefiniranu postavku ili fino podesite radijske postavke.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Odaberite ime i lokaciju koju ovaj uređaj dijeli.",
|
||||
"availableSpaceOnThisDevice": "Raspoloživi prostor na ovom uređaju.",
|
||||
"used": "Korišteno",
|
||||
"total": "Ukupno",
|
||||
"renameValue": "Preimenuj vrijednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Pregled uživo",
|
||||
"refreshSchedule": "Raspored osvježavanja",
|
||||
"noResponse": "Nema odgovora",
|
||||
"refreshing": "Osvježavam",
|
||||
"unavailable": "Nedostupno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Odaberite posrednika ili čvor za praćenje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Već u kontaktima",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se s uređajem prije dodavanja kontakata",
|
||||
"fromContacts": "Iz kontakata",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "U oba",
|
||||
"source": "Izvor",
|
||||
"manualRouteEdit": "Ručno uređivanje putanje",
|
||||
"observedMeshRoute": "Opažena mrežna putanja",
|
||||
"allMessagesCleared": "Sve poruke očišćene",
|
||||
"onlineTraceDatabaseCleared": "Baza mrežnih tragova očišćena",
|
||||
"packetLogsCleared": "Zapisi paketa očišćeni",
|
||||
"hexDataCopiedToClipboard": "Hex podaci kopirani u međuspremnik",
|
||||
"developerModeEnabled": "Razvojni način omogućen",
|
||||
"developerModeDisabled": "Razvojni način onemogućen",
|
||||
"clipboardIsEmpty": "Međuspremnik je prazan",
|
||||
"contactImported": "Kontakt uvezen",
|
||||
"contactLinkCopiedToClipboard": "Poveznica na kontakt kopirana u međuspremnik",
|
||||
"failedToExportContact": "Izvoz kontakta nije uspio",
|
||||
"noLogsToExport": "Nema zapisa za izvoz",
|
||||
"exportAsCsv": "Izvezi kao CSV",
|
||||
"exportAsText": "Izvezi kao tekst",
|
||||
"receivedRfc3339": "Primljeno (RFC3339)",
|
||||
"buildTime": "Vrijeme gradnje",
|
||||
"downloadUrlNotAvailable": "URL za preuzimanje nije dostupan",
|
||||
"cannotOpenDownloadUrl": "Ne mogu otvoriti URL za preuzimanje",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Provjera ažuriranja dostupna je samo na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Koristite najnoviju verziju",
|
||||
"updateAvailableButDownloadUrlNotFound": "Ažuriranje dostupno, ali URL za preuzimanje nije pronađen",
|
||||
"startTictactoe": "Pokreni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nije dostupan",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: protivnik nepoznat",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čekanje na početak",
|
||||
"acceptsShareLinks": "Prihvaća dijeljene veze",
|
||||
"supportsRawHex": "Podržava sirovi hex",
|
||||
"clipboardfriendly": "Pogodno za međuspremnik",
|
||||
"captured": "Zabilježeno",
|
||||
"size": "Veličina",
|
||||
"noCustomChannelsToClear": "Nema prilagođenih kanala za brisanje.",
|
||||
"noDeviceContactsToClear": "Nema kontakata uređaja za brisanje."
|
||||
}
|
||||
|
||||
@@ -1641,5 +1641,252 @@
|
||||
"contactFailedToClearRoute": "Impossibile cancellare il percorso: {error}",
|
||||
"contactRouteCleared": "Percorso cancellato",
|
||||
"contactRouteSet": "Percorso impostato: {route}",
|
||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}"
|
||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Timeout ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Payload",
|
||||
"hops": "Hop",
|
||||
"hashSize": "Dimensione hash",
|
||||
"pathBytes": "Byte percorso",
|
||||
"selectedPath": "Percorso selezionato",
|
||||
"estimatedTx": "Trasmissione stimata",
|
||||
"senderToReceipt": "Mittente a ricevuta",
|
||||
"receivedCopies": "Copie ricevute",
|
||||
"retryCause": "Causa del ritentativo",
|
||||
"retryMode": "Modalità ritentativo",
|
||||
"retryResult": "Risultato ritentativo",
|
||||
"lastRetry": "Ultimo ritentativo",
|
||||
"rxPackets": "Pacchetti RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocità",
|
||||
"window": "Finestra",
|
||||
"posttxDelay": "Ritardo post-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda voce",
|
||||
"frequency": "Frequenza",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Stretto)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nuova Zelanda",
|
||||
"newZealandNarrow": "Nuova Zelanda (Stretto)",
|
||||
"switzerland": "Svizzera",
|
||||
"portugal433": "Portogallo 433",
|
||||
"portugal868": "Portogallo 868",
|
||||
"czechRepublicNarrow": "Repubblica Ceca (Stretto)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Lungo Raggio)",
|
||||
"euukDeprecated": "UE/UK (Deprecato)",
|
||||
"euukNarrow": "UE/UK (Stretto)",
|
||||
"usacanadaRecommended": "USA/Canada (Consigliato)",
|
||||
"vietnamDeprecated": "Vietnam (Deprecato)",
|
||||
"vietnamNarrow": "Vietnam (Stretto)",
|
||||
"active": "Attivo",
|
||||
"addContact": "Aggiungi contatto",
|
||||
"all": "Tutto",
|
||||
"autoResolve": "Risolvi automaticamente",
|
||||
"clearAllLabel": "Cancella tutto",
|
||||
"clearRelays": "Cancella relay",
|
||||
"clearFilters": "Cancella filtri",
|
||||
"clearRoute": "Cancella percorso",
|
||||
"clearMessages": "Cancella messaggi",
|
||||
"clearScale": "Cancella scala",
|
||||
"clearDiscoveries": "Cancella scoperte",
|
||||
"clearOnlineTraceDatabase": "Cancella database tracce",
|
||||
"clearAllChannels": "Cancella tutti i canali",
|
||||
"clearAllContacts": "Cancella tutti i contatti",
|
||||
"clearChannels": "Cancella canali",
|
||||
"clearContacts": "Cancella contatti",
|
||||
"clearPathOnMaxRetry": "Cancella percorso al max ritentativo",
|
||||
"create": "Crea",
|
||||
"custom": "Personalizzato",
|
||||
"defaultValue": "Predefinito",
|
||||
"duplicate": "Duplica",
|
||||
"editName": "Modifica nome",
|
||||
"open": "Apri",
|
||||
"paste": "Incolla",
|
||||
"preview": "Anteprima",
|
||||
"remove": "Rimuovi",
|
||||
"rename": "Rinomina",
|
||||
"resolveAll": "Risolvi tutto",
|
||||
"send": "Invia",
|
||||
"sendAnyway": "Invia comunque",
|
||||
"share": "Condividi",
|
||||
"shareContact": "Condividi contatto",
|
||||
"trace": "Traccia",
|
||||
"use": "Usa",
|
||||
"useSelectedFrequency": "Usa frequenza selezionata",
|
||||
"discovery": "Scoperta",
|
||||
"discoverRepeaters": "Scopri ripetitori",
|
||||
"discoverSensors": "Scopri sensori",
|
||||
"repeaterDiscoverySent": "Scoperta ripetitori inviata",
|
||||
"sensorDiscoverySent": "Scoperta sensori inviata",
|
||||
"clearedPendingDiscoveries": "Scoperte in attesa cancellate.",
|
||||
"autoDiscovery": "Scoperta automatica",
|
||||
"enableAutomaticAdding": "Abilita aggiunta automatica",
|
||||
"autoaddRepeaters": "Aggiungi ripetitori automaticamente",
|
||||
"autoaddRoomServers": "Aggiungi server stanze automaticamente",
|
||||
"autoaddSensors": "Aggiungi sensori automaticamente",
|
||||
"autoaddUsers": "Aggiungi utenti automaticamente",
|
||||
"overwriteOldestWhenFull": "Sovrascrivi i più vecchi quando pieno",
|
||||
"storage": "Archiviazione",
|
||||
"dangerZone": "Zona pericolosa",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Preferiti",
|
||||
"sensors": "Sensori",
|
||||
"others": "Altri",
|
||||
"gpsModule": "Modulo GPS",
|
||||
"liveTraffic": "Traffico in tempo reale",
|
||||
"repeatersMap": "Mappa ripetitori",
|
||||
"spectrumScan": "Scansione spettro",
|
||||
"blePacketLogs": "Log pacchetti BLE",
|
||||
"onlineTraceDatabase": "Database tracce",
|
||||
"routePathByteSize": "Dimensione percorso in byte",
|
||||
"messageNotifications": "Notifiche messaggi",
|
||||
"sarAlerts": "Avvisi SAR",
|
||||
"discoveryNotifications": "Notifiche scoperte",
|
||||
"updateNotifications": "Notifiche aggiornamenti",
|
||||
"muteWhileAppIsOpen": "Silenzia con l'app aperta",
|
||||
"disableContacts": "Disabilita contatti",
|
||||
"enableSensorsTab": "Abilita scheda Sensori",
|
||||
"enableProfiles": "Abilita profili",
|
||||
"autoRouteRotation": "Rotazione automatica percorso",
|
||||
"nearestRepeaterFallback": "Ripetitore più vicino come fallback",
|
||||
"deleteAllStoredMessageHistory": "Elimina tutta la cronologia messaggi",
|
||||
"messageFontSize": "Dimensione carattere messaggi",
|
||||
"rotateMapWithHeading": "Ruota mappa con direzione",
|
||||
"showMapDebugInfo": "Mostra info debug mappa",
|
||||
"openMapInFullscreen": "Apri mappa a schermo intero",
|
||||
"showSarMarkersLabel": "Mostra marcatori SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostra marcatori SAR sulla mappa principale",
|
||||
"showAllContactTrailsLabel": "Mostra tutti i percorsi contatti",
|
||||
"hideRepeatersOnMap": "Nascondi ripetitori sulla mappa",
|
||||
"setMapScale": "Imposta scala mappa",
|
||||
"customMapScaleSaved": "Scala mappa personalizzata salvata",
|
||||
"voiceBitrate": "Bitrate voce",
|
||||
"voiceCompressor": "Compressore voce",
|
||||
"balancesQuietAndLoudSpeechLevels": "Bilancia i livelli di voce bassi e alti",
|
||||
"voiceLimiter": "Limitatore voce",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene il clipping prima della codifica",
|
||||
"micAutoGain": "Guadagno automatico microfono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permette al registratore di regolare il livello",
|
||||
"echoCancellation": "Cancellazione eco",
|
||||
"noiseSuppression": "Soppressione rumore",
|
||||
"trimSilenceInVoiceMessages": "Taglia silenzio nei messaggi vocali",
|
||||
"compressor": "Compressore",
|
||||
"limiter": "Limitatore",
|
||||
"autoGain": "Guadagno automatico",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Rumore",
|
||||
"silenceTrim": "Silenzio",
|
||||
"maxImageSize": "Dimensione massima immagine",
|
||||
"imageCompression": "Compressione immagine",
|
||||
"grayscale": "Scala di grigi",
|
||||
"ultraMode": "Modalità ultra",
|
||||
"fastPrivateGpsUpdates": "Aggiornamenti GPS privati veloci",
|
||||
"movementThreshold": "Soglia di movimento",
|
||||
"fastGpsMovementThreshold": "Soglia movimento GPS veloce",
|
||||
"fastGpsActiveuseInterval": "Intervallo uso attivo GPS veloce",
|
||||
"activeuseUpdateInterval": "Intervallo aggiornamento uso attivo",
|
||||
"repeatNearbyTraffic": "Ripeti traffico vicino",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Inoltra tramite ripetitori nella rete",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo vicini, senza flood dei ripetitori",
|
||||
"multihop": "Multi-hop",
|
||||
"createProfile": "Crea profilo",
|
||||
"renameProfile": "Rinomina profilo",
|
||||
"newProfile": "Nuovo profilo",
|
||||
"manageProfiles": "Gestisci profili",
|
||||
"enableProfilesToStartManagingThem": "Abilita i profili per iniziare a gestirli.",
|
||||
"openMessage": "Apri messaggio",
|
||||
"jumpToTheRelatedSarMessage": "Vai al messaggio SAR correlato",
|
||||
"removeSarMarker": "Rimuovi marcatore SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleziona una destinazione per inviare il marcatore SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcatore SAR trasmesso al canale pubblico",
|
||||
"sarMarkerSentToRoom": "Marcatore SAR inviato alla stanza",
|
||||
"loadFromGallery": "Carica dalla galleria",
|
||||
"replaceImage": "Sostituisci immagine",
|
||||
"selectFromGallery": "Seleziona dalla galleria",
|
||||
"team": "Squadra",
|
||||
"found": "Trovato",
|
||||
"staging": "Area di raccolta",
|
||||
"object": "Oggetto",
|
||||
"quiet": "Silenzioso",
|
||||
"moderate": "Moderato",
|
||||
"busy": "Occupato",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "La scansione spettro non ha trovato frequenze candidate",
|
||||
"searchMessages": "Cerca messaggi",
|
||||
"sendImageFromGallery": "Invia immagine dalla galleria",
|
||||
"takePhoto": "Scatta foto",
|
||||
"dmOnly": "Solo messaggio diretto",
|
||||
"allMessages": "Tutti i messaggi",
|
||||
"sendToPublicChannel": "Inviare al canale pubblico?",
|
||||
"selectMarkerTypeAndDestination": "Seleziona tipo marcatore e destinazione",
|
||||
"noDestinationsAvailableLabel": "Nessuna destinazione disponibile",
|
||||
"image": "Immagine",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensioni",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Trasferimenti",
|
||||
"downloadedBy": "Scaricato da",
|
||||
"saveDiscoverySettings": "Salva impostazioni scoperta",
|
||||
"savePublicInfo": "Salva info pubbliche",
|
||||
"saveRadioSettings": "Salva impostazioni radio",
|
||||
"savePath": "Salva percorso",
|
||||
"wipeDeviceData": "Cancella dati dispositivo",
|
||||
"wipeDevice": "Cancella dispositivo",
|
||||
"destructiveDeviceActions": "Azioni distruttive del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Scegli un preset o personalizza le impostazioni radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Scegli il nome e la posizione condivisi da questo dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Spazio disponibile su questo dispositivo.",
|
||||
"used": "Usato",
|
||||
"total": "Totale",
|
||||
"renameValue": "Rinomina valore",
|
||||
"customizeFields": "Personalizza campi",
|
||||
"livePreview": "Anteprima dal vivo",
|
||||
"refreshSchedule": "Programma aggiornamento",
|
||||
"noResponse": "Nessuna risposta",
|
||||
"refreshing": "Aggiornamento",
|
||||
"unavailable": "Non disponibile",
|
||||
"pickARelayOrNodeToWatchInSensors": "Scegli un relay o nodo da monitorare.",
|
||||
"publicKeyLabel": "Chiave pubblica",
|
||||
"alreadyInContacts": "Già nei contatti",
|
||||
"connectToADeviceBeforeAddingContacts": "Connettiti a un dispositivo prima di aggiungere contatti",
|
||||
"fromContacts": "Dai contatti",
|
||||
"onlineOnly": "Solo online",
|
||||
"inBoth": "In entrambi",
|
||||
"source": "Origine",
|
||||
"manualRouteEdit": "Modifica manuale percorso",
|
||||
"observedMeshRoute": "Percorso mesh osservato",
|
||||
"allMessagesCleared": "Tutti i messaggi cancellati",
|
||||
"onlineTraceDatabaseCleared": "Database tracce cancellata",
|
||||
"packetLogsCleared": "Log pacchetti cancellati",
|
||||
"hexDataCopiedToClipboard": "Dati hex copiati negli appunti",
|
||||
"developerModeEnabled": "Modalità sviluppatore attivata",
|
||||
"developerModeDisabled": "Modalità sviluppatore disattivata",
|
||||
"clipboardIsEmpty": "Gli appunti sono vuoti",
|
||||
"contactImported": "Contatto importato",
|
||||
"contactLinkCopiedToClipboard": "Link contatto copiato negli appunti",
|
||||
"failedToExportContact": "Esportazione contatto fallita",
|
||||
"noLogsToExport": "Nessun log da esportare",
|
||||
"exportAsCsv": "Esporta come CSV",
|
||||
"exportAsText": "Esporta come testo",
|
||||
"receivedRfc3339": "Ricevuto (RFC3339)",
|
||||
"buildTime": "Ora build",
|
||||
"downloadUrlNotAvailable": "URL di download non disponibile",
|
||||
"cannotOpenDownloadUrl": "Impossibile aprire URL di download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Il controllo aggiornamenti è disponibile solo su Android",
|
||||
"youAreRunningTheLatestVersion": "Stai utilizzando l'ultima versione",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aggiornamento disponibile ma URL di download non trovato",
|
||||
"startTictactoe": "Avvia Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe non disponibile",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: avversario sconosciuto",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: in attesa dell'inizio",
|
||||
"acceptsShareLinks": "Accetta link condivisi",
|
||||
"supportsRawHex": "Supporta hex grezzo",
|
||||
"clipboardfriendly": "Compatibile con appunti",
|
||||
"captured": "Catturato",
|
||||
"size": "Dimensione",
|
||||
"noCustomChannelsToClear": "Nessun canale personalizzato da cancellare.",
|
||||
"noDeviceContactsToClear": "Nessun contatto dispositivo da cancellare."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -481,6 +481,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flut';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Angemeldet';
|
||||
|
||||
@@ -1898,6 +1916,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Nutze Paketprotokolle, Spektrumsuche und Entwicklerdiagnosen zur Fehlersuche im Mesh.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'In Sensoren';
|
||||
|
||||
@@ -1929,4 +1954,809 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Pfad konnte nicht gesetzt werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK-Zeitüberschreitung';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Nutzlast';
|
||||
|
||||
@override
|
||||
String get hops => 'Hops';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash-Größe';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Pfad-Bytes';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Ausgewählter Pfad';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Geschätzte Sendung';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Sender bis Empfang';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Empfangene Kopien';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Wiederholungsursache';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Wiederholungsmodus';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Wiederholungsergebnis';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Letzter Versuch';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-Pakete';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Rate';
|
||||
|
||||
@override
|
||||
String get window => 'Fenster';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Sendeverzögerung';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Bandpass';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Bandpassfilter Sprache';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequenz';
|
||||
|
||||
@override
|
||||
String get australia => 'Australien';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australien (Schmal)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australien: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australien: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Neuseeland';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Neuseeland (Schmal)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Schweiz';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Tschechien (Schmal)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Langstrecke)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (Veraltet)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (Schmal)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Kanada (Empfohlen)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Veraltet)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Schmal)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktiv';
|
||||
|
||||
@override
|
||||
String get addContact => 'Kontakt hinzufügen';
|
||||
|
||||
@override
|
||||
String get all => 'Alle';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatisch auflösen';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Alle löschen';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Relais löschen';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Filter löschen';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Route löschen';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Nachrichten löschen';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Maßstab löschen';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Entdeckungen löschen';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Online-Trace-Datenbank löschen';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Alle Kanäle löschen';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Alle Kontakte löschen';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Kanäle löschen';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Kontakte löschen';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Pfad bei max. Wiederholung löschen';
|
||||
|
||||
@override
|
||||
String get create => 'Erstellen';
|
||||
|
||||
@override
|
||||
String get custom => 'Benutzerdefiniert';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Standard';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplizieren';
|
||||
|
||||
@override
|
||||
String get editName => 'Name bearbeiten';
|
||||
|
||||
@override
|
||||
String get open => 'Öffnen';
|
||||
|
||||
@override
|
||||
String get paste => 'Einfügen';
|
||||
|
||||
@override
|
||||
String get preview => 'Vorschau';
|
||||
|
||||
@override
|
||||
String get remove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get rename => 'Umbenennen';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Alle auflösen';
|
||||
|
||||
@override
|
||||
String get send => 'Senden';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Trotzdem senden';
|
||||
|
||||
@override
|
||||
String get share => 'Teilen';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Kontakt teilen';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Verwenden';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Ausgewählte Frequenz verwenden';
|
||||
|
||||
@override
|
||||
String get discovery => 'Erkennung';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Repeater entdecken';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Sensoren entdecken';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Repeater-Erkennung gesendet';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensor-Erkennung gesendet';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Ausstehende Entdeckungen gelöscht.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatische Erkennung';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Automatisches Hinzufügen aktivieren';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Repeater automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Raumserver automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Sensoren automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Benutzer automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Älteste bei vollem Speicher überschreiben';
|
||||
|
||||
@override
|
||||
String get storage => 'Speicher';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Gefahrenbereich';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profile';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriten';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensoren';
|
||||
|
||||
@override
|
||||
String get others => 'Andere';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS-Modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Live-Verkehr';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Repeater-Karte';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektrum-Scan';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE-Paketprotokolle';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Online-Trace-Datenbank';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Pfad-Bytegröße';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Nachrichtenbenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-Alarme';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Erkennungsbenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Updatebenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Stumm bei geöffneter App';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Kontakte deaktivieren';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Sensoren-Tab aktivieren';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Profile aktivieren';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatische Routenrotation';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Nächster Repeater als Fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Gesamten Nachrichtenverlauf löschen';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Nachrichtenschriftgröße';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Karte mit Richtung drehen';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Karten-Debug-Info anzeigen';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Karte im Vollbild öffnen';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'SAR-Markierungen anzeigen';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'SAR-Markierungen auf der Hauptkarte anzeigen';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Alle Kontaktpfade anzeigen';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Repeater auf Karte ausblenden';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Kartenmaßstab einstellen';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved =>
|
||||
'Benutzerdefinierter Kartenmaßstab gespeichert';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Sprach-Bitrate';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Sprachkompressor';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Gleicht leise und laute Sprache aus';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Sprach-Limiter';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Verhindert Clipping-Spitzen vor der Kodierung';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mikrofon-Automatik';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Lässt den Rekorder den Eingangspegel anpassen';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Echounterdrückung';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Rauschunterdrückung';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Stille in Sprachnachrichten trimmen';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatische Verstärkung';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Rauschen';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Stille';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maximale Bildgröße';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Bildkomprimierung';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Graustufen';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra-Modus';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Schnelle private GPS-Updates';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Bewegungsschwelle';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Schneller GPS-Bewegungsschwellenwert';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Schnelles GPS aktive Nutzung Intervall';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Aktive Nutzung Updateintervall';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Nahverkehr wiederholen';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Über Repeater im Mesh weiterleiten';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Nur in der Nähe, ohne Repeater-Flooding';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-Hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Profil erstellen';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Profil umbenennen';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Neues Profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Profile verwalten';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Aktivieren Sie Profile, um sie zu verwalten.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Nachricht öffnen';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Zur zugehörigen SAR-Nachricht springen';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'SAR-Markierung entfernen';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Bitte wählen Sie ein Ziel zum Senden der SAR-Markierung';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-Markierung an öffentlichen Kanal gesendet';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-Markierung an Raum gesendet';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Aus Galerie laden';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Bild ersetzen';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Aus Galerie auswählen';
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get found => 'Gefunden';
|
||||
|
||||
@override
|
||||
String get staging => 'Sammelpunkt';
|
||||
|
||||
@override
|
||||
String get object => 'Objekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Ruhig';
|
||||
|
||||
@override
|
||||
String get moderate => 'Mäßig';
|
||||
|
||||
@override
|
||||
String get busy => 'Beschäftigt';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektrum-Scan hat keine Kandidatenfrequenzen gefunden';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Nachrichten suchen';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Bild aus Galerie senden';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Foto aufnehmen';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Nur Direktnachricht';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Alle Nachrichten';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'An öffentlichen Kanal senden?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Markierungstyp und Ziel auswählen';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Keine Ziele verfügbar';
|
||||
|
||||
@override
|
||||
String get image => 'Bild';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Abmessungen';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmente';
|
||||
|
||||
@override
|
||||
String get transfers => 'Übertragungen';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Heruntergeladen von';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Erkennungseinstellungen speichern';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Öffentliche Info speichern';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Funkeinstellungen speichern';
|
||||
|
||||
@override
|
||||
String get savePath => 'Pfad speichern';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Gerätedaten löschen';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Gerät löschen';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktive Geräteaktionen.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Wählen Sie eine Voreinstellung oder passen Sie die Funkeinstellungen an.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Wählen Sie den Namen und Standort, den dieses Gerät teilt.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Verfügbarer Speicherplatz auf diesem Gerät.';
|
||||
|
||||
@override
|
||||
String get used => 'Belegt';
|
||||
|
||||
@override
|
||||
String get total => 'Gesamt';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Wert umbenennen';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Felder anpassen';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Live-Vorschau';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Aktualisierungsplan';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Keine Antwort';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Wird aktualisiert';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Wählen Sie einen Relay oder Knoten zur Beobachtung.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Öffentlicher Schlüssel';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Bereits in Kontakten';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Verbinden Sie sich mit einem Gerät, bevor Sie Kontakte hinzufügen';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Aus Kontakten';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Nur online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In beiden';
|
||||
|
||||
@override
|
||||
String get source => 'Quelle';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manuelle Routenbearbeitung';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Beobachtete Mesh-Route';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Alle Nachrichten gelöscht';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Online-Trace-Datenbank gelöscht';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Paketprotokolle gelöscht';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Hex-Daten in die Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Entwicklermodus aktiviert';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Entwicklermodus deaktiviert';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Zwischenablage ist leer';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt importiert';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Kontaktlink in Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Kontaktexport fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Keine Protokolle zum Exportieren';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Als CSV exportieren';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Als Text exportieren';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Empfangen (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Build-Zeit';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Download-URL nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Download-URL kann nicht geöffnet werden';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Update-Prüfung nur auf Android verfügbar';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Sie verwenden die neueste Version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Update verfügbar, aber Download-URL nicht gefunden';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Tic-Tac-Toe starten';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: Gegner unbekannt';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: Warten auf Start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Akzeptiert freigegebene Links';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Unterstützt rohes Hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Zwischenablage-freundlich';
|
||||
|
||||
@override
|
||||
String get captured => 'Erfasst';
|
||||
|
||||
@override
|
||||
String get size => 'Größe';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Keine benutzerdefinierten Kanäle zum Löschen.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Keine Gerätekontakte zum Löschen.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -483,6 +483,24 @@ class AppLocalizationsEl extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Πλημμυρικό';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Συνδεδεμένος';
|
||||
|
||||
@@ -1905,6 +1923,13 @@ class AppLocalizationsEl extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Χρησιμοποιήστε καταγραφές πακέτων, σάρωση φάσματος και διαγνωστικά προγραμματιστή κατά την αντιμετώπιση προβλημάτων του mesh.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Στους αισθητήρες';
|
||||
|
||||
@@ -1936,4 +1961,804 @@ class AppLocalizationsEl extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Αποτυχία ορισμού διαδρομής: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Λήξη χρόνου ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Ωφέλιμο φορτίο';
|
||||
|
||||
@override
|
||||
String get hops => 'Αναπηδήσεις';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Μέγεθος hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes διαδρομής';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Επιλεγμένη διαδρομή';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Εκτιμώμενη εκπομπή';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Αποστολέας σε απόδειξη';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Ληφθέντα αντίγραφα';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Αιτία επανάληψης';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Λειτουργία επανάληψης';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Αποτέλεσμα επανάληψης';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Τελευταία επανάληψη';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Πακέτα RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Ρυθμός';
|
||||
|
||||
@override
|
||||
String get window => 'Παράθυρο';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Καθυστέρηση μετά-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Ζωνοπερατό';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Ζωνοπερατό φίλτρο φωνής';
|
||||
|
||||
@override
|
||||
String get frequency => 'Συχνότητα';
|
||||
|
||||
@override
|
||||
String get australia => 'Αυστραλία';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Αυστραλία (Στενό)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Αυστραλία: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Αυστραλία: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Νέα Ζηλανδία';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Νέα Ζηλανδία (Στενό)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Ελβετία';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Πορτογαλία 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Πορτογαλία 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Τσεχία (Στενό)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ΕΕ 433MHz (Μεγάλη Εμβέλεια)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ΕΕ/ΗΒ (Παρωχημένο)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ΕΕ/ΗΒ (Στενό)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ΗΠΑ/Καναδάς (Προτεινόμενο)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Βιετνάμ (Παρωχημένο)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Βιετνάμ (Στενό)';
|
||||
|
||||
@override
|
||||
String get active => 'Ενεργό';
|
||||
|
||||
@override
|
||||
String get addContact => 'Προσθήκη επαφής';
|
||||
|
||||
@override
|
||||
String get all => 'Όλα';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Αυτόματη επίλυση';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Εκκαθάριση όλων';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Εκκαθάριση αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Εκκαθάριση φίλτρων';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Εκκαθάριση διαδρομής';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Εκκαθάριση μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Εκκαθάριση κλίμακας';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Εκκαθάριση ανακαλύψεων';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Εκκαθάριση βάσης ιχνών';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Εκκαθάριση όλων των καναλιών';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Εκκαθάριση όλων των επαφών';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Εκκαθάριση καναλιών';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Εκκαθάριση επαφών';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Εκκαθάριση μονοπατιού στο μέγιστο';
|
||||
|
||||
@override
|
||||
String get create => 'Δημιουργία';
|
||||
|
||||
@override
|
||||
String get custom => 'Προσαρμοσμένο';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Προεπιλογή';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Αντιγραφή';
|
||||
|
||||
@override
|
||||
String get editName => 'Επεξεργασία ονόματος';
|
||||
|
||||
@override
|
||||
String get open => 'Άνοιγμα';
|
||||
|
||||
@override
|
||||
String get paste => 'Επικόλληση';
|
||||
|
||||
@override
|
||||
String get preview => 'Προεπισκόπηση';
|
||||
|
||||
@override
|
||||
String get remove => 'Αφαίρεση';
|
||||
|
||||
@override
|
||||
String get rename => 'Μετονομασία';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Επίλυση όλων';
|
||||
|
||||
@override
|
||||
String get send => 'Αποστολή';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Αποστολή ούτως ή άλλως';
|
||||
|
||||
@override
|
||||
String get share => 'Κοινοποίηση';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Κοινοποίηση επαφής';
|
||||
|
||||
@override
|
||||
String get trace => 'Ίχνος';
|
||||
|
||||
@override
|
||||
String get use => 'Χρήση';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Χρήση επιλεγμένης συχνότητας';
|
||||
|
||||
@override
|
||||
String get discovery => 'Ανακάλυψη';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Ανακάλυψη αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Ανακάλυψη αισθητήρων';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Αποστολή ανακάλυψης αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Αποστολή ανακάλυψης αισθητήρων';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Εκκαθάριση εκκρεμών ανακαλύψεων.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Αυτόματη ανακάλυψη';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Ενεργοποίηση αυτόματης προσθήκης';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Αυτόματη προσθήκη αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Αυτόματη προσθήκη διακομιστών δωματίου';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Αυτόματη προσθήκη αισθητήρων';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Αυτόματη προσθήκη χρηστών';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Αντικατάσταση παλαιότερων όταν γεμίσει';
|
||||
|
||||
@override
|
||||
String get storage => 'Αποθήκευση';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Ζώνη κινδύνου';
|
||||
|
||||
@override
|
||||
String get profiles => 'Προφίλ';
|
||||
|
||||
@override
|
||||
String get favourites => 'Αγαπημένα';
|
||||
|
||||
@override
|
||||
String get sensors => 'Αισθητήρες';
|
||||
|
||||
@override
|
||||
String get others => 'Άλλοι';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Μονάδα GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Ζωντανή κίνηση';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Χάρτης αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Σάρωση φάσματος';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Αρχεία πακέτων BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Βάση δεδομένων ιχνών';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Μέγεθος διαδρομής σε bytes';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Ειδοποιήσεις μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Ειδοποιήσεις SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Ειδοποιήσεις ανακάλυψης';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Ειδοποιήσεις ενημερώσεων';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Σίγαση με ανοιχτή εφαρμογή';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Απενεργοποίηση επαφών';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Ενεργοποίηση καρτέλας Αισθητήρων';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Ενεργοποίηση προφίλ';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Αυτόματη εναλλαγή διαδρομής';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Πλησιέστερος αναμεταδότης ως εφεδρεία';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Διαγραφή όλου του ιστορικού';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Μέγεθος γραμματοσειράς μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Περιστροφή χάρτη με κατεύθυνση';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Εμφάνιση πληροφοριών αποσφαλμάτωσης';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Άνοιγμα χάρτη σε πλήρη οθόνη';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Εμφάνιση δεικτών SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Εμφάνιση δεικτών SAR στον κύριο χάρτη';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Εμφάνιση όλων των ιχνών επαφών';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Απόκρυψη αναμεταδοτών στον χάρτη';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Ρύθμιση κλίμακας χάρτη';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Ρυθμός bit φωνής';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Συμπιεστής φωνής';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Εξισορρόπηση χαμηλών και δυνατών επιπέδων';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Περιοριστής φωνής';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Αποτρέπει κοπή κορυφών πριν την κωδικοποίηση';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Αυτόματη ενίσχυση μικροφώνου';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Αφήνει τη συσκευή να ρυθμίσει την ένταση';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Ακύρωση ηχούς';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Καταστολή θορύβου';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Περικοπή σιωπής σε φωνητικά μηνύματα';
|
||||
|
||||
@override
|
||||
String get compressor => 'Συμπιεστής';
|
||||
|
||||
@override
|
||||
String get limiter => 'Περιοριστής';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Αυτόματη ενίσχυση';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Ηχώ';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Θόρυβος';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Σιωπή';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Μέγιστο μέγεθος εικόνας';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Συμπίεση εικόνας';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Κλίμακα του γκρι';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Λειτουργία ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Γρήγορες ιδιωτικές ενημερώσεις GPS';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Κατώφλι κίνησης';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Κατώφλι κίνησης γρήγορου GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Διάστημα ενεργής χρήσης γρήγορου GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Διάστημα ενημέρωσης ενεργής χρήσης';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Επανάληψη κοντινής κίνησης';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Αναμετάδοση μέσω αναμεταδοτών στο δίκτυο';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Μόνο κοντά, χωρίς πλημμυρισμό';
|
||||
|
||||
@override
|
||||
String get multihop => 'Πολλαπλών αναπηδήσεων';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Δημιουργία προφίλ';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Μετονομασία προφίλ';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Νέο προφίλ';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Διαχείριση προφίλ';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Ενεργοποιήστε τα προφίλ για να τα διαχειριστείτε.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Άνοιγμα μηνύματος';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Μετάβαση στο σχετικό μήνυμα SAR';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Αφαίρεση δείκτη SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Επιλέξτε προορισμό για αποστολή δείκτη SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Δείκτης SAR εκπεμπόμενος στο δημόσιο κανάλι';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Δείκτης SAR εστάλη στο δωμάτιο';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Φόρτωση από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Αντικατάσταση εικόνας';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Επιλογή από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get team => 'Ομάδα';
|
||||
|
||||
@override
|
||||
String get found => 'Βρέθηκε';
|
||||
|
||||
@override
|
||||
String get staging => 'Χώρος συγκέντρωσης';
|
||||
|
||||
@override
|
||||
String get object => 'Αντικείμενο';
|
||||
|
||||
@override
|
||||
String get quiet => 'Ήσυχο';
|
||||
|
||||
@override
|
||||
String get moderate => 'Μέτριο';
|
||||
|
||||
@override
|
||||
String get busy => 'Απασχολημένο';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Η σάρωση φάσματος δεν βρήκε υποψήφιες συχνότητες';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Αναζήτηση μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Αποστολή εικόνας από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Λήψη φωτογραφίας';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Μόνο απευθείας';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Όλα τα μηνύματα';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Αποστολή στο δημόσιο κανάλι;';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Επιλέξτε τύπο δείκτη και προορισμό';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel =>
|
||||
'Δεν υπάρχουν διαθέσιμοι προορισμοί';
|
||||
|
||||
@override
|
||||
String get image => 'Εικόνα';
|
||||
|
||||
@override
|
||||
String get format => 'Μορφή';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Διαστάσεις';
|
||||
|
||||
@override
|
||||
String get segments => 'Τμήματα';
|
||||
|
||||
@override
|
||||
String get transfers => 'Μεταφορές';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Λήφθηκε από';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Αποθήκευση ρυθμίσεων ανακάλυψης';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Αποθήκευση δημόσιων πληροφοριών';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Αποθήκευση ρυθμίσεων ραδιοφώνου';
|
||||
|
||||
@override
|
||||
String get savePath => 'Αποθήκευση διαδρομής';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Διαγραφή δεδομένων συσκευής';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Διαγραφή συσκευής';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Καταστροφικές ενέργειες συσκευής.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Επιλέξτε το όνομα και την τοποθεσία που μοιράζεται αυτή η συσκευή.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Διαθέσιμος χώρος σε αυτή τη συσκευή.';
|
||||
|
||||
@override
|
||||
String get used => 'Χρησιμοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get total => 'Σύνολο';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Μετονομασία τιμής';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Προσαρμογή πεδίων';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Ζωντανή προεπισκόπηση';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Χρονοδιάγραμμα ανανέωσης';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Χωρίς απάντηση';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Ανανέωση';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Μη διαθέσιμο';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Δημόσιο κλειδί';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Ήδη στις επαφές';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Από τις επαφές';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Μόνο συνδεδεμένοι';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Σε αμφότερα';
|
||||
|
||||
@override
|
||||
String get source => 'Πηγή';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Χειροκίνητη επεξεργασία διαδρομής';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Παρατηρούμενη διαδρομή δικτύου';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Όλα τα μηνύματα διαγράφηκαν';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Βάση δεδομένων ιχνών εκκαθαρίστηκε';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Τα αρχεία πακέτων εκκαθαρίστηκαν';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Τα δεδομένα hex αντιγράφηκαν';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Λειτουργία προγραμματιστή ενεργοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled =>
|
||||
'Λειτουργία προγραμματιστή απενεργοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Το πρόχειρο είναι κενό';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Επαφή εισήχθη';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => 'Ο σύνδεσμος επαφής αντιγράφηκε';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Αποτυχία εξαγωγής επαφής';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Δεν υπάρχουν αρχεία για εξαγωγή';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Εξαγωγή ως CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Εξαγωγή ως κείμενο';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Ελήφθη (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Ώρα κατασκευής';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Η URL λήψης δεν είναι διαθέσιμη';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Αδυναμία ανοίγματος URL λήψης';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Ο έλεγχος ενημερώσεων είναι διαθέσιμος μόνο σε Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Χρησιμοποιείτε την πιο πρόσφατη έκδοση';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Διαθέσιμη ενημέρωση αλλά η URL λήψης δεν βρέθηκε';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Εκκίνηση Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe μη διαθέσιμο';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: άγνωστος αντίπαλος';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: αναμονή για εκκίνηση';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Αποδέχεται κοινόχρηστους συνδέσμους';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Υποστηρίζει ακατέργαστο hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Φιλικό προς πρόχειρο';
|
||||
|
||||
@override
|
||||
String get captured => 'Καταγράφηκε';
|
||||
|
||||
@override
|
||||
String get size => 'Μέγεθος';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Δεν υπάρχουν προσαρμοσμένα κανάλια.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Δεν υπάρχουν επαφές συσκευής.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -478,6 +478,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flood';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Logged In';
|
||||
|
||||
@@ -1717,7 +1735,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'This app combines MeshCore messaging, SAR field updates, mapping, and device tools in one place.';
|
||||
'This app combines MeshCore messaging, SAR field updates, discovery, mapping, and device tools in one place.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
@@ -1729,7 +1747,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Connect over BLE or TCP, then manage the companion radio from inside the app.';
|
||||
'Connect over BLE, WiFi, or Serial, then manage the companion radio from inside the app.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Messaging and Field Reports';
|
||||
@@ -1819,11 +1837,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'The Contacts tab organizes the network you discover and the routes you learn over time.';
|
||||
'The Contacts and Discovery flows help you find nearby nodes, store repeaters, and manage the routes you learn over time.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Review team members, repeaters, rooms, channels, and pending adverts in one list.';
|
||||
'Discover repeaters and sensors, then review team members, rooms, channels, and pending adverts in one list.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
@@ -1831,7 +1849,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Create channels and manage network destinations without leaving the app.';
|
||||
'Add new repeaters right after you connect, then create channels and manage network destinations without leaving the app.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Map, Trails, and Shared Geometry';
|
||||
@@ -1869,7 +1887,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Use packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.';
|
||||
'Use live traffic, packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'In Sensors';
|
||||
@@ -1902,4 +1927,800 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Failed to set route: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK timeout';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Payload';
|
||||
|
||||
@override
|
||||
String get hops => 'Hops';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash size';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Path bytes';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Selected path';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Estimated tx';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Sender to receipt';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Received copies';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Retry cause';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Retry mode';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Retry result';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Last retry';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX packets';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Rate';
|
||||
|
||||
@override
|
||||
String get window => 'Window';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Post-tx delay';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Band-pass';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Band-pass filter voice';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequency';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Narrow)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'New Zealand';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'New Zealand (Narrow)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Switzerland';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Czech Republic (Narrow)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Long Range)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (Deprecated)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (Narrow)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Recommended)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Deprecated)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Narrow)';
|
||||
|
||||
@override
|
||||
String get active => 'Active';
|
||||
|
||||
@override
|
||||
String get addContact => 'Add Contact';
|
||||
|
||||
@override
|
||||
String get all => 'All';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Auto resolve';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Clear all';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Clear relays';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Clear filters';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Clear Route';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Clear Messages';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Clear scale';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Clear discoveries';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Clear online trace database';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Clear all channels';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Clear all contacts';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Clear channels';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Clear contacts';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Clear path on max retry';
|
||||
|
||||
@override
|
||||
String get create => 'Create';
|
||||
|
||||
@override
|
||||
String get custom => 'Custom';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Default';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicate';
|
||||
|
||||
@override
|
||||
String get editName => 'Edit name';
|
||||
|
||||
@override
|
||||
String get open => 'Open';
|
||||
|
||||
@override
|
||||
String get paste => 'Paste';
|
||||
|
||||
@override
|
||||
String get preview => 'Preview';
|
||||
|
||||
@override
|
||||
String get remove => 'Remove';
|
||||
|
||||
@override
|
||||
String get rename => 'Rename';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolve all';
|
||||
|
||||
@override
|
||||
String get send => 'Send';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Send anyway';
|
||||
|
||||
@override
|
||||
String get share => 'Share';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Share Contact';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Use';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Use selected frequency';
|
||||
|
||||
@override
|
||||
String get discovery => 'Discovery';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Discover repeaters';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Discover sensors';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Repeater discovery sent';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensor discovery sent';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Cleared pending discoveries.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Auto discovery';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Enable automatic adding';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Auto-add repeaters';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Auto-add room servers';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Auto-add sensors';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Auto-add users';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Overwrite oldest when full';
|
||||
|
||||
@override
|
||||
String get storage => 'Storage';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Danger zone';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profiles';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favourites';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensors';
|
||||
|
||||
@override
|
||||
String get others => 'Others';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS Module';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Live Traffic';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Repeaters Map';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spectrum Scan';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE Packet Logs';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Online trace database';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Route path byte size';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Message notifications';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR alerts';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Discovery notifications';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Update notifications';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Mute while app is open';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Disable Contacts';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Enable Sensors tab';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Enable Profiles';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Auto route rotation';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Nearest repeater fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Delete all stored message history';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Message font size';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotate map with heading';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Show map debug info';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Open map in fullscreen';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Show SAR markers';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Display SAR markers on the main map';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Show all contact trails';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Hide repeaters on map';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Set map scale';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Custom map scale saved';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Voice bitrate';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Voice compressor';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Balances quiet and loud speech levels';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Voice limiter';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Prevents clipping peaks before encoding';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mic auto gain';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Lets the recorder adjust input level';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Echo cancellation';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Noise suppression';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Trim silence in voice messages';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Auto gain';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo cancel';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Noise suppress';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silence trim';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Max image size';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Image compression';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Grayscale';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra mode';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Fast private GPS updates';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Movement threshold';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Fast GPS movement threshold';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Fast GPS active-use interval';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Active-use update interval';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repeat nearby traffic';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Relay through repeaters across the mesh';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Nearby only, without repeater flooding';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Create Profile';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Rename Profile';
|
||||
|
||||
@override
|
||||
String get newProfile => 'New Profile';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Manage profiles';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Enable profiles to start managing them.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Open message';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Jump to the related SAR message';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Remove SAR marker';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Please select a destination to send SAR marker';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR marker broadcast to public channel';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR marker sent to room';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Load from gallery';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Replace image';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Select from gallery';
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get found => 'Found';
|
||||
|
||||
@override
|
||||
String get staging => 'Staging';
|
||||
|
||||
@override
|
||||
String get object => 'Object';
|
||||
|
||||
@override
|
||||
String get quiet => 'Quiet';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderate';
|
||||
|
||||
@override
|
||||
String get busy => 'Busy';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spectrum scan returned no candidate frequencies';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Search messages';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Send image from gallery';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Take photo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'DM only';
|
||||
|
||||
@override
|
||||
String get allMessages => 'All messages';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Send to Public Channel?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Select marker type and destination';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'No destinations available';
|
||||
|
||||
@override
|
||||
String get image => 'Image';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensions';
|
||||
|
||||
@override
|
||||
String get segments => 'Segments';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transfers';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Downloaded by';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Save discovery settings';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Save public info';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Save radio settings';
|
||||
|
||||
@override
|
||||
String get savePath => 'Save Path';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Wipe device data';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Wipe device';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destructive device actions.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Choose a preset or fine-tune custom radio settings.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Choose the name and location this device shares.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => 'Available space on this device.';
|
||||
|
||||
@override
|
||||
String get used => 'Used';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Rename value';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Customize fields';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Live preview';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Refresh schedule';
|
||||
|
||||
@override
|
||||
String get noResponse => 'No response';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Refreshing';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Unavailable';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Pick a relay or node to watch in Sensors.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Public key';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Already in contacts';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connect to a device before adding contacts';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'From contacts';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Online only';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In both';
|
||||
|
||||
@override
|
||||
String get source => 'Source';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manual route edit';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Observed mesh route';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'All messages cleared';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Online trace database cleared';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Packet logs cleared';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex data copied to clipboard';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Developer mode enabled';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Developer mode disabled';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Clipboard is empty';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contact imported';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => 'Contact link copied to clipboard';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Failed to export contact';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'No logs to export';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Export as CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Export as Text';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Received (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Build Time';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Download URL not available';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Cannot open download URL';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Update check is only available on Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'You are running the latest version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Update available but download URL not found';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Start Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe unavailable';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: opponent unknown';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: waiting for start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accepts share links';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supports raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Clipboard-friendly';
|
||||
|
||||
@override
|
||||
String get captured => 'Captured';
|
||||
|
||||
@override
|
||||
String get size => 'Size';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'No custom channels to clear.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'No device contacts to clear.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -481,6 +481,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Inundación';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Sesión iniciada';
|
||||
|
||||
@@ -1901,6 +1919,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Usa registros de paquetes, escaneo de espectro y diagnósticos de desarrollador al solucionar problemas de la malla.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'En Sensores';
|
||||
|
||||
@@ -1932,4 +1957,810 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'No se pudo establecer la ruta: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Tiempo de espera ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Código de operación';
|
||||
|
||||
@override
|
||||
String get payload => 'Carga útil';
|
||||
|
||||
@override
|
||||
String get hops => 'Saltos';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Tamaño del hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes de ruta';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Ruta seleccionada';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Transmisión estimada';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Del remitente al recibo';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copias recibidas';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa del reintento';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modo de reintento';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Resultado del reintento';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Último reintento';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Paquetes RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Velocidad';
|
||||
|
||||
@override
|
||||
String get window => 'Ventana';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Retardo post-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Paso de banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro de paso de banda de voz';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frecuencia';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Estrecho)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nueva Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nueva Zelanda (Estrecho)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suiza';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'República Checa (Estrecho)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Largo Alcance)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Estrecho)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'EE.UU./Canadá (Recomendado)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Estrecho)';
|
||||
|
||||
@override
|
||||
String get active => 'Activo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Añadir contacto';
|
||||
|
||||
@override
|
||||
String get all => 'Todo';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Resolver automáticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Borrar todo';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Borrar repetidores';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Borrar filtros';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Borrar ruta';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Borrar mensajes';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Borrar escala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Borrar descubrimientos';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Borrar base de datos de trazas';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Borrar todos los canales';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Borrar todos los contactos';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Borrar canales';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Borrar contactos';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Borrar ruta en máximo reintento';
|
||||
|
||||
@override
|
||||
String get create => 'Crear';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizado';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Predeterminado';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicar';
|
||||
|
||||
@override
|
||||
String get editName => 'Editar nombre';
|
||||
|
||||
@override
|
||||
String get open => 'Abrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Pegar';
|
||||
|
||||
@override
|
||||
String get preview => 'Vista previa';
|
||||
|
||||
@override
|
||||
String get remove => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get rename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolver todo';
|
||||
|
||||
@override
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Enviar de todos modos';
|
||||
|
||||
@override
|
||||
String get share => 'Compartir';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Compartir contacto';
|
||||
|
||||
@override
|
||||
String get trace => 'Traza';
|
||||
|
||||
@override
|
||||
String get use => 'Usar';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usar frecuencia seleccionada';
|
||||
|
||||
@override
|
||||
String get discovery => 'Descubrimiento';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Descubrir repetidores';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Descubrir sensores';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Descubrimiento de repetidores enviado';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Descubrimiento de sensores enviado';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries =>
|
||||
'Descubrimientos pendientes borrados.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Descubrimiento automático';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Habilitar adición automática';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Añadir repetidores automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Añadir servidores de sala automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Añadir sensores automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Añadir usuarios automáticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Sobrescribir más antiguos cuando esté lleno';
|
||||
|
||||
@override
|
||||
String get storage => 'Almacenamiento';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona de peligro';
|
||||
|
||||
@override
|
||||
String get profiles => 'Perfiles';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensores';
|
||||
|
||||
@override
|
||||
String get others => 'Otros';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Módulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Tráfico en vivo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa de repetidores';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Escaneo de espectro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Registros de paquetes BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de datos de trazas';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Tamaño en bytes de la ruta';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notificaciones de mensajes';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertas SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notificaciones de descubrimiento';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notificaciones de actualización';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenciar con la app abierta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Desactivar contactos';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Habilitar pestaña Sensores';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Habilitar perfiles';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotación automática de ruta';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Repetidor más cercano como reserva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Eliminar todo el historial de mensajes';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Tamaño de fuente de mensajes';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotar mapa con rumbo';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostrar info de depuración del mapa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Abrir mapa en pantalla completa';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostrar marcadores SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostrar marcadores SAR en el mapa principal';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Mostrar todos los rastros de contactos';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ocultar repetidores en el mapa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Establecer escala del mapa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Escala de mapa personalizada guardada';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Tasa de bits de voz';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compresor de voz';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Equilibra niveles de habla suaves y altos';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitador de voz';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previene recorte de picos antes de codificar';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Ganancia automática del micrófono';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permite al grabador ajustar el nivel de entrada';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancelación de eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Supresión de ruido';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Recortar silencio en mensajes de voz';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitador';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Ganancia automática';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Ruido';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silencio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Tamaño máximo de imagen';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compresión de imagen';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Escala de grises';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modo ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Actualizaciones GPS privadas rápidas';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Umbral de movimiento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Umbral de movimiento GPS rápido';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervalo de uso activo GPS rápido';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Intervalo de actualización de uso activo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repetir tráfico cercano';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Retransmitir a través de repetidores en la red';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Solo cercanos, sin inundación de repetidores';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-salto';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Crear perfil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renombrar perfil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nuevo perfil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gestionar perfiles';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Habilite los perfiles para comenzar a gestionarlos.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Abrir mensaje';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Ir al mensaje SAR relacionado';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Eliminar marcador SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Seleccione un destino para enviar el marcador SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcador SAR transmitido al canal público';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcador SAR enviado a la sala';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Cargar desde galería';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Reemplazar imagen';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Seleccionar de galería';
|
||||
|
||||
@override
|
||||
String get team => 'Equipo';
|
||||
|
||||
@override
|
||||
String get found => 'Encontrado';
|
||||
|
||||
@override
|
||||
String get staging => 'Área de reunión';
|
||||
|
||||
@override
|
||||
String get object => 'Objeto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silencioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderado';
|
||||
|
||||
@override
|
||||
String get busy => 'Ocupado';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'El escaneo de espectro no encontró frecuencias candidatas';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Buscar mensajes';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Enviar imagen desde galería';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Tomar foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Solo mensaje directo';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Todos los mensajes';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => '¿Enviar al canal público?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Seleccione tipo de marcador y destino';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'No hay destinos disponibles';
|
||||
|
||||
@override
|
||||
String get image => 'Imagen';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensiones';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentos';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferencias';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Descargado por';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Guardar ajustes de descubrimiento';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Guardar información pública';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Guardar ajustes de radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Guardar ruta';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Borrar datos del dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Borrar dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions =>
|
||||
'Acciones destructivas del dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Elija un preajuste o ajuste la configuración de radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Elija el nombre y la ubicación que comparte este dispositivo.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espacio disponible en este dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usado';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renombrar valor';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizar campos';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Vista previa en vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Programación de actualización';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Sin respuesta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Actualizando';
|
||||
|
||||
@override
|
||||
String get unavailable => 'No disponible';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Elija un relay o nodo para observar.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Clave pública';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Ya en contactos';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Conéctese a un dispositivo antes de añadir contactos';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'De contactos';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Solo en línea';
|
||||
|
||||
@override
|
||||
String get inBoth => 'En ambos';
|
||||
|
||||
@override
|
||||
String get source => 'Fuente';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Edición manual de ruta';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Ruta mesh observada';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Todos los mensajes borrados';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de datos de trazas borrada';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Registros de paquetes borrados';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Datos hex copiados al portapapeles';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modo desarrollador activado';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modo desarrollador desactivado';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'El portapapeles está vacío';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contacto importado';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Enlace de contacto copiado al portapapeles';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Error al exportar el contacto';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'No hay registros para exportar';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exportar como CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exportar como texto';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Recibido (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Hora de compilación';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de descarga no disponible';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'No se puede abrir la URL de descarga';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'La comprobación de actualizaciones solo está disponible en Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Está ejecutando la última versión';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Actualización disponible pero URL de descarga no encontrada';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Iniciar Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe no disponible';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: oponente desconocido';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: esperando inicio';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Acepta enlaces compartidos';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Soporta hex sin procesar';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatible con portapapeles';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturado';
|
||||
|
||||
@override
|
||||
String get size => 'Tamaño';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'No hay canales personalizados que borrar.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'No hay contactos del dispositivo que borrar.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -483,6 +483,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Inondation';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Connecté';
|
||||
|
||||
@@ -1905,6 +1923,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Utilisez les journaux de paquets, le balayage du spectre et les diagnostics développeur pour dépanner le réseau maillé.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Dans Capteurs';
|
||||
|
||||
@@ -1936,4 +1961,810 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Échec du réglage de la route : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Délai ACK expiré';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Charge utile';
|
||||
|
||||
@override
|
||||
String get hops => 'Sauts';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Taille du hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Octets du chemin';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Chemin sélectionné';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Émission estimée';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Émetteur au reçu';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copies reçues';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Cause de la relance';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Mode de relance';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Résultat de la relance';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Dernière relance';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Paquets RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Débit';
|
||||
|
||||
@override
|
||||
String get window => 'Fenêtre';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Délai post-émission';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passe-bande';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtre passe-bande voix';
|
||||
|
||||
@override
|
||||
String get frequency => 'Fréquence';
|
||||
|
||||
@override
|
||||
String get australia => 'Australie';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australie (Étroit)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australie: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australie: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nouvelle-Zélande';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nouvelle-Zélande (Étroit)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suisse';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'République tchèque (Étroit)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Longue Portée)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsolète)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Étroit)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Recommandé)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Obsolète)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Étroit)';
|
||||
|
||||
@override
|
||||
String get active => 'Actif';
|
||||
|
||||
@override
|
||||
String get addContact => 'Ajouter un contact';
|
||||
|
||||
@override
|
||||
String get all => 'Tout';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Résolution automatique';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Tout effacer';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Effacer les relais';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Effacer les filtres';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Effacer la route';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Effacer les messages';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Effacer l\'échelle';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Effacer les découvertes';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Effacer la base de traces';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Effacer tous les canaux';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Effacer tous les contacts';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Effacer les canaux';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Effacer les contacts';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Effacer le chemin au max de relances';
|
||||
|
||||
@override
|
||||
String get create => 'Créer';
|
||||
|
||||
@override
|
||||
String get custom => 'Personnalisé';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Par défaut';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Dupliquer';
|
||||
|
||||
@override
|
||||
String get editName => 'Modifier le nom';
|
||||
|
||||
@override
|
||||
String get open => 'Ouvrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Coller';
|
||||
|
||||
@override
|
||||
String get preview => 'Aperçu';
|
||||
|
||||
@override
|
||||
String get remove => 'Supprimer';
|
||||
|
||||
@override
|
||||
String get rename => 'Renommer';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Résoudre tout';
|
||||
|
||||
@override
|
||||
String get send => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Envoyer quand même';
|
||||
|
||||
@override
|
||||
String get share => 'Partager';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Partager le contact';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Utiliser';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Utiliser la fréquence sélectionnée';
|
||||
|
||||
@override
|
||||
String get discovery => 'Découverte';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Découvrir les répéteurs';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Découvrir les capteurs';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Découverte de répéteurs envoyée';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Découverte de capteurs envoyée';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Découvertes en attente effacées.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Découverte automatique';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Activer l\'ajout automatique';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Ajout auto des répéteurs';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Ajout auto des serveurs de salle';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Ajout auto des capteurs';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Ajout auto des utilisateurs';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Écraser les plus anciens si plein';
|
||||
|
||||
@override
|
||||
String get storage => 'Stockage';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zone de danger';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profils';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoris';
|
||||
|
||||
@override
|
||||
String get sensors => 'Capteurs';
|
||||
|
||||
@override
|
||||
String get others => 'Autres';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Module GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Trafic en direct';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Carte des répéteurs';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Scan spectral';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Journaux de paquets BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de données de traces';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Taille en octets du chemin';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notifications de messages';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertes SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notifications de découverte';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notifications de mise à jour';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Muet quand l\'appli est ouverte';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Désactiver les contacts';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Activer l\'onglet Capteurs';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Activer les profils';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotation automatique de route';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Répéteur le plus proche en repli';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Supprimer tout l\'historique';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Taille de police des messages';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Tourner la carte avec le cap';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Afficher les infos de débogage';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Ouvrir la carte en plein écran';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Afficher les marqueurs SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Afficher les marqueurs SAR sur la carte';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Afficher toutes les traces de contacts';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Masquer les répéteurs sur la carte';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Définir l\'échelle de la carte';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved =>
|
||||
'Échelle de carte personnalisée enregistrée';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Débit vocal';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compresseur vocal';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Équilibre les niveaux faibles et forts';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limiteur vocal';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Empêche l\'écrêtage avant l\'encodage';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Gain auto du micro';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Laisse l\'enregistreur ajuster le niveau d\'entrée';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Annulation d\'écho';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Suppression du bruit';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Couper le silence dans les messages vocaux';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compresseur';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiteur';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Gain automatique';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Écho';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Bruit';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silence';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Taille d\'image maximale';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compression d\'image';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Niveaux de gris';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Mode ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Mises à jour GPS privées rapides';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Seuil de mouvement';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Seuil de mouvement GPS rapide';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Intervalle d\'utilisation active GPS rapide';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Intervalle de mise à jour en utilisation active';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Répéter le trafic proche';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Relayer via les répéteurs du réseau';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Proximité seulement, sans inondation';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-saut';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Créer un profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renommer le profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nouveau profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gérer les profils';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Activez les profils pour commencer à les gérer.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Ouvrir le message';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Aller au message SAR associé';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Supprimer le marqueur SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Sélectionnez une destination pour envoyer le marqueur';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marqueur SAR diffusé sur le canal public';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marqueur SAR envoyé à la salle';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Charger depuis la galerie';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Remplacer l\'image';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Sélectionner depuis la galerie';
|
||||
|
||||
@override
|
||||
String get team => 'Équipe';
|
||||
|
||||
@override
|
||||
String get found => 'Trouvé';
|
||||
|
||||
@override
|
||||
String get staging => 'Zone de regroupement';
|
||||
|
||||
@override
|
||||
String get object => 'Objet';
|
||||
|
||||
@override
|
||||
String get quiet => 'Calme';
|
||||
|
||||
@override
|
||||
String get moderate => 'Modéré';
|
||||
|
||||
@override
|
||||
String get busy => 'Occupé';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Le scan spectral n\'a trouvé aucune fréquence candidate';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Rechercher des messages';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Envoyer une image de la galerie';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Prendre une photo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Message direct uniquement';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tous les messages';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Envoyer au canal public ?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Sélectionnez le type de marqueur et la destination';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Aucune destination disponible';
|
||||
|
||||
@override
|
||||
String get image => 'Image';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensions';
|
||||
|
||||
@override
|
||||
String get segments => 'Segments';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferts';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Téléchargé par';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings =>
|
||||
'Enregistrer les paramètres de découverte';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Enregistrer les infos publiques';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Enregistrer les paramètres radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Enregistrer le chemin';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Effacer les données de l\'appareil';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Effacer l\'appareil';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Actions destructives de l\'appareil.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Choisissez un préréglage ou ajustez les paramètres radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Choisissez le nom et l\'emplacement partagés par cet appareil.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espace disponible sur cet appareil.';
|
||||
|
||||
@override
|
||||
String get used => 'Utilisé';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renommer la valeur';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personnaliser les champs';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Aperçu en direct';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Planification d\'actualisation';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Pas de réponse';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Actualisation';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Indisponible';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Choisissez un relais ou nœud à surveiller.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Clé publique';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Déjà dans les contacts';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connectez-vous à un appareil avant d\'ajouter des contacts';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Des contacts';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'En ligne uniquement';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Dans les deux';
|
||||
|
||||
@override
|
||||
String get source => 'Source';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Édition manuelle de route';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Route mesh observée';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tous les messages effacés';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de données de traces effacée';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Journaux de paquets effacés';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Données hex copiées dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Mode développeur activé';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Mode développeur désactivé';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Le presse-papiers est vide';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contact importé';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Lien du contact copié dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Échec de l\'exportation du contact';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Aucun journal à exporter';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exporter en CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exporter en texte';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Reçu (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Heure de build';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de téléchargement non disponible';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl =>
|
||||
'Impossible d\'ouvrir l\'URL de téléchargement';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'La vérification des mises à jour n\'est disponible que sur Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Vous utilisez la dernière version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Mise à jour disponible mais URL de téléchargement introuvable';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Démarrer Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe indisponible';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: adversaire inconnu';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: en attente du début';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accepte les liens partagés';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supporte le hex brut';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatible avec le presse-papiers';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturé';
|
||||
|
||||
@override
|
||||
String get size => 'Taille';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Aucun canal personnalisé à effacer.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Aucun contact de l\'appareil à effacer.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -476,6 +476,24 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Preplavljanje';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Prijavljen';
|
||||
|
||||
@@ -1881,6 +1899,13 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Koristite zapisnike paketa, skeniranje spektra i razvojnu dijagnostiku pri rješavanju problema s mesh mrežom.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'U Senzorima';
|
||||
|
||||
@@ -1912,4 +1937,803 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Postavljanje rute nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK istek vremena';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opkod';
|
||||
|
||||
@override
|
||||
String get payload => 'Sadržaj';
|
||||
|
||||
@override
|
||||
String get hops => 'Skokovi';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Veličina hash-a';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajtovi putanje';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Odabrana putanja';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Procijenjeno slanje';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od pošiljatelja do primitka';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Primljene kopije';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Razlog ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Način ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Rezultat ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Zadnji pokušaj';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketi';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Brzina';
|
||||
|
||||
@override
|
||||
String get window => 'Prozor';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Kašnjenje nakon slanja';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pojasni filtar';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Pojasni filtar glasa';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekvencija';
|
||||
|
||||
@override
|
||||
String get australia => 'Australija';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australija (usko)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australija: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australija: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Novi Zeland';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Novi Zeland (usko)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Švicarska';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Češka (usko)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (veliki domet)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (zastarjelo)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (usko)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'SAD/Kanada (preporučeno)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vijetnam (zastarjelo)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vijetnam (usko)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktivno';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj kontakt';
|
||||
|
||||
@override
|
||||
String get all => 'Sve';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatski razriješi';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Očisti sve';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Očisti posrednike';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Očisti filtre';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Očisti putanju';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Očisti poruke';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Očisti mjerilo';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Očisti otkrića';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Očisti bazu mrežnih tragova';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Očisti sve kanale';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Očisti sve kontakte';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Očisti kanale';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Očisti kontakte';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Očisti putanju kod maks. pokušaja';
|
||||
|
||||
@override
|
||||
String get create => 'Stvori';
|
||||
|
||||
@override
|
||||
String get custom => 'Prilagođeno';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Zadano';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Dupliciraj';
|
||||
|
||||
@override
|
||||
String get editName => 'Uredi ime';
|
||||
|
||||
@override
|
||||
String get open => 'Otvori';
|
||||
|
||||
@override
|
||||
String get paste => 'Zalijepi';
|
||||
|
||||
@override
|
||||
String get preview => 'Pregled';
|
||||
|
||||
@override
|
||||
String get remove => 'Ukloni';
|
||||
|
||||
@override
|
||||
String get rename => 'Preimenuj';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Razriješi sve';
|
||||
|
||||
@override
|
||||
String get send => 'Pošalji';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Svejedno pošalji';
|
||||
|
||||
@override
|
||||
String get share => 'Dijeli';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Dijeli kontakt';
|
||||
|
||||
@override
|
||||
String get trace => 'Praćenje';
|
||||
|
||||
@override
|
||||
String get use => 'Koristi';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Koristi odabranu frekvenciju';
|
||||
|
||||
@override
|
||||
String get discovery => 'Otkrivanje';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Otkrij posrednike';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Otkrij senzore';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Otkrivanje posrednika poslano';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Otkrivanje senzora poslano';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Očišćena čekajuća otkrića.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatsko otkrivanje';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Omogući automatsko dodavanje';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Automatski dodaj posrednike';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Automatski dodaj poslužitelje soba';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Automatski dodaj senzore';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Automatski dodaj korisnike';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Prepiši najstarije kad je puno';
|
||||
|
||||
@override
|
||||
String get storage => 'Pohrana';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Opasna zona';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriti';
|
||||
|
||||
@override
|
||||
String get sensors => 'Senzori';
|
||||
|
||||
@override
|
||||
String get others => 'Ostali';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Promet uživo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Karta posrednika';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektralno skeniranje';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE zapisi paketa';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza mrežnih tragova';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Veličina putanje u bajtima';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Obavijesti o porukama';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR upozorenja';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Obavijesti o otkrivanju';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Obavijesti o ažuriranju';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Utišaj dok je aplikacija otvorena';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Onemogući kontakte';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Omogući karticu Senzori';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Omogući profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatsko rotiranje putanje';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbliži posrednik kao rezerva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Izbriši svu pohranjenu povijest poruka';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Veličina fonta poruka';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotiraj kartu prema smjeru';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Prikaži debug info karte';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Otvori kartu na cijelom zaslonu';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Prikaži SAR oznake';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Prikaži SAR oznake na glavnoj karti';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Prikaži sve tragove kontakata';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Sakrij posrednike na karti';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Postavi mjerilo karte';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Prilagođeno mjerilo karte spremljeno';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitna brzina glasa';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor glasa';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Izravnava tihe i glasne razine govora';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Ograničivač glasa';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Sprječava rezanje vrhova prije kodiranja';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Automatsko pojačanje mikrofona';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Dopušta snimaču prilagodbu razine ulaza';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Poništavanje odjeka';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Potiskivanje šuma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Izreži tišinu u glasovnim porukama';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Ograničivač';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatsko pojačanje';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Odjek';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Šum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Tišina';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksimalna veličina slike';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Kompresija slike';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Sivine';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra način';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Brza privatna GPS ažuriranja';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Prag kretanja';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Prag kretanja za brzi GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Interval aktivnog korištenja brzog GPS-a';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interval ažuriranja pri aktivnom korištenju';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ponovi obližnji promet';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Proslijedi kroz posrednike kroz mrežu';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Samo obližnji, bez preplavljivanja posrednika';
|
||||
|
||||
@override
|
||||
String get multihop => 'Višeskočno';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Stvori profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Preimenuj profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Novi profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Upravljaj profilima';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Omogućite profile da biste ih počeli upravljati.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Otvori poruku';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Skoči na povezanu SAR poruku';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Ukloni SAR oznaku';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Odaberite odredište za slanje SAR oznake';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR oznaka emitirana na javni kanal';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR oznaka poslana u sobu';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Učitaj iz galerije';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamijeni sliku';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Odaberi iz galerije';
|
||||
|
||||
@override
|
||||
String get team => 'Tim';
|
||||
|
||||
@override
|
||||
String get found => 'Pronađeno';
|
||||
|
||||
@override
|
||||
String get staging => 'Sabirno mjesto';
|
||||
|
||||
@override
|
||||
String get object => 'Objekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Tiho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Umjereno';
|
||||
|
||||
@override
|
||||
String get busy => 'Zauzeto';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektralno skeniranje nije pronašlo prikladne frekvencije';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Pretraži poruke';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Pošalji sliku iz galerije';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Snimi fotografiju';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Samo izravno';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Sve poruke';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Pošalji na javni kanal?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Odaberite vrstu oznake i odredište';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nema dostupnih odredišta';
|
||||
|
||||
@override
|
||||
String get image => 'Slika';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimenzije';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Prijenosi';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Preuzeo';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Spremi postavke otkrivanja';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Spremi javne podatke';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Spremi radijske postavke';
|
||||
|
||||
@override
|
||||
String get savePath => 'Spremi putanju';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Obriši podatke uređaja';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Obriši uređaj';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktivne radnje na uređaju.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Odaberite predefiniranu postavku ili fino podesite radijske postavke.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Odaberite ime i lokaciju koju ovaj uređaj dijeli.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Raspoloživi prostor na ovom uređaju.';
|
||||
|
||||
@override
|
||||
String get used => 'Korišteno';
|
||||
|
||||
@override
|
||||
String get total => 'Ukupno';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Preimenuj vrijednost';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Prilagodi polja';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Pregled uživo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Raspored osvježavanja';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Nema odgovora';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Osvježavam';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nedostupno';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Odaberite posrednika ili čvor za praćenje.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Javni ključ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Već u kontaktima';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Povežite se s uređajem prije dodavanja kontakata';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Iz kontakata';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Samo povezani';
|
||||
|
||||
@override
|
||||
String get inBoth => 'U oba';
|
||||
|
||||
@override
|
||||
String get source => 'Izvor';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ručno uređivanje putanje';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Opažena mrežna putanja';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Sve poruke očišćene';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza mrežnih tragova očišćena';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Zapisi paketa očišćeni';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex podaci kopirani u međuspremnik';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Razvojni način omogućen';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Razvojni način onemogućen';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Međuspremnik je prazan';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt uvezen';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Poveznica na kontakt kopirana u međuspremnik';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Izvoz kontakta nije uspio';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Nema zapisa za izvoz';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Izvezi kao CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Izvezi kao tekst';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Primljeno (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Vrijeme gradnje';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL za preuzimanje nije dostupan';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Ne mogu otvoriti URL za preuzimanje';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Provjera ažuriranja dostupna je samo na Androidu';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Koristite najnoviju verziju';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Ažuriranje dostupno, ali URL za preuzimanje nije pronađen';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Pokreni Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe nije dostupan';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: protivnik nepoznat';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: čekanje na početak';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Prihvaća dijeljene veze';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Podržava sirovi hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Pogodno za međuspremnik';
|
||||
|
||||
@override
|
||||
String get captured => 'Zabilježeno';
|
||||
|
||||
@override
|
||||
String get size => 'Veličina';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Nema prilagođenih kanala za brisanje.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Nema kontakata uređaja za brisanje.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -480,6 +480,24 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flood';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Connesso';
|
||||
|
||||
@@ -1896,6 +1914,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Usa log dei pacchetti, scansione dello spettro e diagnostica sviluppatore per risolvere problemi della mesh.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Nei Sensori';
|
||||
|
||||
@@ -1927,4 +1952,805 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Impossibile impostare il percorso: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Timeout ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Payload';
|
||||
|
||||
@override
|
||||
String get hops => 'Hop';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Dimensione hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Byte percorso';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Percorso selezionato';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Trasmissione stimata';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Mittente a ricevuta';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copie ricevute';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa del ritentativo';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modalità ritentativo';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Risultato ritentativo';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Ultimo ritentativo';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pacchetti RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Velocità';
|
||||
|
||||
@override
|
||||
String get window => 'Finestra';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Ritardo post-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passa-banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro passa-banda voce';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequenza';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Stretto)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nuova Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nuova Zelanda (Stretto)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Svizzera';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portogallo 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portogallo 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Repubblica Ceca (Stretto)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Lungo Raggio)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/UK (Deprecato)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/UK (Stretto)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Consigliato)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Deprecato)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Stretto)';
|
||||
|
||||
@override
|
||||
String get active => 'Attivo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Aggiungi contatto';
|
||||
|
||||
@override
|
||||
String get all => 'Tutto';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Risolvi automaticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Cancella tutto';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Cancella relay';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Cancella filtri';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Cancella percorso';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Cancella messaggi';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Cancella scala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Cancella scoperte';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Cancella database tracce';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Cancella tutti i canali';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Cancella tutti i contatti';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Cancella canali';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Cancella contatti';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Cancella percorso al max ritentativo';
|
||||
|
||||
@override
|
||||
String get create => 'Crea';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizzato';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Predefinito';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplica';
|
||||
|
||||
@override
|
||||
String get editName => 'Modifica nome';
|
||||
|
||||
@override
|
||||
String get open => 'Apri';
|
||||
|
||||
@override
|
||||
String get paste => 'Incolla';
|
||||
|
||||
@override
|
||||
String get preview => 'Anteprima';
|
||||
|
||||
@override
|
||||
String get remove => 'Rimuovi';
|
||||
|
||||
@override
|
||||
String get rename => 'Rinomina';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Risolvi tutto';
|
||||
|
||||
@override
|
||||
String get send => 'Invia';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Invia comunque';
|
||||
|
||||
@override
|
||||
String get share => 'Condividi';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Condividi contatto';
|
||||
|
||||
@override
|
||||
String get trace => 'Traccia';
|
||||
|
||||
@override
|
||||
String get use => 'Usa';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usa frequenza selezionata';
|
||||
|
||||
@override
|
||||
String get discovery => 'Scoperta';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Scopri ripetitori';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Scopri sensori';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Scoperta ripetitori inviata';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Scoperta sensori inviata';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Scoperte in attesa cancellate.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Scoperta automatica';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Abilita aggiunta automatica';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Aggiungi ripetitori automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Aggiungi server stanze automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Aggiungi sensori automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Aggiungi utenti automaticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Sovrascrivi i più vecchi quando pieno';
|
||||
|
||||
@override
|
||||
String get storage => 'Archiviazione';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona pericolosa';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Preferiti';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensori';
|
||||
|
||||
@override
|
||||
String get others => 'Altri';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Modulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Traffico in tempo reale';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mappa ripetitori';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Scansione spettro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Log pacchetti BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Database tracce';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Dimensione percorso in byte';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notifiche messaggi';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Avvisi SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notifiche scoperte';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notifiche aggiornamenti';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenzia con l\'app aperta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Disabilita contatti';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Abilita scheda Sensori';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Abilita profili';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotazione automatica percorso';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Ripetitore più vicino come fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Elimina tutta la cronologia messaggi';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Dimensione carattere messaggi';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Ruota mappa con direzione';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostra info debug mappa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Apri mappa a schermo intero';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostra marcatori SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostra marcatori SAR sulla mappa principale';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Mostra tutti i percorsi contatti';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Nascondi ripetitori sulla mappa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Imposta scala mappa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Scala mappa personalizzata salvata';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitrate voce';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compressore voce';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Bilancia i livelli di voce bassi e alti';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitatore voce';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previene il clipping prima della codifica';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Guadagno automatico microfono';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permette al registratore di regolare il livello';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancellazione eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Soppressione rumore';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Taglia silenzio nei messaggi vocali';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressore';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitatore';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Guadagno automatico';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Rumore';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silenzio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Dimensione massima immagine';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compressione immagine';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Scala di grigi';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modalità ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Aggiornamenti GPS privati veloci';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Soglia di movimento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Soglia movimento GPS veloce';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervallo uso attivo GPS veloce';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Intervallo aggiornamento uso attivo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ripeti traffico vicino';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Inoltra tramite ripetitori nella rete';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Solo vicini, senza flood dei ripetitori';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Crea profilo';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Rinomina profilo';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nuovo profilo';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gestisci profili';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Abilita i profili per iniziare a gestirli.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Apri messaggio';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Vai al messaggio SAR correlato';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Rimuovi marcatore SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Seleziona una destinazione per inviare il marcatore SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcatore SAR trasmesso al canale pubblico';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcatore SAR inviato alla stanza';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Carica dalla galleria';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Sostituisci immagine';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Seleziona dalla galleria';
|
||||
|
||||
@override
|
||||
String get team => 'Squadra';
|
||||
|
||||
@override
|
||||
String get found => 'Trovato';
|
||||
|
||||
@override
|
||||
String get staging => 'Area di raccolta';
|
||||
|
||||
@override
|
||||
String get object => 'Oggetto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silenzioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderato';
|
||||
|
||||
@override
|
||||
String get busy => 'Occupato';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'La scansione spettro non ha trovato frequenze candidate';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Cerca messaggi';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Invia immagine dalla galleria';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Scatta foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Solo messaggio diretto';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tutti i messaggi';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Inviare al canale pubblico?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Seleziona tipo marcatore e destinazione';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nessuna destinazione disponibile';
|
||||
|
||||
@override
|
||||
String get image => 'Immagine';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensioni';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Trasferimenti';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Scaricato da';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Salva impostazioni scoperta';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Salva info pubbliche';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Salva impostazioni radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Salva percorso';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Cancella dati dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Cancella dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Azioni distruttive del dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Scegli un preset o personalizza le impostazioni radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Scegli il nome e la posizione condivisi da questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Spazio disponibile su questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usato';
|
||||
|
||||
@override
|
||||
String get total => 'Totale';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Rinomina valore';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizza campi';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Anteprima dal vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Programma aggiornamento';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Nessuna risposta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Aggiornamento';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Non disponibile';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Scegli un relay o nodo da monitorare.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Chiave pubblica';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Già nei contatti';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connettiti a un dispositivo prima di aggiungere contatti';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Dai contatti';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Solo online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In entrambi';
|
||||
|
||||
@override
|
||||
String get source => 'Origine';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Modifica manuale percorso';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Percorso mesh osservato';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tutti i messaggi cancellati';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Database tracce cancellata';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Log pacchetti cancellati';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Dati hex copiati negli appunti';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modalità sviluppatore attivata';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modalità sviluppatore disattivata';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Gli appunti sono vuoti';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contatto importato';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link contatto copiato negli appunti';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Esportazione contatto fallita';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Nessun log da esportare';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Esporta come CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Esporta come testo';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Ricevuto (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Ora build';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL di download non disponibile';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Impossibile aprire URL di download';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Il controllo aggiornamenti è disponibile solo su Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Stai utilizzando l\'ultima versione';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Aggiornamento disponibile ma URL di download non trovato';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Avvia Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe non disponibile';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: avversario sconosciuto';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: in attesa dell\'inizio';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accetta link condivisi';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supporta hex grezzo';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatibile con appunti';
|
||||
|
||||
@override
|
||||
String get captured => 'Catturato';
|
||||
|
||||
@override
|
||||
String get size => 'Dimensione';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Nessun canale personalizzato da cancellare.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Nessun contatto dispositivo da cancellare.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -481,6 +481,24 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Rozgłoszeniowo';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Zalogowano';
|
||||
|
||||
@@ -1893,6 +1911,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Używaj logów pakietów, skanowania widma i diagnostyki deweloperskiej podczas rozwiązywania problemów z mesh.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'W Czujnikach';
|
||||
|
||||
@@ -1924,4 +1949,805 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Nie udało się ustawić trasy: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Limit czasu ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Ładunek';
|
||||
|
||||
@override
|
||||
String get hops => 'Skoki';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Rozmiar hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajty ścieżki';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Wybrana ścieżka';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Szacowane nadawanie';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od nadawcy do odbioru';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Otrzymane kopie';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Przyczyna ponowienia';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Tryb ponowienia';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Wynik ponowienia';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Ostatnia próba';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pakiety RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Szybkość';
|
||||
|
||||
@override
|
||||
String get window => 'Okno';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Opóźnienie po nadawaniu';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pasmowy';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtr pasmowy głosu';
|
||||
|
||||
@override
|
||||
String get frequency => 'Częstotliwość';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (wąski)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nowa Zelandia';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nowa Zelandia (wąski)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Szwajcaria';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugalia 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugalia 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Czechy (wąski)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (daleki zasięg)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (przestarzały)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (wąski)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Kanada (zalecany)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Wietnam (przestarzały)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Wietnam (wąski)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktywny';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj kontakt';
|
||||
|
||||
@override
|
||||
String get all => 'Wszystko';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatyczne rozwiązanie';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Wyczyść wszystko';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Wyczyść przekaźniki';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Wyczyść filtry';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Wyczyść trasę';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Wyczyść wiadomości';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Wyczyść skalę';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Wyczyść odkrycia';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Wyczyść bazę śladów';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Wyczyść wszystkie kanały';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Wyczyść wszystkie kontakty';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Wyczyść kanały';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Wyczyść kontakty';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Wyczyść ścieżkę przy maks. próbie';
|
||||
|
||||
@override
|
||||
String get create => 'Utwórz';
|
||||
|
||||
@override
|
||||
String get custom => 'Niestandardowy';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Domyślny';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplikuj';
|
||||
|
||||
@override
|
||||
String get editName => 'Edytuj nazwę';
|
||||
|
||||
@override
|
||||
String get open => 'Otwórz';
|
||||
|
||||
@override
|
||||
String get paste => 'Wklej';
|
||||
|
||||
@override
|
||||
String get preview => 'Podgląd';
|
||||
|
||||
@override
|
||||
String get remove => 'Usuń';
|
||||
|
||||
@override
|
||||
String get rename => 'Zmień nazwę';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Rozwiąż wszystko';
|
||||
|
||||
@override
|
||||
String get send => 'Wyślij';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Wyślij mimo to';
|
||||
|
||||
@override
|
||||
String get share => 'Udostępnij';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Udostępnij kontakt';
|
||||
|
||||
@override
|
||||
String get trace => 'Śledzenie';
|
||||
|
||||
@override
|
||||
String get use => 'Użyj';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Użyj wybranej częstotliwości';
|
||||
|
||||
@override
|
||||
String get discovery => 'Wykrywanie';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Wykryj przekaźniki';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Wykryj czujniki';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Wykrywanie przekaźników wysłane';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Wykrywanie czujników wysłane';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Wyczyszczono oczekujące odkrycia.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatyczne wykrywanie';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Włącz automatyczne dodawanie';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Automatycznie dodaj przekaźniki';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Automatycznie dodaj serwery pokoi';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Automatycznie dodaj czujniki';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Automatycznie dodaj użytkowników';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Nadpisz najstarsze gdy pełne';
|
||||
|
||||
@override
|
||||
String get storage => 'Pamięć';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Strefa zagrożenia';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profile';
|
||||
|
||||
@override
|
||||
String get favourites => 'Ulubione';
|
||||
|
||||
@override
|
||||
String get sensors => 'Czujniki';
|
||||
|
||||
@override
|
||||
String get others => 'Inne';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Moduł GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Ruch na żywo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa przekaźników';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Skanowanie spektrum';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Logi pakietów BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza śladów';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Rozmiar ścieżki w bajtach';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Powiadomienia o wiadomościach';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alerty SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Powiadomienia o wykrywaniu';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Powiadomienia o aktualizacjach';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Wycisz gdy aplikacja jest otwarta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Wyłącz kontakty';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Włącz kartę Czujniki';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Włącz profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatyczna rotacja trasy';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbliższy przekaźnik awaryjny';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Usuń całą historię wiadomości';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Rozmiar czcionki wiadomości';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Obracaj mapę z kierunkiem';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Pokaż info debugowania mapy';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Otwórz mapę na pełnym ekranie';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Pokaż znaczniki SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Wyświetlaj znaczniki SAR na głównej mapie';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Pokaż wszystkie ślady kontaktów';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ukryj przekaźniki na mapie';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Ustaw skalę mapy';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Niestandardowa skala mapy zapisana';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitrate głosu';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor głosu';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Wyrównuje ciche i głośne poziomy mowy';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limiter głosu';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Zapobiega przycinaniu szczytów przed kodowaniem';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Automatyczne wzmocnienie mikrofonu';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Pozwala nagrywarkowi dostosować poziom wejścia';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Usuwanie echa';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Tłumienie szumu';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Przytnij ciszę w wiadomościach głosowych';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatyczne wzmocnienie';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Szum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Cisza';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksymalny rozmiar obrazu';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Kompresja obrazu';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Skala szarości';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Tryb ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Szybkie prywatne aktualizacje GPS';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Próg ruchu';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Próg ruchu szybkiego GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Interwał aktywnego użycia szybkiego GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interwał aktualizacji przy aktywnym użyciu';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Powtarzaj pobliski ruch';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Przekazuj przez przekaźniki w sieci';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Tylko w pobliżu, bez zalewania przekaźników';
|
||||
|
||||
@override
|
||||
String get multihop => 'Wieloskokowy';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Utwórz profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Zmień nazwę profilu';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nowy profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Zarządzaj profilami';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Włącz profile, aby zacząć nimi zarządzać.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Otwórz wiadomość';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Przejdź do powiązanej wiadomości SAR';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Usuń znacznik SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Wybierz cel wysłania znacznika SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Znacznik SAR wysłany na kanał publiczny';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Znacznik SAR wysłany do pokoju';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Załaduj z galerii';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamień obraz';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Wybierz z galerii';
|
||||
|
||||
@override
|
||||
String get team => 'Zespół';
|
||||
|
||||
@override
|
||||
String get found => 'Znaleziono';
|
||||
|
||||
@override
|
||||
String get staging => 'Punkt zbiórki';
|
||||
|
||||
@override
|
||||
String get object => 'Obiekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Cicho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Umiarkowane';
|
||||
|
||||
@override
|
||||
String get busy => 'Zajęty';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Skanowanie spektrum nie znalazło częstotliwości kandydatów';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Szukaj wiadomości';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Wyślij obraz z galerii';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Zrób zdjęcie';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Tylko wiadomość bezpośrednia';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Wszystkie wiadomości';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Wysłać na kanał publiczny?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => 'Wybierz typ znacznika i cel';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Brak dostępnych celów';
|
||||
|
||||
@override
|
||||
String get image => 'Obraz';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Wymiary';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenty';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transfery';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Pobrane przez';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Zapisz ustawienia wykrywania';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Zapisz informacje publiczne';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Zapisz ustawienia radia';
|
||||
|
||||
@override
|
||||
String get savePath => 'Zapisz ścieżkę';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Wyczyść dane urządzenia';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Wyczyść urządzenie';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destrukcyjne akcje na urządzeniu.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Wybierz ustawienie wstępne lub dostosuj ustawienia radia.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Wybierz nazwę i lokalizację udostępnianą przez to urządzenie.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Dostępne miejsce na tym urządzeniu.';
|
||||
|
||||
@override
|
||||
String get used => 'Użyto';
|
||||
|
||||
@override
|
||||
String get total => 'Razem';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Zmień nazwę wartości';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Dostosuj pola';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Podgląd na żywo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Harmonogram odświeżania';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Brak odpowiedzi';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Odświeżanie';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Niedostępny';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Wybierz przekaźnik lub węzeł do obserwacji.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Klucz publiczny';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Już w kontaktach';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Połącz się z urządzeniem przed dodaniem kontaktów';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Z kontaktów';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Tylko online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'W obu';
|
||||
|
||||
@override
|
||||
String get source => 'Źródło';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ręczna edycja trasy';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Obserwowana trasa sieci';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Wszystkie wiadomości wyczyszczone';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza śladów wyczyszczona';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Logi pakietów wyczyszczone';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Dane hex skopiowane do schowka';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Tryb programisty włączony';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Tryb programisty wyłączony';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Schowek jest pusty';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt zaimportowany';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link kontaktu skopiowany do schowka';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Nie udało się wyeksportować kontaktu';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Brak logów do eksportu';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Eksportuj jako CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Eksportuj jako tekst';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Odebrano (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Czas budowania';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL pobierania niedostępny';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Nie można otworzyć URL pobierania';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Sprawdzanie aktualizacji dostępne tylko na Androidzie';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Używasz najnowszej wersji';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Aktualizacja dostępna, ale URL pobierania nie znaleziony';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Rozpocznij Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe niedostępny';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: przeciwnik nieznany';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: oczekiwanie na start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Akceptuje udostępnione linki';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Obsługuje surowy hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Przyjazne dla schowka';
|
||||
|
||||
@override
|
||||
String get captured => 'Przechwycone';
|
||||
|
||||
@override
|
||||
String get size => 'Rozmiar';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Brak niestandardowych kanałów do wyczyszczenia.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Brak kontaktów urządzenia do wyczyszczenia.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -482,6 +482,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Inundação';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Conectado';
|
||||
|
||||
@@ -1900,6 +1918,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Use logs de pacotes, varredura de espectro e diagnóstico de desenvolvedor ao solucionar problemas da malha.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Nos Sensores';
|
||||
|
||||
@@ -1931,4 +1956,809 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Falha ao definir a rota: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Tempo limite ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Carga útil';
|
||||
|
||||
@override
|
||||
String get hops => 'Saltos';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Tamanho do hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes do caminho';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Caminho selecionado';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Transmissão estimada';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Do remetente ao recibo';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Cópias recebidas';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa da nova tentativa';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modo de nova tentativa';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Resultado da nova tentativa';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Última tentativa';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pacotes RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Taxa';
|
||||
|
||||
@override
|
||||
String get window => 'Janela';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Atraso pós-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passa-banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro passa-banda de voz';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequência';
|
||||
|
||||
@override
|
||||
String get australia => 'Austrália';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Austrália (Estreito)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Austrália: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Austrália: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nova Zelândia';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nova Zelândia (Estreito)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suíça';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'República Checa (Estreito)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Longo Alcance)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Estreito)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'EUA/Canadá (Recomendado)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietname (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietname (Estreito)';
|
||||
|
||||
@override
|
||||
String get active => 'Ativo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Adicionar contacto';
|
||||
|
||||
@override
|
||||
String get all => 'Tudo';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Resolver automaticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Limpar tudo';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Limpar relays';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Limpar filtros';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Limpar rota';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Limpar mensagens';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Limpar escala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Limpar descobertas';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Limpar base de dados de rastreio';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Limpar todos os canais';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Limpar todos os contactos';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Limpar canais';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Limpar contactos';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Limpar caminho no máx. de tentativas';
|
||||
|
||||
@override
|
||||
String get create => 'Criar';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizado';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Padrão';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicar';
|
||||
|
||||
@override
|
||||
String get editName => 'Editar nome';
|
||||
|
||||
@override
|
||||
String get open => 'Abrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Colar';
|
||||
|
||||
@override
|
||||
String get preview => 'Pré-visualização';
|
||||
|
||||
@override
|
||||
String get remove => 'Remover';
|
||||
|
||||
@override
|
||||
String get rename => 'Renomear';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolver tudo';
|
||||
|
||||
@override
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Enviar mesmo assim';
|
||||
|
||||
@override
|
||||
String get share => 'Partilhar';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Partilhar contacto';
|
||||
|
||||
@override
|
||||
String get trace => 'Rastreio';
|
||||
|
||||
@override
|
||||
String get use => 'Usar';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usar frequência selecionada';
|
||||
|
||||
@override
|
||||
String get discovery => 'Descoberta';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Descobrir repetidores';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Descobrir sensores';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Descoberta de repetidores enviada';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Descoberta de sensores enviada';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Descobertas pendentes limpas.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Descoberta automática';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Ativar adição automática';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Adicionar repetidores automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers =>
|
||||
'Adicionar servidores de sala automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Adicionar sensores automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Adicionar utilizadores automaticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Sobrescrever mais antigos quando cheio';
|
||||
|
||||
@override
|
||||
String get storage => 'Armazenamento';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona de perigo';
|
||||
|
||||
@override
|
||||
String get profiles => 'Perfis';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensores';
|
||||
|
||||
@override
|
||||
String get others => 'Outros';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Módulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Tráfego ao vivo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa de repetidores';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Varredura de espectro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Registos de pacotes BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de dados de rastreio';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Tamanho do caminho em bytes';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notificações de mensagens';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertas SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notificações de descoberta';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notificações de atualização';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenciar com a app aberta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Desativar contactos';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Ativar separador Sensores';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Ativar perfis';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotação automática de rota';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Repetidor mais próximo como reserva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Apagar todo o histórico de mensagens';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Tamanho da fonte de mensagens';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rodar mapa com direção';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostrar info de depuração do mapa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Abrir mapa em ecrã inteiro';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostrar marcadores SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostrar marcadores SAR no mapa principal';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Mostrar todos os rastos de contactos';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ocultar repetidores no mapa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Definir escala do mapa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Escala de mapa personalizada guardada';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Taxa de bits de voz';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compressor de voz';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Equilibra níveis de fala suaves e altos';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitador de voz';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previne corte de picos antes da codificação';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Ganho automático do microfone';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permite ao gravador ajustar o nível de entrada';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancelamento de eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Supressão de ruído';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Cortar silêncio em mensagens de voz';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitador';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Ganho automático';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Ruído';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silêncio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Tamanho máximo de imagem';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compressão de imagem';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Escala de cinzentos';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modo ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Atualizações GPS privadas rápidas';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Limiar de movimento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Limiar de movimento GPS rápido';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervalo de uso ativo GPS rápido';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Intervalo de atualização de uso ativo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repetir tráfego próximo';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Retransmitir através de repetidores na rede';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Apenas próximos, sem flood de repetidores';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-salto';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Criar perfil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renomear perfil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Novo perfil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gerir perfis';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Ative os perfis para começar a geri-los.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Abrir mensagem';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Ir para a mensagem SAR relacionada';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Remover marcador SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Selecione um destino para enviar o marcador SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcador SAR transmitido no canal público';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcador SAR enviado para a sala';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Carregar da galeria';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Substituir imagem';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Selecionar da galeria';
|
||||
|
||||
@override
|
||||
String get team => 'Equipa';
|
||||
|
||||
@override
|
||||
String get found => 'Encontrado';
|
||||
|
||||
@override
|
||||
String get staging => 'Área de reunião';
|
||||
|
||||
@override
|
||||
String get object => 'Objeto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silencioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderado';
|
||||
|
||||
@override
|
||||
String get busy => 'Ocupado';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'A varredura de espectro não encontrou frequências candidatas';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Pesquisar mensagens';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Enviar imagem da galeria';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Tirar foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Apenas mensagem direta';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Todas as mensagens';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Enviar para o canal público?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Selecione tipo de marcador e destino';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nenhum destino disponível';
|
||||
|
||||
@override
|
||||
String get image => 'Imagem';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensões';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentos';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferências';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Transferido por';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Guardar definições de descoberta';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Guardar informação pública';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Guardar definições de rádio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Guardar caminho';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Apagar dados do dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Apagar dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Ações destrutivas do dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Escolha uma predefinição ou ajuste as definições de rádio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Escolha o nome e a localização que este dispositivo partilha.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espaço disponível neste dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usado';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renomear valor';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizar campos';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Pré-visualização ao vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Agenda de atualização';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Sem resposta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'A atualizar';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Indisponível';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Escolha um relay ou nó para observar.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Chave pública';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Já nos contactos';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Conecte-se a um dispositivo antes de adicionar contactos';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Dos contactos';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Apenas online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Em ambos';
|
||||
|
||||
@override
|
||||
String get source => 'Fonte';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Edição manual de rota';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Rota mesh observada';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Todas as mensagens limpas';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de dados de rastreio limpa';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Registos de pacotes limpos';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Dados hex copiados para a área de transferência';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modo de programador ativado';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modo de programador desativado';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'A área de transferência está vazia';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contacto importado';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link do contacto copiado para a área de transferência';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Falha ao exportar contacto';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Sem registos para exportar';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exportar como CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exportar como texto';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Recebido (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Hora de compilação';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de download não disponível';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Não é possível abrir o URL de download';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'A verificação de atualizações só está disponível no Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Está a usar a versão mais recente';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Atualização disponível mas URL de download não encontrado';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Iniciar Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe indisponível';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: oponente desconhecido';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: a aguardar início';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Aceita links partilhados';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Suporta hex bruto';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatível com a área de transferência';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturado';
|
||||
|
||||
@override
|
||||
String get size => 'Tamanho';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Sem canais personalizados para limpar.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Sem contactos do dispositivo para limpar.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -480,6 +480,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Широковещательно';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Вход выполнен';
|
||||
|
||||
@@ -1890,6 +1908,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Используйте журналы пакетов, сканирование спектра и диагностику разработчика для устранения проблем mesh-сети.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'В датчиках';
|
||||
|
||||
@@ -1921,4 +1946,804 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Не удалось задать маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Таймаут ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Опкод';
|
||||
|
||||
@override
|
||||
String get payload => 'Полезная нагрузка';
|
||||
|
||||
@override
|
||||
String get hops => 'Хопы';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Размер хеша';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Байты пути';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Выбранный путь';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Ожидаемая передача';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'От отправителя до получения';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Полученные копии';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Причина повтора';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Режим повтора';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Результат повтора';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Последний повтор';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-пакеты';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Скорость';
|
||||
|
||||
@override
|
||||
String get window => 'Окно';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Задержка после передачи';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Полосовой';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Полосовой фильтр голоса';
|
||||
|
||||
@override
|
||||
String get frequency => 'Частота';
|
||||
|
||||
@override
|
||||
String get australia => 'Австралия';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Австралия (Узкий)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Австралия: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Австралия: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Новая Зеландия';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Новая Зеландия (Узкий)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Швейцария';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Португалия 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Португалия 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Чехия (Узкий)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ЕС 433МГц (Дальний)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ЕС/Великобритания (Устарело)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ЕС/Великобритания (Узкий)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'США/Канада (Рекомендуется)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Вьетнам (Устарело)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Вьетнам (Узкий)';
|
||||
|
||||
@override
|
||||
String get active => 'Активно';
|
||||
|
||||
@override
|
||||
String get addContact => 'Добавить контакт';
|
||||
|
||||
@override
|
||||
String get all => 'Все';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Автоматическое разрешение';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Очистить все';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Очистить реле';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Очистить фильтры';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Очистить маршрут';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Очистить сообщения';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Очистить масштаб';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Очистить обнаружения';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Очистить базу трассировок';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Очистить все каналы';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Очистить все контакты';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Очистить каналы';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Очистить контакты';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Очистить путь при макс. повторе';
|
||||
|
||||
@override
|
||||
String get create => 'Создать';
|
||||
|
||||
@override
|
||||
String get custom => 'Пользовательский';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'По умолчанию';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Дублировать';
|
||||
|
||||
@override
|
||||
String get editName => 'Редактировать имя';
|
||||
|
||||
@override
|
||||
String get open => 'Открыть';
|
||||
|
||||
@override
|
||||
String get paste => 'Вставить';
|
||||
|
||||
@override
|
||||
String get preview => 'Предпросмотр';
|
||||
|
||||
@override
|
||||
String get remove => 'Удалить';
|
||||
|
||||
@override
|
||||
String get rename => 'Переименовать';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Разрешить все';
|
||||
|
||||
@override
|
||||
String get send => 'Отправить';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Отправить все равно';
|
||||
|
||||
@override
|
||||
String get share => 'Поделиться';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Поделиться контактом';
|
||||
|
||||
@override
|
||||
String get trace => 'Трассировка';
|
||||
|
||||
@override
|
||||
String get use => 'Использовать';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Использовать выбранную частоту';
|
||||
|
||||
@override
|
||||
String get discovery => 'Обнаружение';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Обнаружить повторители';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Обнаружить датчики';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Обнаружение повторителей отправлено';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Обнаружение датчиков отправлено';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Ожидающие обнаружения очищены.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Автоматическое обнаружение';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Включить автоматическое добавление';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Автодобавление повторителей';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Автодобавление серверов комнат';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Автодобавление датчиков';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Автодобавление пользователей';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Перезаписать старейшие при заполнении';
|
||||
|
||||
@override
|
||||
String get storage => 'Хранилище';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Опасная зона';
|
||||
|
||||
@override
|
||||
String get profiles => 'Профили';
|
||||
|
||||
@override
|
||||
String get favourites => 'Избранное';
|
||||
|
||||
@override
|
||||
String get sensors => 'Датчики';
|
||||
|
||||
@override
|
||||
String get others => 'Другие';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Модуль GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Живой трафик';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Карта повторителей';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Сканирование спектра';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Журналы BLE-пакетов';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'База трассировок';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Размер пути в байтах';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Уведомления о сообщениях';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-оповещения';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Уведомления об обнаружении';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Уведомления об обновлениях';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Без звука при открытом приложении';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Отключить контакты';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Включить вкладку Датчики';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Включить профили';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Автоматическая ротация маршрута';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Ближайший повторитель как резерв';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Удалить всю историю сообщений';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Размер шрифта сообщений';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Вращать карту по направлению';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показать отладочную информацию карты';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Открыть карту на весь экран';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Показать SAR-маркеры';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Отображать SAR-маркеры на основной карте';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Показать все следы контактов';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Скрыть повторители на карте';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Установить масштаб карты';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Пользовательский масштаб карты сохранён';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Битрейт голоса';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Компрессор голоса';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Выравнивает тихую и громкую речь';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Лимитер голоса';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Предотвращает обрезку пиков перед кодированием';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Автоусиление микрофона';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Позволяет рекордеру регулировать уровень входа';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Подавление эха';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Подавление шума';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Обрезать тишину в голосовых сообщениях';
|
||||
|
||||
@override
|
||||
String get compressor => 'Компрессор';
|
||||
|
||||
@override
|
||||
String get limiter => 'Лимитер';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Автоусиление';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Эхо';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Шум';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Тишина';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Максимальный размер изображения';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Сжатие изображения';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Оттенки серого';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Режим ультра';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Быстрые приватные GPS-обновления';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Порог движения';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Порог движения быстрого GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Интервал активного использования быстрого GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Интервал обновления при активном использовании';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Повторять ближний трафик';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ретранслировать через повторители по сети';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Только рядом, без flood повторителей';
|
||||
|
||||
@override
|
||||
String get multihop => 'Многохоповый';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Создать профиль';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Переименовать профиль';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Новый профиль';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Управление профилями';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Включите профили, чтобы начать управлять ими.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Открыть сообщение';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Перейти к связанному SAR-сообщению';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Удалить SAR-маркер';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Выберите назначение для отправки SAR-маркера';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-маркер отправлен в публичный канал';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-маркер отправлен в комнату';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Загрузить из галереи';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Заменить изображение';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Выбрать из галереи';
|
||||
|
||||
@override
|
||||
String get team => 'Команда';
|
||||
|
||||
@override
|
||||
String get found => 'Найдено';
|
||||
|
||||
@override
|
||||
String get staging => 'Место сбора';
|
||||
|
||||
@override
|
||||
String get object => 'Объект';
|
||||
|
||||
@override
|
||||
String get quiet => 'Тихо';
|
||||
|
||||
@override
|
||||
String get moderate => 'Умеренно';
|
||||
|
||||
@override
|
||||
String get busy => 'Занято';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Сканирование спектра не нашло частот-кандидатов';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Поиск сообщений';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Отправить изображение из галереи';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Сделать фото';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Только личные сообщения';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Все сообщения';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Отправить в публичный канал?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Выберите тип маркера и назначение';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Нет доступных назначений';
|
||||
|
||||
@override
|
||||
String get image => 'Изображение';
|
||||
|
||||
@override
|
||||
String get format => 'Формат';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Размеры';
|
||||
|
||||
@override
|
||||
String get segments => 'Сегменты';
|
||||
|
||||
@override
|
||||
String get transfers => 'Передачи';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Загружено';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Сохранить настройки обнаружения';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Сохранить публичную информацию';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Сохранить настройки радио';
|
||||
|
||||
@override
|
||||
String get savePath => 'Сохранить путь';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Стереть данные устройства';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Стереть устройство';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Деструктивные действия устройства.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Выберите пресет или настройте радио вручную.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Выберите имя и местоположение, которыми делится устройство.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Доступное место на этом устройстве.';
|
||||
|
||||
@override
|
||||
String get used => 'Использовано';
|
||||
|
||||
@override
|
||||
String get total => 'Всего';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Переименовать значение';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Настроить поля';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Предпросмотр в реальном времени';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Расписание обновления';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Нет ответа';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Обновление';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Недоступно';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Выберите реле или узел для наблюдения.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Публичный ключ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Уже в контактах';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Подключитесь к устройству перед добавлением контактов';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Из контактов';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Только онлайн';
|
||||
|
||||
@override
|
||||
String get inBoth => 'В обоих';
|
||||
|
||||
@override
|
||||
String get source => 'Источник';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ручное редактирование маршрута';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Наблюдаемый mesh-маршрут';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Все сообщения очищены';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'База трассировок очищена';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Журналы пакетов очищены';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex-данные скопированы в буфер';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Режим разработчика включён';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Режим разработчика отключён';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Буфер обмена пуст';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Контакт импортирован';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Ссылка на контакт скопирована в буфер';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Не удалось экспортировать контакт';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Нет журналов для экспорта';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Экспортировать как CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Экспортировать как текст';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Получено (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Время сборки';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL загрузки недоступен';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Не удаётся открыть URL загрузки';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Проверка обновлений доступна только на Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Вы используете последнюю версию';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Обновление доступно, но URL загрузки не найден';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Начать Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe недоступно';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: противник неизвестен';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: ожидание начала';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Принимает общие ссылки';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Поддерживает raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Удобно для буфера';
|
||||
|
||||
@override
|
||||
String get captured => 'Захвачено';
|
||||
|
||||
@override
|
||||
String get size => 'Размер';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Нет пользовательских каналов для очистки.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Нет контактов устройства для очистки.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -477,6 +477,24 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Razpršitev';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Prijavljen';
|
||||
|
||||
@@ -1881,6 +1899,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Uporabite dnevnik paketov, pregled spektra in razvojno diagnostiko pri odpravljanju težav v mesh omrežju.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'V senzorjih';
|
||||
|
||||
@@ -1912,4 +1937,803 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Nastavitev poti ni uspela: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Časovna omejitev ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opkoda';
|
||||
|
||||
@override
|
||||
String get payload => 'Vsebina';
|
||||
|
||||
@override
|
||||
String get hops => 'Skoki';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Velikost zgoščevalne vrednosti';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajti poti';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Izbrana pot';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Ocenjeni čas oddaje';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od pošiljatelja do prejema';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Prejete kopije';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Razlog ponovitve';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Način ponovitve';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Rezultat ponovitve';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Zadnja ponovitev';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketi';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Hitrost';
|
||||
|
||||
@override
|
||||
String get window => 'Okno';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Zakasnitev po oddaji';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pasovno sito';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Pasovno sito za glas';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekvenca';
|
||||
|
||||
@override
|
||||
String get australia => 'Avstralija';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Avstralija (ozko)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Avstralija: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Avstralija: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nova Zelandija';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nova Zelandija (ozko)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Švica';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugalska 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugalska 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Češka (ozko)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (dolg doseg)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (zastarelo)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (ozko)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ZDA/Kanada (priporočeno)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (zastarelo)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (ozko)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktivno';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj stik';
|
||||
|
||||
@override
|
||||
String get all => 'Vse';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Samodejno razreši';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Počisti vse';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Počisti posrednike';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Počisti filtre';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Počisti pot';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Počisti sporočila';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Počisti merilo';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Počisti odkritja';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Počisti bazo sledi';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Počisti vse kanale';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Počisti vse stike';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Počisti kanale';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Počisti stike';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Počisti pot ob maks. ponovitvi';
|
||||
|
||||
@override
|
||||
String get create => 'Ustvari';
|
||||
|
||||
@override
|
||||
String get custom => 'Po meri';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Privzeto';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Podvoji';
|
||||
|
||||
@override
|
||||
String get editName => 'Uredi ime';
|
||||
|
||||
@override
|
||||
String get open => 'Odpri';
|
||||
|
||||
@override
|
||||
String get paste => 'Prilepi';
|
||||
|
||||
@override
|
||||
String get preview => 'Predogled';
|
||||
|
||||
@override
|
||||
String get remove => 'Odstrani';
|
||||
|
||||
@override
|
||||
String get rename => 'Preimenuj';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Razreši vse';
|
||||
|
||||
@override
|
||||
String get send => 'Pošlji';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Vseeno pošlji';
|
||||
|
||||
@override
|
||||
String get share => 'Deli';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Deli stik';
|
||||
|
||||
@override
|
||||
String get trace => 'Sledenje';
|
||||
|
||||
@override
|
||||
String get use => 'Uporabi';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Uporabi izbrano frekvenco';
|
||||
|
||||
@override
|
||||
String get discovery => 'Odkrivanje';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Odkrij posrednike';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Odkrij senzorje';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Odkrivanje posrednikov poslano';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Odkrivanje senzorjev poslano';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Čakalna odkritja počiščena.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Samodejno odkrivanje';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Omogoči samodejno dodajanje';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Samodejno dodaj posrednike';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Samodejno dodaj strežnike sob';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Samodejno dodaj senzorje';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Samodejno dodaj uporabnike';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Prepiši najstarejše ob polnem';
|
||||
|
||||
@override
|
||||
String get storage => 'Shramba';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Nevarno območje';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Priljubljeni';
|
||||
|
||||
@override
|
||||
String get sensors => 'Senzorji';
|
||||
|
||||
@override
|
||||
String get others => 'Ostali';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Promet v živo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Zemljevid posrednikov';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektralno skeniranje';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Dnevniki BLE paketov';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza sledi';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Velikost poti v bajtih';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Obvestila o sporočilih';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR opozorila';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Obvestila o odkritjih';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Obvestila o posodobitvah';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Utišaj ko je aplikacija odprta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Onemogoči stike';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Omogoči zavihek Senzorji';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Omogoči profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Samodejno kroženje poti';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbližji posrednik kot rezerva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Izbriši vso shranjeno zgodovino sporočil';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Velikost pisave sporočil';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Vrti zemljevid s smerjo';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Prikaži razhroščevalne info zemljevida';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Odpri zemljevid na cel zaslon';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Prikaži SAR oznake';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Prikaži SAR oznake na glavnem zemljevidu';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Prikaži vse sledi stikov';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Skrij posrednike na zemljevidu';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Nastavi merilo zemljevida';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Merilo zemljevida shranjeno';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitna hitrost glasu';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor glasu';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Izravnava tihe in glasne govorne ravni';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Omejevalnik glasu';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Preprečuje rezanje vrhov pred kodiranjem';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Samodejna ojačitev mikrofona';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Omogoči snemalniku prilagoditev vhodne ravni';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Odstranjevanje odmeva';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Dušenje šuma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Obreži tišino v glasovnih sporočilih';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Omejevalnik';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Samodejna ojačitev';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Odmev';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Šum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Tišina';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Največja velikost slike';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Stiskanje slik';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Sivine';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra način';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Hitri zasebni GPS podatki';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Prag gibanja';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Prag gibanja za hitri GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Interval uporabe hitrega GPS-a';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interval posodobitve pri aktivni uporabi';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ponavljaj bližnji promet';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Posreduj skozi posrednike po mreži';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Samo bližnji, brez poplavljanja posrednikov';
|
||||
|
||||
@override
|
||||
String get multihop => 'Večskočno';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Ustvari profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Preimenuj profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nov profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Upravljaj profile';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Omogočite profile, da jih začnete upravljati.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Odpri sporočilo';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Skoči na povezano SAR sporočilo';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Odstrani SAR oznako';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Izberite cilj za pošiljanje SAR oznake';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR oznaka poslana na javni kanal';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR oznaka poslana v sobo';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Naloži iz galerije';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamenjaj sliko';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Izberi iz galerije';
|
||||
|
||||
@override
|
||||
String get team => 'Ekipa';
|
||||
|
||||
@override
|
||||
String get found => 'Najdeno';
|
||||
|
||||
@override
|
||||
String get staging => 'Zbirno mesto';
|
||||
|
||||
@override
|
||||
String get object => 'Predmet';
|
||||
|
||||
@override
|
||||
String get quiet => 'Tiho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Zmerno';
|
||||
|
||||
@override
|
||||
String get busy => 'Zasedeno';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektralno skeniranje ni našlo primernih frekvenc';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Iskanje sporočil';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Pošlji sliko iz galerije';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Posnemi fotografijo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Samo neposredno';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Vsa sporočila';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Pošlji na javni kanal?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => 'Izberite vrsto oznake in cilj';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Ni razpoložljivih ciljev';
|
||||
|
||||
@override
|
||||
String get image => 'Slika';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimenzije';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Prenosi';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Preneseno od';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Shrani nastavitve odkrivanja';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Shrani javne podatke';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Shrani radijske nastavitve';
|
||||
|
||||
@override
|
||||
String get savePath => 'Shrani pot';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Izbriši podatke naprave';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Izbriši napravo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktivna dejanja na napravi.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Izberite prednastavitev ali natančno prilagodite radijske nastavitve.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Izberite ime in lokacijo, ki ju naprava deli.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Razpoložljiv prostor na tej napravi.';
|
||||
|
||||
@override
|
||||
String get used => 'Uporabljeno';
|
||||
|
||||
@override
|
||||
String get total => 'Skupaj';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Preimenuj vrednost';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Prilagodi polja';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Predogled v živo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Interval osvežitve';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Ni odgovora';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Osvežujem';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nedostopno';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Izberite posrednika ali vozlišče za opazovanje.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Javni ključ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Že med stiki';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Povežite se z napravo pred dodajanjem stikov';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Iz stikov';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Samo povezani';
|
||||
|
||||
@override
|
||||
String get inBoth => 'V obeh';
|
||||
|
||||
@override
|
||||
String get source => 'Vir';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ročno urejanje poti';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Opazovana mrežna pot';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Vsa sporočila počiščena';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza sledi počiščena';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Dnevniki paketov počiščeni';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex podatki kopirani v odložišče';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Razvojni način omogočen';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Razvojni način onemogočen';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Odložišče je prazno';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Stik uvožen';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Povezava na stik kopirana v odložišče';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Izvoz stika ni uspel';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Ni dnevnikov za izvoz';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Izvozi kot CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Izvozi kot besedilo';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Prejeto (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Čas gradnje';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL za prenos ni na voljo';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'URL za prenos ni mogoče odpreti';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Preverjanje posodobitev je na voljo le na Androidu';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Uporabljate najnovejšo različico';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Posodobitev na voljo, a URL za prenos ni najden';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Začni Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe ni na voljo';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: nasprotnik neznan';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: čakanje na začetek';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Sprejema deljene povezave';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Podpira surove hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Primerno za odložišče';
|
||||
|
||||
@override
|
||||
String get captured => 'Zajeto';
|
||||
|
||||
@override
|
||||
String get size => 'Velikost';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Ni prilagojenih kanalov za brisanje.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Ni stikov naprave za brisanje.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -480,6 +480,24 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Yayılım';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Giriş yapıldı';
|
||||
|
||||
@@ -1883,6 +1901,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Mesh sorunlarını giderirken paket günlükleri, spektrum taraması ve geliştirici tanılamalarını kullanın.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Sensörlerde';
|
||||
|
||||
@@ -1914,4 +1939,799 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Rota ayarlanamadı: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK zaman aşımı';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Yük';
|
||||
|
||||
@override
|
||||
String get hops => 'Atlama';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash boyutu';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Yol baytları';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Seçilen yol';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Tahmini gönderim';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Gönderenden alındıya';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Alınan kopyalar';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Yeniden deneme nedeni';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Yeniden deneme modu';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Yeniden deneme sonucu';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Son deneme';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketleri';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Hız';
|
||||
|
||||
@override
|
||||
String get window => 'Pencere';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Gönderim sonrası gecikme';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Bant geçiren';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Bant geçiren filtre ses';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekans';
|
||||
|
||||
@override
|
||||
String get australia => 'Avustralya';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Avustralya (Dar)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Avustralya: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Avustralya: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Yeni Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Yeni Zelanda (Dar)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'İsviçre';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portekiz 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portekiz 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Çek Cumhuriyeti (Dar)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'AB 433MHz (Uzun Menzil)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'AB/BK (Kullanımdan kalktı)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'AB/BK (Dar)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ABD/Kanada (Önerilen)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Kullanımdan kalktı)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Dar)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktif';
|
||||
|
||||
@override
|
||||
String get addContact => 'Kişi ekle';
|
||||
|
||||
@override
|
||||
String get all => 'Tümü';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Otomatik çöz';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Tümünü temizle';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Röleleri temizle';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Filtreleri temizle';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Rotayı temizle';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Mesajları temizle';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Ölçeği temizle';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Keşifleri temizle';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Çevrimiçi iz veritabanını temizle';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Tüm kanalları temizle';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Tüm kişileri temizle';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Kanalları temizle';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Kişileri temizle';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Maks denemede yolu temizle';
|
||||
|
||||
@override
|
||||
String get create => 'Oluştur';
|
||||
|
||||
@override
|
||||
String get custom => 'Özel';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Varsayılan';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Çoğalt';
|
||||
|
||||
@override
|
||||
String get editName => 'Adı düzenle';
|
||||
|
||||
@override
|
||||
String get open => 'Aç';
|
||||
|
||||
@override
|
||||
String get paste => 'Yapıştır';
|
||||
|
||||
@override
|
||||
String get preview => 'Önizleme';
|
||||
|
||||
@override
|
||||
String get remove => 'Kaldır';
|
||||
|
||||
@override
|
||||
String get rename => 'Yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Tümünü çöz';
|
||||
|
||||
@override
|
||||
String get send => 'Gönder';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Yine de gönder';
|
||||
|
||||
@override
|
||||
String get share => 'Paylaş';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Kişiyi paylaş';
|
||||
|
||||
@override
|
||||
String get trace => 'İz';
|
||||
|
||||
@override
|
||||
String get use => 'Kullan';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Seçilen frekansı kullan';
|
||||
|
||||
@override
|
||||
String get discovery => 'Keşif';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Tekrarlayıcıları keşfet';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Sensörleri keşfet';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Tekrarlayıcı keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensör keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Bekleyen keşifler temizlendi.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Otomatik keşif';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Otomatik eklemeyi etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Tekrarlayıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Oda sunucularını otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Sensörleri otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Kullanıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Dolduğunda en eskiyi üzerine yaz';
|
||||
|
||||
@override
|
||||
String get storage => 'Depolama';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Tehlikeli bölge';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profiller';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriler';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensörler';
|
||||
|
||||
@override
|
||||
String get others => 'Diğerleri';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS Modülü';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Canlı trafik';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Tekrarlayıcılar haritası';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektrum taraması';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE paket günlükleri';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Çevrimiçi iz veritabanı';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Yol bayt boyutu';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Mesaj bildirimleri';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR uyarıları';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Keşif bildirimleri';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Güncelleme bildirimleri';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Uygulama açıkken sessize al';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Kişileri devre dışı bırak';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Sensörler sekmesini etkinleştir';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Profilleri etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Otomatik rota rotasyonu';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'En yakın tekrarlayıcıya geri dönüş';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Tüm mesaj geçmişini sil';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Mesaj yazı tipi boyutu';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Haritayı yön ile döndür';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Harita hata ayıklama bilgisini göster';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Haritayı tam ekranda aç';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'SAR işaretçilerini göster';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'SAR işaretçilerini ana haritada göster';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Tüm kişi izlerini göster';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Haritada tekrarlayıcıları gizle';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Harita ölçeğini ayarla';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Özel harita ölçeği kaydedildi';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Ses bit hızı';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Ses sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Sessiz ve yüksek konuşma seviyelerini dengeler';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Ses sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Kodlama öncesi tepe kırpmasını önler';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mikrofon otomatik kazancı';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Kaydedicinin giriş seviyesini ayarlamasına izin verir';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Yankı iptali';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Gürültü bastırma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Sesli mesajlarda sessizliği kırp';
|
||||
|
||||
@override
|
||||
String get compressor => 'Sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get limiter => 'Sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Otomatik kazanç';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Yankı';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Gürültü';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Sessizlik';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksimum resim boyutu';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Resim sıkıştırma';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Gri tonlama';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra mod';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Hızlı özel GPS güncellemeleri';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Hızlı GPS hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Hızlı GPS aktif kullanım aralığı';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Aktif kullanım güncelleme aralığı';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Yakın trafiği tekrarla';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Yalnızca yakın, tekrarlayıcı taşkını olmadan';
|
||||
|
||||
@override
|
||||
String get multihop => 'Çok atlamalı';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Profil oluştur';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Profili yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Yeni profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Profilleri yönet';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Profilleri yönetmeye başlamak için etkinleştirin.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Mesajı aç';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'İlgili SAR mesajına git';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'SAR işaretçisini kaldır';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'SAR işaretçisi göndermek için bir hedef seçin';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR işaretçisi genel kanala yayınlandı';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR işaretçisi odaya gönderildi';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Galeriden yükle';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Resmi değiştir';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Galeriden seç';
|
||||
|
||||
@override
|
||||
String get team => 'Takım';
|
||||
|
||||
@override
|
||||
String get found => 'Bulundu';
|
||||
|
||||
@override
|
||||
String get staging => 'Toplanma alanı';
|
||||
|
||||
@override
|
||||
String get object => 'Nesne';
|
||||
|
||||
@override
|
||||
String get quiet => 'Sessiz';
|
||||
|
||||
@override
|
||||
String get moderate => 'Orta';
|
||||
|
||||
@override
|
||||
String get busy => 'Meşgul';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektrum taraması aday frekans bulamadı';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Mesajlarda ara';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Galeriden resim gönder';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Fotoğraf çek';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Yalnızca DM';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tüm mesajlar';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Genel kanala gönderilsin mi?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'İşaretçi türünü ve hedefi seçin';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Kullanılabilir hedef yok';
|
||||
|
||||
@override
|
||||
String get image => 'Resim';
|
||||
|
||||
@override
|
||||
String get format => 'Biçim';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Boyutlar';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentler';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferler';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'İndiren';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Keşif ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Genel bilgileri kaydet';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Radyo ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePath => 'Yolu kaydet';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Cihaz verilerini sil';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Cihazı sil';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Yıkıcı cihaz işlemleri.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Bu cihazın paylaştığı ad ve konumu seçin.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => 'Bu cihazda kullanılabilir alan.';
|
||||
|
||||
@override
|
||||
String get used => 'Kullanılan';
|
||||
|
||||
@override
|
||||
String get total => 'Toplam';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Değeri yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Alanları özelleştir';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Canlı önizleme';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Yenileme programı';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Yanıt yok';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Yenileniyor';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Kullanılamaz';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'İzlemek için bir röle veya düğüm seçin.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Genel anahtar';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Zaten kişilerde';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Kişi eklemeden önce bir cihaza bağlanın';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Kişilerden';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Yalnızca çevrimiçi';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Her ikisinde';
|
||||
|
||||
@override
|
||||
String get source => 'Kaynak';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manuel rota düzenle';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Gözlemlenen mesh rotası';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tüm mesajlar temizlendi';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Çevrimiçi iz veritabanı temizlendi';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Paket günlükleri temizlendi';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex verisi panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Geliştirici modu etkinleştirildi';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Geliştirici modu devre dışı bırakıldı';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Pano boş';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kişi içe aktarıldı';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Kişi bağlantısı panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Kişi dışa aktarılamadı';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Dışa aktarılacak günlük yok';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'CSV olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Metin olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Alındı (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Derleme zamanı';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'İndirme URL\'si mevcut değil';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'İndirme URL\'si açılamıyor';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Güncelleme kontrolü yalnızca Android\'de kullanılabilir';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'En son sürümü kullanıyorsunuz';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Güncelleme mevcut ancak indirme URL\'si bulunamadı';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Tic-Tac-Toe başlat';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe kullanılamıyor';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: rakip bilinmiyor';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: başlangıç bekleniyor';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Paylaşım bağlantılarını kabul eder';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Ham hex destekler';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Pano dostu';
|
||||
|
||||
@override
|
||||
String get captured => 'Yakalandı';
|
||||
|
||||
@override
|
||||
String get size => 'Boyut';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Temizlenecek özel kanal yok.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Temizlenecek cihaz kişisi yok.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -480,6 +480,24 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Широкомовно';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Увійшли';
|
||||
|
||||
@@ -1896,6 +1914,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get wizardToolsFeature3 =>
|
||||
'Використовуйте журнали пакетів, сканування спектра та діагностику розробника для усунення проблем mesh-мережі.';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'У датчиках';
|
||||
|
||||
@@ -1927,4 +1952,809 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Не вдалося задати маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Тайм-аут ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Опкод';
|
||||
|
||||
@override
|
||||
String get payload => 'Корисне навантаження';
|
||||
|
||||
@override
|
||||
String get hops => 'Хопи';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Розмір хешу';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Байти шляху';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Обраний шлях';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Очікувана передача';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Від відправника до отримання';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Отримані копії';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Причина повтору';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Режим повтору';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Результат повтору';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Остання спроба';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-пакети';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Швидкість';
|
||||
|
||||
@override
|
||||
String get window => 'Вікно';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Затримка після передачі';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Смуговий';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Смуговий фільтр голосу';
|
||||
|
||||
@override
|
||||
String get frequency => 'Частота';
|
||||
|
||||
@override
|
||||
String get australia => 'Австралія';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Австралія (Вузький)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Австралія: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Австралія: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Нова Зеландія';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Нова Зеландія (Вузький)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Швейцарія';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Португалія 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Португалія 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Чехія (Вузький)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ЄС 433МГц (Далекий)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ЄС/Великобританія (Застаріле)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ЄС/Великобританія (Вузький)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'США/Канада (Рекомендовано)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'В\'єтнам (Застаріле)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'В\'єтнам (Вузький)';
|
||||
|
||||
@override
|
||||
String get active => 'Активно';
|
||||
|
||||
@override
|
||||
String get addContact => 'Додати контакт';
|
||||
|
||||
@override
|
||||
String get all => 'Усі';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Автоматичне вирішення';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Очистити все';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Очистити реле';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Очистити фільтри';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Очистити маршрут';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Очистити повідомлення';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Очистити масштаб';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Очистити виявлення';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Очистити базу трасувань';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Очистити всі канали';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Очистити всі контакти';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Очистити канали';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Очистити контакти';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Очистити шлях при макс. спробі';
|
||||
|
||||
@override
|
||||
String get create => 'Створити';
|
||||
|
||||
@override
|
||||
String get custom => 'Користувацький';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'За замовчуванням';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Дублювати';
|
||||
|
||||
@override
|
||||
String get editName => 'Редагувати ім\'я';
|
||||
|
||||
@override
|
||||
String get open => 'Відкрити';
|
||||
|
||||
@override
|
||||
String get paste => 'Вставити';
|
||||
|
||||
@override
|
||||
String get preview => 'Попередній перегляд';
|
||||
|
||||
@override
|
||||
String get remove => 'Видалити';
|
||||
|
||||
@override
|
||||
String get rename => 'Перейменувати';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Вирішити все';
|
||||
|
||||
@override
|
||||
String get send => 'Надіслати';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Надіслати все одно';
|
||||
|
||||
@override
|
||||
String get share => 'Поділитися';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Поділитися контактом';
|
||||
|
||||
@override
|
||||
String get trace => 'Трасування';
|
||||
|
||||
@override
|
||||
String get use => 'Використати';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Використати обрану частоту';
|
||||
|
||||
@override
|
||||
String get discovery => 'Виявлення';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Виявити повторювачі';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Виявити датчики';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Виявлення повторювачів надіслано';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Виявлення датчиків надіслано';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Очікувані виявлення очищено.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Автоматичне виявлення';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Увімкнути автоматичне додавання';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Автододавання повторювачів';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Автододавання серверів кімнат';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Автододавання датчиків';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Автододавання користувачів';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Перезаписати найстаріші при заповненні';
|
||||
|
||||
@override
|
||||
String get storage => 'Сховище';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Небезпечна зона';
|
||||
|
||||
@override
|
||||
String get profiles => 'Профілі';
|
||||
|
||||
@override
|
||||
String get favourites => 'Обране';
|
||||
|
||||
@override
|
||||
String get sensors => 'Датчики';
|
||||
|
||||
@override
|
||||
String get others => 'Інші';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Модуль GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Живий трафік';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Карта повторювачів';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Сканування спектру';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Журнали BLE-пакетів';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'База трасувань';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Розмір шляху в байтах';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Сповіщення про повідомлення';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-сповіщення';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Сповіщення про виявлення';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Сповіщення про оновлення';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Без звуку при відкритому додатку';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Вимкнути контакти';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Увімкнути вкладку Датчики';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Увімкнути профілі';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Автоматична ротація маршруту';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Найближчий повторювач як резерв';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Видалити всю історію повідомлень';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Розмір шрифту повідомлень';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Обертати карту за напрямком';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показати налагоджувальну інфо карти';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Відкрити карту на повний екран';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Показати SAR-маркери';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Показувати SAR-маркери на головній карті';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Показати всі сліди контактів';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Сховати повторювачі на карті';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Встановити масштаб карти';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Користувацький масштаб карти збережено';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Бітрейт голосу';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Компресор голосу';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Вирівнює тихий і гучний рівні мови';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Лімітер голосу';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Запобігає обрізанню піків перед кодуванням';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Автопідсилення мікрофона';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Дозволяє рекордеру налаштувати рівень входу';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Придушення ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Придушення шуму';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Обрізати тишу в голосових повідомленнях';
|
||||
|
||||
@override
|
||||
String get compressor => 'Компресор';
|
||||
|
||||
@override
|
||||
String get limiter => 'Лімітер';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Автопідсилення';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Шум';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Тиша';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Максимальний розмір зображення';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Стиснення зображення';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Відтінки сірого';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ультра режим';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Швидкі приватні GPS-оновлення';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Поріг руху';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Поріг руху швидкого GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Інтервал активного використання швидкого GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Інтервал оновлення при активному використанні';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Повторювати близький трафік';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ретранслювати через повторювачі по мережі';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Лише поблизу, без flood повторювачів';
|
||||
|
||||
@override
|
||||
String get multihop => 'Багатохоповий';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Створити профіль';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Перейменувати профіль';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Новий профіль';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Керувати профілями';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Увімкніть профілі, щоб почати керувати ними.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Відкрити повідомлення';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Перейти до пов\'язаного SAR-повідомлення';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Видалити SAR-маркер';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Виберіть призначення для надсилання SAR-маркера';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-маркер надіслано на публічний канал';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-маркер надіслано в кімнату';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Завантажити з галереї';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Замінити зображення';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Вибрати з галереї';
|
||||
|
||||
@override
|
||||
String get team => 'Команда';
|
||||
|
||||
@override
|
||||
String get found => 'Знайдено';
|
||||
|
||||
@override
|
||||
String get staging => 'Місце збору';
|
||||
|
||||
@override
|
||||
String get object => 'Об\'єкт';
|
||||
|
||||
@override
|
||||
String get quiet => 'Тихо';
|
||||
|
||||
@override
|
||||
String get moderate => 'Помірно';
|
||||
|
||||
@override
|
||||
String get busy => 'Зайнято';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Сканування спектру не знайшло частот-кандидатів';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Пошук повідомлень';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Надіслати зображення з галереї';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Зробити фото';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Лише особисті повідомлення';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Усі повідомлення';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Надіслати в публічний канал?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Виберіть тип маркера та призначення';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Немає доступних призначень';
|
||||
|
||||
@override
|
||||
String get image => 'Зображення';
|
||||
|
||||
@override
|
||||
String get format => 'Формат';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Розміри';
|
||||
|
||||
@override
|
||||
String get segments => 'Сегменти';
|
||||
|
||||
@override
|
||||
String get transfers => 'Передачі';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Завантажено';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Зберегти налаштування виявлення';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Зберегти публічну інформацію';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Зберегти налаштування радіо';
|
||||
|
||||
@override
|
||||
String get savePath => 'Зберегти шлях';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Стерти дані пристрою';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Стерти пристрій';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Деструктивні дії пристрою.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Оберіть пресет або налаштуйте радіо вручну.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Оберіть ім\'я та місцеположення, якими ділиться пристрій.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Доступний простір на цьому пристрої.';
|
||||
|
||||
@override
|
||||
String get used => 'Використано';
|
||||
|
||||
@override
|
||||
String get total => 'Загалом';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Перейменувати значення';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Налаштувати поля';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Попередній перегляд наживо';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Розклад оновлення';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Немає відповіді';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Оновлення';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Недоступно';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Оберіть реле або вузол для спостереження.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Публічний ключ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Вже в контактах';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Підключіться до пристрою перед додаванням контактів';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'З контактів';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Лише онлайн';
|
||||
|
||||
@override
|
||||
String get inBoth => 'В обох';
|
||||
|
||||
@override
|
||||
String get source => 'Джерело';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ручне редагування маршруту';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Спостережуваний mesh-маршрут';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Усі повідомлення очищено';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Базу трасувань очищено';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Журнали пакетів очищено';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex-дані скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Режим розробника увімкнено';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Режим розробника вимкнено';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Буфер обміну порожній';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Контакт імпортовано';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Посилання на контакт скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Не вдалося експортувати контакт';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Немає журналів для експорту';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Експортувати як CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Експортувати як текст';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Отримано (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Час збірки';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL завантаження недоступний';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Не вдається відкрити URL завантаження';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Перевірка оновлень доступна лише на Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Ви використовуєте останню версію';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Оновлення доступне, але URL завантаження не знайдено';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Почати Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe недоступно';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: суперник невідомий';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: очікування початку';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Приймає посилання для обміну';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Підтримує raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Зручно для буфера';
|
||||
|
||||
@override
|
||||
String get captured => 'Захоплено';
|
||||
|
||||
@override
|
||||
String get size => 'Розмір';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Немає користувацьких каналів для очищення.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Немає контактів пристрою для очищення.';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -464,6 +464,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get flood => '泛洪';
|
||||
|
||||
@override
|
||||
String get autoSend => 'Auto Send';
|
||||
|
||||
@override
|
||||
String get autoSendDescription => 'Send via current path.';
|
||||
|
||||
@override
|
||||
String get sendDirect => 'Send Direct';
|
||||
|
||||
@override
|
||||
String get sendDirectDescription => 'Send directly to this contact.';
|
||||
|
||||
@override
|
||||
String get sendFlood => 'Send Flood';
|
||||
|
||||
@override
|
||||
String get sendFloodDescription => 'Send via all repeaters.';
|
||||
|
||||
@override
|
||||
String get loggedIn => '已登录';
|
||||
|
||||
@@ -1796,6 +1814,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get wizardToolsFeature3 => '在排查 mesh 网络问题时使用数据包日志、频谱扫描和开发者诊断。';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryTitle => 'Discover repeaters now?';
|
||||
|
||||
@override
|
||||
String get postConnectDiscoveryDescription =>
|
||||
'Run repeater discovery right after connecting so you can see nearby MeshCore nodes and add them to your network faster.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => '在传感器中';
|
||||
|
||||
@@ -1827,4 +1852,781 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return '设置路由失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK 超时';
|
||||
|
||||
@override
|
||||
String get opcode => '操作码';
|
||||
|
||||
@override
|
||||
String get payload => '有效载荷';
|
||||
|
||||
@override
|
||||
String get hops => '跳数';
|
||||
|
||||
@override
|
||||
String get hashSize => '哈希大小';
|
||||
|
||||
@override
|
||||
String get pathBytes => '路径字节';
|
||||
|
||||
@override
|
||||
String get selectedPath => '选定路径';
|
||||
|
||||
@override
|
||||
String get estimatedTx => '预计传输';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => '发送到接收';
|
||||
|
||||
@override
|
||||
String get receivedCopies => '收到的副本';
|
||||
|
||||
@override
|
||||
String get retryCause => '重试原因';
|
||||
|
||||
@override
|
||||
String get retryMode => '重试模式';
|
||||
|
||||
@override
|
||||
String get retryResult => '重试结果';
|
||||
|
||||
@override
|
||||
String get lastRetry => '最后重试';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX 数据包';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => '速率';
|
||||
|
||||
@override
|
||||
String get window => '窗口';
|
||||
|
||||
@override
|
||||
String get posttxDelay => '发送后延迟';
|
||||
|
||||
@override
|
||||
String get bandpass => '带通';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => '带通语音滤波';
|
||||
|
||||
@override
|
||||
String get frequency => '频率';
|
||||
|
||||
@override
|
||||
String get australia => '澳大利亚';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => '澳大利亚(窄带)';
|
||||
|
||||
@override
|
||||
String get australiaQld => '澳大利亚:QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => '澳大利亚:SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => '新西兰';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => '新西兰(窄带)';
|
||||
|
||||
@override
|
||||
String get switzerland => '瑞士';
|
||||
|
||||
@override
|
||||
String get portugal433 => '葡萄牙 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => '葡萄牙 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => '捷克(窄带)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => '欧盟 433MHz(远距离)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => '欧盟/英国(已弃用)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => '欧盟/英国(窄带)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => '美国/加拿大(推荐)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => '越南(已弃用)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => '越南(窄带)';
|
||||
|
||||
@override
|
||||
String get active => '活动';
|
||||
|
||||
@override
|
||||
String get addContact => '添加联系人';
|
||||
|
||||
@override
|
||||
String get all => '全部';
|
||||
|
||||
@override
|
||||
String get autoResolve => '自动解析';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => '清除全部';
|
||||
|
||||
@override
|
||||
String get clearRelays => '清除中继';
|
||||
|
||||
@override
|
||||
String get clearFilters => '清除过滤器';
|
||||
|
||||
@override
|
||||
String get clearRoute => '清除路由';
|
||||
|
||||
@override
|
||||
String get clearMessages => '清除消息';
|
||||
|
||||
@override
|
||||
String get clearScale => '清除比例';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => '清除发现';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => '清除在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => '清除所有频道';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => '清除所有联系人';
|
||||
|
||||
@override
|
||||
String get clearChannels => '清除频道';
|
||||
|
||||
@override
|
||||
String get clearContacts => '清除联系人';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => '最大重试时清除路径';
|
||||
|
||||
@override
|
||||
String get create => '创建';
|
||||
|
||||
@override
|
||||
String get custom => '自定义';
|
||||
|
||||
@override
|
||||
String get defaultValue => '默认';
|
||||
|
||||
@override
|
||||
String get duplicate => '复制';
|
||||
|
||||
@override
|
||||
String get editName => '编辑名称';
|
||||
|
||||
@override
|
||||
String get open => '打开';
|
||||
|
||||
@override
|
||||
String get paste => '粘贴';
|
||||
|
||||
@override
|
||||
String get preview => '预览';
|
||||
|
||||
@override
|
||||
String get remove => '移除';
|
||||
|
||||
@override
|
||||
String get rename => '重命名';
|
||||
|
||||
@override
|
||||
String get resolveAll => '解析全部';
|
||||
|
||||
@override
|
||||
String get send => '发送';
|
||||
|
||||
@override
|
||||
String get sendAnyway => '仍然发送';
|
||||
|
||||
@override
|
||||
String get share => '分享';
|
||||
|
||||
@override
|
||||
String get shareContact => '分享联系人';
|
||||
|
||||
@override
|
||||
String get trace => '追踪';
|
||||
|
||||
@override
|
||||
String get use => '使用';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => '使用选定频率';
|
||||
|
||||
@override
|
||||
String get discovery => '发现';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => '发现中继器';
|
||||
|
||||
@override
|
||||
String get discoverSensors => '发现传感器';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => '中继器发现已发送';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => '传感器发现已发送';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => '已清除待处理的发现。';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => '自动发现';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => '启用自动添加';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => '自动添加中继器';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => '自动添加房间服务器';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => '自动添加传感器';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => '自动添加用户';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => '满时覆盖最旧的';
|
||||
|
||||
@override
|
||||
String get storage => '存储';
|
||||
|
||||
@override
|
||||
String get dangerZone => '危险区域';
|
||||
|
||||
@override
|
||||
String get profiles => '配置文件';
|
||||
|
||||
@override
|
||||
String get favourites => '收藏夹';
|
||||
|
||||
@override
|
||||
String get sensors => '传感器';
|
||||
|
||||
@override
|
||||
String get others => '其他';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS 模块';
|
||||
|
||||
@override
|
||||
String get liveTraffic => '实时流量';
|
||||
|
||||
@override
|
||||
String get repeatersMap => '中继器地图';
|
||||
|
||||
@override
|
||||
String get spectrumScan => '频谱扫描';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE 数据包日志';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => '在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => '路由路径字节大小';
|
||||
|
||||
@override
|
||||
String get messageNotifications => '消息通知';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR 警报';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => '发现通知';
|
||||
|
||||
@override
|
||||
String get updateNotifications => '更新通知';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => '应用打开时静音';
|
||||
|
||||
@override
|
||||
String get disableContacts => '禁用联系人';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => '启用传感器标签';
|
||||
|
||||
@override
|
||||
String get enableProfiles => '启用配置文件';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => '自动路由轮换';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => '最近中继器回退';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => '删除所有存储的消息历史';
|
||||
|
||||
@override
|
||||
String get messageFontSize => '消息字体大小';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => '随航向旋转地图';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => '显示地图调试信息';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => '全屏打开地图';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => '显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap => '在主地图上显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => '显示所有联系人轨迹';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => '在地图上隐藏中继器';
|
||||
|
||||
@override
|
||||
String get setMapScale => '设置地图比例';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => '自定义地图比例已保存';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => '语音比特率';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => '语音压缩器';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels => '平衡安静和响亮的语音级别';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => '语音限制器';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding => '防止编码前的削波';
|
||||
|
||||
@override
|
||||
String get micAutoGain => '麦克风自动增益';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel => '让录音器调整输入级别';
|
||||
|
||||
@override
|
||||
String get echoCancellation => '回声消除';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => '噪音抑制';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => '修剪语音消息中的静音';
|
||||
|
||||
@override
|
||||
String get compressor => '压缩器';
|
||||
|
||||
@override
|
||||
String get limiter => '限制器';
|
||||
|
||||
@override
|
||||
String get autoGain => '自动增益';
|
||||
|
||||
@override
|
||||
String get echoCancel => '回声';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => '噪声';
|
||||
|
||||
@override
|
||||
String get silenceTrim => '静音';
|
||||
|
||||
@override
|
||||
String get maxImageSize => '最大图片大小';
|
||||
|
||||
@override
|
||||
String get imageCompression => '图片压缩';
|
||||
|
||||
@override
|
||||
String get grayscale => '灰度';
|
||||
|
||||
@override
|
||||
String get ultraMode => '超级模式';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => '快速私有 GPS 更新';
|
||||
|
||||
@override
|
||||
String get movementThreshold => '移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => '快速 GPS 移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => '快速 GPS 活动使用间隔';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => '活动使用更新间隔';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => '转发附近流量';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh => '通过网格中的中继器转发';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding => '仅附近,无中继器泛洪';
|
||||
|
||||
@override
|
||||
String get multihop => '多跳';
|
||||
|
||||
@override
|
||||
String get createProfile => '创建配置文件';
|
||||
|
||||
@override
|
||||
String get renameProfile => '重命名配置文件';
|
||||
|
||||
@override
|
||||
String get newProfile => '新配置文件';
|
||||
|
||||
@override
|
||||
String get manageProfiles => '管理配置文件';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem => '启用配置文件以开始管理它们。';
|
||||
|
||||
@override
|
||||
String get openMessage => '打开消息';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => '跳转到相关的 SAR 消息';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => '移除 SAR 标记';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker => '请选择目标以发送 SAR 标记';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel => 'SAR 标记已广播到公共频道';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR 标记已发送到房间';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => '从图库加载';
|
||||
|
||||
@override
|
||||
String get replaceImage => '替换图片';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => '从图库选择';
|
||||
|
||||
@override
|
||||
String get team => '团队';
|
||||
|
||||
@override
|
||||
String get found => '已找到';
|
||||
|
||||
@override
|
||||
String get staging => '集结区';
|
||||
|
||||
@override
|
||||
String get object => '物体';
|
||||
|
||||
@override
|
||||
String get quiet => '安静';
|
||||
|
||||
@override
|
||||
String get moderate => '中等';
|
||||
|
||||
@override
|
||||
String get busy => '繁忙';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies => '频谱扫描未找到候选频率';
|
||||
|
||||
@override
|
||||
String get searchMessages => '搜索消息';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => '从图库发送图片';
|
||||
|
||||
@override
|
||||
String get takePhoto => '拍照';
|
||||
|
||||
@override
|
||||
String get dmOnly => '仅私信';
|
||||
|
||||
@override
|
||||
String get allMessages => '所有消息';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => '发送到公共频道?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => '选择标记类型和目标';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => '没有可用的目标';
|
||||
|
||||
@override
|
||||
String get image => '图片';
|
||||
|
||||
@override
|
||||
String get format => '格式';
|
||||
|
||||
@override
|
||||
String get dimensions => '尺寸';
|
||||
|
||||
@override
|
||||
String get segments => '分段';
|
||||
|
||||
@override
|
||||
String get transfers => '传输';
|
||||
|
||||
@override
|
||||
String get downloadedBy => '已下载';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => '保存发现设置';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => '保存公共信息';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => '保存无线电设置';
|
||||
|
||||
@override
|
||||
String get savePath => '保存路径';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => '擦除设备数据';
|
||||
|
||||
@override
|
||||
String get wipeDevice => '擦除设备';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => '破坏性设备操作。';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings => '选择预设或微调自定义无线电设置。';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares => '选择此设备共享的名称和位置。';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => '此设备上的可用空间。';
|
||||
|
||||
@override
|
||||
String get used => '已使用';
|
||||
|
||||
@override
|
||||
String get total => '总计';
|
||||
|
||||
@override
|
||||
String get renameValue => '重命名值';
|
||||
|
||||
@override
|
||||
String get customizeFields => '自定义字段';
|
||||
|
||||
@override
|
||||
String get livePreview => '实时预览';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => '刷新计划';
|
||||
|
||||
@override
|
||||
String get noResponse => '无响应';
|
||||
|
||||
@override
|
||||
String get refreshing => '刷新中';
|
||||
|
||||
@override
|
||||
String get unavailable => '不可用';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors => '选择要在传感器中监视的中继或节点。';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => '公钥';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => '已在联系人中';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts => '添加联系人前请连接到设备';
|
||||
|
||||
@override
|
||||
String get fromContacts => '来自联系人';
|
||||
|
||||
@override
|
||||
String get onlineOnly => '仅在线';
|
||||
|
||||
@override
|
||||
String get inBoth => '两者中';
|
||||
|
||||
@override
|
||||
String get source => '来源';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => '手动路由编辑';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => '观察到的网格路由';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => '所有消息已清除';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => '在线追踪数据库已清除';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => '数据包日志已清除';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => '十六进制数据已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => '开发者模式已启用';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => '开发者模式已禁用';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => '剪贴板为空';
|
||||
|
||||
@override
|
||||
String get contactImported => '联系人已导入';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => '联系人链接已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => '导出联系人失败';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => '没有日志可导出';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => '导出为 CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => '导出为文本';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => '收到 (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => '构建时间';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => '下载链接不可用';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => '无法打开下载链接';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid => '更新检查仅在 Android 上可用';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => '您正在使用最新版本';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound => '有更新可用但未找到下载链接';
|
||||
|
||||
@override
|
||||
String get startTictactoe => '开始井字棋';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => '井字棋不可用';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => '井字棋:对手未知';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => '井字棋:等待开始';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => '接受分享链接';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => '支持原始十六进制';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => '剪贴板友好';
|
||||
|
||||
@override
|
||||
String get captured => '已捕获';
|
||||
|
||||
@override
|
||||
String get size => '大小';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => '没有自定义频道可清除。';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => '没有设备联系人可清除。';
|
||||
|
||||
@override
|
||||
String get setRegionScope => 'Set region scope';
|
||||
|
||||
@override
|
||||
String get regionScope => 'Region scope';
|
||||
|
||||
@override
|
||||
String get regionScopeNone => 'None (global)';
|
||||
|
||||
@override
|
||||
String get clearRegionScope => 'Clear scope';
|
||||
|
||||
@override
|
||||
String get regionScopeWarning =>
|
||||
'Only repeaters allowing this region will forward.';
|
||||
|
||||
@override
|
||||
String get discoverRegions => 'Discover from repeaters';
|
||||
|
||||
@override
|
||||
String get discoveringRegions => 'Discovering regions...';
|
||||
|
||||
@override
|
||||
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||
|
||||
@override
|
||||
String get noRegionsFound => 'No regions found on this repeater.';
|
||||
|
||||
@override
|
||||
String regionScopeSet(String name) {
|
||||
return 'Region scope set to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get regionScopeCleared => 'Region scope cleared';
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Nie udało się wyczyścić trasy: {error}",
|
||||
"contactRouteCleared": "Trasa wyczyszczona",
|
||||
"contactRouteSet": "Ustawiono trasę: {route}",
|
||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}"
|
||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Limit czasu ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ładunek",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Rozmiar hash",
|
||||
"pathBytes": "Bajty ścieżki",
|
||||
"selectedPath": "Wybrana ścieżka",
|
||||
"estimatedTx": "Szacowane nadawanie",
|
||||
"senderToReceipt": "Od nadawcy do odbioru",
|
||||
"receivedCopies": "Otrzymane kopie",
|
||||
"retryCause": "Przyczyna ponowienia",
|
||||
"retryMode": "Tryb ponowienia",
|
||||
"retryResult": "Wynik ponowienia",
|
||||
"lastRetry": "Ostatnia próba",
|
||||
"rxPackets": "Pakiety RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Szybkość",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Opóźnienie po nadawaniu",
|
||||
"bandpass": "Pasmowy",
|
||||
"bandpassFilterVoice": "Filtr pasmowy głosu",
|
||||
"frequency": "Częstotliwość",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (wąski)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nowa Zelandia",
|
||||
"newZealandNarrow": "Nowa Zelandia (wąski)",
|
||||
"switzerland": "Szwajcaria",
|
||||
"portugal433": "Portugalia 433",
|
||||
"portugal868": "Portugalia 868",
|
||||
"czechRepublicNarrow": "Czechy (wąski)",
|
||||
"eu433mhzLongRange": "EU 433MHz (daleki zasięg)",
|
||||
"euukDeprecated": "EU/UK (przestarzały)",
|
||||
"euukNarrow": "EU/UK (wąski)",
|
||||
"usacanadaRecommended": "USA/Kanada (zalecany)",
|
||||
"vietnamDeprecated": "Wietnam (przestarzały)",
|
||||
"vietnamNarrow": "Wietnam (wąski)",
|
||||
"active": "Aktywny",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Wszystko",
|
||||
"autoResolve": "Automatyczne rozwiązanie",
|
||||
"clearAllLabel": "Wyczyść wszystko",
|
||||
"clearRelays": "Wyczyść przekaźniki",
|
||||
"clearFilters": "Wyczyść filtry",
|
||||
"clearRoute": "Wyczyść trasę",
|
||||
"clearMessages": "Wyczyść wiadomości",
|
||||
"clearScale": "Wyczyść skalę",
|
||||
"clearDiscoveries": "Wyczyść odkrycia",
|
||||
"clearOnlineTraceDatabase": "Wyczyść bazę śladów",
|
||||
"clearAllChannels": "Wyczyść wszystkie kanały",
|
||||
"clearAllContacts": "Wyczyść wszystkie kontakty",
|
||||
"clearChannels": "Wyczyść kanały",
|
||||
"clearContacts": "Wyczyść kontakty",
|
||||
"clearPathOnMaxRetry": "Wyczyść ścieżkę przy maks. próbie",
|
||||
"create": "Utwórz",
|
||||
"custom": "Niestandardowy",
|
||||
"defaultValue": "Domyślny",
|
||||
"duplicate": "Duplikuj",
|
||||
"editName": "Edytuj nazwę",
|
||||
"open": "Otwórz",
|
||||
"paste": "Wklej",
|
||||
"preview": "Podgląd",
|
||||
"remove": "Usuń",
|
||||
"rename": "Zmień nazwę",
|
||||
"resolveAll": "Rozwiąż wszystko",
|
||||
"send": "Wyślij",
|
||||
"sendAnyway": "Wyślij mimo to",
|
||||
"share": "Udostępnij",
|
||||
"shareContact": "Udostępnij kontakt",
|
||||
"trace": "Śledzenie",
|
||||
"use": "Użyj",
|
||||
"useSelectedFrequency": "Użyj wybranej częstotliwości",
|
||||
"discovery": "Wykrywanie",
|
||||
"discoverRepeaters": "Wykryj przekaźniki",
|
||||
"discoverSensors": "Wykryj czujniki",
|
||||
"repeaterDiscoverySent": "Wykrywanie przekaźników wysłane",
|
||||
"sensorDiscoverySent": "Wykrywanie czujników wysłane",
|
||||
"clearedPendingDiscoveries": "Wyczyszczono oczekujące odkrycia.",
|
||||
"autoDiscovery": "Automatyczne wykrywanie",
|
||||
"enableAutomaticAdding": "Włącz automatyczne dodawanie",
|
||||
"autoaddRepeaters": "Automatycznie dodaj przekaźniki",
|
||||
"autoaddRoomServers": "Automatycznie dodaj serwery pokoi",
|
||||
"autoaddSensors": "Automatycznie dodaj czujniki",
|
||||
"autoaddUsers": "Automatycznie dodaj użytkowników",
|
||||
"overwriteOldestWhenFull": "Nadpisz najstarsze gdy pełne",
|
||||
"storage": "Pamięć",
|
||||
"dangerZone": "Strefa zagrożenia",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Ulubione",
|
||||
"sensors": "Czujniki",
|
||||
"others": "Inne",
|
||||
"gpsModule": "Moduł GPS",
|
||||
"liveTraffic": "Ruch na żywo",
|
||||
"repeatersMap": "Mapa przekaźników",
|
||||
"spectrumScan": "Skanowanie spektrum",
|
||||
"blePacketLogs": "Logi pakietów BLE",
|
||||
"onlineTraceDatabase": "Baza śladów",
|
||||
"routePathByteSize": "Rozmiar ścieżki w bajtach",
|
||||
"messageNotifications": "Powiadomienia o wiadomościach",
|
||||
"sarAlerts": "Alerty SAR",
|
||||
"discoveryNotifications": "Powiadomienia o wykrywaniu",
|
||||
"updateNotifications": "Powiadomienia o aktualizacjach",
|
||||
"muteWhileAppIsOpen": "Wycisz gdy aplikacja jest otwarta",
|
||||
"disableContacts": "Wyłącz kontakty",
|
||||
"enableSensorsTab": "Włącz kartę Czujniki",
|
||||
"enableProfiles": "Włącz profile",
|
||||
"autoRouteRotation": "Automatyczna rotacja trasy",
|
||||
"nearestRepeaterFallback": "Najbliższy przekaźnik awaryjny",
|
||||
"deleteAllStoredMessageHistory": "Usuń całą historię wiadomości",
|
||||
"messageFontSize": "Rozmiar czcionki wiadomości",
|
||||
"rotateMapWithHeading": "Obracaj mapę z kierunkiem",
|
||||
"showMapDebugInfo": "Pokaż info debugowania mapy",
|
||||
"openMapInFullscreen": "Otwórz mapę na pełnym ekranie",
|
||||
"showSarMarkersLabel": "Pokaż znaczniki SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Wyświetlaj znaczniki SAR na głównej mapie",
|
||||
"showAllContactTrailsLabel": "Pokaż wszystkie ślady kontaktów",
|
||||
"hideRepeatersOnMap": "Ukryj przekaźniki na mapie",
|
||||
"setMapScale": "Ustaw skalę mapy",
|
||||
"customMapScaleSaved": "Niestandardowa skala mapy zapisana",
|
||||
"voiceBitrate": "Bitrate głosu",
|
||||
"voiceCompressor": "Kompresor głosu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Wyrównuje ciche i głośne poziomy mowy",
|
||||
"voiceLimiter": "Limiter głosu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Zapobiega przycinaniu szczytów przed kodowaniem",
|
||||
"micAutoGain": "Automatyczne wzmocnienie mikrofonu",
|
||||
"letsTheRecorderAdjustInputLevel": "Pozwala nagrywarkowi dostosować poziom wejścia",
|
||||
"echoCancellation": "Usuwanie echa",
|
||||
"noiseSuppression": "Tłumienie szumu",
|
||||
"trimSilenceInVoiceMessages": "Przytnij ciszę w wiadomościach głosowych",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatyczne wzmocnienie",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Szum",
|
||||
"silenceTrim": "Cisza",
|
||||
"maxImageSize": "Maksymalny rozmiar obrazu",
|
||||
"imageCompression": "Kompresja obrazu",
|
||||
"grayscale": "Skala szarości",
|
||||
"ultraMode": "Tryb ultra",
|
||||
"fastPrivateGpsUpdates": "Szybkie prywatne aktualizacje GPS",
|
||||
"movementThreshold": "Próg ruchu",
|
||||
"fastGpsMovementThreshold": "Próg ruchu szybkiego GPS",
|
||||
"fastGpsActiveuseInterval": "Interwał aktywnego użycia szybkiego GPS",
|
||||
"activeuseUpdateInterval": "Interwał aktualizacji przy aktywnym użyciu",
|
||||
"repeatNearbyTraffic": "Powtarzaj pobliski ruch",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Przekazuj przez przekaźniki w sieci",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Tylko w pobliżu, bez zalewania przekaźników",
|
||||
"multihop": "Wieloskokowy",
|
||||
"createProfile": "Utwórz profil",
|
||||
"renameProfile": "Zmień nazwę profilu",
|
||||
"newProfile": "Nowy profil",
|
||||
"manageProfiles": "Zarządzaj profilami",
|
||||
"enableProfilesToStartManagingThem": "Włącz profile, aby zacząć nimi zarządzać.",
|
||||
"openMessage": "Otwórz wiadomość",
|
||||
"jumpToTheRelatedSarMessage": "Przejdź do powiązanej wiadomości SAR",
|
||||
"removeSarMarker": "Usuń znacznik SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Wybierz cel wysłania znacznika SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Znacznik SAR wysłany na kanał publiczny",
|
||||
"sarMarkerSentToRoom": "Znacznik SAR wysłany do pokoju",
|
||||
"loadFromGallery": "Załaduj z galerii",
|
||||
"replaceImage": "Zamień obraz",
|
||||
"selectFromGallery": "Wybierz z galerii",
|
||||
"team": "Zespół",
|
||||
"found": "Znaleziono",
|
||||
"staging": "Punkt zbiórki",
|
||||
"object": "Obiekt",
|
||||
"quiet": "Cicho",
|
||||
"moderate": "Umiarkowane",
|
||||
"busy": "Zajęty",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Skanowanie spektrum nie znalazło częstotliwości kandydatów",
|
||||
"searchMessages": "Szukaj wiadomości",
|
||||
"sendImageFromGallery": "Wyślij obraz z galerii",
|
||||
"takePhoto": "Zrób zdjęcie",
|
||||
"dmOnly": "Tylko wiadomość bezpośrednia",
|
||||
"allMessages": "Wszystkie wiadomości",
|
||||
"sendToPublicChannel": "Wysłać na kanał publiczny?",
|
||||
"selectMarkerTypeAndDestination": "Wybierz typ znacznika i cel",
|
||||
"noDestinationsAvailableLabel": "Brak dostępnych celów",
|
||||
"image": "Obraz",
|
||||
"format": "Format",
|
||||
"dimensions": "Wymiary",
|
||||
"segments": "Segmenty",
|
||||
"transfers": "Transfery",
|
||||
"downloadedBy": "Pobrane przez",
|
||||
"saveDiscoverySettings": "Zapisz ustawienia wykrywania",
|
||||
"savePublicInfo": "Zapisz informacje publiczne",
|
||||
"saveRadioSettings": "Zapisz ustawienia radia",
|
||||
"savePath": "Zapisz ścieżkę",
|
||||
"wipeDeviceData": "Wyczyść dane urządzenia",
|
||||
"wipeDevice": "Wyczyść urządzenie",
|
||||
"destructiveDeviceActions": "Destrukcyjne akcje na urządzeniu.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wybierz ustawienie wstępne lub dostosuj ustawienia radia.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wybierz nazwę i lokalizację udostępnianą przez to urządzenie.",
|
||||
"availableSpaceOnThisDevice": "Dostępne miejsce na tym urządzeniu.",
|
||||
"used": "Użyto",
|
||||
"total": "Razem",
|
||||
"renameValue": "Zmień nazwę wartości",
|
||||
"customizeFields": "Dostosuj pola",
|
||||
"livePreview": "Podgląd na żywo",
|
||||
"refreshSchedule": "Harmonogram odświeżania",
|
||||
"noResponse": "Brak odpowiedzi",
|
||||
"refreshing": "Odświeżanie",
|
||||
"unavailable": "Niedostępny",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wybierz przekaźnik lub węzeł do obserwacji.",
|
||||
"publicKeyLabel": "Klucz publiczny",
|
||||
"alreadyInContacts": "Już w kontaktach",
|
||||
"connectToADeviceBeforeAddingContacts": "Połącz się z urządzeniem przed dodaniem kontaktów",
|
||||
"fromContacts": "Z kontaktów",
|
||||
"onlineOnly": "Tylko online",
|
||||
"inBoth": "W obu",
|
||||
"source": "Źródło",
|
||||
"manualRouteEdit": "Ręczna edycja trasy",
|
||||
"observedMeshRoute": "Obserwowana trasa sieci",
|
||||
"allMessagesCleared": "Wszystkie wiadomości wyczyszczone",
|
||||
"onlineTraceDatabaseCleared": "Baza śladów wyczyszczona",
|
||||
"packetLogsCleared": "Logi pakietów wyczyszczone",
|
||||
"hexDataCopiedToClipboard": "Dane hex skopiowane do schowka",
|
||||
"developerModeEnabled": "Tryb programisty włączony",
|
||||
"developerModeDisabled": "Tryb programisty wyłączony",
|
||||
"clipboardIsEmpty": "Schowek jest pusty",
|
||||
"contactImported": "Kontakt zaimportowany",
|
||||
"contactLinkCopiedToClipboard": "Link kontaktu skopiowany do schowka",
|
||||
"failedToExportContact": "Nie udało się wyeksportować kontaktu",
|
||||
"noLogsToExport": "Brak logów do eksportu",
|
||||
"exportAsCsv": "Eksportuj jako CSV",
|
||||
"exportAsText": "Eksportuj jako tekst",
|
||||
"receivedRfc3339": "Odebrano (RFC3339)",
|
||||
"buildTime": "Czas budowania",
|
||||
"downloadUrlNotAvailable": "URL pobierania niedostępny",
|
||||
"cannotOpenDownloadUrl": "Nie można otworzyć URL pobierania",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Sprawdzanie aktualizacji dostępne tylko na Androidzie",
|
||||
"youAreRunningTheLatestVersion": "Używasz najnowszej wersji",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aktualizacja dostępna, ale URL pobierania nie znaleziony",
|
||||
"startTictactoe": "Rozpocznij Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe niedostępny",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: przeciwnik nieznany",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: oczekiwanie na start",
|
||||
"acceptsShareLinks": "Akceptuje udostępnione linki",
|
||||
"supportsRawHex": "Obsługuje surowy hex",
|
||||
"clipboardfriendly": "Przyjazne dla schowka",
|
||||
"captured": "Przechwycone",
|
||||
"size": "Rozmiar",
|
||||
"noCustomChannelsToClear": "Brak niestandardowych kanałów do wyczyszczenia.",
|
||||
"noDeviceContactsToClear": "Brak kontaktów urządzenia do wyczyszczenia."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Falha ao limpar a rota: {error}",
|
||||
"contactRouteCleared": "Rota limpa",
|
||||
"contactRouteSet": "Rota definida: {route}",
|
||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}"
|
||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tempo limite ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamanho do hash",
|
||||
"pathBytes": "Bytes do caminho",
|
||||
"selectedPath": "Caminho selecionado",
|
||||
"estimatedTx": "Transmissão estimada",
|
||||
"senderToReceipt": "Do remetente ao recibo",
|
||||
"receivedCopies": "Cópias recebidas",
|
||||
"retryCause": "Causa da nova tentativa",
|
||||
"retryMode": "Modo de nova tentativa",
|
||||
"retryResult": "Resultado da nova tentativa",
|
||||
"lastRetry": "Última tentativa",
|
||||
"rxPackets": "Pacotes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Taxa",
|
||||
"window": "Janela",
|
||||
"posttxDelay": "Atraso pós-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda de voz",
|
||||
"frequency": "Frequência",
|
||||
"australia": "Austrália",
|
||||
"australiaNarrow": "Austrália (Estreito)",
|
||||
"australiaQld": "Austrália: QLD",
|
||||
"australiaSaWa": "Austrália: SA, WA",
|
||||
"newZealand": "Nova Zelândia",
|
||||
"newZealandNarrow": "Nova Zelândia (Estreito)",
|
||||
"switzerland": "Suíça",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estreito)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estreito)",
|
||||
"usacanadaRecommended": "EUA/Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietname (Obsoleto)",
|
||||
"vietnamNarrow": "Vietname (Estreito)",
|
||||
"active": "Ativo",
|
||||
"addContact": "Adicionar contacto",
|
||||
"all": "Tudo",
|
||||
"autoResolve": "Resolver automaticamente",
|
||||
"clearAllLabel": "Limpar tudo",
|
||||
"clearRelays": "Limpar relays",
|
||||
"clearFilters": "Limpar filtros",
|
||||
"clearRoute": "Limpar rota",
|
||||
"clearMessages": "Limpar mensagens",
|
||||
"clearScale": "Limpar escala",
|
||||
"clearDiscoveries": "Limpar descobertas",
|
||||
"clearOnlineTraceDatabase": "Limpar base de dados de rastreio",
|
||||
"clearAllChannels": "Limpar todos os canais",
|
||||
"clearAllContacts": "Limpar todos os contactos",
|
||||
"clearChannels": "Limpar canais",
|
||||
"clearContacts": "Limpar contactos",
|
||||
"clearPathOnMaxRetry": "Limpar caminho no máx. de tentativas",
|
||||
"create": "Criar",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Padrão",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nome",
|
||||
"open": "Abrir",
|
||||
"paste": "Colar",
|
||||
"preview": "Pré-visualização",
|
||||
"remove": "Remover",
|
||||
"rename": "Renomear",
|
||||
"resolveAll": "Resolver tudo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar mesmo assim",
|
||||
"share": "Partilhar",
|
||||
"shareContact": "Partilhar contacto",
|
||||
"trace": "Rastreio",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frequência selecionada",
|
||||
"discovery": "Descoberta",
|
||||
"discoverRepeaters": "Descobrir repetidores",
|
||||
"discoverSensors": "Descobrir sensores",
|
||||
"repeaterDiscoverySent": "Descoberta de repetidores enviada",
|
||||
"sensorDiscoverySent": "Descoberta de sensores enviada",
|
||||
"clearedPendingDiscoveries": "Descobertas pendentes limpas.",
|
||||
"autoDiscovery": "Descoberta automática",
|
||||
"enableAutomaticAdding": "Ativar adição automática",
|
||||
"autoaddRepeaters": "Adicionar repetidores automaticamente",
|
||||
"autoaddRoomServers": "Adicionar servidores de sala automaticamente",
|
||||
"autoaddSensors": "Adicionar sensores automaticamente",
|
||||
"autoaddUsers": "Adicionar utilizadores automaticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescrever mais antigos quando cheio",
|
||||
"storage": "Armazenamento",
|
||||
"dangerZone": "Zona de perigo",
|
||||
"profiles": "Perfis",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Outros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfego ao vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Varredura de espectro",
|
||||
"blePacketLogs": "Registos de pacotes BLE",
|
||||
"onlineTraceDatabase": "Base de dados de rastreio",
|
||||
"routePathByteSize": "Tamanho do caminho em bytes",
|
||||
"messageNotifications": "Notificações de mensagens",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificações de descoberta",
|
||||
"updateNotifications": "Notificações de atualização",
|
||||
"muteWhileAppIsOpen": "Silenciar com a app aberta",
|
||||
"disableContacts": "Desativar contactos",
|
||||
"enableSensorsTab": "Ativar separador Sensores",
|
||||
"enableProfiles": "Ativar perfis",
|
||||
"autoRouteRotation": "Rotação automática de rota",
|
||||
"nearestRepeaterFallback": "Repetidor mais próximo como reserva",
|
||||
"deleteAllStoredMessageHistory": "Apagar todo o histórico de mensagens",
|
||||
"messageFontSize": "Tamanho da fonte de mensagens",
|
||||
"rotateMapWithHeading": "Rodar mapa com direção",
|
||||
"showMapDebugInfo": "Mostrar info de depuração do mapa",
|
||||
"openMapInFullscreen": "Abrir mapa em ecrã inteiro",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR no mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos os rastos de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores no mapa",
|
||||
"setMapScale": "Definir escala do mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Taxa de bits de voz",
|
||||
"voiceCompressor": "Compressor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra níveis de fala suaves e altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previne corte de picos antes da codificação",
|
||||
"micAutoGain": "Ganho automático do microfone",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite ao gravador ajustar o nível de entrada",
|
||||
"echoCancellation": "Cancelamento de eco",
|
||||
"noiseSuppression": "Supressão de ruído",
|
||||
"trimSilenceInVoiceMessages": "Cortar silêncio em mensagens de voz",
|
||||
"compressor": "Compressor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganho automático",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruído",
|
||||
"silenceTrim": "Silêncio",
|
||||
"maxImageSize": "Tamanho máximo de imagem",
|
||||
"imageCompression": "Compressão de imagem",
|
||||
"grayscale": "Escala de cinzentos",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Atualizações GPS privadas rápidas",
|
||||
"movementThreshold": "Limiar de movimento",
|
||||
"fastGpsMovementThreshold": "Limiar de movimento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso ativo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de atualização de uso ativo",
|
||||
"repeatNearbyTraffic": "Repetir tráfego próximo",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir através de repetidores na rede",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Apenas próximos, sem flood de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Criar perfil",
|
||||
"renameProfile": "Renomear perfil",
|
||||
"newProfile": "Novo perfil",
|
||||
"manageProfiles": "Gerir perfis",
|
||||
"enableProfilesToStartManagingThem": "Ative os perfis para começar a geri-los.",
|
||||
"openMessage": "Abrir mensagem",
|
||||
"jumpToTheRelatedSarMessage": "Ir para a mensagem SAR relacionada",
|
||||
"removeSarMarker": "Remover marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Selecione um destino para enviar o marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido no canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado para a sala",
|
||||
"loadFromGallery": "Carregar da galeria",
|
||||
"replaceImage": "Substituir imagem",
|
||||
"selectFromGallery": "Selecionar da galeria",
|
||||
"team": "Equipa",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunião",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "A varredura de espectro não encontrou frequências candidatas",
|
||||
"searchMessages": "Pesquisar mensagens",
|
||||
"sendImageFromGallery": "Enviar imagem da galeria",
|
||||
"takePhoto": "Tirar foto",
|
||||
"dmOnly": "Apenas mensagem direta",
|
||||
"allMessages": "Todas as mensagens",
|
||||
"sendToPublicChannel": "Enviar para o canal público?",
|
||||
"selectMarkerTypeAndDestination": "Selecione tipo de marcador e destino",
|
||||
"noDestinationsAvailableLabel": "Nenhum destino disponível",
|
||||
"image": "Imagem",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensões",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferências",
|
||||
"downloadedBy": "Transferido por",
|
||||
"saveDiscoverySettings": "Guardar definições de descoberta",
|
||||
"savePublicInfo": "Guardar informação pública",
|
||||
"saveRadioSettings": "Guardar definições de rádio",
|
||||
"savePath": "Guardar caminho",
|
||||
"wipeDeviceData": "Apagar dados do dispositivo",
|
||||
"wipeDevice": "Apagar dispositivo",
|
||||
"destructiveDeviceActions": "Ações destrutivas do dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Escolha uma predefinição ou ajuste as definições de rádio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Escolha o nome e a localização que este dispositivo partilha.",
|
||||
"availableSpaceOnThisDevice": "Espaço disponível neste dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renomear valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Pré-visualização ao vivo",
|
||||
"refreshSchedule": "Agenda de atualização",
|
||||
"noResponse": "Sem resposta",
|
||||
"refreshing": "A atualizar",
|
||||
"unavailable": "Indisponível",
|
||||
"pickARelayOrNodeToWatchInSensors": "Escolha um relay ou nó para observar.",
|
||||
"publicKeyLabel": "Chave pública",
|
||||
"alreadyInContacts": "Já nos contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conecte-se a um dispositivo antes de adicionar contactos",
|
||||
"fromContacts": "Dos contactos",
|
||||
"onlineOnly": "Apenas online",
|
||||
"inBoth": "Em ambos",
|
||||
"source": "Fonte",
|
||||
"manualRouteEdit": "Edição manual de rota",
|
||||
"observedMeshRoute": "Rota mesh observada",
|
||||
"allMessagesCleared": "Todas as mensagens limpas",
|
||||
"onlineTraceDatabaseCleared": "Base de dados de rastreio limpa",
|
||||
"packetLogsCleared": "Registos de pacotes limpos",
|
||||
"hexDataCopiedToClipboard": "Dados hex copiados para a área de transferência",
|
||||
"developerModeEnabled": "Modo de programador ativado",
|
||||
"developerModeDisabled": "Modo de programador desativado",
|
||||
"clipboardIsEmpty": "A área de transferência está vazia",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Link do contacto copiado para a área de transferência",
|
||||
"failedToExportContact": "Falha ao exportar contacto",
|
||||
"noLogsToExport": "Sem registos para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recebido (RFC3339)",
|
||||
"buildTime": "Hora de compilação",
|
||||
"downloadUrlNotAvailable": "URL de download não disponível",
|
||||
"cannotOpenDownloadUrl": "Não é possível abrir o URL de download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "A verificação de atualizações só está disponível no Android",
|
||||
"youAreRunningTheLatestVersion": "Está a usar a versão mais recente",
|
||||
"updateAvailableButDownloadUrlNotFound": "Atualização disponível mas URL de download não encontrado",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponível",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconhecido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: a aguardar início",
|
||||
"acceptsShareLinks": "Aceita links partilhados",
|
||||
"supportsRawHex": "Suporta hex bruto",
|
||||
"clipboardfriendly": "Compatível com a área de transferência",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamanho",
|
||||
"noCustomChannelsToClear": "Sem canais personalizados para limpar.",
|
||||
"noDeviceContactsToClear": "Sem contactos do dispositivo para limpar."
|
||||
}
|
||||
|
||||
@@ -563,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Не удалось очистить маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищен",
|
||||
"contactRouteSet": "Маршрут задан: {route}",
|
||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}"
|
||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Таймаут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Полезная нагрузка",
|
||||
"hops": "Хопы",
|
||||
"hashSize": "Размер хеша",
|
||||
"pathBytes": "Байты пути",
|
||||
"selectedPath": "Выбранный путь",
|
||||
"estimatedTx": "Ожидаемая передача",
|
||||
"senderToReceipt": "От отправителя до получения",
|
||||
"receivedCopies": "Полученные копии",
|
||||
"retryCause": "Причина повтора",
|
||||
"retryMode": "Режим повтора",
|
||||
"retryResult": "Результат повтора",
|
||||
"lastRetry": "Последний повтор",
|
||||
"rxPackets": "RX-пакеты",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Скорость",
|
||||
"window": "Окно",
|
||||
"posttxDelay": "Задержка после передачи",
|
||||
"bandpass": "Полосовой",
|
||||
"bandpassFilterVoice": "Полосовой фильтр голоса",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралия",
|
||||
"australiaNarrow": "Австралия (Узкий)",
|
||||
"australiaQld": "Австралия: QLD",
|
||||
"australiaSaWa": "Австралия: SA, WA",
|
||||
"newZealand": "Новая Зеландия",
|
||||
"newZealandNarrow": "Новая Зеландия (Узкий)",
|
||||
"switzerland": "Швейцария",
|
||||
"portugal433": "Португалия 433",
|
||||
"portugal868": "Португалия 868",
|
||||
"czechRepublicNarrow": "Чехия (Узкий)",
|
||||
"eu433mhzLongRange": "ЕС 433МГц (Дальний)",
|
||||
"euukDeprecated": "ЕС/Великобритания (Устарело)",
|
||||
"euukNarrow": "ЕС/Великобритания (Узкий)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендуется)",
|
||||
"vietnamDeprecated": "Вьетнам (Устарело)",
|
||||
"vietnamNarrow": "Вьетнам (Узкий)",
|
||||
"active": "Активно",
|
||||
"addContact": "Добавить контакт",
|
||||
"all": "Все",
|
||||
"autoResolve": "Автоматическое разрешение",
|
||||
"clearAllLabel": "Очистить все",
|
||||
"clearRelays": "Очистить реле",
|
||||
"clearFilters": "Очистить фильтры",
|
||||
"clearRoute": "Очистить маршрут",
|
||||
"clearMessages": "Очистить сообщения",
|
||||
"clearScale": "Очистить масштаб",
|
||||
"clearDiscoveries": "Очистить обнаружения",
|
||||
"clearOnlineTraceDatabase": "Очистить базу трассировок",
|
||||
"clearAllChannels": "Очистить все каналы",
|
||||
"clearAllContacts": "Очистить все контакты",
|
||||
"clearChannels": "Очистить каналы",
|
||||
"clearContacts": "Очистить контакты",
|
||||
"clearPathOnMaxRetry": "Очистить путь при макс. повторе",
|
||||
"create": "Создать",
|
||||
"custom": "Пользовательский",
|
||||
"defaultValue": "По умолчанию",
|
||||
"duplicate": "Дублировать",
|
||||
"editName": "Редактировать имя",
|
||||
"open": "Открыть",
|
||||
"paste": "Вставить",
|
||||
"preview": "Предпросмотр",
|
||||
"remove": "Удалить",
|
||||
"rename": "Переименовать",
|
||||
"resolveAll": "Разрешить все",
|
||||
"send": "Отправить",
|
||||
"sendAnyway": "Отправить все равно",
|
||||
"share": "Поделиться",
|
||||
"shareContact": "Поделиться контактом",
|
||||
"trace": "Трассировка",
|
||||
"use": "Использовать",
|
||||
"useSelectedFrequency": "Использовать выбранную частоту",
|
||||
"discovery": "Обнаружение",
|
||||
"discoverRepeaters": "Обнаружить повторители",
|
||||
"discoverSensors": "Обнаружить датчики",
|
||||
"repeaterDiscoverySent": "Обнаружение повторителей отправлено",
|
||||
"sensorDiscoverySent": "Обнаружение датчиков отправлено",
|
||||
"clearedPendingDiscoveries": "Ожидающие обнаружения очищены.",
|
||||
"autoDiscovery": "Автоматическое обнаружение",
|
||||
"enableAutomaticAdding": "Включить автоматическое добавление",
|
||||
"autoaddRepeaters": "Автодобавление повторителей",
|
||||
"autoaddRoomServers": "Автодобавление серверов комнат",
|
||||
"autoaddSensors": "Автодобавление датчиков",
|
||||
"autoaddUsers": "Автодобавление пользователей",
|
||||
"overwriteOldestWhenFull": "Перезаписать старейшие при заполнении",
|
||||
"storage": "Хранилище",
|
||||
"dangerZone": "Опасная зона",
|
||||
"profiles": "Профили",
|
||||
"favourites": "Избранное",
|
||||
"sensors": "Датчики",
|
||||
"others": "Другие",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живой трафик",
|
||||
"repeatersMap": "Карта повторителей",
|
||||
"spectrumScan": "Сканирование спектра",
|
||||
"blePacketLogs": "Журналы BLE-пакетов",
|
||||
"onlineTraceDatabase": "База трассировок",
|
||||
"routePathByteSize": "Размер пути в байтах",
|
||||
"messageNotifications": "Уведомления о сообщениях",
|
||||
"sarAlerts": "SAR-оповещения",
|
||||
"discoveryNotifications": "Уведомления об обнаружении",
|
||||
"updateNotifications": "Уведомления об обновлениях",
|
||||
"muteWhileAppIsOpen": "Без звука при открытом приложении",
|
||||
"disableContacts": "Отключить контакты",
|
||||
"enableSensorsTab": "Включить вкладку Датчики",
|
||||
"enableProfiles": "Включить профили",
|
||||
"autoRouteRotation": "Автоматическая ротация маршрута",
|
||||
"nearestRepeaterFallback": "Ближайший повторитель как резерв",
|
||||
"deleteAllStoredMessageHistory": "Удалить всю историю сообщений",
|
||||
"messageFontSize": "Размер шрифта сообщений",
|
||||
"rotateMapWithHeading": "Вращать карту по направлению",
|
||||
"showMapDebugInfo": "Показать отладочную информацию карты",
|
||||
"openMapInFullscreen": "Открыть карту на весь экран",
|
||||
"showSarMarkersLabel": "Показать SAR-маркеры",
|
||||
"displaySarMarkersOnTheMainMap": "Отображать SAR-маркеры на основной карте",
|
||||
"showAllContactTrailsLabel": "Показать все следы контактов",
|
||||
"hideRepeatersOnMap": "Скрыть повторители на карте",
|
||||
"setMapScale": "Установить масштаб карты",
|
||||
"customMapScaleSaved": "Пользовательский масштаб карты сохранён",
|
||||
"voiceBitrate": "Битрейт голоса",
|
||||
"voiceCompressor": "Компрессор голоса",
|
||||
"balancesQuietAndLoudSpeechLevels": "Выравнивает тихую и громкую речь",
|
||||
"voiceLimiter": "Лимитер голоса",
|
||||
"preventsClippingPeaksBeforeEncoding": "Предотвращает обрезку пиков перед кодированием",
|
||||
"micAutoGain": "Автоусиление микрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Позволяет рекордеру регулировать уровень входа",
|
||||
"echoCancellation": "Подавление эха",
|
||||
"noiseSuppression": "Подавление шума",
|
||||
"trimSilenceInVoiceMessages": "Обрезать тишину в голосовых сообщениях",
|
||||
"compressor": "Компрессор",
|
||||
"limiter": "Лимитер",
|
||||
"autoGain": "Автоусиление",
|
||||
"echoCancel": "Эхо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тишина",
|
||||
"maxImageSize": "Максимальный размер изображения",
|
||||
"imageCompression": "Сжатие изображения",
|
||||
"grayscale": "Оттенки серого",
|
||||
"ultraMode": "Режим ультра",
|
||||
"fastPrivateGpsUpdates": "Быстрые приватные GPS-обновления",
|
||||
"movementThreshold": "Порог движения",
|
||||
"fastGpsMovementThreshold": "Порог движения быстрого GPS",
|
||||
"fastGpsActiveuseInterval": "Интервал активного использования быстрого GPS",
|
||||
"activeuseUpdateInterval": "Интервал обновления при активном использовании",
|
||||
"repeatNearbyTraffic": "Повторять ближний трафик",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслировать через повторители по сети",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Только рядом, без flood повторителей",
|
||||
"multihop": "Многохоповый",
|
||||
"createProfile": "Создать профиль",
|
||||
"renameProfile": "Переименовать профиль",
|
||||
"newProfile": "Новый профиль",
|
||||
"manageProfiles": "Управление профилями",
|
||||
"enableProfilesToStartManagingThem": "Включите профили, чтобы начать управлять ими.",
|
||||
"openMessage": "Открыть сообщение",
|
||||
"jumpToTheRelatedSarMessage": "Перейти к связанному SAR-сообщению",
|
||||
"removeSarMarker": "Удалить SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Выберите назначение для отправки SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер отправлен в публичный канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер отправлен в комнату",
|
||||
"loadFromGallery": "Загрузить из галереи",
|
||||
"replaceImage": "Заменить изображение",
|
||||
"selectFromGallery": "Выбрать из галереи",
|
||||
"team": "Команда",
|
||||
"found": "Найдено",
|
||||
"staging": "Место сбора",
|
||||
"object": "Объект",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Умеренно",
|
||||
"busy": "Занято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканирование спектра не нашло частот-кандидатов",
|
||||
"searchMessages": "Поиск сообщений",
|
||||
"sendImageFromGallery": "Отправить изображение из галереи",
|
||||
"takePhoto": "Сделать фото",
|
||||
"dmOnly": "Только личные сообщения",
|
||||
"allMessages": "Все сообщения",
|
||||
"sendToPublicChannel": "Отправить в публичный канал?",
|
||||
"selectMarkerTypeAndDestination": "Выберите тип маркера и назначение",
|
||||
"noDestinationsAvailableLabel": "Нет доступных назначений",
|
||||
"image": "Изображение",
|
||||
"format": "Формат",
|
||||
"dimensions": "Размеры",
|
||||
"segments": "Сегменты",
|
||||
"transfers": "Передачи",
|
||||
"downloadedBy": "Загружено",
|
||||
"saveDiscoverySettings": "Сохранить настройки обнаружения",
|
||||
"savePublicInfo": "Сохранить публичную информацию",
|
||||
"saveRadioSettings": "Сохранить настройки радио",
|
||||
"savePath": "Сохранить путь",
|
||||
"wipeDeviceData": "Стереть данные устройства",
|
||||
"wipeDevice": "Стереть устройство",
|
||||
"destructiveDeviceActions": "Деструктивные действия устройства.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Выберите пресет или настройте радио вручную.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Выберите имя и местоположение, которыми делится устройство.",
|
||||
"availableSpaceOnThisDevice": "Доступное место на этом устройстве.",
|
||||
"used": "Использовано",
|
||||
"total": "Всего",
|
||||
"renameValue": "Переименовать значение",
|
||||
"customizeFields": "Настроить поля",
|
||||
"livePreview": "Предпросмотр в реальном времени",
|
||||
"refreshSchedule": "Расписание обновления",
|
||||
"noResponse": "Нет ответа",
|
||||
"refreshing": "Обновление",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Выберите реле или узел для наблюдения.",
|
||||
"publicKeyLabel": "Публичный ключ",
|
||||
"alreadyInContacts": "Уже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Подключитесь к устройству перед добавлением контактов",
|
||||
"fromContacts": "Из контактов",
|
||||
"onlineOnly": "Только онлайн",
|
||||
"inBoth": "В обоих",
|
||||
"source": "Источник",
|
||||
"manualRouteEdit": "Ручное редактирование маршрута",
|
||||
"observedMeshRoute": "Наблюдаемый mesh-маршрут",
|
||||
"allMessagesCleared": "Все сообщения очищены",
|
||||
"onlineTraceDatabaseCleared": "База трассировок очищена",
|
||||
"packetLogsCleared": "Журналы пакетов очищены",
|
||||
"hexDataCopiedToClipboard": "Hex-данные скопированы в буфер",
|
||||
"developerModeEnabled": "Режим разработчика включён",
|
||||
"developerModeDisabled": "Режим разработчика отключён",
|
||||
"clipboardIsEmpty": "Буфер обмена пуст",
|
||||
"contactImported": "Контакт импортирован",
|
||||
"contactLinkCopiedToClipboard": "Ссылка на контакт скопирована в буфер",
|
||||
"failedToExportContact": "Не удалось экспортировать контакт",
|
||||
"noLogsToExport": "Нет журналов для экспорта",
|
||||
"exportAsCsv": "Экспортировать как CSV",
|
||||
"exportAsText": "Экспортировать как текст",
|
||||
"receivedRfc3339": "Получено (RFC3339)",
|
||||
"buildTime": "Время сборки",
|
||||
"downloadUrlNotAvailable": "URL загрузки недоступен",
|
||||
"cannotOpenDownloadUrl": "Не удаётся открыть URL загрузки",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Проверка обновлений доступна только на Android",
|
||||
"youAreRunningTheLatestVersion": "Вы используете последнюю версию",
|
||||
"updateAvailableButDownloadUrlNotFound": "Обновление доступно, но URL загрузки не найден",
|
||||
"startTictactoe": "Начать Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: противник неизвестен",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: ожидание начала",
|
||||
"acceptsShareLinks": "Принимает общие ссылки",
|
||||
"supportsRawHex": "Поддерживает raw hex",
|
||||
"clipboardfriendly": "Удобно для буфера",
|
||||
"captured": "Захвачено",
|
||||
"size": "Размер",
|
||||
"noCustomChannelsToClear": "Нет пользовательских каналов для очистки.",
|
||||
"noDeviceContactsToClear": "Нет контактов устройства для очистки."
|
||||
}
|
||||
|
||||
@@ -511,8 +511,7 @@
|
||||
"rawDump": "Surovi izpis",
|
||||
"cannotRetryMissingRecipient": "Ponovnega pošiljanja ni mogoče: informacije o prejemniku manjkajo",
|
||||
"voiceUnavailable": "Glas trenutno ni na voljo",
|
||||
"requestingVoice": "Zahteva za glasom"
|
||||
,
|
||||
"requestingVoice": "Zahteva za glasom",
|
||||
"device": "naprava",
|
||||
"change": "Spremeni",
|
||||
"wizardOverviewDescription": "Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.",
|
||||
@@ -564,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Brisanje poti ni uspelo: {error}",
|
||||
"contactRouteCleared": "Pot izbrisana",
|
||||
"contactRouteSet": "Pot nastavljena: {route}",
|
||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}"
|
||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Časovna omejitev ACK",
|
||||
"opcode": "Opkoda",
|
||||
"payload": "Vsebina",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Velikost zgoščevalne vrednosti",
|
||||
"pathBytes": "Bajti poti",
|
||||
"selectedPath": "Izbrana pot",
|
||||
"estimatedTx": "Ocenjeni čas oddaje",
|
||||
"senderToReceipt": "Od pošiljatelja do prejema",
|
||||
"receivedCopies": "Prejete kopije",
|
||||
"retryCause": "Razlog ponovitve",
|
||||
"retryMode": "Način ponovitve",
|
||||
"retryResult": "Rezultat ponovitve",
|
||||
"lastRetry": "Zadnja ponovitev",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hitrost",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Zakasnitev po oddaji",
|
||||
"bandpass": "Pasovno sito",
|
||||
"bandpassFilterVoice": "Pasovno sito za glas",
|
||||
"frequency": "Frekvenca",
|
||||
"australia": "Avstralija",
|
||||
"australiaNarrow": "Avstralija (ozko)",
|
||||
"australiaQld": "Avstralija: QLD",
|
||||
"australiaSaWa": "Avstralija: SA, WA",
|
||||
"newZealand": "Nova Zelandija",
|
||||
"newZealandNarrow": "Nova Zelandija (ozko)",
|
||||
"switzerland": "Švica",
|
||||
"portugal433": "Portugalska 433",
|
||||
"portugal868": "Portugalska 868",
|
||||
"czechRepublicNarrow": "Češka (ozko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (dolg doseg)",
|
||||
"euukDeprecated": "EU/UK (zastarelo)",
|
||||
"euukNarrow": "EU/UK (ozko)",
|
||||
"usacanadaRecommended": "ZDA/Kanada (priporočeno)",
|
||||
"vietnamDeprecated": "Vietnam (zastarelo)",
|
||||
"vietnamNarrow": "Vietnam (ozko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj stik",
|
||||
"all": "Vse",
|
||||
"autoResolve": "Samodejno razreši",
|
||||
"clearAllLabel": "Počisti vse",
|
||||
"clearRelays": "Počisti posrednike",
|
||||
"clearFilters": "Počisti filtre",
|
||||
"clearRoute": "Počisti pot",
|
||||
"clearMessages": "Počisti sporočila",
|
||||
"clearScale": "Počisti merilo",
|
||||
"clearDiscoveries": "Počisti odkritja",
|
||||
"clearOnlineTraceDatabase": "Počisti bazo sledi",
|
||||
"clearAllChannels": "Počisti vse kanale",
|
||||
"clearAllContacts": "Počisti vse stike",
|
||||
"clearChannels": "Počisti kanale",
|
||||
"clearContacts": "Počisti stike",
|
||||
"clearPathOnMaxRetry": "Počisti pot ob maks. ponovitvi",
|
||||
"create": "Ustvari",
|
||||
"custom": "Po meri",
|
||||
"defaultValue": "Privzeto",
|
||||
"duplicate": "Podvoji",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Odpri",
|
||||
"paste": "Prilepi",
|
||||
"preview": "Predogled",
|
||||
"remove": "Odstrani",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razreši vse",
|
||||
"send": "Pošlji",
|
||||
"sendAnyway": "Vseeno pošlji",
|
||||
"share": "Deli",
|
||||
"shareContact": "Deli stik",
|
||||
"trace": "Sledenje",
|
||||
"use": "Uporabi",
|
||||
"useSelectedFrequency": "Uporabi izbrano frekvenco",
|
||||
"discovery": "Odkrivanje",
|
||||
"discoverRepeaters": "Odkrij posrednike",
|
||||
"discoverSensors": "Odkrij senzorje",
|
||||
"repeaterDiscoverySent": "Odkrivanje posrednikov poslano",
|
||||
"sensorDiscoverySent": "Odkrivanje senzorjev poslano",
|
||||
"clearedPendingDiscoveries": "Čakalna odkritja počiščena.",
|
||||
"autoDiscovery": "Samodejno odkrivanje",
|
||||
"enableAutomaticAdding": "Omogoči samodejno dodajanje",
|
||||
"autoaddRepeaters": "Samodejno dodaj posrednike",
|
||||
"autoaddRoomServers": "Samodejno dodaj strežnike sob",
|
||||
"autoaddSensors": "Samodejno dodaj senzorje",
|
||||
"autoaddUsers": "Samodejno dodaj uporabnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarejše ob polnem",
|
||||
"storage": "Shramba",
|
||||
"dangerZone": "Nevarno območje",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Priljubljeni",
|
||||
"sensors": "Senzorji",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet v živo",
|
||||
"repeatersMap": "Zemljevid posrednikov",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "Dnevniki BLE paketov",
|
||||
"onlineTraceDatabase": "Baza sledi",
|
||||
"routePathByteSize": "Velikost poti v bajtih",
|
||||
"messageNotifications": "Obvestila o sporočilih",
|
||||
"sarAlerts": "SAR opozorila",
|
||||
"discoveryNotifications": "Obvestila o odkritjih",
|
||||
"updateNotifications": "Obvestila o posodobitvah",
|
||||
"muteWhileAppIsOpen": "Utišaj ko je aplikacija odprta",
|
||||
"disableContacts": "Onemogoči stike",
|
||||
"enableSensorsTab": "Omogoči zavihek Senzorji",
|
||||
"enableProfiles": "Omogoči profile",
|
||||
"autoRouteRotation": "Samodejno kroženje poti",
|
||||
"nearestRepeaterFallback": "Najbližji posrednik kot rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši vso shranjeno zgodovino sporočil",
|
||||
"messageFontSize": "Velikost pisave sporočil",
|
||||
"rotateMapWithHeading": "Vrti zemljevid s smerjo",
|
||||
"showMapDebugInfo": "Prikaži razhroščevalne info zemljevida",
|
||||
"openMapInFullscreen": "Odpri zemljevid na cel zaslon",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnem zemljevidu",
|
||||
"showAllContactTrailsLabel": "Prikaži vse sledi stikov",
|
||||
"hideRepeatersOnMap": "Skrij posrednike na zemljevidu",
|
||||
"setMapScale": "Nastavi merilo zemljevida",
|
||||
"customMapScaleSaved": "Merilo zemljevida shranjeno",
|
||||
"voiceBitrate": "Bitna hitrost glasu",
|
||||
"voiceCompressor": "Kompresor glasu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe in glasne govorne ravni",
|
||||
"voiceLimiter": "Omejevalnik glasu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Preprečuje rezanje vrhov pred kodiranjem",
|
||||
"micAutoGain": "Samodejna ojačitev mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Omogoči snemalniku prilagoditev vhodne ravni",
|
||||
"echoCancellation": "Odstranjevanje odmeva",
|
||||
"noiseSuppression": "Dušenje šuma",
|
||||
"trimSilenceInVoiceMessages": "Obreži tišino v glasovnih sporočilih",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Omejevalnik",
|
||||
"autoGain": "Samodejna ojačitev",
|
||||
"echoCancel": "Odmev",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Največja velikost slike",
|
||||
"imageCompression": "Stiskanje slik",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Hitri zasebni GPS podatki",
|
||||
"movementThreshold": "Prag gibanja",
|
||||
"fastGpsMovementThreshold": "Prag gibanja za hitri GPS",
|
||||
"fastGpsActiveuseInterval": "Interval uporabe hitrega GPS-a",
|
||||
"activeuseUpdateInterval": "Interval posodobitve pri aktivni uporabi",
|
||||
"repeatNearbyTraffic": "Ponavljaj bližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Posreduj skozi posrednike po mreži",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo bližnji, brez poplavljanja posrednikov",
|
||||
"multihop": "Večskočno",
|
||||
"createProfile": "Ustvari profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Nov profil",
|
||||
"manageProfiles": "Upravljaj profile",
|
||||
"enableProfilesToStartManagingThem": "Omogočite profile, da jih začnete upravljati.",
|
||||
"openMessage": "Odpri sporočilo",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezano SAR sporočilo",
|
||||
"removeSarMarker": "Odstrani SAR oznako",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Izberite cilj za pošiljanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka poslana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana v sobo",
|
||||
"loadFromGallery": "Naloži iz galerije",
|
||||
"replaceImage": "Zamenjaj sliko",
|
||||
"selectFromGallery": "Izberi iz galerije",
|
||||
"team": "Ekipa",
|
||||
"found": "Najdeno",
|
||||
"staging": "Zbirno mesto",
|
||||
"object": "Predmet",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Zmerno",
|
||||
"busy": "Zasedeno",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje ni našlo primernih frekvenc",
|
||||
"searchMessages": "Iskanje sporočil",
|
||||
"sendImageFromGallery": "Pošlji sliko iz galerije",
|
||||
"takePhoto": "Posnemi fotografijo",
|
||||
"dmOnly": "Samo neposredno",
|
||||
"allMessages": "Vsa sporočila",
|
||||
"sendToPublicChannel": "Pošlji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Izberite vrsto oznake in cilj",
|
||||
"noDestinationsAvailableLabel": "Ni razpoložljivih ciljev",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prenosi",
|
||||
"downloadedBy": "Preneseno od",
|
||||
"saveDiscoverySettings": "Shrani nastavitve odkrivanja",
|
||||
"savePublicInfo": "Shrani javne podatke",
|
||||
"saveRadioSettings": "Shrani radijske nastavitve",
|
||||
"savePath": "Shrani pot",
|
||||
"wipeDeviceData": "Izbriši podatke naprave",
|
||||
"wipeDevice": "Izbriši napravo",
|
||||
"destructiveDeviceActions": "Destruktivna dejanja na napravi.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Izberite prednastavitev ali natančno prilagodite radijske nastavitve.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Izberite ime in lokacijo, ki ju naprava deli.",
|
||||
"availableSpaceOnThisDevice": "Razpoložljiv prostor na tej napravi.",
|
||||
"used": "Uporabljeno",
|
||||
"total": "Skupaj",
|
||||
"renameValue": "Preimenuj vrednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Predogled v živo",
|
||||
"refreshSchedule": "Interval osvežitve",
|
||||
"noResponse": "Ni odgovora",
|
||||
"refreshing": "Osvežujem",
|
||||
"unavailable": "Nedostopno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Izberite posrednika ali vozlišče za opazovanje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Že med stiki",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se z napravo pred dodajanjem stikov",
|
||||
"fromContacts": "Iz stikov",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "V obeh",
|
||||
"source": "Vir",
|
||||
"manualRouteEdit": "Ročno urejanje poti",
|
||||
"observedMeshRoute": "Opazovana mrežna pot",
|
||||
"allMessagesCleared": "Vsa sporočila počiščena",
|
||||
"onlineTraceDatabaseCleared": "Baza sledi počiščena",
|
||||
"packetLogsCleared": "Dnevniki paketov počiščeni",
|
||||
"hexDataCopiedToClipboard": "Hex podatki kopirani v odložišče",
|
||||
"developerModeEnabled": "Razvojni način omogočen",
|
||||
"developerModeDisabled": "Razvojni način onemogočen",
|
||||
"clipboardIsEmpty": "Odložišče je prazno",
|
||||
"contactImported": "Stik uvožen",
|
||||
"contactLinkCopiedToClipboard": "Povezava na stik kopirana v odložišče",
|
||||
"failedToExportContact": "Izvoz stika ni uspel",
|
||||
"noLogsToExport": "Ni dnevnikov za izvoz",
|
||||
"exportAsCsv": "Izvozi kot CSV",
|
||||
"exportAsText": "Izvozi kot besedilo",
|
||||
"receivedRfc3339": "Prejeto (RFC3339)",
|
||||
"buildTime": "Čas gradnje",
|
||||
"downloadUrlNotAvailable": "URL za prenos ni na voljo",
|
||||
"cannotOpenDownloadUrl": "URL za prenos ni mogoče odpreti",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Preverjanje posodobitev je na voljo le na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Uporabljate najnovejšo različico",
|
||||
"updateAvailableButDownloadUrlNotFound": "Posodobitev na voljo, a URL za prenos ni najden",
|
||||
"startTictactoe": "Začni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe ni na voljo",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: nasprotnik neznan",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čakanje na začetek",
|
||||
"acceptsShareLinks": "Sprejema deljene povezave",
|
||||
"supportsRawHex": "Podpira surove hex",
|
||||
"clipboardfriendly": "Primerno za odložišče",
|
||||
"captured": "Zajeto",
|
||||
"size": "Velikost",
|
||||
"noCustomChannelsToClear": "Ni prilagojenih kanalov za brisanje.",
|
||||
"noDeviceContactsToClear": "Ni stikov naprave za brisanje."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Rota temizlenemedi: {error}",
|
||||
"contactRouteCleared": "Rota temizlendi",
|
||||
"contactRouteSet": "Rota ayarlandı: {route}",
|
||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}"
|
||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK zaman aşımı",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Yük",
|
||||
"hops": "Atlama",
|
||||
"hashSize": "Hash boyutu",
|
||||
"pathBytes": "Yol baytları",
|
||||
"selectedPath": "Seçilen yol",
|
||||
"estimatedTx": "Tahmini gönderim",
|
||||
"senderToReceipt": "Gönderenden alındıya",
|
||||
"receivedCopies": "Alınan kopyalar",
|
||||
"retryCause": "Yeniden deneme nedeni",
|
||||
"retryMode": "Yeniden deneme modu",
|
||||
"retryResult": "Yeniden deneme sonucu",
|
||||
"lastRetry": "Son deneme",
|
||||
"rxPackets": "RX paketleri",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hız",
|
||||
"window": "Pencere",
|
||||
"posttxDelay": "Gönderim sonrası gecikme",
|
||||
"bandpass": "Bant geçiren",
|
||||
"bandpassFilterVoice": "Bant geçiren filtre ses",
|
||||
"frequency": "Frekans",
|
||||
"australia": "Avustralya",
|
||||
"australiaNarrow": "Avustralya (Dar)",
|
||||
"australiaQld": "Avustralya: QLD",
|
||||
"australiaSaWa": "Avustralya: SA, WA",
|
||||
"newZealand": "Yeni Zelanda",
|
||||
"newZealandNarrow": "Yeni Zelanda (Dar)",
|
||||
"switzerland": "İsviçre",
|
||||
"portugal433": "Portekiz 433",
|
||||
"portugal868": "Portekiz 868",
|
||||
"czechRepublicNarrow": "Çek Cumhuriyeti (Dar)",
|
||||
"eu433mhzLongRange": "AB 433MHz (Uzun Menzil)",
|
||||
"euukDeprecated": "AB/BK (Kullanımdan kalktı)",
|
||||
"euukNarrow": "AB/BK (Dar)",
|
||||
"usacanadaRecommended": "ABD/Kanada (Önerilen)",
|
||||
"vietnamDeprecated": "Vietnam (Kullanımdan kalktı)",
|
||||
"vietnamNarrow": "Vietnam (Dar)",
|
||||
"active": "Aktif",
|
||||
"addContact": "Kişi ekle",
|
||||
"all": "Tümü",
|
||||
"autoResolve": "Otomatik çöz",
|
||||
"clearAllLabel": "Tümünü temizle",
|
||||
"clearRelays": "Röleleri temizle",
|
||||
"clearFilters": "Filtreleri temizle",
|
||||
"clearRoute": "Rotayı temizle",
|
||||
"clearMessages": "Mesajları temizle",
|
||||
"clearScale": "Ölçeği temizle",
|
||||
"clearDiscoveries": "Keşifleri temizle",
|
||||
"clearOnlineTraceDatabase": "Çevrimiçi iz veritabanını temizle",
|
||||
"clearAllChannels": "Tüm kanalları temizle",
|
||||
"clearAllContacts": "Tüm kişileri temizle",
|
||||
"clearChannels": "Kanalları temizle",
|
||||
"clearContacts": "Kişileri temizle",
|
||||
"clearPathOnMaxRetry": "Maks denemede yolu temizle",
|
||||
"create": "Oluştur",
|
||||
"custom": "Özel",
|
||||
"defaultValue": "Varsayılan",
|
||||
"duplicate": "Çoğalt",
|
||||
"editName": "Adı düzenle",
|
||||
"open": "Aç",
|
||||
"paste": "Yapıştır",
|
||||
"preview": "Önizleme",
|
||||
"remove": "Kaldır",
|
||||
"rename": "Yeniden adlandır",
|
||||
"resolveAll": "Tümünü çöz",
|
||||
"send": "Gönder",
|
||||
"sendAnyway": "Yine de gönder",
|
||||
"share": "Paylaş",
|
||||
"shareContact": "Kişiyi paylaş",
|
||||
"trace": "İz",
|
||||
"use": "Kullan",
|
||||
"useSelectedFrequency": "Seçilen frekansı kullan",
|
||||
"discovery": "Keşif",
|
||||
"discoverRepeaters": "Tekrarlayıcıları keşfet",
|
||||
"discoverSensors": "Sensörleri keşfet",
|
||||
"repeaterDiscoverySent": "Tekrarlayıcı keşfi gönderildi",
|
||||
"sensorDiscoverySent": "Sensör keşfi gönderildi",
|
||||
"clearedPendingDiscoveries": "Bekleyen keşifler temizlendi.",
|
||||
"autoDiscovery": "Otomatik keşif",
|
||||
"enableAutomaticAdding": "Otomatik eklemeyi etkinleştir",
|
||||
"autoaddRepeaters": "Tekrarlayıcıları otomatik ekle",
|
||||
"autoaddRoomServers": "Oda sunucularını otomatik ekle",
|
||||
"autoaddSensors": "Sensörleri otomatik ekle",
|
||||
"autoaddUsers": "Kullanıcıları otomatik ekle",
|
||||
"overwriteOldestWhenFull": "Dolduğunda en eskiyi üzerine yaz",
|
||||
"storage": "Depolama",
|
||||
"dangerZone": "Tehlikeli bölge",
|
||||
"profiles": "Profiller",
|
||||
"favourites": "Favoriler",
|
||||
"sensors": "Sensörler",
|
||||
"others": "Diğerleri",
|
||||
"gpsModule": "GPS Modülü",
|
||||
"liveTraffic": "Canlı trafik",
|
||||
"repeatersMap": "Tekrarlayıcılar haritası",
|
||||
"spectrumScan": "Spektrum taraması",
|
||||
"blePacketLogs": "BLE paket günlükleri",
|
||||
"onlineTraceDatabase": "Çevrimiçi iz veritabanı",
|
||||
"routePathByteSize": "Yol bayt boyutu",
|
||||
"messageNotifications": "Mesaj bildirimleri",
|
||||
"sarAlerts": "SAR uyarıları",
|
||||
"discoveryNotifications": "Keşif bildirimleri",
|
||||
"updateNotifications": "Güncelleme bildirimleri",
|
||||
"muteWhileAppIsOpen": "Uygulama açıkken sessize al",
|
||||
"disableContacts": "Kişileri devre dışı bırak",
|
||||
"enableSensorsTab": "Sensörler sekmesini etkinleştir",
|
||||
"enableProfiles": "Profilleri etkinleştir",
|
||||
"autoRouteRotation": "Otomatik rota rotasyonu",
|
||||
"nearestRepeaterFallback": "En yakın tekrarlayıcıya geri dönüş",
|
||||
"deleteAllStoredMessageHistory": "Tüm mesaj geçmişini sil",
|
||||
"messageFontSize": "Mesaj yazı tipi boyutu",
|
||||
"rotateMapWithHeading": "Haritayı yön ile döndür",
|
||||
"showMapDebugInfo": "Harita hata ayıklama bilgisini göster",
|
||||
"openMapInFullscreen": "Haritayı tam ekranda aç",
|
||||
"showSarMarkersLabel": "SAR işaretçilerini göster",
|
||||
"displaySarMarkersOnTheMainMap": "SAR işaretçilerini ana haritada göster",
|
||||
"showAllContactTrailsLabel": "Tüm kişi izlerini göster",
|
||||
"hideRepeatersOnMap": "Haritada tekrarlayıcıları gizle",
|
||||
"setMapScale": "Harita ölçeğini ayarla",
|
||||
"customMapScaleSaved": "Özel harita ölçeği kaydedildi",
|
||||
"voiceBitrate": "Ses bit hızı",
|
||||
"voiceCompressor": "Ses sıkıştırıcı",
|
||||
"balancesQuietAndLoudSpeechLevels": "Sessiz ve yüksek konuşma seviyelerini dengeler",
|
||||
"voiceLimiter": "Ses sınırlayıcı",
|
||||
"preventsClippingPeaksBeforeEncoding": "Kodlama öncesi tepe kırpmasını önler",
|
||||
"micAutoGain": "Mikrofon otomatik kazancı",
|
||||
"letsTheRecorderAdjustInputLevel": "Kaydedicinin giriş seviyesini ayarlamasına izin verir",
|
||||
"echoCancellation": "Yankı iptali",
|
||||
"noiseSuppression": "Gürültü bastırma",
|
||||
"trimSilenceInVoiceMessages": "Sesli mesajlarda sessizliği kırp",
|
||||
"compressor": "Sıkıştırıcı",
|
||||
"limiter": "Sınırlayıcı",
|
||||
"autoGain": "Otomatik kazanç",
|
||||
"echoCancel": "Yankı",
|
||||
"noiseSuppress": "Gürültü",
|
||||
"silenceTrim": "Sessizlik",
|
||||
"maxImageSize": "Maksimum resim boyutu",
|
||||
"imageCompression": "Resim sıkıştırma",
|
||||
"grayscale": "Gri tonlama",
|
||||
"ultraMode": "Ultra mod",
|
||||
"fastPrivateGpsUpdates": "Hızlı özel GPS güncellemeleri",
|
||||
"movementThreshold": "Hareket eşiği",
|
||||
"fastGpsMovementThreshold": "Hızlı GPS hareket eşiği",
|
||||
"fastGpsActiveuseInterval": "Hızlı GPS aktif kullanım aralığı",
|
||||
"activeuseUpdateInterval": "Aktif kullanım güncelleme aralığı",
|
||||
"repeatNearbyTraffic": "Yakın trafiği tekrarla",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Yalnızca yakın, tekrarlayıcı taşkını olmadan",
|
||||
"multihop": "Çok atlamalı",
|
||||
"createProfile": "Profil oluştur",
|
||||
"renameProfile": "Profili yeniden adlandır",
|
||||
"newProfile": "Yeni profil",
|
||||
"manageProfiles": "Profilleri yönet",
|
||||
"enableProfilesToStartManagingThem": "Profilleri yönetmeye başlamak için etkinleştirin.",
|
||||
"openMessage": "Mesajı aç",
|
||||
"jumpToTheRelatedSarMessage": "İlgili SAR mesajına git",
|
||||
"removeSarMarker": "SAR işaretçisini kaldır",
|
||||
"pleaseSelectADestinationToSendSarMarker": "SAR işaretçisi göndermek için bir hedef seçin",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR işaretçisi genel kanala yayınlandı",
|
||||
"sarMarkerSentToRoom": "SAR işaretçisi odaya gönderildi",
|
||||
"loadFromGallery": "Galeriden yükle",
|
||||
"replaceImage": "Resmi değiştir",
|
||||
"selectFromGallery": "Galeriden seç",
|
||||
"team": "Takım",
|
||||
"found": "Bulundu",
|
||||
"staging": "Toplanma alanı",
|
||||
"object": "Nesne",
|
||||
"quiet": "Sessiz",
|
||||
"moderate": "Orta",
|
||||
"busy": "Meşgul",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum taraması aday frekans bulamadı",
|
||||
"searchMessages": "Mesajlarda ara",
|
||||
"sendImageFromGallery": "Galeriden resim gönder",
|
||||
"takePhoto": "Fotoğraf çek",
|
||||
"dmOnly": "Yalnızca DM",
|
||||
"allMessages": "Tüm mesajlar",
|
||||
"sendToPublicChannel": "Genel kanala gönderilsin mi?",
|
||||
"selectMarkerTypeAndDestination": "İşaretçi türünü ve hedefi seçin",
|
||||
"noDestinationsAvailableLabel": "Kullanılabilir hedef yok",
|
||||
"image": "Resim",
|
||||
"format": "Biçim",
|
||||
"dimensions": "Boyutlar",
|
||||
"segments": "Segmentler",
|
||||
"transfers": "Transferler",
|
||||
"downloadedBy": "İndiren",
|
||||
"saveDiscoverySettings": "Keşif ayarlarını kaydet",
|
||||
"savePublicInfo": "Genel bilgileri kaydet",
|
||||
"saveRadioSettings": "Radyo ayarlarını kaydet",
|
||||
"savePath": "Yolu kaydet",
|
||||
"wipeDeviceData": "Cihaz verilerini sil",
|
||||
"wipeDevice": "Cihazı sil",
|
||||
"destructiveDeviceActions": "Yıkıcı cihaz işlemleri.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Bu cihazın paylaştığı ad ve konumu seçin.",
|
||||
"availableSpaceOnThisDevice": "Bu cihazda kullanılabilir alan.",
|
||||
"used": "Kullanılan",
|
||||
"total": "Toplam",
|
||||
"renameValue": "Değeri yeniden adlandır",
|
||||
"customizeFields": "Alanları özelleştir",
|
||||
"livePreview": "Canlı önizleme",
|
||||
"refreshSchedule": "Yenileme programı",
|
||||
"noResponse": "Yanıt yok",
|
||||
"refreshing": "Yenileniyor",
|
||||
"unavailable": "Kullanılamaz",
|
||||
"pickARelayOrNodeToWatchInSensors": "İzlemek için bir röle veya düğüm seçin.",
|
||||
"publicKeyLabel": "Genel anahtar",
|
||||
"alreadyInContacts": "Zaten kişilerde",
|
||||
"connectToADeviceBeforeAddingContacts": "Kişi eklemeden önce bir cihaza bağlanın",
|
||||
"fromContacts": "Kişilerden",
|
||||
"onlineOnly": "Yalnızca çevrimiçi",
|
||||
"inBoth": "Her ikisinde",
|
||||
"source": "Kaynak",
|
||||
"manualRouteEdit": "Manuel rota düzenle",
|
||||
"observedMeshRoute": "Gözlemlenen mesh rotası",
|
||||
"allMessagesCleared": "Tüm mesajlar temizlendi",
|
||||
"onlineTraceDatabaseCleared": "Çevrimiçi iz veritabanı temizlendi",
|
||||
"packetLogsCleared": "Paket günlükleri temizlendi",
|
||||
"hexDataCopiedToClipboard": "Hex verisi panoya kopyalandı",
|
||||
"developerModeEnabled": "Geliştirici modu etkinleştirildi",
|
||||
"developerModeDisabled": "Geliştirici modu devre dışı bırakıldı",
|
||||
"clipboardIsEmpty": "Pano boş",
|
||||
"contactImported": "Kişi içe aktarıldı",
|
||||
"contactLinkCopiedToClipboard": "Kişi bağlantısı panoya kopyalandı",
|
||||
"failedToExportContact": "Kişi dışa aktarılamadı",
|
||||
"noLogsToExport": "Dışa aktarılacak günlük yok",
|
||||
"exportAsCsv": "CSV olarak dışa aktar",
|
||||
"exportAsText": "Metin olarak dışa aktar",
|
||||
"receivedRfc3339": "Alındı (RFC3339)",
|
||||
"buildTime": "Derleme zamanı",
|
||||
"downloadUrlNotAvailable": "İndirme URL'si mevcut değil",
|
||||
"cannotOpenDownloadUrl": "İndirme URL'si açılamıyor",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Güncelleme kontrolü yalnızca Android'de kullanılabilir",
|
||||
"youAreRunningTheLatestVersion": "En son sürümü kullanıyorsunuz",
|
||||
"updateAvailableButDownloadUrlNotFound": "Güncelleme mevcut ancak indirme URL'si bulunamadı",
|
||||
"startTictactoe": "Tic-Tac-Toe başlat",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe kullanılamıyor",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: rakip bilinmiyor",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: başlangıç bekleniyor",
|
||||
"acceptsShareLinks": "Paylaşım bağlantılarını kabul eder",
|
||||
"supportsRawHex": "Ham hex destekler",
|
||||
"clipboardfriendly": "Pano dostu",
|
||||
"captured": "Yakalandı",
|
||||
"size": "Boyut",
|
||||
"noCustomChannelsToClear": "Temizlenecek özel kanal yok.",
|
||||
"noDeviceContactsToClear": "Temizlenecek cihaz kişisi yok."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Не вдалося очистити маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищено",
|
||||
"contactRouteSet": "Маршрут задано: {route}",
|
||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}"
|
||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Тайм-аут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Корисне навантаження",
|
||||
"hops": "Хопи",
|
||||
"hashSize": "Розмір хешу",
|
||||
"pathBytes": "Байти шляху",
|
||||
"selectedPath": "Обраний шлях",
|
||||
"estimatedTx": "Очікувана передача",
|
||||
"senderToReceipt": "Від відправника до отримання",
|
||||
"receivedCopies": "Отримані копії",
|
||||
"retryCause": "Причина повтору",
|
||||
"retryMode": "Режим повтору",
|
||||
"retryResult": "Результат повтору",
|
||||
"lastRetry": "Остання спроба",
|
||||
"rxPackets": "RX-пакети",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Швидкість",
|
||||
"window": "Вікно",
|
||||
"posttxDelay": "Затримка після передачі",
|
||||
"bandpass": "Смуговий",
|
||||
"bandpassFilterVoice": "Смуговий фільтр голосу",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралія",
|
||||
"australiaNarrow": "Австралія (Вузький)",
|
||||
"australiaQld": "Австралія: QLD",
|
||||
"australiaSaWa": "Австралія: SA, WA",
|
||||
"newZealand": "Нова Зеландія",
|
||||
"newZealandNarrow": "Нова Зеландія (Вузький)",
|
||||
"switzerland": "Швейцарія",
|
||||
"portugal433": "Португалія 433",
|
||||
"portugal868": "Португалія 868",
|
||||
"czechRepublicNarrow": "Чехія (Вузький)",
|
||||
"eu433mhzLongRange": "ЄС 433МГц (Далекий)",
|
||||
"euukDeprecated": "ЄС/Великобританія (Застаріле)",
|
||||
"euukNarrow": "ЄС/Великобританія (Вузький)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендовано)",
|
||||
"vietnamDeprecated": "В'єтнам (Застаріле)",
|
||||
"vietnamNarrow": "В'єтнам (Вузький)",
|
||||
"active": "Активно",
|
||||
"addContact": "Додати контакт",
|
||||
"all": "Усі",
|
||||
"autoResolve": "Автоматичне вирішення",
|
||||
"clearAllLabel": "Очистити все",
|
||||
"clearRelays": "Очистити реле",
|
||||
"clearFilters": "Очистити фільтри",
|
||||
"clearRoute": "Очистити маршрут",
|
||||
"clearMessages": "Очистити повідомлення",
|
||||
"clearScale": "Очистити масштаб",
|
||||
"clearDiscoveries": "Очистити виявлення",
|
||||
"clearOnlineTraceDatabase": "Очистити базу трасувань",
|
||||
"clearAllChannels": "Очистити всі канали",
|
||||
"clearAllContacts": "Очистити всі контакти",
|
||||
"clearChannels": "Очистити канали",
|
||||
"clearContacts": "Очистити контакти",
|
||||
"clearPathOnMaxRetry": "Очистити шлях при макс. спробі",
|
||||
"create": "Створити",
|
||||
"custom": "Користувацький",
|
||||
"defaultValue": "За замовчуванням",
|
||||
"duplicate": "Дублювати",
|
||||
"editName": "Редагувати ім'я",
|
||||
"open": "Відкрити",
|
||||
"paste": "Вставити",
|
||||
"preview": "Попередній перегляд",
|
||||
"remove": "Видалити",
|
||||
"rename": "Перейменувати",
|
||||
"resolveAll": "Вирішити все",
|
||||
"send": "Надіслати",
|
||||
"sendAnyway": "Надіслати все одно",
|
||||
"share": "Поділитися",
|
||||
"shareContact": "Поділитися контактом",
|
||||
"trace": "Трасування",
|
||||
"use": "Використати",
|
||||
"useSelectedFrequency": "Використати обрану частоту",
|
||||
"discovery": "Виявлення",
|
||||
"discoverRepeaters": "Виявити повторювачі",
|
||||
"discoverSensors": "Виявити датчики",
|
||||
"repeaterDiscoverySent": "Виявлення повторювачів надіслано",
|
||||
"sensorDiscoverySent": "Виявлення датчиків надіслано",
|
||||
"clearedPendingDiscoveries": "Очікувані виявлення очищено.",
|
||||
"autoDiscovery": "Автоматичне виявлення",
|
||||
"enableAutomaticAdding": "Увімкнути автоматичне додавання",
|
||||
"autoaddRepeaters": "Автододавання повторювачів",
|
||||
"autoaddRoomServers": "Автододавання серверів кімнат",
|
||||
"autoaddSensors": "Автододавання датчиків",
|
||||
"autoaddUsers": "Автододавання користувачів",
|
||||
"overwriteOldestWhenFull": "Перезаписати найстаріші при заповненні",
|
||||
"storage": "Сховище",
|
||||
"dangerZone": "Небезпечна зона",
|
||||
"profiles": "Профілі",
|
||||
"favourites": "Обране",
|
||||
"sensors": "Датчики",
|
||||
"others": "Інші",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живий трафік",
|
||||
"repeatersMap": "Карта повторювачів",
|
||||
"spectrumScan": "Сканування спектру",
|
||||
"blePacketLogs": "Журнали BLE-пакетів",
|
||||
"onlineTraceDatabase": "База трасувань",
|
||||
"routePathByteSize": "Розмір шляху в байтах",
|
||||
"messageNotifications": "Сповіщення про повідомлення",
|
||||
"sarAlerts": "SAR-сповіщення",
|
||||
"discoveryNotifications": "Сповіщення про виявлення",
|
||||
"updateNotifications": "Сповіщення про оновлення",
|
||||
"muteWhileAppIsOpen": "Без звуку при відкритому додатку",
|
||||
"disableContacts": "Вимкнути контакти",
|
||||
"enableSensorsTab": "Увімкнути вкладку Датчики",
|
||||
"enableProfiles": "Увімкнути профілі",
|
||||
"autoRouteRotation": "Автоматична ротація маршруту",
|
||||
"nearestRepeaterFallback": "Найближчий повторювач як резерв",
|
||||
"deleteAllStoredMessageHistory": "Видалити всю історію повідомлень",
|
||||
"messageFontSize": "Розмір шрифту повідомлень",
|
||||
"rotateMapWithHeading": "Обертати карту за напрямком",
|
||||
"showMapDebugInfo": "Показати налагоджувальну інфо карти",
|
||||
"openMapInFullscreen": "Відкрити карту на повний екран",
|
||||
"showSarMarkersLabel": "Показати SAR-маркери",
|
||||
"displaySarMarkersOnTheMainMap": "Показувати SAR-маркери на головній карті",
|
||||
"showAllContactTrailsLabel": "Показати всі сліди контактів",
|
||||
"hideRepeatersOnMap": "Сховати повторювачі на карті",
|
||||
"setMapScale": "Встановити масштаб карти",
|
||||
"customMapScaleSaved": "Користувацький масштаб карти збережено",
|
||||
"voiceBitrate": "Бітрейт голосу",
|
||||
"voiceCompressor": "Компресор голосу",
|
||||
"balancesQuietAndLoudSpeechLevels": "Вирівнює тихий і гучний рівні мови",
|
||||
"voiceLimiter": "Лімітер голосу",
|
||||
"preventsClippingPeaksBeforeEncoding": "Запобігає обрізанню піків перед кодуванням",
|
||||
"micAutoGain": "Автопідсилення мікрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Дозволяє рекордеру налаштувати рівень входу",
|
||||
"echoCancellation": "Придушення ехо",
|
||||
"noiseSuppression": "Придушення шуму",
|
||||
"trimSilenceInVoiceMessages": "Обрізати тишу в голосових повідомленнях",
|
||||
"compressor": "Компресор",
|
||||
"limiter": "Лімітер",
|
||||
"autoGain": "Автопідсилення",
|
||||
"echoCancel": "Ехо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тиша",
|
||||
"maxImageSize": "Максимальний розмір зображення",
|
||||
"imageCompression": "Стиснення зображення",
|
||||
"grayscale": "Відтінки сірого",
|
||||
"ultraMode": "Ультра режим",
|
||||
"fastPrivateGpsUpdates": "Швидкі приватні GPS-оновлення",
|
||||
"movementThreshold": "Поріг руху",
|
||||
"fastGpsMovementThreshold": "Поріг руху швидкого GPS",
|
||||
"fastGpsActiveuseInterval": "Інтервал активного використання швидкого GPS",
|
||||
"activeuseUpdateInterval": "Інтервал оновлення при активному використанні",
|
||||
"repeatNearbyTraffic": "Повторювати близький трафік",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслювати через повторювачі по мережі",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Лише поблизу, без flood повторювачів",
|
||||
"multihop": "Багатохоповий",
|
||||
"createProfile": "Створити профіль",
|
||||
"renameProfile": "Перейменувати профіль",
|
||||
"newProfile": "Новий профіль",
|
||||
"manageProfiles": "Керувати профілями",
|
||||
"enableProfilesToStartManagingThem": "Увімкніть профілі, щоб почати керувати ними.",
|
||||
"openMessage": "Відкрити повідомлення",
|
||||
"jumpToTheRelatedSarMessage": "Перейти до пов'язаного SAR-повідомлення",
|
||||
"removeSarMarker": "Видалити SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Виберіть призначення для надсилання SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер надіслано на публічний канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер надіслано в кімнату",
|
||||
"loadFromGallery": "Завантажити з галереї",
|
||||
"replaceImage": "Замінити зображення",
|
||||
"selectFromGallery": "Вибрати з галереї",
|
||||
"team": "Команда",
|
||||
"found": "Знайдено",
|
||||
"staging": "Місце збору",
|
||||
"object": "Об'єкт",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Помірно",
|
||||
"busy": "Зайнято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканування спектру не знайшло частот-кандидатів",
|
||||
"searchMessages": "Пошук повідомлень",
|
||||
"sendImageFromGallery": "Надіслати зображення з галереї",
|
||||
"takePhoto": "Зробити фото",
|
||||
"dmOnly": "Лише особисті повідомлення",
|
||||
"allMessages": "Усі повідомлення",
|
||||
"sendToPublicChannel": "Надіслати в публічний канал?",
|
||||
"selectMarkerTypeAndDestination": "Виберіть тип маркера та призначення",
|
||||
"noDestinationsAvailableLabel": "Немає доступних призначень",
|
||||
"image": "Зображення",
|
||||
"format": "Формат",
|
||||
"dimensions": "Розміри",
|
||||
"segments": "Сегменти",
|
||||
"transfers": "Передачі",
|
||||
"downloadedBy": "Завантажено",
|
||||
"saveDiscoverySettings": "Зберегти налаштування виявлення",
|
||||
"savePublicInfo": "Зберегти публічну інформацію",
|
||||
"saveRadioSettings": "Зберегти налаштування радіо",
|
||||
"savePath": "Зберегти шлях",
|
||||
"wipeDeviceData": "Стерти дані пристрою",
|
||||
"wipeDevice": "Стерти пристрій",
|
||||
"destructiveDeviceActions": "Деструктивні дії пристрою.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Оберіть пресет або налаштуйте радіо вручну.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Оберіть ім'я та місцеположення, якими ділиться пристрій.",
|
||||
"availableSpaceOnThisDevice": "Доступний простір на цьому пристрої.",
|
||||
"used": "Використано",
|
||||
"total": "Загалом",
|
||||
"renameValue": "Перейменувати значення",
|
||||
"customizeFields": "Налаштувати поля",
|
||||
"livePreview": "Попередній перегляд наживо",
|
||||
"refreshSchedule": "Розклад оновлення",
|
||||
"noResponse": "Немає відповіді",
|
||||
"refreshing": "Оновлення",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Оберіть реле або вузол для спостереження.",
|
||||
"publicKeyLabel": "Публічний ключ",
|
||||
"alreadyInContacts": "Вже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Підключіться до пристрою перед додаванням контактів",
|
||||
"fromContacts": "З контактів",
|
||||
"onlineOnly": "Лише онлайн",
|
||||
"inBoth": "В обох",
|
||||
"source": "Джерело",
|
||||
"manualRouteEdit": "Ручне редагування маршруту",
|
||||
"observedMeshRoute": "Спостережуваний mesh-маршрут",
|
||||
"allMessagesCleared": "Усі повідомлення очищено",
|
||||
"onlineTraceDatabaseCleared": "Базу трасувань очищено",
|
||||
"packetLogsCleared": "Журнали пакетів очищено",
|
||||
"hexDataCopiedToClipboard": "Hex-дані скопійовано в буфер",
|
||||
"developerModeEnabled": "Режим розробника увімкнено",
|
||||
"developerModeDisabled": "Режим розробника вимкнено",
|
||||
"clipboardIsEmpty": "Буфер обміну порожній",
|
||||
"contactImported": "Контакт імпортовано",
|
||||
"contactLinkCopiedToClipboard": "Посилання на контакт скопійовано в буфер",
|
||||
"failedToExportContact": "Не вдалося експортувати контакт",
|
||||
"noLogsToExport": "Немає журналів для експорту",
|
||||
"exportAsCsv": "Експортувати як CSV",
|
||||
"exportAsText": "Експортувати як текст",
|
||||
"receivedRfc3339": "Отримано (RFC3339)",
|
||||
"buildTime": "Час збірки",
|
||||
"downloadUrlNotAvailable": "URL завантаження недоступний",
|
||||
"cannotOpenDownloadUrl": "Не вдається відкрити URL завантаження",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Перевірка оновлень доступна лише на Android",
|
||||
"youAreRunningTheLatestVersion": "Ви використовуєте останню версію",
|
||||
"updateAvailableButDownloadUrlNotFound": "Оновлення доступне, але URL завантаження не знайдено",
|
||||
"startTictactoe": "Почати Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: суперник невідомий",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: очікування початку",
|
||||
"acceptsShareLinks": "Приймає посилання для обміну",
|
||||
"supportsRawHex": "Підтримує raw hex",
|
||||
"clipboardfriendly": "Зручно для буфера",
|
||||
"captured": "Захоплено",
|
||||
"size": "Розмір",
|
||||
"noCustomChannelsToClear": "Немає користувацьких каналів для очищення.",
|
||||
"noDeviceContactsToClear": "Немає контактів пристрою для очищення."
|
||||
}
|
||||
|
||||
@@ -2228,80 +2228,55 @@
|
||||
"description": "没有可发送的聊天联系人时的消息"
|
||||
},
|
||||
"reply": "回复",
|
||||
"@reply": {
|
||||
},
|
||||
"@reply": {},
|
||||
"technicalDetails": "技术细节",
|
||||
"@technicalDetails": {
|
||||
},
|
||||
"@technicalDetails": {},
|
||||
"messageTechnicalDetails": "消息技术细节",
|
||||
"@messageTechnicalDetails": {
|
||||
},
|
||||
"@messageTechnicalDetails": {},
|
||||
"linkQuality": "链路质量",
|
||||
"@linkQuality": {
|
||||
},
|
||||
"@linkQuality": {},
|
||||
"delivery": "投递",
|
||||
"@delivery": {
|
||||
},
|
||||
"@delivery": {},
|
||||
"status": "状态",
|
||||
"@status": {
|
||||
},
|
||||
"@status": {},
|
||||
"expectedAckTag": "预期的 ACK 标签",
|
||||
"@expectedAckTag": {
|
||||
},
|
||||
"@expectedAckTag": {},
|
||||
"roundTrip": "往返时间",
|
||||
"@roundTrip": {
|
||||
},
|
||||
"@roundTrip": {},
|
||||
"retryAttempt": "重试尝试",
|
||||
"@retryAttempt": {
|
||||
},
|
||||
"@retryAttempt": {},
|
||||
"floodFallback": "泛洪回退",
|
||||
"@floodFallback": {
|
||||
},
|
||||
"@floodFallback": {},
|
||||
"identity": "身份",
|
||||
"@identity": {
|
||||
},
|
||||
"@identity": {},
|
||||
"messageId": "消息 ID",
|
||||
"@messageId": {
|
||||
},
|
||||
"@messageId": {},
|
||||
"sender": "发送者",
|
||||
"@sender": {
|
||||
},
|
||||
"@sender": {},
|
||||
"senderKey": "发送者密钥",
|
||||
"@senderKey": {
|
||||
},
|
||||
"@senderKey": {},
|
||||
"recipient": "接收者",
|
||||
"@recipient": {
|
||||
},
|
||||
"@recipient": {},
|
||||
"recipientKey": "接收者密钥",
|
||||
"@recipientKey": {
|
||||
},
|
||||
"@recipientKey": {},
|
||||
"voice": "语音",
|
||||
"@voice": {
|
||||
},
|
||||
"@voice": {},
|
||||
"voiceId": "语音 ID",
|
||||
"@voiceId": {
|
||||
},
|
||||
"@voiceId": {},
|
||||
"envelope": "信封",
|
||||
"@envelope": {
|
||||
},
|
||||
"@envelope": {},
|
||||
"sessionProgress": "会话进度",
|
||||
"@sessionProgress": {
|
||||
},
|
||||
"@sessionProgress": {},
|
||||
"complete": "完成",
|
||||
"@complete": {
|
||||
},
|
||||
"@complete": {},
|
||||
"rawDump": "原始数据",
|
||||
"@rawDump": {
|
||||
},
|
||||
"@rawDump": {},
|
||||
"cannotRetryMissingRecipient": "无法重试:缺少接收者信息",
|
||||
"@cannotRetryMissingRecipient": {
|
||||
},
|
||||
"@cannotRetryMissingRecipient": {},
|
||||
"voiceUnavailable": "当前语音不可用",
|
||||
"@voiceUnavailable": {
|
||||
},
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "正在请求语音",
|
||||
"@requestingVoice": {
|
||||
},
|
||||
"@requestingVoice": {},
|
||||
"device": "设备",
|
||||
"change": "更改",
|
||||
"wizardOverviewDescription": "此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。",
|
||||
@@ -2353,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "清除路由失败:{error}",
|
||||
"contactRouteCleared": "路由已清除",
|
||||
"contactRouteSet": "路由已设置:{route}",
|
||||
"contactFailedToSetRoute": "设置路由失败:{error}"
|
||||
"contactFailedToSetRoute": "设置路由失败:{error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK 超时",
|
||||
"opcode": "操作码",
|
||||
"payload": "有效载荷",
|
||||
"hops": "跳数",
|
||||
"hashSize": "哈希大小",
|
||||
"pathBytes": "路径字节",
|
||||
"selectedPath": "选定路径",
|
||||
"estimatedTx": "预计传输",
|
||||
"senderToReceipt": "发送到接收",
|
||||
"receivedCopies": "收到的副本",
|
||||
"retryCause": "重试原因",
|
||||
"retryMode": "重试模式",
|
||||
"retryResult": "重试结果",
|
||||
"lastRetry": "最后重试",
|
||||
"rxPackets": "RX 数据包",
|
||||
"mesh": "Mesh",
|
||||
"rate": "速率",
|
||||
"window": "窗口",
|
||||
"posttxDelay": "发送后延迟",
|
||||
"bandpass": "带通",
|
||||
"bandpassFilterVoice": "带通语音滤波",
|
||||
"frequency": "频率",
|
||||
"australia": "澳大利亚",
|
||||
"australiaNarrow": "澳大利亚(窄带)",
|
||||
"australiaQld": "澳大利亚:QLD",
|
||||
"australiaSaWa": "澳大利亚:SA, WA",
|
||||
"newZealand": "新西兰",
|
||||
"newZealandNarrow": "新西兰(窄带)",
|
||||
"switzerland": "瑞士",
|
||||
"portugal433": "葡萄牙 433",
|
||||
"portugal868": "葡萄牙 868",
|
||||
"czechRepublicNarrow": "捷克(窄带)",
|
||||
"eu433mhzLongRange": "欧盟 433MHz(远距离)",
|
||||
"euukDeprecated": "欧盟/英国(已弃用)",
|
||||
"euukNarrow": "欧盟/英国(窄带)",
|
||||
"usacanadaRecommended": "美国/加拿大(推荐)",
|
||||
"vietnamDeprecated": "越南(已弃用)",
|
||||
"vietnamNarrow": "越南(窄带)",
|
||||
"active": "活动",
|
||||
"addContact": "添加联系人",
|
||||
"all": "全部",
|
||||
"autoResolve": "自动解析",
|
||||
"clearAllLabel": "清除全部",
|
||||
"clearRelays": "清除中继",
|
||||
"clearFilters": "清除过滤器",
|
||||
"clearRoute": "清除路由",
|
||||
"clearMessages": "清除消息",
|
||||
"clearScale": "清除比例",
|
||||
"clearDiscoveries": "清除发现",
|
||||
"clearOnlineTraceDatabase": "清除在线追踪数据库",
|
||||
"clearAllChannels": "清除所有频道",
|
||||
"clearAllContacts": "清除所有联系人",
|
||||
"clearChannels": "清除频道",
|
||||
"clearContacts": "清除联系人",
|
||||
"clearPathOnMaxRetry": "最大重试时清除路径",
|
||||
"create": "创建",
|
||||
"custom": "自定义",
|
||||
"defaultValue": "默认",
|
||||
"duplicate": "复制",
|
||||
"editName": "编辑名称",
|
||||
"open": "打开",
|
||||
"paste": "粘贴",
|
||||
"preview": "预览",
|
||||
"remove": "移除",
|
||||
"rename": "重命名",
|
||||
"resolveAll": "解析全部",
|
||||
"send": "发送",
|
||||
"sendAnyway": "仍然发送",
|
||||
"share": "分享",
|
||||
"shareContact": "分享联系人",
|
||||
"trace": "追踪",
|
||||
"use": "使用",
|
||||
"useSelectedFrequency": "使用选定频率",
|
||||
"discovery": "发现",
|
||||
"discoverRepeaters": "发现中继器",
|
||||
"discoverSensors": "发现传感器",
|
||||
"repeaterDiscoverySent": "中继器发现已发送",
|
||||
"sensorDiscoverySent": "传感器发现已发送",
|
||||
"clearedPendingDiscoveries": "已清除待处理的发现。",
|
||||
"autoDiscovery": "自动发现",
|
||||
"enableAutomaticAdding": "启用自动添加",
|
||||
"autoaddRepeaters": "自动添加中继器",
|
||||
"autoaddRoomServers": "自动添加房间服务器",
|
||||
"autoaddSensors": "自动添加传感器",
|
||||
"autoaddUsers": "自动添加用户",
|
||||
"overwriteOldestWhenFull": "满时覆盖最旧的",
|
||||
"storage": "存储",
|
||||
"dangerZone": "危险区域",
|
||||
"profiles": "配置文件",
|
||||
"favourites": "收藏夹",
|
||||
"sensors": "传感器",
|
||||
"others": "其他",
|
||||
"gpsModule": "GPS 模块",
|
||||
"liveTraffic": "实时流量",
|
||||
"repeatersMap": "中继器地图",
|
||||
"spectrumScan": "频谱扫描",
|
||||
"blePacketLogs": "BLE 数据包日志",
|
||||
"onlineTraceDatabase": "在线追踪数据库",
|
||||
"routePathByteSize": "路由路径字节大小",
|
||||
"messageNotifications": "消息通知",
|
||||
"sarAlerts": "SAR 警报",
|
||||
"discoveryNotifications": "发现通知",
|
||||
"updateNotifications": "更新通知",
|
||||
"muteWhileAppIsOpen": "应用打开时静音",
|
||||
"disableContacts": "禁用联系人",
|
||||
"enableSensorsTab": "启用传感器标签",
|
||||
"enableProfiles": "启用配置文件",
|
||||
"autoRouteRotation": "自动路由轮换",
|
||||
"nearestRepeaterFallback": "最近中继器回退",
|
||||
"deleteAllStoredMessageHistory": "删除所有存储的消息历史",
|
||||
"messageFontSize": "消息字体大小",
|
||||
"rotateMapWithHeading": "随航向旋转地图",
|
||||
"showMapDebugInfo": "显示地图调试信息",
|
||||
"openMapInFullscreen": "全屏打开地图",
|
||||
"showSarMarkersLabel": "显示 SAR 标记",
|
||||
"displaySarMarkersOnTheMainMap": "在主地图上显示 SAR 标记",
|
||||
"showAllContactTrailsLabel": "显示所有联系人轨迹",
|
||||
"hideRepeatersOnMap": "在地图上隐藏中继器",
|
||||
"setMapScale": "设置地图比例",
|
||||
"customMapScaleSaved": "自定义地图比例已保存",
|
||||
"voiceBitrate": "语音比特率",
|
||||
"voiceCompressor": "语音压缩器",
|
||||
"balancesQuietAndLoudSpeechLevels": "平衡安静和响亮的语音级别",
|
||||
"voiceLimiter": "语音限制器",
|
||||
"preventsClippingPeaksBeforeEncoding": "防止编码前的削波",
|
||||
"micAutoGain": "麦克风自动增益",
|
||||
"letsTheRecorderAdjustInputLevel": "让录音器调整输入级别",
|
||||
"echoCancellation": "回声消除",
|
||||
"noiseSuppression": "噪音抑制",
|
||||
"trimSilenceInVoiceMessages": "修剪语音消息中的静音",
|
||||
"compressor": "压缩器",
|
||||
"limiter": "限制器",
|
||||
"autoGain": "自动增益",
|
||||
"echoCancel": "回声",
|
||||
"noiseSuppress": "噪声",
|
||||
"silenceTrim": "静音",
|
||||
"maxImageSize": "最大图片大小",
|
||||
"imageCompression": "图片压缩",
|
||||
"grayscale": "灰度",
|
||||
"ultraMode": "超级模式",
|
||||
"fastPrivateGpsUpdates": "快速私有 GPS 更新",
|
||||
"movementThreshold": "移动阈值",
|
||||
"fastGpsMovementThreshold": "快速 GPS 移动阈值",
|
||||
"fastGpsActiveuseInterval": "快速 GPS 活动使用间隔",
|
||||
"activeuseUpdateInterval": "活动使用更新间隔",
|
||||
"repeatNearbyTraffic": "转发附近流量",
|
||||
"relayThroughRepeatersAcrossTheMesh": "通过网格中的中继器转发",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "仅附近,无中继器泛洪",
|
||||
"multihop": "多跳",
|
||||
"createProfile": "创建配置文件",
|
||||
"renameProfile": "重命名配置文件",
|
||||
"newProfile": "新配置文件",
|
||||
"manageProfiles": "管理配置文件",
|
||||
"enableProfilesToStartManagingThem": "启用配置文件以开始管理它们。",
|
||||
"openMessage": "打开消息",
|
||||
"jumpToTheRelatedSarMessage": "跳转到相关的 SAR 消息",
|
||||
"removeSarMarker": "移除 SAR 标记",
|
||||
"pleaseSelectADestinationToSendSarMarker": "请选择目标以发送 SAR 标记",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR 标记已广播到公共频道",
|
||||
"sarMarkerSentToRoom": "SAR 标记已发送到房间",
|
||||
"loadFromGallery": "从图库加载",
|
||||
"replaceImage": "替换图片",
|
||||
"selectFromGallery": "从图库选择",
|
||||
"team": "团队",
|
||||
"found": "已找到",
|
||||
"staging": "集结区",
|
||||
"object": "物体",
|
||||
"quiet": "安静",
|
||||
"moderate": "中等",
|
||||
"busy": "繁忙",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "频谱扫描未找到候选频率",
|
||||
"searchMessages": "搜索消息",
|
||||
"sendImageFromGallery": "从图库发送图片",
|
||||
"takePhoto": "拍照",
|
||||
"dmOnly": "仅私信",
|
||||
"allMessages": "所有消息",
|
||||
"sendToPublicChannel": "发送到公共频道?",
|
||||
"selectMarkerTypeAndDestination": "选择标记类型和目标",
|
||||
"noDestinationsAvailableLabel": "没有可用的目标",
|
||||
"image": "图片",
|
||||
"format": "格式",
|
||||
"dimensions": "尺寸",
|
||||
"segments": "分段",
|
||||
"transfers": "传输",
|
||||
"downloadedBy": "已下载",
|
||||
"saveDiscoverySettings": "保存发现设置",
|
||||
"savePublicInfo": "保存公共信息",
|
||||
"saveRadioSettings": "保存无线电设置",
|
||||
"savePath": "保存路径",
|
||||
"wipeDeviceData": "擦除设备数据",
|
||||
"wipeDevice": "擦除设备",
|
||||
"destructiveDeviceActions": "破坏性设备操作。",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "选择预设或微调自定义无线电设置。",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "选择此设备共享的名称和位置。",
|
||||
"availableSpaceOnThisDevice": "此设备上的可用空间。",
|
||||
"used": "已使用",
|
||||
"total": "总计",
|
||||
"renameValue": "重命名值",
|
||||
"customizeFields": "自定义字段",
|
||||
"livePreview": "实时预览",
|
||||
"refreshSchedule": "刷新计划",
|
||||
"noResponse": "无响应",
|
||||
"refreshing": "刷新中",
|
||||
"unavailable": "不可用",
|
||||
"pickARelayOrNodeToWatchInSensors": "选择要在传感器中监视的中继或节点。",
|
||||
"publicKeyLabel": "公钥",
|
||||
"alreadyInContacts": "已在联系人中",
|
||||
"connectToADeviceBeforeAddingContacts": "添加联系人前请连接到设备",
|
||||
"fromContacts": "来自联系人",
|
||||
"onlineOnly": "仅在线",
|
||||
"inBoth": "两者中",
|
||||
"source": "来源",
|
||||
"manualRouteEdit": "手动路由编辑",
|
||||
"observedMeshRoute": "观察到的网格路由",
|
||||
"allMessagesCleared": "所有消息已清除",
|
||||
"onlineTraceDatabaseCleared": "在线追踪数据库已清除",
|
||||
"packetLogsCleared": "数据包日志已清除",
|
||||
"hexDataCopiedToClipboard": "十六进制数据已复制到剪贴板",
|
||||
"developerModeEnabled": "开发者模式已启用",
|
||||
"developerModeDisabled": "开发者模式已禁用",
|
||||
"clipboardIsEmpty": "剪贴板为空",
|
||||
"contactImported": "联系人已导入",
|
||||
"contactLinkCopiedToClipboard": "联系人链接已复制到剪贴板",
|
||||
"failedToExportContact": "导出联系人失败",
|
||||
"noLogsToExport": "没有日志可导出",
|
||||
"exportAsCsv": "导出为 CSV",
|
||||
"exportAsText": "导出为文本",
|
||||
"receivedRfc3339": "收到 (RFC3339)",
|
||||
"buildTime": "构建时间",
|
||||
"downloadUrlNotAvailable": "下载链接不可用",
|
||||
"cannotOpenDownloadUrl": "无法打开下载链接",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "更新检查仅在 Android 上可用",
|
||||
"youAreRunningTheLatestVersion": "您正在使用最新版本",
|
||||
"updateAvailableButDownloadUrlNotFound": "有更新可用但未找到下载链接",
|
||||
"startTictactoe": "开始井字棋",
|
||||
"tictactoeUnavailable": "井字棋不可用",
|
||||
"tictactoeOpponentUnknown": "井字棋:对手未知",
|
||||
"tictactoeWaitingForStart": "井字棋:等待开始",
|
||||
"acceptsShareLinks": "接受分享链接",
|
||||
"supportsRawHex": "支持原始十六进制",
|
||||
"clipboardfriendly": "剪贴板友好",
|
||||
"captured": "已捕获",
|
||||
"size": "大小",
|
||||
"noCustomChannelsToClear": "没有自定义频道可清除。",
|
||||
"noDeviceContactsToClear": "没有设备联系人可清除。"
|
||||
}
|
||||
|
||||
@@ -1 +1,287 @@
|
||||
{}
|
||||
{
|
||||
"de": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"el": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"hr": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"autoSend",
|
||||
"autoSendDescription",
|
||||
"sendDirect",
|
||||
"sendDirectDescription",
|
||||
"sendFlood",
|
||||
"sendFloodDescription",
|
||||
"postConnectDiscoveryTitle",
|
||||
"postConnectDiscoveryDescription",
|
||||
"setRegionScope",
|
||||
"regionScope",
|
||||
"regionScopeNone",
|
||||
"clearRegionScope",
|
||||
"regionScopeWarning",
|
||||
"discoverRegions",
|
||||
"discoveringRegions",
|
||||
"enterRegionName",
|
||||
"noRegionsFound",
|
||||
"regionScopeSet",
|
||||
"regionScopeCleared"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'providers/connection_provider.dart';
|
||||
import 'providers/contacts_provider.dart';
|
||||
import 'providers/messages_provider.dart';
|
||||
@@ -23,6 +24,9 @@ import 'services/voice_player_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'services/locale_preferences.dart';
|
||||
import 'services/mesh_map_nodes_service.dart';
|
||||
import 'services/profile_manager.dart';
|
||||
import 'services/profile_workspace_coordinator.dart';
|
||||
import 'services/profiles_feature_service.dart';
|
||||
import 'services/update_checker_service.dart';
|
||||
import 'services/wizard_preferences.dart';
|
||||
import 'screens/discovery_screen.dart';
|
||||
@@ -31,7 +35,9 @@ import 'screens/welcome_wizard_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await WakelockPlus.enable();
|
||||
runApp(const MeshCoreSarApp());
|
||||
}
|
||||
|
||||
@@ -58,6 +64,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
final activeProfileId =
|
||||
prefs.getString(ProfileManager.activeProfileIdKey) ?? 'default';
|
||||
await ProfileStorageScope.bootstrap(
|
||||
profilesEnabled: profilesEnabled,
|
||||
activeProfileId: activeProfileId,
|
||||
);
|
||||
await _loadThemePreference();
|
||||
await _loadLocalePreference();
|
||||
|
||||
@@ -271,6 +285,13 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) {
|
||||
final manager = ProfileManager();
|
||||
manager.initialize();
|
||||
return manager;
|
||||
},
|
||||
),
|
||||
|
||||
// Voice provider (packet reassembly + playback)
|
||||
ChangeNotifierProvider(
|
||||
@@ -323,6 +344,39 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
imageProvider: context.read<ip.ImageProvider>(),
|
||||
),
|
||||
),
|
||||
ProxyProvider6<
|
||||
ProfileManager,
|
||||
AppProvider,
|
||||
ConnectionProvider,
|
||||
ContactsProvider,
|
||||
MessagesProvider,
|
||||
MapProvider,
|
||||
ProfileWorkspaceCoordinator
|
||||
>(
|
||||
update:
|
||||
(
|
||||
context,
|
||||
profileManager,
|
||||
appProvider,
|
||||
connectionProvider,
|
||||
contactsProvider,
|
||||
messagesProvider,
|
||||
mapProvider,
|
||||
previous,
|
||||
) => ProfileWorkspaceCoordinator(
|
||||
profileManager: profileManager,
|
||||
connectionProvider: connectionProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
messagesProvider: messagesProvider,
|
||||
sensorsProvider: context.read<SensorsProvider>(),
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: context.read<DrawingProvider>(),
|
||||
channelsProvider: context.read<ChannelsProvider>(),
|
||||
voiceProvider: context.read<VoiceProvider>(),
|
||||
imageProvider: context.read<ip.ImageProvider>(),
|
||||
appProvider: appProvider,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _buildMaterialApp(),
|
||||
);
|
||||
|
||||
@@ -57,9 +57,7 @@ class Channel {
|
||||
} else {
|
||||
// Normal channel: require explicit secret
|
||||
if (explicitSecret == null || explicitSecret.length != 16) {
|
||||
throw ArgumentError(
|
||||
'Normal channels require a 16-byte secret',
|
||||
);
|
||||
throw ArgumentError('Normal channels require a 16-byte secret');
|
||||
}
|
||||
return Channel(
|
||||
index: index,
|
||||
@@ -78,6 +76,18 @@ class Channel {
|
||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||
}
|
||||
|
||||
static bool isHashChannelName(String channelName) {
|
||||
return channelName.trim().startsWith('#');
|
||||
}
|
||||
|
||||
static String pskBase64ForHashChannelName(String channelName) {
|
||||
final normalized = channelName.trim();
|
||||
if (!isHashChannelName(normalized)) {
|
||||
throw ArgumentError('Only #channels can export derived psk_base64');
|
||||
}
|
||||
return base64.encode(_generateHashChannelSecret(normalized));
|
||||
}
|
||||
|
||||
/// Create the default public channel (channel 0)
|
||||
/// Uses the well-known pre-shared key from MeshCore
|
||||
factory Channel.publicChannel() {
|
||||
@@ -85,8 +95,22 @@ class Channel {
|
||||
index: 0,
|
||||
name: 'Public Channel',
|
||||
secret: Uint8List.fromList([
|
||||
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
||||
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
|
||||
0x8b,
|
||||
0x33,
|
||||
0x87,
|
||||
0xe9,
|
||||
0xc5,
|
||||
0xcd,
|
||||
0xea,
|
||||
0x6a,
|
||||
0xc9,
|
||||
0xe5,
|
||||
0xed,
|
||||
0xba,
|
||||
0xa1,
|
||||
0x15,
|
||||
0xcd,
|
||||
0x72,
|
||||
]),
|
||||
flags: null,
|
||||
);
|
||||
@@ -95,6 +119,18 @@ class Channel {
|
||||
/// Check if this is a hash-based channel (name starts with '#')
|
||||
bool get isHashChannel => name.startsWith('#');
|
||||
|
||||
/// Base64-encoded PSK for sharing with firmware CLI and related tooling.
|
||||
String get pskBase64 => base64.encode(secret);
|
||||
|
||||
/// 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
|
||||
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
||||
String get displayName {
|
||||
@@ -131,12 +167,7 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
Channel copyWith({
|
||||
int? index,
|
||||
String? name,
|
||||
Uint8List? secret,
|
||||
int? flags,
|
||||
}) {
|
||||
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
|
||||
return Channel(
|
||||
index: index ?? this.index,
|
||||
name: name ?? this.name,
|
||||
|
||||
523
lib/models/config_profile.dart
Normal file
523
lib/models/config_profile.dart
Normal file
@@ -0,0 +1,523 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'channel.dart';
|
||||
|
||||
@immutable
|
||||
class DeviceConfigProfileSection {
|
||||
final int? frequencyKhz;
|
||||
final int? bandwidth;
|
||||
final int? spreadingFactor;
|
||||
final int? codingRate;
|
||||
final bool? repeatEnabled;
|
||||
final int? txPower;
|
||||
final int? telemetryModes;
|
||||
final int? advertLocationPolicy;
|
||||
final int? multiAcks;
|
||||
final bool? manualAddContacts;
|
||||
final bool? autoAddUsers;
|
||||
final bool? autoAddRepeaters;
|
||||
final bool? autoAddRoomServers;
|
||||
final bool? autoAddSensors;
|
||||
final bool? autoAddOverwriteOldest;
|
||||
final double? publicLatitude;
|
||||
final double? publicLongitude;
|
||||
|
||||
const DeviceConfigProfileSection({
|
||||
this.frequencyKhz,
|
||||
this.bandwidth,
|
||||
this.spreadingFactor,
|
||||
this.codingRate,
|
||||
this.repeatEnabled,
|
||||
this.txPower,
|
||||
this.telemetryModes,
|
||||
this.advertLocationPolicy,
|
||||
this.multiAcks,
|
||||
this.manualAddContacts,
|
||||
this.autoAddUsers,
|
||||
this.autoAddRepeaters,
|
||||
this.autoAddRoomServers,
|
||||
this.autoAddSensors,
|
||||
this.autoAddOverwriteOldest,
|
||||
this.publicLatitude,
|
||||
this.publicLongitude,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
frequencyKhz == null &&
|
||||
bandwidth == null &&
|
||||
spreadingFactor == null &&
|
||||
codingRate == null &&
|
||||
repeatEnabled == null &&
|
||||
txPower == null &&
|
||||
telemetryModes == null &&
|
||||
advertLocationPolicy == null &&
|
||||
multiAcks == null &&
|
||||
manualAddContacts == null &&
|
||||
autoAddUsers == null &&
|
||||
autoAddRepeaters == null &&
|
||||
autoAddRoomServers == null &&
|
||||
autoAddSensors == null &&
|
||||
autoAddOverwriteOldest == null &&
|
||||
publicLatitude == null &&
|
||||
publicLongitude == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'frequencyKhz': frequencyKhz,
|
||||
'bandwidth': bandwidth,
|
||||
'spreadingFactor': spreadingFactor,
|
||||
'codingRate': codingRate,
|
||||
'repeatEnabled': repeatEnabled,
|
||||
'txPower': txPower,
|
||||
'telemetryModes': telemetryModes,
|
||||
'advertLocationPolicy': advertLocationPolicy,
|
||||
'multiAcks': multiAcks,
|
||||
'manualAddContacts': manualAddContacts,
|
||||
'autoAddUsers': autoAddUsers,
|
||||
'autoAddRepeaters': autoAddRepeaters,
|
||||
'autoAddRoomServers': autoAddRoomServers,
|
||||
'autoAddSensors': autoAddSensors,
|
||||
'autoAddOverwriteOldest': autoAddOverwriteOldest,
|
||||
'publicLatitude': publicLatitude,
|
||||
'publicLongitude': publicLongitude,
|
||||
};
|
||||
|
||||
factory DeviceConfigProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceConfigProfileSection(
|
||||
frequencyKhz: json['frequencyKhz'] as int?,
|
||||
bandwidth: json['bandwidth'] as int?,
|
||||
spreadingFactor: json['spreadingFactor'] as int?,
|
||||
codingRate: json['codingRate'] as int?,
|
||||
repeatEnabled: json['repeatEnabled'] as bool?,
|
||||
txPower: json['txPower'] as int?,
|
||||
telemetryModes: json['telemetryModes'] as int?,
|
||||
advertLocationPolicy: json['advertLocationPolicy'] as int?,
|
||||
multiAcks: json['multiAcks'] as int?,
|
||||
manualAddContacts: json['manualAddContacts'] as bool?,
|
||||
autoAddUsers: json['autoAddUsers'] as bool?,
|
||||
autoAddRepeaters: json['autoAddRepeaters'] as bool?,
|
||||
autoAddRoomServers: json['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: json['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: json['autoAddOverwriteOldest'] as bool?,
|
||||
publicLatitude: (json['publicLatitude'] as num?)?.toDouble(),
|
||||
publicLongitude: (json['publicLongitude'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AppSettingsProfileSection {
|
||||
final bool? mapEnabled;
|
||||
final bool? contactsEnabled;
|
||||
final bool? sensorsEnabled;
|
||||
final bool? voiceSilenceTrimmingEnabled;
|
||||
final bool? voiceBandPassFilterEnabled;
|
||||
final bool? voiceCompressorEnabled;
|
||||
final bool? voiceLimiterEnabled;
|
||||
final bool? voiceAutoGainEnabled;
|
||||
final bool? voiceEchoCancellationEnabled;
|
||||
final bool? voiceNoiseSuppressionEnabled;
|
||||
final double? messageFontScale;
|
||||
final bool? autoRouteRotationEnabled;
|
||||
final bool? clearPathOnMaxRetry;
|
||||
final bool? nearestRelayFallbackEnabled;
|
||||
final int? voiceBitrate;
|
||||
final int? routeHashSize;
|
||||
final int? imageMaxSize;
|
||||
final int? imageCompression;
|
||||
final bool? imageGrayscale;
|
||||
final bool? imageUltraMode;
|
||||
final bool? showRxTxIndicators;
|
||||
final bool? fastLocationUpdatesEnabled;
|
||||
final double? fastLocationMovementThresholdMeters;
|
||||
final int? fastLocationActiveCadenceSeconds;
|
||||
|
||||
const AppSettingsProfileSection({
|
||||
this.mapEnabled,
|
||||
this.contactsEnabled,
|
||||
this.sensorsEnabled,
|
||||
this.voiceSilenceTrimmingEnabled,
|
||||
this.voiceBandPassFilterEnabled,
|
||||
this.voiceCompressorEnabled,
|
||||
this.voiceLimiterEnabled,
|
||||
this.voiceAutoGainEnabled,
|
||||
this.voiceEchoCancellationEnabled,
|
||||
this.voiceNoiseSuppressionEnabled,
|
||||
this.messageFontScale,
|
||||
this.autoRouteRotationEnabled,
|
||||
this.clearPathOnMaxRetry,
|
||||
this.nearestRelayFallbackEnabled,
|
||||
this.voiceBitrate,
|
||||
this.routeHashSize,
|
||||
this.imageMaxSize,
|
||||
this.imageCompression,
|
||||
this.imageGrayscale,
|
||||
this.imageUltraMode,
|
||||
this.showRxTxIndicators,
|
||||
this.fastLocationUpdatesEnabled,
|
||||
this.fastLocationMovementThresholdMeters,
|
||||
this.fastLocationActiveCadenceSeconds,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
mapEnabled == null &&
|
||||
contactsEnabled == null &&
|
||||
sensorsEnabled == null &&
|
||||
voiceSilenceTrimmingEnabled == null &&
|
||||
voiceBandPassFilterEnabled == null &&
|
||||
voiceCompressorEnabled == null &&
|
||||
voiceLimiterEnabled == null &&
|
||||
voiceAutoGainEnabled == null &&
|
||||
voiceEchoCancellationEnabled == null &&
|
||||
voiceNoiseSuppressionEnabled == null &&
|
||||
messageFontScale == null &&
|
||||
autoRouteRotationEnabled == null &&
|
||||
clearPathOnMaxRetry == null &&
|
||||
nearestRelayFallbackEnabled == null &&
|
||||
voiceBitrate == null &&
|
||||
routeHashSize == null &&
|
||||
imageMaxSize == null &&
|
||||
imageCompression == null &&
|
||||
imageGrayscale == null &&
|
||||
imageUltraMode == null &&
|
||||
showRxTxIndicators == null &&
|
||||
fastLocationUpdatesEnabled == null &&
|
||||
fastLocationMovementThresholdMeters == null &&
|
||||
fastLocationActiveCadenceSeconds == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'mapEnabled': mapEnabled,
|
||||
'contactsEnabled': contactsEnabled,
|
||||
'sensorsEnabled': sensorsEnabled,
|
||||
'voiceSilenceTrimmingEnabled': voiceSilenceTrimmingEnabled,
|
||||
'voiceBandPassFilterEnabled': voiceBandPassFilterEnabled,
|
||||
'voiceCompressorEnabled': voiceCompressorEnabled,
|
||||
'voiceLimiterEnabled': voiceLimiterEnabled,
|
||||
'voiceAutoGainEnabled': voiceAutoGainEnabled,
|
||||
'voiceEchoCancellationEnabled': voiceEchoCancellationEnabled,
|
||||
'voiceNoiseSuppressionEnabled': voiceNoiseSuppressionEnabled,
|
||||
'messageFontScale': messageFontScale,
|
||||
'autoRouteRotationEnabled': autoRouteRotationEnabled,
|
||||
'clearPathOnMaxRetry': clearPathOnMaxRetry,
|
||||
'nearestRelayFallbackEnabled': nearestRelayFallbackEnabled,
|
||||
'voiceBitrate': voiceBitrate,
|
||||
'routeHashSize': routeHashSize,
|
||||
'imageMaxSize': imageMaxSize,
|
||||
'imageCompression': imageCompression,
|
||||
'imageGrayscale': imageGrayscale,
|
||||
'imageUltraMode': imageUltraMode,
|
||||
'showRxTxIndicators': showRxTxIndicators,
|
||||
'fastLocationUpdatesEnabled': fastLocationUpdatesEnabled,
|
||||
'fastLocationMovementThresholdMeters': fastLocationMovementThresholdMeters,
|
||||
'fastLocationActiveCadenceSeconds': fastLocationActiveCadenceSeconds,
|
||||
};
|
||||
|
||||
factory AppSettingsProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
return AppSettingsProfileSection(
|
||||
mapEnabled: json['mapEnabled'] as bool?,
|
||||
contactsEnabled: json['contactsEnabled'] as bool?,
|
||||
sensorsEnabled: json['sensorsEnabled'] as bool?,
|
||||
voiceSilenceTrimmingEnabled: json['voiceSilenceTrimmingEnabled'] as bool?,
|
||||
voiceBandPassFilterEnabled: json['voiceBandPassFilterEnabled'] as bool?,
|
||||
voiceCompressorEnabled: json['voiceCompressorEnabled'] as bool?,
|
||||
voiceLimiterEnabled: json['voiceLimiterEnabled'] as bool?,
|
||||
voiceAutoGainEnabled: json['voiceAutoGainEnabled'] as bool?,
|
||||
voiceEchoCancellationEnabled:
|
||||
json['voiceEchoCancellationEnabled'] as bool?,
|
||||
voiceNoiseSuppressionEnabled:
|
||||
json['voiceNoiseSuppressionEnabled'] as bool?,
|
||||
messageFontScale: (json['messageFontScale'] as num?)?.toDouble(),
|
||||
autoRouteRotationEnabled: json['autoRouteRotationEnabled'] as bool?,
|
||||
clearPathOnMaxRetry: json['clearPathOnMaxRetry'] as bool?,
|
||||
nearestRelayFallbackEnabled: json['nearestRelayFallbackEnabled'] as bool?,
|
||||
voiceBitrate: json['voiceBitrate'] as int?,
|
||||
routeHashSize: json['routeHashSize'] as int?,
|
||||
imageMaxSize: json['imageMaxSize'] as int?,
|
||||
imageCompression: json['imageCompression'] as int?,
|
||||
imageGrayscale: json['imageGrayscale'] as bool?,
|
||||
imageUltraMode: json['imageUltraMode'] as bool?,
|
||||
showRxTxIndicators: json['showRxTxIndicators'] as bool?,
|
||||
fastLocationUpdatesEnabled: json['fastLocationUpdatesEnabled'] as bool?,
|
||||
fastLocationMovementThresholdMeters:
|
||||
(json['fastLocationMovementThresholdMeters'] as num?)?.toDouble(),
|
||||
fastLocationActiveCadenceSeconds:
|
||||
json['fastLocationActiveCadenceSeconds'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MapWorkspaceProfileSection {
|
||||
final Map<String, dynamic>? mapPrefs;
|
||||
final List<Map<String, dynamic>> drawings;
|
||||
final Map<String, dynamic>? currentTrail;
|
||||
final List<Map<String, dynamic>> trailHistory;
|
||||
final Map<String, dynamic>? importedTrail;
|
||||
final bool? isTrailVisible;
|
||||
final bool? showCadastralOverlay;
|
||||
final bool? showForestRoadsOverlay;
|
||||
final bool? showHikingTrailsOverlay;
|
||||
final bool? showMainRoadsOverlay;
|
||||
final bool? showHouseNumbersOverlay;
|
||||
final bool? showFireHazardZonesOverlay;
|
||||
final bool? showHistoricalFiresOverlay;
|
||||
final bool? showFirebreaksOverlay;
|
||||
final bool? showKrasFireZonesOverlay;
|
||||
final bool? showPlaceNamesOverlay;
|
||||
final bool? showMunicipalityBordersOverlay;
|
||||
final bool? hideRepeatersOnMap;
|
||||
|
||||
const MapWorkspaceProfileSection({
|
||||
this.mapPrefs,
|
||||
this.drawings = const [],
|
||||
this.currentTrail,
|
||||
this.trailHistory = const [],
|
||||
this.importedTrail,
|
||||
this.isTrailVisible,
|
||||
this.showCadastralOverlay,
|
||||
this.showForestRoadsOverlay,
|
||||
this.showHikingTrailsOverlay,
|
||||
this.showMainRoadsOverlay,
|
||||
this.showHouseNumbersOverlay,
|
||||
this.showFireHazardZonesOverlay,
|
||||
this.showHistoricalFiresOverlay,
|
||||
this.showFirebreaksOverlay,
|
||||
this.showKrasFireZonesOverlay,
|
||||
this.showPlaceNamesOverlay,
|
||||
this.showMunicipalityBordersOverlay,
|
||||
this.hideRepeatersOnMap,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
mapPrefs == null &&
|
||||
drawings.isEmpty &&
|
||||
currentTrail == null &&
|
||||
trailHistory.isEmpty &&
|
||||
importedTrail == null &&
|
||||
isTrailVisible == null &&
|
||||
showCadastralOverlay == null &&
|
||||
showForestRoadsOverlay == null &&
|
||||
showHikingTrailsOverlay == null &&
|
||||
showMainRoadsOverlay == null &&
|
||||
showHouseNumbersOverlay == null &&
|
||||
showFireHazardZonesOverlay == null &&
|
||||
showHistoricalFiresOverlay == null &&
|
||||
showFirebreaksOverlay == null &&
|
||||
showKrasFireZonesOverlay == null &&
|
||||
showPlaceNamesOverlay == null &&
|
||||
showMunicipalityBordersOverlay == null &&
|
||||
hideRepeatersOnMap == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'mapPrefs': mapPrefs,
|
||||
'drawings': drawings,
|
||||
'currentTrail': currentTrail,
|
||||
'trailHistory': trailHistory,
|
||||
'importedTrail': importedTrail,
|
||||
'isTrailVisible': isTrailVisible,
|
||||
'showCadastralOverlay': showCadastralOverlay,
|
||||
'showForestRoadsOverlay': showForestRoadsOverlay,
|
||||
'showHikingTrailsOverlay': showHikingTrailsOverlay,
|
||||
'showMainRoadsOverlay': showMainRoadsOverlay,
|
||||
'showHouseNumbersOverlay': showHouseNumbersOverlay,
|
||||
'showFireHazardZonesOverlay': showFireHazardZonesOverlay,
|
||||
'showHistoricalFiresOverlay': showHistoricalFiresOverlay,
|
||||
'showFirebreaksOverlay': showFirebreaksOverlay,
|
||||
'showKrasFireZonesOverlay': showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': showMunicipalityBordersOverlay,
|
||||
'hideRepeatersOnMap': hideRepeatersOnMap,
|
||||
};
|
||||
|
||||
factory MapWorkspaceProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
List<Map<String, dynamic>> decodeList(String key) {
|
||||
final raw = json[key] as List<dynamic>? ?? const [];
|
||||
return raw.whereType<Map<String, dynamic>>().toList();
|
||||
}
|
||||
|
||||
return MapWorkspaceProfileSection(
|
||||
mapPrefs: json['mapPrefs'] as Map<String, dynamic>?,
|
||||
drawings: decodeList('drawings'),
|
||||
currentTrail: json['currentTrail'] as Map<String, dynamic>?,
|
||||
trailHistory: decodeList('trailHistory'),
|
||||
importedTrail: json['importedTrail'] as Map<String, dynamic>?,
|
||||
isTrailVisible: json['isTrailVisible'] as bool?,
|
||||
showCadastralOverlay: json['showCadastralOverlay'] as bool?,
|
||||
showForestRoadsOverlay: json['showForestRoadsOverlay'] as bool?,
|
||||
showHikingTrailsOverlay: json['showHikingTrailsOverlay'] as bool?,
|
||||
showMainRoadsOverlay: json['showMainRoadsOverlay'] as bool?,
|
||||
showHouseNumbersOverlay: json['showHouseNumbersOverlay'] as bool?,
|
||||
showFireHazardZonesOverlay: json['showFireHazardZonesOverlay'] as bool?,
|
||||
showHistoricalFiresOverlay: json['showHistoricalFiresOverlay'] as bool?,
|
||||
showFirebreaksOverlay: json['showFirebreaksOverlay'] as bool?,
|
||||
showKrasFireZonesOverlay: json['showKrasFireZonesOverlay'] as bool?,
|
||||
showPlaceNamesOverlay: json['showPlaceNamesOverlay'] as bool?,
|
||||
showMunicipalityBordersOverlay:
|
||||
json['showMunicipalityBordersOverlay'] as bool?,
|
||||
hideRepeatersOnMap: json['hideRepeatersOnMap'] as bool?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ConfigProfileSections {
|
||||
final DeviceConfigProfileSection? deviceConfig;
|
||||
final AppSettingsProfileSection? appSettings;
|
||||
final MapWorkspaceProfileSection? mapWorkspace;
|
||||
final List<Channel> channels;
|
||||
|
||||
const ConfigProfileSections({
|
||||
this.deviceConfig,
|
||||
this.appSettings,
|
||||
this.mapWorkspace,
|
||||
this.channels = const [],
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
(deviceConfig == null || deviceConfig!.isEmpty) &&
|
||||
(appSettings == null || appSettings!.isEmpty) &&
|
||||
(mapWorkspace == null || mapWorkspace!.isEmpty) &&
|
||||
channels.isEmpty;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'deviceConfig': deviceConfig?.toJson(),
|
||||
'appSettings': appSettings?.toJson(),
|
||||
'mapWorkspace': mapWorkspace?.toJson(),
|
||||
'channels': channels.map((channel) => channel.toJson()).toList(),
|
||||
};
|
||||
|
||||
factory ConfigProfileSections.fromJson(Map<String, dynamic> json) {
|
||||
final rawChannels = json['channels'] as List<dynamic>? ?? const [];
|
||||
return ConfigProfileSections(
|
||||
deviceConfig: json['deviceConfig'] is Map<String, dynamic>
|
||||
? DeviceConfigProfileSection.fromJson(
|
||||
json['deviceConfig'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
appSettings: json['appSettings'] is Map<String, dynamic>
|
||||
? AppSettingsProfileSection.fromJson(
|
||||
json['appSettings'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
mapWorkspace: json['mapWorkspace'] is Map<String, dynamic>
|
||||
? MapWorkspaceProfileSection.fromJson(
|
||||
json['mapWorkspace'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
channels: rawChannels
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Channel.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ConfigProfile {
|
||||
static const String defaultProfileId = 'default';
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String? notes;
|
||||
final ConfigProfileSections sections;
|
||||
|
||||
const ConfigProfile({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.sections,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
bool get isDefault => id == defaultProfileId;
|
||||
|
||||
factory ConfigProfile.defaultProfile() {
|
||||
final now = DateTime.now();
|
||||
return ConfigProfile(
|
||||
id: defaultProfileId,
|
||||
name: 'Default',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
sections: const ConfigProfileSections(),
|
||||
);
|
||||
}
|
||||
|
||||
ConfigProfile copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? notes,
|
||||
ConfigProfileSections? sections,
|
||||
}) {
|
||||
return ConfigProfile(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
notes: notes ?? this.notes,
|
||||
sections: sections ?? this.sections,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'notes': notes,
|
||||
'sections': sections.toJson(),
|
||||
};
|
||||
|
||||
factory ConfigProfile.fromJson(Map<String, dynamic> json) {
|
||||
return ConfigProfile(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
notes: json['notes'] as String?,
|
||||
sections: ConfigProfileSections.fromJson(
|
||||
json['sections'] as Map<String, dynamic>? ?? const {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProfileDiff {
|
||||
final List<String> changedSections;
|
||||
|
||||
const ProfileDiff({required this.changedSections});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProfileTransferRecord {
|
||||
final String profileId;
|
||||
final String direction;
|
||||
final DateTime timestamp;
|
||||
final String detail;
|
||||
|
||||
const ProfileTransferRecord({
|
||||
required this.profileId,
|
||||
required this.direction,
|
||||
required this.timestamp,
|
||||
required this.detail,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'profileId': profileId,
|
||||
'direction': direction,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'detail': detail,
|
||||
};
|
||||
|
||||
factory ProfileTransferRecord.fromJson(Map<String, dynamic> json) {
|
||||
return ProfileTransferRecord(
|
||||
profileId: json['profileId'] as String,
|
||||
direction: json['direction'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
detail: json['detail'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,23 @@ class ContactRouteCodec {
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute direct({int hashSize = 1}) {
|
||||
if (hashSize < 1 || hashSize > maxHashSize) {
|
||||
throw ArgumentError.value(hashSize, 'hashSize', 'Must be 1, 2, or 3.');
|
||||
}
|
||||
|
||||
final encodedPathLen = (hashSize - 1) << 6;
|
||||
return ParsedContactRoute(
|
||||
canonicalText: '',
|
||||
hashSize: hashSize,
|
||||
hopCount: 0,
|
||||
encodedPathLen: encodedPathLen,
|
||||
signedEncodedPathLen: toSignedDescriptor(encodedPathLen),
|
||||
pathBytes: Uint8List(0),
|
||||
paddedPathBytes: Uint8List(maxPathBytes),
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute? fromContact(Contact contact) {
|
||||
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
||||
return null;
|
||||
|
||||
151
lib/models/custom_map_config.dart
Normal file
151
lib/models/custom_map_config.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
class CustomMapConfig {
|
||||
final String filePath;
|
||||
final String displayName;
|
||||
final String mapId;
|
||||
final int imageWidth;
|
||||
final int imageHeight;
|
||||
final double? metersPerPixel;
|
||||
final LatLng? calibrationPointA;
|
||||
final LatLng? calibrationPointB;
|
||||
|
||||
const CustomMapConfig({
|
||||
required this.filePath,
|
||||
required this.displayName,
|
||||
required this.mapId,
|
||||
required this.imageWidth,
|
||||
required this.imageHeight,
|
||||
this.metersPerPixel,
|
||||
this.calibrationPointA,
|
||||
this.calibrationPointB,
|
||||
});
|
||||
|
||||
bool get isCalibrated => metersPerPixel != null && metersPerPixel! > 0;
|
||||
|
||||
double get _displayScale {
|
||||
final heightScale = imageHeight / 90.0;
|
||||
final widthScale = imageWidth / 180.0;
|
||||
return math.max(1.0, math.max(heightScale, widthScale));
|
||||
}
|
||||
|
||||
double get _displayHeight => imageHeight / _displayScale;
|
||||
|
||||
double get _displayWidth => imageWidth / _displayScale;
|
||||
|
||||
LatLngBounds get bounds =>
|
||||
LatLngBounds(const LatLng(0, 0), LatLng(_displayHeight, _displayWidth));
|
||||
|
||||
LatLngBounds get displayBounds =>
|
||||
LatLngBounds(LatLng(_displayHeight, 0), LatLng(0, _displayWidth));
|
||||
|
||||
LatLng toDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
_displayHeight - (point.latitude / _displayScale),
|
||||
point.longitude / _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
LatLng fromDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
(_displayHeight - point.latitude) * _displayScale,
|
||||
point.longitude * _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'filePath': filePath,
|
||||
'displayName': displayName,
|
||||
'mapId': mapId,
|
||||
'imageWidth': imageWidth,
|
||||
'imageHeight': imageHeight,
|
||||
'metersPerPixel': metersPerPixel,
|
||||
'calibrationPointA': calibrationPointA != null
|
||||
? {
|
||||
'lat': calibrationPointA!.latitude,
|
||||
'lon': calibrationPointA!.longitude,
|
||||
}
|
||||
: null,
|
||||
'calibrationPointB': calibrationPointB != null
|
||||
? {
|
||||
'lat': calibrationPointB!.latitude,
|
||||
'lon': calibrationPointB!.longitude,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
CustomMapConfig copyWith({
|
||||
String? filePath,
|
||||
String? displayName,
|
||||
String? mapId,
|
||||
int? imageWidth,
|
||||
int? imageHeight,
|
||||
double? metersPerPixel,
|
||||
bool clearMetersPerPixel = false,
|
||||
LatLng? calibrationPointA,
|
||||
bool clearCalibrationPointA = false,
|
||||
LatLng? calibrationPointB,
|
||||
bool clearCalibrationPointB = false,
|
||||
}) {
|
||||
return CustomMapConfig(
|
||||
filePath: filePath ?? this.filePath,
|
||||
displayName: displayName ?? this.displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? this.mapId,
|
||||
imageWidth: imageWidth ?? this.imageWidth,
|
||||
imageHeight: imageHeight ?? this.imageHeight,
|
||||
metersPerPixel: clearMetersPerPixel
|
||||
? null
|
||||
: (metersPerPixel ?? this.metersPerPixel),
|
||||
calibrationPointA: clearCalibrationPointA
|
||||
? null
|
||||
: (calibrationPointA ?? this.calibrationPointA),
|
||||
calibrationPointB: clearCalibrationPointB
|
||||
? null
|
||||
: (calibrationPointB ?? this.calibrationPointB),
|
||||
);
|
||||
}
|
||||
|
||||
static CustomMapConfig? fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) return null;
|
||||
|
||||
final filePath = json['filePath'];
|
||||
final displayName = json['displayName'];
|
||||
final mapId = json['mapId'];
|
||||
final imageWidth = json['imageWidth'];
|
||||
final imageHeight = json['imageHeight'];
|
||||
if (filePath is! String ||
|
||||
displayName is! String ||
|
||||
mapId is! String ||
|
||||
imageWidth is! int ||
|
||||
imageHeight is! int) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng? parsePoint(dynamic raw) {
|
||||
if (raw is! Map<String, dynamic>) return null;
|
||||
final lat = raw['lat'];
|
||||
final lon = raw['lon'];
|
||||
if (lat is! num || lon is! num) return null;
|
||||
return LatLng(lat.toDouble(), lon.toDouble());
|
||||
}
|
||||
|
||||
final metersPerPixel = json['metersPerPixel'];
|
||||
return CustomMapConfig(
|
||||
filePath: filePath,
|
||||
displayName: displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? mapId,
|
||||
imageWidth: imageWidth,
|
||||
imageHeight: imageHeight,
|
||||
metersPerPixel: metersPerPixel is num ? metersPerPixel.toDouble() : null,
|
||||
calibrationPointA: parsePoint(json['calibrationPointA']),
|
||||
calibrationPointB: parsePoint(json['calibrationPointB']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@ enum ConnectionMode {
|
||||
/// Direct BLE connection to MeshCore device (default)
|
||||
ble,
|
||||
|
||||
/// Act as SSE server - share BLE device with multiple clients
|
||||
sseServer,
|
||||
|
||||
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
|
||||
tcp,
|
||||
|
||||
/// USB serial connection (Android OTG or Web Serial API)
|
||||
usb,
|
||||
}
|
||||
|
||||
extension ConnectionModeExtension on ConnectionMode {
|
||||
@@ -26,10 +26,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
||||
switch (this) {
|
||||
case ConnectionMode.ble:
|
||||
return 'Direct (BLE)';
|
||||
case ConnectionMode.sseServer:
|
||||
return 'Share Device (Server)';
|
||||
case ConnectionMode.tcp:
|
||||
return 'Direct (WiFi)';
|
||||
case ConnectionMode.usb:
|
||||
return 'Direct (Serial)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
||||
switch (this) {
|
||||
case ConnectionMode.ble:
|
||||
return 'Direct BLE connection to MeshCore device';
|
||||
case ConnectionMode.sseServer:
|
||||
return 'Share BLE device with multiple clients over network';
|
||||
case ConnectionMode.tcp:
|
||||
return 'Direct WiFi/TCP connection to MeshCore device';
|
||||
case ConnectionMode.usb:
|
||||
return 'Direct serial connection to MeshCore device';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ class DeviceInfo {
|
||||
final bool? autoAddRoomServers;
|
||||
final bool? autoAddSensors;
|
||||
final bool? autoAddOverwriteOldest;
|
||||
final int? autoAddMaxHops;
|
||||
final int? radioFreq;
|
||||
final int? radioBw;
|
||||
final int? radioSf;
|
||||
@@ -95,6 +96,7 @@ class DeviceInfo {
|
||||
|
||||
// Repeat mode (firmware v9+)
|
||||
final bool? clientRepeat;
|
||||
final int? pathHashMode;
|
||||
final bool? supportsSpectrumScan;
|
||||
final int? spectrumScanMinKhz;
|
||||
final int? spectrumScanMaxKhz;
|
||||
@@ -123,6 +125,7 @@ class DeviceInfo {
|
||||
this.autoAddRoomServers,
|
||||
this.autoAddSensors,
|
||||
this.autoAddOverwriteOldest,
|
||||
this.autoAddMaxHops,
|
||||
this.radioFreq,
|
||||
this.radioBw,
|
||||
this.radioSf,
|
||||
@@ -139,6 +142,7 @@ class DeviceInfo {
|
||||
this.manufacturerModel,
|
||||
this.semanticVersion,
|
||||
this.clientRepeat,
|
||||
this.pathHashMode,
|
||||
this.supportsSpectrumScan,
|
||||
this.spectrumScanMinKhz,
|
||||
this.spectrumScanMaxKhz,
|
||||
@@ -254,6 +258,7 @@ class DeviceInfo {
|
||||
bool? autoAddRoomServers,
|
||||
bool? autoAddSensors,
|
||||
bool? autoAddOverwriteOldest,
|
||||
int? autoAddMaxHops,
|
||||
int? radioFreq,
|
||||
int? radioBw,
|
||||
int? radioSf,
|
||||
@@ -270,6 +275,7 @@ class DeviceInfo {
|
||||
String? manufacturerModel,
|
||||
String? semanticVersion,
|
||||
bool? clientRepeat,
|
||||
int? pathHashMode,
|
||||
bool? supportsSpectrumScan,
|
||||
int? spectrumScanMinKhz,
|
||||
int? spectrumScanMaxKhz,
|
||||
@@ -299,6 +305,7 @@ class DeviceInfo {
|
||||
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
||||
autoAddOverwriteOldest:
|
||||
autoAddOverwriteOldest ?? this.autoAddOverwriteOldest,
|
||||
autoAddMaxHops: autoAddMaxHops ?? this.autoAddMaxHops,
|
||||
radioFreq: radioFreq ?? this.radioFreq,
|
||||
radioBw: radioBw ?? this.radioBw,
|
||||
radioSf: radioSf ?? this.radioSf,
|
||||
@@ -315,6 +322,7 @@ class DeviceInfo {
|
||||
manufacturerModel: manufacturerModel ?? this.manufacturerModel,
|
||||
semanticVersion: semanticVersion ?? this.semanticVersion,
|
||||
clientRepeat: clientRepeat ?? this.clientRepeat,
|
||||
pathHashMode: pathHashMode ?? this.pathHashMode,
|
||||
supportsSpectrumScan: supportsSpectrumScan ?? this.supportsSpectrumScan,
|
||||
spectrumScanMinKhz: spectrumScanMinKhz ?? this.spectrumScanMinKhz,
|
||||
spectrumScanMaxKhz: spectrumScanMaxKhz ?? this.spectrumScanMaxKhz,
|
||||
|
||||
11
lib/models/map_coordinate_space.dart
Normal file
11
lib/models/map_coordinate_space.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
enum MapCoordinateSpace {
|
||||
geo,
|
||||
customMap;
|
||||
|
||||
static MapCoordinateSpace fromName(String? value) {
|
||||
return MapCoordinateSpace.values.firstWhere(
|
||||
(space) => space.name == value,
|
||||
orElse: () => MapCoordinateSpace.geo,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,26 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
/// Drawing shape type
|
||||
enum DrawingShapeType {
|
||||
line,
|
||||
rectangle,
|
||||
}
|
||||
enum DrawingShapeType { line, rectangle }
|
||||
|
||||
/// Drawing color enum for compact network transmission
|
||||
enum DrawingColor {
|
||||
red, // 0
|
||||
blue, // 1
|
||||
green, // 2
|
||||
yellow, // 3
|
||||
orange, // 4
|
||||
purple, // 5
|
||||
pink, // 6
|
||||
cyan, // 7
|
||||
}
|
||||
enum DrawingColor { red, blue, green, yellow, orange, purple, pink, cyan }
|
||||
|
||||
/// Drawing colors available for user selection
|
||||
class DrawingColors {
|
||||
static const List<Color> palette = [
|
||||
Colors.red, // index 0
|
||||
Colors.blue, // index 1
|
||||
Colors.green, // index 2
|
||||
Colors.yellow, // index 3
|
||||
Colors.orange, // index 4
|
||||
Colors.purple, // index 5
|
||||
Colors.pink, // index 6
|
||||
Colors.cyan, // index 7
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.yellow,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.pink,
|
||||
Colors.cyan,
|
||||
];
|
||||
|
||||
static String colorToName(Color color) {
|
||||
@@ -45,37 +36,36 @@ class DrawingColors {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/// Convert Color to enum index for network transmission
|
||||
static int colorToIndex(Color color) {
|
||||
for (int i = 0; i < palette.length; i++) {
|
||||
if (palette[i].toARGB32() == color.toARGB32()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // Default to red if not found
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Convert enum index to Color for network reception
|
||||
static Color indexToColor(int index) {
|
||||
if (index >= 0 && index < palette.length) {
|
||||
return palette[index];
|
||||
}
|
||||
return palette[0]; // Default to red if invalid index
|
||||
return palette[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for map drawings
|
||||
abstract class MapDrawing {
|
||||
final String id;
|
||||
final DrawingShapeType type;
|
||||
final Color color;
|
||||
final DateTime createdAt;
|
||||
final String? senderName; // Name of sender (null if local drawing)
|
||||
final bool isReceived; // True if drawing was received from another node
|
||||
final String? messageId; // ID of the source message (for navigation)
|
||||
final bool isShared; // Whether drawing has been broadcast over mesh
|
||||
final bool isSent; // Whether this is a sent drawing (vs received)
|
||||
final bool isHidden; // Temporary visibility toggle (session only, not persisted)
|
||||
final String? senderName;
|
||||
final bool isReceived;
|
||||
final String? messageId;
|
||||
final bool isShared;
|
||||
final bool isSent;
|
||||
final bool isHidden;
|
||||
final MapCoordinateSpace coordinateSpace;
|
||||
final String? mapId;
|
||||
|
||||
MapDrawing({
|
||||
required this.id,
|
||||
@@ -88,79 +78,79 @@ abstract class MapDrawing {
|
||||
this.isShared = false,
|
||||
this.isSent = false,
|
||||
this.isHidden = false,
|
||||
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||
this.mapId,
|
||||
});
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
bool get isCustomMap => coordinateSpace == MapCoordinateSpace.customMap;
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
/// Convert to JSON for network transmission (compact format)
|
||||
/// Uses short field names and excludes createdAt to minimize message size
|
||||
/// Sender will be fetched from packet metadata
|
||||
Map<String, dynamic> toNetworkJson();
|
||||
|
||||
/// Parse network JSON (compact format)
|
||||
/// senderName and messageId will be populated from packet metadata
|
||||
static MapDrawing? fromNetworkJson(
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
final typeNum = json['t'] as int?;
|
||||
if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) {
|
||||
if (typeNum == null ||
|
||||
typeNum < 0 ||
|
||||
typeNum >= DrawingShapeType.values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values[typeNum];
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
static MapDrawing? fromJson(Map<String, dynamic> json) {
|
||||
final typeStr = json['type'] as String?;
|
||||
if (typeStr == null) return null;
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values.firstWhere(
|
||||
(e) => e.toString() == 'DrawingShapeType.$typeStr',
|
||||
(value) => value.name == typeStr,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromJson(json);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromJson(json);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the center point of the drawing
|
||||
LatLng getCenter();
|
||||
|
||||
/// Get the bounds of the drawing
|
||||
LatLngBounds getBounds();
|
||||
}
|
||||
|
||||
/// Line drawing on map
|
||||
class LineDrawing extends MapDrawing {
|
||||
final List<LatLng> points;
|
||||
|
||||
@@ -175,6 +165,8 @@ class LineDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.line);
|
||||
|
||||
@override
|
||||
@@ -184,31 +176,51 @@ class LineDrawing extends MapDrawing {
|
||||
'type': type.name,
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'points': points.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(),
|
||||
'points': points
|
||||
.map((point) => {'lat': point.latitude, 'lon': point.longitude})
|
||||
.toList(),
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), p=points
|
||||
// Points are encoded as flat array [lat1,lon1,lat2,lon2,...]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'p': points.expand((p) => [
|
||||
double.parse(p.latitude.toStringAsFixed(5)),
|
||||
double.parse(p.longitude.toStringAsFixed(5)),
|
||||
]).toList(),
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['p'] = points
|
||||
.expand((point) => [point.latitude.round(), point.longitude.round()])
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['p'] = points
|
||||
.expand(
|
||||
(point) => [
|
||||
double.parse(point.latitude.toStringAsFixed(5)),
|
||||
double.parse(point.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
)
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
static LineDrawing fromJson(Map<String, dynamic> json) {
|
||||
final pointsJson = json['points'] as List<dynamic>;
|
||||
final points = pointsJson.map((p) => LatLng(p['lat'] as double, p['lon'] as double)).toList();
|
||||
final points = pointsJson
|
||||
.map(
|
||||
(point) => LatLng(
|
||||
(point['lat'] as num).toDouble(),
|
||||
(point['lon'] as num).toDouble(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final senderName = json['sender'] as String?;
|
||||
|
||||
return LineDrawing(
|
||||
@@ -217,8 +229,12 @@ class LineDrawing extends MapDrawing {
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,42 +242,60 @@ class LineDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
|
||||
final flatPoints = (json['p'] as List<dynamic>).cast<num>();
|
||||
final points = <LatLng>[];
|
||||
for (int i = 0; i < pointsFlat.length; i += 2) {
|
||||
points.add(LatLng(pointsFlat[i], pointsFlat[i + 1]));
|
||||
for (int i = 0; i < flatPoints.length; i += 2) {
|
||||
points.add(
|
||||
LatLng(flatPoints[i].toDouble(), flatPoints[i + 1].toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
return LineDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated points
|
||||
LineDrawing copyWith({List<LatLng>? points}) {
|
||||
LineDrawing copyWith({
|
||||
List<LatLng>? points,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return LineDrawing(
|
||||
id: id,
|
||||
color: color,
|
||||
createdAt: createdAt,
|
||||
points: points ?? this.points,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
if (points.isEmpty) return LatLng(0, 0);
|
||||
if (points.isEmpty) return const LatLng(0, 0);
|
||||
if (points.length == 1) return points[0];
|
||||
|
||||
// Calculate center as average of all points
|
||||
double sumLat = 0;
|
||||
double sumLon = 0;
|
||||
for (final point in points) {
|
||||
@@ -273,8 +307,12 @@ class LineDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
if (points.isEmpty) return LatLngBounds(LatLng(0, 0), LatLng(0, 0));
|
||||
if (points.length == 1) return LatLngBounds(points[0], points[0]);
|
||||
if (points.isEmpty) {
|
||||
return LatLngBounds(const LatLng(0, 0), const LatLng(0, 0));
|
||||
}
|
||||
if (points.length == 1) {
|
||||
return LatLngBounds(points[0], points[0]);
|
||||
}
|
||||
|
||||
double minLat = points[0].latitude;
|
||||
double maxLat = points[0].latitude;
|
||||
@@ -292,7 +330,6 @@ class LineDrawing extends MapDrawing {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle drawing on map
|
||||
class RectangleDrawing extends MapDrawing {
|
||||
final LatLng topLeft;
|
||||
final LatLng bottomRight;
|
||||
@@ -309,16 +346,17 @@ class RectangleDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.rectangle);
|
||||
|
||||
/// Get all corner points for rendering
|
||||
List<LatLng> get corners => [
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude), // top right
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude), // bottom left
|
||||
topLeft, // close the rectangle
|
||||
];
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude),
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude),
|
||||
topLeft,
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -328,27 +366,41 @@ class RectangleDrawing extends MapDrawing {
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
||||
'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude},
|
||||
'bottomRight': {
|
||||
'lat': bottomRight.latitude,
|
||||
'lon': bottomRight.longitude,
|
||||
},
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), b=bounds [lat1,lon1,lat2,lon2]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'b': [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['b'] = [
|
||||
topLeft.latitude.round(),
|
||||
topLeft.longitude.round(),
|
||||
bottomRight.latitude.round(),
|
||||
bottomRight.longitude.round(),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['b'] = [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
static RectangleDrawing fromJson(Map<String, dynamic> json) {
|
||||
@@ -360,11 +412,21 @@ class RectangleDrawing extends MapDrawing {
|
||||
id: json['id'] as String,
|
||||
color: Color(json['color'] as int),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double),
|
||||
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),
|
||||
topLeft: LatLng(
|
||||
(topLeftJson['lat'] as num).toDouble(),
|
||||
(topLeftJson['lon'] as num).toDouble(),
|
||||
),
|
||||
bottomRight: LatLng(
|
||||
(bottomRightJson['lat'] as num).toDouble(),
|
||||
(bottomRightJson['lon'] as num).toDouble(),
|
||||
),
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -372,27 +434,35 @@ class RectangleDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final bounds = (json['b'] as List<dynamic>).cast<double>();
|
||||
final bounds = (json['b'] as List<dynamic>).cast<num>();
|
||||
|
||||
return RectangleDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: LatLng(bounds[0], bounds[1]),
|
||||
bottomRight: LatLng(bounds[2], bounds[3]),
|
||||
topLeft: LatLng(bounds[0].toDouble(), bounds[1].toDouble()),
|
||||
bottomRight: LatLng(bounds[2].toDouble(), bounds[3].toDouble()),
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated corners
|
||||
RectangleDrawing copyWith({
|
||||
LatLng? topLeft,
|
||||
LatLng? bottomRight,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return RectangleDrawing(
|
||||
id: id,
|
||||
@@ -400,12 +470,18 @@ class RectangleDrawing extends MapDrawing {
|
||||
createdAt: createdAt,
|
||||
topLeft: topLeft ?? this.topLeft,
|
||||
bottomRight: bottomRight ?? this.bottomRight,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
// Center is the midpoint between top-left and bottom-right
|
||||
return LatLng(
|
||||
(topLeft.latitude + bottomRight.latitude) / 2,
|
||||
(topLeft.longitude + bottomRight.longitude) / 2,
|
||||
@@ -414,7 +490,6 @@ class RectangleDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
// Bounds are simply the two corners
|
||||
return LatLngBounds(topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export 'package:meshcore_client/meshcore_client.dart'
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'sar_marker.dart';
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
|
||||
extension MessageVoiceExtension on Message {
|
||||
@@ -47,7 +48,7 @@ extension MessageSarExtension on Message {
|
||||
|
||||
/// Convert to a [SarMarker] if this message contains SAR data.
|
||||
SarMarker? toSarMarker() {
|
||||
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
||||
if (!isSarMarker || sarMarkerType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -57,16 +58,26 @@ extension MessageSarExtension on Message {
|
||||
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
||||
debugPrint(' message.sarCustomEmoji: "$sarCustomEmoji"');
|
||||
|
||||
final coordinateSpace = sarCustomMapPoint != null && sarCustomMapId != null
|
||||
? MapCoordinateSpace.customMap
|
||||
: MapCoordinateSpace.geo;
|
||||
final location = sarCustomMapPoint ?? sarGpsCoordinates;
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SarMarker(
|
||||
id: id,
|
||||
type: sarMarkerType!,
|
||||
location: sarGpsCoordinates!,
|
||||
location: location,
|
||||
timestamp: sentAt,
|
||||
senderPublicKey: senderPublicKeyPrefix,
|
||||
senderName: senderName,
|
||||
notes: sarNotes,
|
||||
customEmoji: sarCustomEmoji,
|
||||
colorIndex: sarColorIndex,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: sarCustomMapId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ class PathRecord {
|
||||
final int failureCount;
|
||||
final int lastRoundTripTimeMs;
|
||||
final DateTime lastUsedAt;
|
||||
final DateTime? lastSucceededAt;
|
||||
final double? senderLatitude;
|
||||
final double? senderLongitude;
|
||||
final double? recipientLatitude;
|
||||
final double? recipientLongitude;
|
||||
|
||||
const PathRecord({
|
||||
required this.pathBytes,
|
||||
@@ -19,6 +24,11 @@ class PathRecord {
|
||||
required this.failureCount,
|
||||
required this.lastRoundTripTimeMs,
|
||||
required this.lastUsedAt,
|
||||
required this.lastSucceededAt,
|
||||
required this.senderLatitude,
|
||||
required this.senderLongitude,
|
||||
required this.recipientLatitude,
|
||||
required this.recipientLongitude,
|
||||
});
|
||||
|
||||
String get signature =>
|
||||
@@ -36,6 +46,11 @@ class PathRecord {
|
||||
int? failureCount,
|
||||
int? lastRoundTripTimeMs,
|
||||
DateTime? lastUsedAt,
|
||||
DateTime? lastSucceededAt,
|
||||
double? senderLatitude,
|
||||
double? senderLongitude,
|
||||
double? recipientLatitude,
|
||||
double? recipientLongitude,
|
||||
}) {
|
||||
return PathRecord(
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
@@ -46,6 +61,11 @@ class PathRecord {
|
||||
failureCount: failureCount ?? this.failureCount,
|
||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
||||
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
|
||||
lastSucceededAt: lastSucceededAt ?? this.lastSucceededAt,
|
||||
senderLatitude: senderLatitude ?? this.senderLatitude,
|
||||
senderLongitude: senderLongitude ?? this.senderLongitude,
|
||||
recipientLatitude: recipientLatitude ?? this.recipientLatitude,
|
||||
recipientLongitude: recipientLongitude ?? this.recipientLongitude,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +79,11 @@ class PathRecord {
|
||||
'failure_count': failureCount,
|
||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
||||
'last_used_at': lastUsedAt.toIso8601String(),
|
||||
'last_succeeded_at': lastSucceededAt?.toIso8601String(),
|
||||
'sender_latitude': senderLatitude,
|
||||
'sender_longitude': senderLongitude,
|
||||
'recipient_latitude': recipientLatitude,
|
||||
'recipient_longitude': recipientLongitude,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +104,13 @@ class PathRecord {
|
||||
lastUsedAt:
|
||||
DateTime.tryParse(json['last_used_at'] as String? ?? '') ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
lastSucceededAt: DateTime.tryParse(
|
||||
json['last_succeeded_at'] as String? ?? '',
|
||||
),
|
||||
senderLatitude: (json['sender_latitude'] as num?)?.toDouble(),
|
||||
senderLongitude: (json['sender_longitude'] as num?)?.toDouble(),
|
||||
recipientLatitude: (json['recipient_latitude'] as num?)?.toDouble(),
|
||||
recipientLongitude: (json['recipient_longitude'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../services/sar_template_service.dart';
|
||||
|
||||
/// SAR (Search & Rescue) marker types
|
||||
@@ -78,6 +79,8 @@ class SarMarker {
|
||||
final String? notes;
|
||||
final String? customEmoji; // For custom SAR markers not in predefined types
|
||||
final int? colorIndex; // Color index (0-7) from standard palette
|
||||
final MapCoordinateSpace coordinateSpace;
|
||||
final String? mapId;
|
||||
|
||||
SarMarker({
|
||||
required this.id,
|
||||
@@ -89,8 +92,13 @@ class SarMarker {
|
||||
this.notes,
|
||||
this.customEmoji,
|
||||
this.colorIndex,
|
||||
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||
this.mapId,
|
||||
});
|
||||
|
||||
bool get isCustomMapMarker =>
|
||||
coordinateSpace == MapCoordinateSpace.customMap && mapId != null;
|
||||
|
||||
/// Get sender public key as hex string (short)
|
||||
String? get senderKeyShort {
|
||||
if (senderPublicKey == null || senderPublicKey!.length < 8) return null;
|
||||
@@ -167,6 +175,8 @@ class SarMarker {
|
||||
String? notes,
|
||||
String? customEmoji,
|
||||
int? colorIndex,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return SarMarker(
|
||||
id: id ?? this.id,
|
||||
@@ -178,6 +188,8 @@ class SarMarker {
|
||||
notes: notes ?? this.notes,
|
||||
customEmoji: customEmoji ?? this.customEmoji,
|
||||
colorIndex: colorIndex ?? this.colorIndex,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/// SSE Server Configuration Model
|
||||
///
|
||||
/// Configuration for the SSE (Server-Sent Events) web server that enables
|
||||
/// multiple app instances to share a single MeshCore BLE device.
|
||||
class SseServerConfig {
|
||||
/// Server bind address (e.g., "0.0.0.0" for all interfaces, "127.0.0.1" for localhost)
|
||||
final String host;
|
||||
|
||||
/// Server port (default: 12929)
|
||||
final int port;
|
||||
|
||||
/// Whether the SSE server is enabled
|
||||
final bool enabled;
|
||||
|
||||
/// Optional authentication token for basic security
|
||||
/// Clients must include this token in Authorization header
|
||||
final String? authToken;
|
||||
|
||||
const SseServerConfig({
|
||||
this.host = '0.0.0.0',
|
||||
this.port = 12929,
|
||||
this.enabled = false,
|
||||
this.authToken,
|
||||
});
|
||||
|
||||
/// Create a copy with updated fields
|
||||
SseServerConfig copyWith({
|
||||
String? host,
|
||||
int? port,
|
||||
bool? enabled,
|
||||
String? authToken,
|
||||
}) {
|
||||
return SseServerConfig(
|
||||
host: host ?? this.host,
|
||||
port: port ?? this.port,
|
||||
enabled: enabled ?? this.enabled,
|
||||
authToken: authToken ?? this.authToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get server URL for clients to connect to
|
||||
String getServerUrl({String? ipAddress}) {
|
||||
final ip = ipAddress ?? host;
|
||||
return 'http://$ip:$port';
|
||||
}
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'enabled': enabled,
|
||||
'authToken': authToken,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory SseServerConfig.fromJson(Map<String, dynamic> json) {
|
||||
return SseServerConfig(
|
||||
host: json['host'] as String? ?? '0.0.0.0',
|
||||
port: json['port'] as int? ?? 12929,
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
authToken: json['authToken'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SseServerConfig(host: $host, port: $port, enabled: $enabled, hasAuth: ${authToken != null})';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,8 @@ class ChannelsProvider with ChangeNotifier {
|
||||
int _selectedChannelIndex = 0; // Default to public channel
|
||||
|
||||
/// 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
|
||||
Channel? getChannel(int index) => _channels[index];
|
||||
@@ -28,6 +29,16 @@ class ChannelsProvider with ChangeNotifier {
|
||||
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
|
||||
void addOrUpdateChannel({
|
||||
required int index,
|
||||
@@ -54,12 +65,12 @@ class ChannelsProvider with ChangeNotifier {
|
||||
void removeChannel(int index) {
|
||||
if (_channels.containsKey(index)) {
|
||||
_channels.remove(index);
|
||||
|
||||
|
||||
// If the deleted channel was selected, switch to public channel
|
||||
if (_selectedChannelIndex == index) {
|
||||
_selectedChannelIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -96,6 +107,13 @@ class ChannelsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear runtime channel state before a live device sync begins.
|
||||
void prepareForDeviceSync() {
|
||||
_channels.clear();
|
||||
_selectedChannelIndex = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if channels have been loaded
|
||||
bool get hasChannels => _channels.isNotEmpty;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,9 @@ import '../models/contact_group.dart';
|
||||
import '../models/message_contact_location.dart';
|
||||
import '../services/cayenne_lpp_parser.dart';
|
||||
import '../services/contact_storage_service.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/fast_gps_packet.dart';
|
||||
import '../utils/rssi_location_estimator.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
|
||||
class PendingAdvert {
|
||||
@@ -125,85 +127,99 @@ class _RetainedRoute {
|
||||
class ContactsProvider with ChangeNotifier {
|
||||
static const double _firstHopFallbackOffsetMeters = 100.0;
|
||||
static const String autoGroupIdPrefix = 'auto_group_';
|
||||
static const String _rawTelemetryHexKey = '__raw_lpp_hex';
|
||||
final Map<String, Contact> _contacts = {};
|
||||
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
||||
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();
|
||||
bool _isInitialized = false;
|
||||
bool _isPersisting = false;
|
||||
bool _persistRequested = false;
|
||||
bool _isPersistingPendingAdverts = false;
|
||||
bool _persistPendingAdvertsRequested = false;
|
||||
String? _storageNamespace;
|
||||
String? _selfPublicKeyHex;
|
||||
ContactTelemetry? _selfTelemetry;
|
||||
|
||||
// Add default public channel on initialization
|
||||
ContactsProvider() {
|
||||
ContactsProvider()
|
||||
: _storageNamespace = ProfileStorageScope.effectiveNamespace {
|
||||
_ensurePublicChannelExists();
|
||||
}
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
String? get storageNamespace => _storageNamespace;
|
||||
|
||||
/// Initialize and load persisted contacts at app startup
|
||||
/// This loads contacts without filtering, allowing offline viewing
|
||||
Future<void> initializeEarly() async {
|
||||
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 {
|
||||
_resetInMemoryState();
|
||||
debugPrint(
|
||||
'📦 [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(
|
||||
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)
|
||||
const publicChannelKey =
|
||||
@@ -225,7 +241,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
devicePublicKey: devicePublicKey,
|
||||
);
|
||||
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
|
||||
@@ -233,12 +249,60 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error initializing: $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 {
|
||||
_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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
||||
void _removeSelfContact(Uint8List devicePublicKey) {
|
||||
final selfKeyHex = devicePublicKey
|
||||
@@ -287,7 +351,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
try {
|
||||
while (_persistRequested) {
|
||||
_persistRequested = false;
|
||||
await _storageService.saveContacts(_contactsForStorage());
|
||||
await _storageService.saveContacts(
|
||||
_contactsForStorage(),
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
|
||||
@@ -305,6 +372,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_persistPendingAdvertsRequested = false;
|
||||
await _storageService.savePendingAdverts(
|
||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -315,6 +383,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
List<Contact> get contacts => _contacts.values.toList();
|
||||
ContactTelemetry? get selfTelemetry => _selfTelemetry;
|
||||
List<Contact> get favouriteContacts =>
|
||||
_contacts.values.where((c) => c.isFavourite).toList();
|
||||
List<SavedContactGroup> get savedContactGroups =>
|
||||
List<SavedContactGroup>.from(_savedContactGroups)
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
@@ -436,7 +507,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
Future<void> _persistSavedGroups() async {
|
||||
try {
|
||||
await _storageService.saveContactGroups(_savedContactGroups);
|
||||
await _storageService.saveContactGroups(
|
||||
_savedContactGroups,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error persisting contact groups: $e');
|
||||
}
|
||||
@@ -450,6 +524,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
List<Contact> get repeaters =>
|
||||
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
||||
|
||||
List<Contact> get sensorContacts =>
|
||||
contacts.where((c) => c.isSensor).toList()..sort(_sortByLastSeen);
|
||||
|
||||
List<Contact> get rooms =>
|
||||
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||||
|
||||
@@ -466,13 +543,73 @@ class ContactsProvider with ChangeNotifier {
|
||||
..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)
|
||||
/// Includes contacts with estimated locations from RSSI.
|
||||
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)
|
||||
List<Contact> get chatContactsWithLocation =>
|
||||
chatContacts.where((c) => c.displayLocation != null).toList();
|
||||
chatContacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||
|
||||
MessageContactLocation? buildMessageContactLocationSnapshot(
|
||||
Contact contact, {
|
||||
@@ -541,7 +678,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// Check if this is a new contact
|
||||
final existingContact = _contacts[contact.publicKeyHex];
|
||||
final existingContact =
|
||||
_contacts[contact.publicKeyHex] ??
|
||||
_retainedContactsForSync[contact.publicKeyHex];
|
||||
final isNewContact = existingContact == null;
|
||||
debugPrint(
|
||||
' isNew: $isNewContact, total contacts before: ${_contacts.length}',
|
||||
@@ -553,6 +692,13 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
_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(
|
||||
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
||||
);
|
||||
@@ -576,11 +722,18 @@ class ContactsProvider with ChangeNotifier {
|
||||
excluded++;
|
||||
continue;
|
||||
}
|
||||
final existingContact = _contacts[contact.publicKeyHex];
|
||||
final existingContact =
|
||||
_contacts[contact.publicKeyHex] ??
|
||||
_retainedContactsForSync[contact.publicKeyHex];
|
||||
_contacts[contact.publicKeyHex] = _mergeIncomingContact(
|
||||
incomingContact: contact,
|
||||
existingContact: existingContact,
|
||||
);
|
||||
_retainedContactsForSync.remove(contact.publicKeyHex);
|
||||
if (contact.type != ContactType.repeater &&
|
||||
contact.type != ContactType.sensor) {
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
if (excluded > 0) {
|
||||
debugPrint(
|
||||
@@ -820,6 +973,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
|
||||
debugPrint(
|
||||
@@ -827,19 +984,46 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
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
|
||||
final contact = _findContactByPrefix(publicKeyPrefix);
|
||||
if (contact == null) {
|
||||
final isSelfTelemetry =
|
||||
contact == null && _matchesSelfPrefix(publicKeyPrefix);
|
||||
if (contact == null && !isSelfTelemetry) {
|
||||
debugPrint(' ❌ Contact not found for this prefix');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(' ✅ Found contact: ${contact.advName}');
|
||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||
if (isSelfTelemetry) {
|
||||
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 {
|
||||
// Parse Cayenne LPP data
|
||||
var telemetry = CayenneLppParser.parse(lppData);
|
||||
var telemetry = _withRawTelemetryHex(
|
||||
CayenneLppParser.parse(lppData),
|
||||
lppData,
|
||||
);
|
||||
debugPrint(' ✅ Parsed new telemetry');
|
||||
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
||||
|
||||
@@ -858,7 +1042,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
final previousTelemetry = contact.telemetry;
|
||||
final previousTelemetry = isSelfTelemetry
|
||||
? _selfTelemetry
|
||||
: contact!.telemetry;
|
||||
|
||||
final mergedTelemetry = _mergeTelemetryForContact(
|
||||
existingTelemetry: previousTelemetry,
|
||||
@@ -870,31 +1056,49 @@ class ContactsProvider with ChangeNotifier {
|
||||
telemetry.gpsLocation,
|
||||
)) {
|
||||
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
|
||||
// lastAdvert is Unix timestamp in seconds
|
||||
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
|
||||
.round();
|
||||
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
||||
debugPrint(' Old lastAdvert: ${resolvedContact.lastAdvert}');
|
||||
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||
|
||||
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
||||
final updatedContact = contact.copyWith(
|
||||
final updatedContact = resolvedContact.copyWith(
|
||||
telemetry: telemetry,
|
||||
lastAdvert: currentTimestamp, // Update last seen time
|
||||
advLat: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.latitude)
|
||||
: contact.advLat,
|
||||
: resolvedContact.advLat,
|
||||
advLon: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.longitude)
|
||||
: contact.advLon,
|
||||
: resolvedContact.advLon,
|
||||
);
|
||||
_contacts[contact.publicKeyHex] = updatedContact;
|
||||
_contacts[resolvedContact.publicKeyHex] = updatedContact;
|
||||
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||
|
||||
_persistContacts();
|
||||
@@ -933,13 +1137,34 @@ class ContactsProvider with ChangeNotifier {
|
||||
),
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'📥 [ContactsProvider] Fast GPS received '
|
||||
'sender=${packet.senderKey6} '
|
||||
'contact=${contact.displayName} '
|
||||
'contactKey=${contact.publicKeyHex} '
|
||||
'lat=${packet.latitude} '
|
||||
'lon=${packet.longitude} '
|
||||
'ts=${packet.timestampSeconds}',
|
||||
);
|
||||
|
||||
final updatedContact = contact.copyWith(
|
||||
telemetry: updatedTelemetry,
|
||||
lastAdvert: packet.timestampSeconds,
|
||||
lastMod: packet.timestampSeconds,
|
||||
advLat: _coordinateToAdvertMicrodegrees(packet.latitude),
|
||||
advLon: _coordinateToAdvertMicrodegrees(packet.longitude),
|
||||
);
|
||||
_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();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -988,10 +1213,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
// last known reading for any field that is omitted in the incoming update.
|
||||
final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation);
|
||||
final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation);
|
||||
final mergedExtraSensorData = <String, dynamic>{
|
||||
...?existingTelemetry?.extraSensorData,
|
||||
...?incomingTelemetry.extraSensorData,
|
||||
};
|
||||
final mergedExtraSensorData = _mergeExtraSensorData(
|
||||
existingTelemetry?.extraSensorData,
|
||||
incomingTelemetry.extraSensorData,
|
||||
);
|
||||
|
||||
return ContactTelemetry(
|
||||
gpsLocation: incomingGps ?? previousGps,
|
||||
@@ -1012,16 +1237,82 @@ 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,
|
||||
) {
|
||||
final merged = <String, dynamic>{...?existingExtraSensorData};
|
||||
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
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) {
|
||||
return (coordinate * 1e6).round();
|
||||
}
|
||||
|
||||
/// Find contact by public key prefix (6 bytes)
|
||||
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
|
||||
.sublist(0, 6)
|
||||
.sublist(0, takeLen)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('');
|
||||
|
||||
@@ -1057,6 +1348,36 @@ class ContactsProvider with ChangeNotifier {
|
||||
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
|
||||
/// prefer flood routing until the radio reports a fresh route.
|
||||
void markPathUnhealthy(Uint8List publicKey) {
|
||||
@@ -1354,6 +1675,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
void clearContacts() {
|
||||
_contacts.clear();
|
||||
_pendingAdverts.clear();
|
||||
_retainedContactsForSync.clear();
|
||||
_ensurePublicChannelExists();
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
@@ -1378,6 +1700,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
// Then remove from local storage
|
||||
_contacts.remove(publicKeyHex);
|
||||
_pendingAdverts.remove(publicKeyHex);
|
||||
_retainedContactsForSync.remove(publicKeyHex);
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
@@ -1385,11 +1708,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
return await _storageService.getStorageStats();
|
||||
}
|
||||
|
||||
Future<void> persistNow() async {
|
||||
await _storageService.saveContacts(_contactsForStorage());
|
||||
return await _storageService.getStorageStats(namespace: _storageNamespace);
|
||||
}
|
||||
|
||||
Future<void> clearPendingAdverts() async {
|
||||
@@ -1397,7 +1716,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_pendingAdverts.clear();
|
||||
await _storageService.clearPendingAdverts();
|
||||
await _storageService.clearPendingAdverts(namespace: _storageNamespace);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1460,6 +1779,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) {
|
||||
return {
|
||||
'publicKey': base64Encode(advert.publicKey),
|
||||
@@ -1526,6 +1855,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
return {
|
||||
'chat': chatContacts.length,
|
||||
'repeater': repeaters.length,
|
||||
'sensor': sensorContacts.length,
|
||||
'room': rooms.length,
|
||||
'total': contacts.length,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
|
||||
/// Drawing mode state
|
||||
@@ -33,12 +36,18 @@ class DrawingProvider with ChangeNotifier {
|
||||
LatLng? _measurementPoint1;
|
||||
LatLng? _measurementPoint2;
|
||||
double? _measuredDistance; // in meters
|
||||
MapCoordinateSpace _activeCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _activeMapId;
|
||||
double? _activeMetersPerPixel;
|
||||
|
||||
// Getters
|
||||
DrawingMode get drawingMode => _drawingMode;
|
||||
Color get selectedColor => _selectedColor;
|
||||
bool get showReceivedDrawings => _showReceivedDrawings;
|
||||
bool get showSarMarkers => _showSarMarkers;
|
||||
MapCoordinateSpace get activeCoordinateSpace => _activeCoordinateSpace;
|
||||
String? get activeMapId => _activeMapId;
|
||||
double? get activeMetersPerPixel => _activeMetersPerPixel;
|
||||
List<MapDrawing> get drawings {
|
||||
// Filter out hidden drawings first
|
||||
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
||||
@@ -48,8 +57,19 @@ class DrawingProvider with ChangeNotifier {
|
||||
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
|
||||
}
|
||||
|
||||
visibleDrawings = visibleDrawings.where((drawing) {
|
||||
if (drawing.coordinateSpace != _activeCoordinateSpace) {
|
||||
return false;
|
||||
}
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return drawing.mapId != null && drawing.mapId == _activeMapId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return List.unmodifiable(visibleDrawings.toList());
|
||||
}
|
||||
|
||||
MapDrawing? get currentDrawing => _currentDrawing;
|
||||
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
||||
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
||||
@@ -66,6 +86,35 @@ class DrawingProvider with ChangeNotifier {
|
||||
_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
|
||||
void setDrawingMode(DrawingMode mode) {
|
||||
if (_drawingMode != mode) {
|
||||
@@ -98,15 +147,19 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showReceivedDrawings = prefs.getBool(_showReceivedDrawingsKey) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_showSarMarkersKey) ?? true;
|
||||
_showReceivedDrawings =
|
||||
prefs.getBool(_scopedKey(_showReceivedDrawingsKey)) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_scopedKey(_showSarMarkersKey)) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_showReceivedDrawingsKey, _showReceivedDrawings);
|
||||
await prefs.setBool(_showSarMarkersKey, _showSarMarkers);
|
||||
await prefs.setBool(
|
||||
_scopedKey(_showReceivedDrawingsKey),
|
||||
_showReceivedDrawings,
|
||||
);
|
||||
await prefs.setBool(_scopedKey(_showSarMarkersKey), _showSarMarkers);
|
||||
}
|
||||
|
||||
/// Start drawing a line
|
||||
@@ -137,6 +190,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: List.from(_currentLinePoints),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -180,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
? _rectangleStartPoint!.longitude
|
||||
: endPoint.longitude,
|
||||
),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -216,6 +277,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: topLeft,
|
||||
bottomRight: bottomRight,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -237,7 +302,9 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Set second measurement point and calculate distance
|
||||
void setMeasurementPoint2(LatLng point) {
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_measurementPoint2 = point;
|
||||
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
|
||||
@@ -246,6 +313,13 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Calculate distance between two points using Haversine formula
|
||||
double _calculateDistance(LatLng point1, LatLng point2) {
|
||||
if (_activeCoordinateSpace == MapCoordinateSpace.customMap &&
|
||||
_activeMetersPerPixel != null) {
|
||||
final dy = point2.latitude - point1.latitude;
|
||||
final dx = point2.longitude - point1.longitude;
|
||||
final pixelDistance = math.sqrt((dx * dx) + (dy * dy));
|
||||
return pixelDistance * _activeMetersPerPixel!;
|
||||
}
|
||||
const Distance distance = Distance();
|
||||
return distance.as(LengthUnit.Meter, point1, point2);
|
||||
}
|
||||
@@ -302,7 +376,7 @@ class DrawingProvider with ChangeNotifier {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = _drawings.map((d) => d.toJson()).toList();
|
||||
final jsonString = jsonEncode(jsonList);
|
||||
await prefs.setString(_storageKey, jsonString);
|
||||
await prefs.setString(_scopedKey(_storageKey), jsonString);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving drawings: $e');
|
||||
}
|
||||
@@ -312,8 +386,11 @@ class DrawingProvider with ChangeNotifier {
|
||||
Future<void> _loadDrawings() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_storageKey);
|
||||
if (jsonString == null) return;
|
||||
final jsonString = prefs.getString(_scopedKey(_storageKey));
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
_drawings.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
_drawings.clear();
|
||||
@@ -339,6 +416,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: _currentLinePoints,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
} else if (_drawingMode == DrawingMode.rectangle &&
|
||||
_currentDrawing != null) {
|
||||
@@ -376,6 +457,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
return RectangleDrawing(
|
||||
@@ -390,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
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)
|
||||
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
|
||||
@@ -428,6 +531,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -442,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,6 +576,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -483,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../models/message.dart';
|
||||
import '../../models/contact.dart';
|
||||
@@ -19,10 +20,19 @@ class MessageRetryManager {
|
||||
final Map<String, DateTime> _lastRetryTimes = {};
|
||||
final Map<String, int> _pathFailureStreaks = {};
|
||||
|
||||
static const int maxRetryAttempts = 4;
|
||||
/// Max retry attempts when the contact has a known path.
|
||||
/// 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.
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000, 8000];
|
||||
static const int _defaultLoRaSf = 10;
|
||||
static const int _defaultLoRaCr = 5;
|
||||
static const int _defaultLoRaBwHz = 250000;
|
||||
@@ -38,32 +48,57 @@ class MessageRetryManager {
|
||||
return _retryDelays[attempt];
|
||||
}
|
||||
|
||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
||||
/// provide one or returns an invalid value.
|
||||
static final math.Random _rng = math.Random();
|
||||
|
||||
/// Calculate a delivery-ACK timeout with random jitter.
|
||||
///
|
||||
/// Matches the official MeshCore app: `suggestedTimeout + random(1-8s)`.
|
||||
/// The jitter prevents collision when multiple messages are in flight.
|
||||
int calculateAckTimeoutMs({
|
||||
required String text,
|
||||
required Contact? contact,
|
||||
int? suggestedTimeoutMs,
|
||||
}) {
|
||||
int baseTimeout;
|
||||
if (suggestedTimeoutMs != null && suggestedTimeoutMs > 0) {
|
||||
return suggestedTimeoutMs;
|
||||
baseTimeout = suggestedTimeoutMs;
|
||||
} else {
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
|
||||
if (hopCount < 0) {
|
||||
baseTimeout = ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
} else {
|
||||
baseTimeout = ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
}
|
||||
}
|
||||
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
// Add random jitter: 500-2000ms to avoid collision
|
||||
final jitterMs = 500 + _rng.nextInt(1501);
|
||||
return baseTimeout + jitterMs;
|
||||
}
|
||||
|
||||
if (hopCount < 0) {
|
||||
return ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
}
|
||||
|
||||
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
/// Max attempts for a given contact based on whether it has a known path.
|
||||
static int maxRetryAttemptsForContact(Contact? contact) {
|
||||
final hasPath = contact?.routeHasPath ?? false;
|
||||
return hasPath ? maxRetryAttemptsWithPath : maxRetryAttemptsFloodOnly;
|
||||
}
|
||||
|
||||
bool canRetry(Message message, Contact contact) {
|
||||
return message.retryAttempt < maxRetryAttempts;
|
||||
return message.retryAttempt < maxRetryAttemptsForContact(contact);
|
||||
}
|
||||
|
||||
/// Whether this is the last retry attempt.
|
||||
/// When true, the caller should reset the path to force flood mode.
|
||||
///
|
||||
/// Note: called AFTER retryAttempt has been incremented, so we compare
|
||||
/// directly against maxAttempts (not +1).
|
||||
bool isLastAttempt(Message message, Contact contact) {
|
||||
final maxAttempts = maxRetryAttemptsForContact(contact);
|
||||
return maxAttempts > 1 && message.retryAttempt >= maxAttempts;
|
||||
}
|
||||
|
||||
/// Track a retry attempt for a message
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
|
||||
@@ -75,6 +76,8 @@ class ImageProvider with ChangeNotifier {
|
||||
_restore();
|
||||
}
|
||||
|
||||
String _scopedStorageKey() => ProfileStorageScope.scopedKey(_storageKey);
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
ImageSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -292,18 +295,22 @@ class ImageProvider with ChangeNotifier {
|
||||
// ── Persistence ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> clearAll() async {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storageKey);
|
||||
await prefs.remove(_scopedStorageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to clear storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_resetInMemoryState();
|
||||
await _restore();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _evictExpiredOutgoing() {
|
||||
final now = DateTime.now();
|
||||
_outgoing.removeWhere((_, s) => now.difference(s.cachedAt) > _outgoingTtl);
|
||||
@@ -343,7 +350,7 @@ class ImageProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
||||
await prefs.setString(_scopedStorageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to persist: $e');
|
||||
}
|
||||
@@ -352,7 +359,7 @@ class ImageProvider with ChangeNotifier {
|
||||
Future<void> _restore() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
final raw = prefs.getString(_scopedStorageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
@@ -428,6 +435,12 @@ class ImageProvider with ChangeNotifier {
|
||||
debugPrint('❌ [ImageProvider] Failed to restore: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingSession {
|
||||
|
||||
@@ -1,30 +1,46 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/custom_map_config.dart';
|
||||
import '../models/location_trail.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
class MapProvider with ChangeNotifier {
|
||||
static const String _customMapConfigKey = 'custom_map_config_v1';
|
||||
static const String _customMapModeKey = 'custom_map_mode_v1';
|
||||
static const String _customMapsDirName = 'custom_maps';
|
||||
|
||||
MapProvider() {
|
||||
unawaited(_loadInitialState());
|
||||
}
|
||||
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
LatLng? _targetLocation;
|
||||
LatLngBounds? _targetBounds;
|
||||
double? _targetZoom;
|
||||
bool _shouldAnimate = false;
|
||||
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _targetMapId;
|
||||
|
||||
// Track which contact paths are currently visible
|
||||
final Set<String> _visibleContactPaths = {};
|
||||
|
||||
// Location trail tracking
|
||||
LocationTrail? _currentTrail;
|
||||
bool _isTrailVisible = true;
|
||||
final List<LocationTrail> _trailHistory = [];
|
||||
|
||||
// WMS overlay toggles
|
||||
bool _showCadastralOverlay = false;
|
||||
bool _showForestRoadsOverlay = false;
|
||||
bool _showHikingTrailsOverlay = false;
|
||||
@@ -37,29 +53,27 @@ class MapProvider with ChangeNotifier {
|
||||
bool _showPlaceNamesOverlay = false;
|
||||
bool _showMunicipalityBordersOverlay = false;
|
||||
|
||||
// Contact trail toggles
|
||||
bool _showAllContactTrails = true; // Default to showing all contact trails
|
||||
bool _hideRepeatersOnMap = false;
|
||||
|
||||
// Imported trail (from GPX)
|
||||
LocationTrail? _importedTrail;
|
||||
|
||||
// Download area selection
|
||||
bool _isSelectingDownloadArea = false;
|
||||
LatLngBounds? _downloadAreaBounds;
|
||||
|
||||
CustomMapConfig? _customMapConfig;
|
||||
bool _isUsingCustomMap = false;
|
||||
|
||||
LatLng? get targetLocation => _targetLocation;
|
||||
LatLngBounds? get targetBounds => _targetBounds;
|
||||
double? get targetZoom => _targetZoom;
|
||||
bool get shouldAnimate => _shouldAnimate;
|
||||
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
|
||||
|
||||
// Trail getters
|
||||
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
|
||||
String? get targetMapId => _targetMapId;
|
||||
LocationTrail? get currentTrail => _currentTrail;
|
||||
bool get isTrailVisible => _isTrailVisible;
|
||||
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
||||
bool get isTrailActive => _currentTrail?.isActive ?? false;
|
||||
|
||||
// WMS overlay getters
|
||||
bool get showCadastralOverlay => _showCadastralOverlay;
|
||||
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
||||
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
|
||||
@@ -72,69 +86,138 @@ class MapProvider with ChangeNotifier {
|
||||
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
||||
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
||||
|
||||
// Contact trail getters
|
||||
bool get showAllContactTrails => _showAllContactTrails;
|
||||
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
||||
|
||||
// Imported trail getters
|
||||
LocationTrail? get importedTrail => _importedTrail;
|
||||
|
||||
// Download area getters
|
||||
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
|
||||
LatLngBounds? get downloadAreaBounds => _downloadAreaBounds;
|
||||
|
||||
CustomMapConfig? get customMapConfig => _customMapConfig;
|
||||
bool get hasCustomMap => _customMapConfig != null;
|
||||
bool get isUsingCustomMap => _isUsingCustomMap && _customMapConfig != null;
|
||||
bool get shouldHideGpsData => isUsingCustomMap;
|
||||
|
||||
LatLngBounds? get customMapBounds => _customMapConfig?.bounds;
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _loadInitialState();
|
||||
}
|
||||
|
||||
bool matchesActiveCustomMap(String? mapId) {
|
||||
return hasCustomMap &&
|
||||
normalizeCustomMapId(_customMapConfig!.mapId) ==
|
||||
normalizeCustomMapId(mapId);
|
||||
}
|
||||
|
||||
void navigateToLocation({
|
||||
required LatLng location,
|
||||
double zoom = 15.0,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
_targetLocation = location;
|
||||
_targetBounds = null;
|
||||
_targetZoom = zoom;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
_targetMapId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? navigateToMapPoint({
|
||||
required LatLng point,
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
double zoom = 15.0,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(mapId)) {
|
||||
return 'Load the matching custom map to view this item.';
|
||||
}
|
||||
if (!_isUsingCustomMap) {
|
||||
_isUsingCustomMap = true;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
} else if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
|
||||
_targetLocation = point;
|
||||
_targetBounds = null;
|
||||
_targetZoom = zoom;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = coordinateSpace;
|
||||
_targetMapId = mapId;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
String? navigateToBounds({
|
||||
required LatLngBounds bounds,
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(mapId)) {
|
||||
return 'Load the matching custom map to view this item.';
|
||||
}
|
||||
if (!_isUsingCustomMap) {
|
||||
_isUsingCustomMap = true;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
} else if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
|
||||
_targetBounds = bounds;
|
||||
_targetLocation = bounds.center;
|
||||
_targetZoom = null;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = coordinateSpace;
|
||||
_targetMapId = mapId;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
void clearNavigation() {
|
||||
_targetLocation = null;
|
||||
_targetBounds = null;
|
||||
_targetZoom = null;
|
||||
_shouldAnimate = false;
|
||||
// Don't notify listeners to avoid rebuilds
|
||||
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
_targetMapId = null;
|
||||
}
|
||||
|
||||
/// Navigate to a drawing by its ID
|
||||
void navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||
debugPrint('🗺️ [MapProvider] navigateToDrawing called with ID: $drawingId');
|
||||
// Find the drawing in the provider
|
||||
final drawings = drawingProvider.drawings as List;
|
||||
debugPrint('🗺️ [MapProvider] Total drawings in provider: ${drawings.length}');
|
||||
final drawing = drawings.cast<dynamic>().firstWhere(
|
||||
(d) => d.id == drawingId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
String? navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||
final drawing = drawingProvider.getDrawingById(drawingId) as MapDrawing?;
|
||||
if (drawing == null) {
|
||||
debugPrint('⚠️ [MapProvider] Drawing $drawingId not found');
|
||||
debugPrint('⚠️ [MapProvider] Available drawing IDs: ${drawings.map((d) => d.id).toList()}');
|
||||
return;
|
||||
return 'Drawing not found.';
|
||||
}
|
||||
|
||||
// Use MapDrawing's built-in getCenter and getBounds methods
|
||||
final center = drawing.getCenter();
|
||||
final bounds = drawing.getBounds();
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(drawing.mapId)) {
|
||||
return 'Load the matching custom map to view this drawing.';
|
||||
}
|
||||
return navigateToBounds(
|
||||
bounds: drawing.getBounds(),
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate appropriate zoom level based on bounds
|
||||
// For larger drawings, use lower zoom to fit the whole drawing
|
||||
// For smaller drawings, use higher zoom for better detail
|
||||
final bounds = drawing.getBounds();
|
||||
final latDiff = (bounds.north - bounds.south).abs();
|
||||
final lonDiff = (bounds.east - bounds.west).abs();
|
||||
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
|
||||
|
||||
// Zoom scale: smaller drawings get higher zoom
|
||||
// 0.001 degrees (~100m) -> zoom 17
|
||||
// 0.005 degrees (~500m) -> zoom 16
|
||||
// 0.01 degrees (~1km) -> zoom 15
|
||||
// 0.05 degrees (~5km) -> zoom 13
|
||||
// 0.1 degrees (~10km) -> zoom 12
|
||||
double zoom = 15.0;
|
||||
if (maxDiff < 0.001) {
|
||||
zoom = 17.0;
|
||||
@@ -150,9 +233,125 @@ class MapProvider with ChangeNotifier {
|
||||
zoom = 10.0;
|
||||
}
|
||||
|
||||
final typeStr = drawing is LineDrawing ? 'line' : 'rectangle';
|
||||
debugPrint('🗺️ [MapProvider] Navigating to drawing: $typeStr, zoom: $zoom');
|
||||
navigateToLocation(location: center, zoom: zoom, animate: true);
|
||||
navigateToLocation(
|
||||
location: drawing.getCenter(),
|
||||
zoom: zoom,
|
||||
animate: true,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
String? navigateToSarMarker(SarMarker marker) {
|
||||
if (marker.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return navigateToMapPoint(
|
||||
point: marker.location,
|
||||
coordinateSpace: MapCoordinateSpace.customMap,
|
||||
mapId: marker.mapId,
|
||||
);
|
||||
}
|
||||
navigateToLocation(location: marker.location, zoom: 15.0, animate: true);
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> loadCustomMapFromGallery() async {
|
||||
final picked = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (picked == null) {
|
||||
return false;
|
||||
}
|
||||
final bytes = await picked.readAsBytes();
|
||||
await setCustomMapImage(bytes: bytes, displayName: picked.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> replaceCustomMap() async {
|
||||
await loadCustomMapFromGallery();
|
||||
}
|
||||
|
||||
Future<void> setCustomMapImage({
|
||||
required Uint8List bytes,
|
||||
required String displayName,
|
||||
}) async {
|
||||
final dimensions = await _decodeImageSize(bytes);
|
||||
final mapId = normalizeCustomMapId(sha256.convert(bytes).toString())!;
|
||||
final documentsDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${documentsDir.path}/$_customMapsDirName');
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
final extension = _normalizedExtension(displayName);
|
||||
final nextPath = '${dir.path}/custom_map_$mapId.$extension';
|
||||
final nextFile = File(nextPath);
|
||||
await nextFile.writeAsBytes(bytes, flush: true);
|
||||
|
||||
final previousPath = _customMapConfig?.filePath;
|
||||
_customMapConfig = CustomMapConfig(
|
||||
filePath: nextPath,
|
||||
displayName: displayName,
|
||||
mapId: mapId,
|
||||
imageWidth: dimensions.$1,
|
||||
imageHeight: dimensions.$2,
|
||||
);
|
||||
_isUsingCustomMap = true;
|
||||
await _saveCustomMapState();
|
||||
|
||||
if (previousPath != null && previousPath != nextPath) {
|
||||
unawaited(_deleteFileIfExists(previousPath));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setCustomMapCalibration({
|
||||
required LatLng pointA,
|
||||
required LatLng pointB,
|
||||
required double metersPerPixel,
|
||||
}) async {
|
||||
if (_customMapConfig == null) return;
|
||||
_customMapConfig = _customMapConfig!.copyWith(
|
||||
calibrationPointA: pointA,
|
||||
calibrationPointB: pointB,
|
||||
metersPerPixel: metersPerPixel,
|
||||
);
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> clearCustomMapCalibration() async {
|
||||
if (_customMapConfig == null) return;
|
||||
_customMapConfig = _customMapConfig!.copyWith(
|
||||
clearMetersPerPixel: true,
|
||||
clearCalibrationPointA: true,
|
||||
clearCalibrationPointB: true,
|
||||
);
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeCustomMap() async {
|
||||
final filePath = _customMapConfig?.filePath;
|
||||
_customMapConfig = null;
|
||||
_isUsingCustomMap = false;
|
||||
clearNavigation();
|
||||
await _saveCustomMapState();
|
||||
if (filePath != null) {
|
||||
await _deleteFileIfExists(filePath);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> enterCustomMapMode() async {
|
||||
if (!hasCustomMap || _isUsingCustomMap) return;
|
||||
_isUsingCustomMap = true;
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> exitCustomMapMode() async {
|
||||
if (!_isUsingCustomMap) return;
|
||||
_isUsingCustomMap = false;
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateZoom(double zoom) {
|
||||
@@ -160,37 +359,7 @@ class MapProvider with ChangeNotifier {
|
||||
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() {
|
||||
// End current trail if active
|
||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||
endTrail();
|
||||
}
|
||||
@@ -203,22 +372,22 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Add a point to the current trail
|
||||
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
|
||||
if (_currentTrail == null || !_currentTrail!.isActive) {
|
||||
startTrail();
|
||||
}
|
||||
|
||||
_currentTrail!.addPoint(TrailPoint(
|
||||
position: position,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: accuracy,
|
||||
speed: speed,
|
||||
));
|
||||
_currentTrail!.addPoint(
|
||||
TrailPoint(
|
||||
position: position,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: accuracy,
|
||||
speed: speed,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// End the current trail
|
||||
void endTrail() {
|
||||
if (_currentTrail != null) {
|
||||
_currentTrail!.isActive = false;
|
||||
@@ -231,13 +400,11 @@ class MapProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle trail visibility
|
||||
void toggleTrailVisibility() {
|
||||
_isTrailVisible = !_isTrailVisible;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear the current trail
|
||||
void clearCurrentTrail() {
|
||||
if (_currentTrail != null) {
|
||||
_currentTrail = null;
|
||||
@@ -245,158 +412,177 @@ class MapProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all trail history
|
||||
void clearAllTrails() {
|
||||
_currentTrail = null;
|
||||
_trailHistory.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Get total trail distance in meters
|
||||
double get totalTrailDistance {
|
||||
if (_currentTrail == null) return 0;
|
||||
return _currentTrail!.totalDistance;
|
||||
}
|
||||
|
||||
/// Get trail duration
|
||||
Duration get trailDuration {
|
||||
if (_currentTrail == null) return Duration.zero;
|
||||
return _currentTrail!.duration;
|
||||
}
|
||||
|
||||
/// Toggle cadastral parcels overlay
|
||||
Future<void> toggleCadastralOverlay() async {
|
||||
_showCadastralOverlay = !_showCadastralOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle forest roads overlay
|
||||
Future<void> toggleForestRoadsOverlay() async {
|
||||
_showForestRoadsOverlay = !_showForestRoadsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle hiking trails overlay
|
||||
Future<void> toggleHikingTrailsOverlay() async {
|
||||
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle main roads overlay
|
||||
Future<void> toggleMainRoadsOverlay() async {
|
||||
_showMainRoadsOverlay = !_showMainRoadsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle house numbers overlay
|
||||
Future<void> toggleHouseNumbersOverlay() async {
|
||||
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle fire hazard zones overlay
|
||||
Future<void> toggleFireHazardZonesOverlay() async {
|
||||
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle historical fires overlay
|
||||
Future<void> toggleHistoricalFiresOverlay() async {
|
||||
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle firebreaks overlay
|
||||
Future<void> toggleFirebreaksOverlay() async {
|
||||
_showFirebreaksOverlay = !_showFirebreaksOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle Kras fire zones overlay
|
||||
Future<void> toggleKrasFireZonesOverlay() async {
|
||||
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle place names overlay
|
||||
Future<void> togglePlaceNamesOverlay() async {
|
||||
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle municipality borders overlay
|
||||
Future<void> toggleMunicipalityBordersOverlay() async {
|
||||
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Load overlay state from SharedPreferences
|
||||
Future<void> loadOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showCadastralOverlay = prefs.getBool('map_show_cadastral_overlay') ?? false;
|
||||
_showForestRoadsOverlay = prefs.getBool('map_show_forest_roads_overlay') ?? false;
|
||||
_showHikingTrailsOverlay = prefs.getBool('map_show_hiking_trails_overlay') ?? false;
|
||||
_showMainRoadsOverlay = prefs.getBool('map_show_main_roads_overlay') ?? false;
|
||||
_showHouseNumbersOverlay = prefs.getBool('map_show_house_numbers_overlay') ?? false;
|
||||
_showFireHazardZonesOverlay = prefs.getBool('map_show_fire_hazard_zones_overlay') ?? false;
|
||||
_showHistoricalFiresOverlay = prefs.getBool('map_show_historical_fires_overlay') ?? false;
|
||||
_showFirebreaksOverlay = prefs.getBool('map_show_firebreaks_overlay') ?? false;
|
||||
_showKrasFireZonesOverlay = prefs.getBool('map_show_kras_fire_zones_overlay') ?? false;
|
||||
_showPlaceNamesOverlay = prefs.getBool('map_show_place_names_overlay') ?? false;
|
||||
_showMunicipalityBordersOverlay = prefs.getBool('map_show_municipality_borders_overlay') ?? false;
|
||||
_showCadastralOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_cadastral_overlay')) ?? false;
|
||||
_showForestRoadsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_forest_roads_overlay')) ?? false;
|
||||
_showHikingTrailsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_hiking_trails_overlay')) ?? false;
|
||||
_showMainRoadsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_main_roads_overlay')) ?? false;
|
||||
_showHouseNumbersOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_house_numbers_overlay')) ?? false;
|
||||
_showFireHazardZonesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_fire_hazard_zones_overlay')) ??
|
||||
false;
|
||||
_showHistoricalFiresOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_historical_fires_overlay')) ?? false;
|
||||
_showFirebreaksOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_firebreaks_overlay')) ?? false;
|
||||
_showKrasFireZonesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_kras_fire_zones_overlay')) ?? false;
|
||||
_showPlaceNamesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_place_names_overlay')) ?? false;
|
||||
_showMunicipalityBordersOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_municipality_borders_overlay')) ??
|
||||
false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialState() async {
|
||||
await Future.wait([loadOverlayState(), loadTrailSettings()]);
|
||||
await loadRepeaterVisibilitySettings();
|
||||
await Future.wait([
|
||||
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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
|
||||
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
|
||||
await prefs.setBool('map_show_hiking_trails_overlay', _showHikingTrailsOverlay);
|
||||
await prefs.setBool('map_show_main_roads_overlay', _showMainRoadsOverlay);
|
||||
await prefs.setBool('map_show_house_numbers_overlay', _showHouseNumbersOverlay);
|
||||
await prefs.setBool('map_show_fire_hazard_zones_overlay', _showFireHazardZonesOverlay);
|
||||
await prefs.setBool('map_show_historical_fires_overlay', _showHistoricalFiresOverlay);
|
||||
await prefs.setBool('map_show_firebreaks_overlay', _showFirebreaksOverlay);
|
||||
await prefs.setBool('map_show_kras_fire_zones_overlay', _showKrasFireZonesOverlay);
|
||||
await prefs.setBool('map_show_place_names_overlay', _showPlaceNamesOverlay);
|
||||
await prefs.setBool('map_show_municipality_borders_overlay', _showMunicipalityBordersOverlay);
|
||||
}
|
||||
|
||||
/// Toggle all contact trails on/off
|
||||
Future<void> toggleAllContactTrails() async {
|
||||
_showAllContactTrails = !_showAllContactTrails;
|
||||
notifyListeners();
|
||||
await _saveTrailSettings();
|
||||
}
|
||||
|
||||
/// Load trail settings from SharedPreferences
|
||||
Future<void> loadTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showAllContactTrails = prefs.getBool('map_show_all_contact_trails') ?? true; // Default to true (show all)
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Save trail settings to SharedPreferences
|
||||
Future<void> _saveTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_cadastral_overlay'),
|
||||
_showCadastralOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_forest_roads_overlay'),
|
||||
_showForestRoadsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_hiking_trails_overlay'),
|
||||
_showHikingTrailsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_main_roads_overlay'),
|
||||
_showMainRoadsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_house_numbers_overlay'),
|
||||
_showHouseNumbersOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_fire_hazard_zones_overlay'),
|
||||
_showFireHazardZonesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_historical_fires_overlay'),
|
||||
_showHistoricalFiresOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_firebreaks_overlay'),
|
||||
_showFirebreaksOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_kras_fire_zones_overlay'),
|
||||
_showKrasFireZonesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_place_names_overlay'),
|
||||
_showPlaceNamesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_municipality_borders_overlay'),
|
||||
_showMunicipalityBordersOverlay,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setHideRepeatersOnMap(bool hide) async {
|
||||
@@ -404,57 +590,166 @@ class MapProvider with ChangeNotifier {
|
||||
_hideRepeatersOnMap = hide;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_hide_repeaters', _hideRepeatersOnMap);
|
||||
await prefs.setBool(_scopedKey('map_hide_repeaters'), _hideRepeatersOnMap);
|
||||
}
|
||||
|
||||
Future<void> loadRepeaterVisibilitySettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_hideRepeatersOnMap = prefs.getBool('map_hide_repeaters') ?? false;
|
||||
_hideRepeatersOnMap =
|
||||
prefs.getBool(_scopedKey('map_hide_repeaters')) ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set imported trail (from GPX import)
|
||||
void setImportedTrail(LocationTrail trail) {
|
||||
_importedTrail = trail;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear imported trail
|
||||
void clearImportedTrail() {
|
||||
_importedTrail = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Replace current trail with imported trail
|
||||
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
||||
// End current trail if active
|
||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||
endTrail();
|
||||
}
|
||||
|
||||
// Set imported trail as current trail
|
||||
_currentTrail = importedTrail;
|
||||
_isTrailVisible = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Enter download area selection mode with initial bounds
|
||||
void enterDownloadAreaMode(LatLngBounds initialBounds) {
|
||||
_isSelectingDownloadArea = true;
|
||||
_downloadAreaBounds = initialBounds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Exit download area selection mode
|
||||
void exitDownloadAreaMode() {
|
||||
_isSelectingDownloadArea = false;
|
||||
_downloadAreaBounds = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Update the download area bounds (while dragging/resizing)
|
||||
void updateDownloadAreaBounds(LatLngBounds bounds) {
|
||||
_downloadAreaBounds = bounds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadCustomMapState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final configJson = prefs.getString(_scopedKey(_customMapConfigKey));
|
||||
if (configJson != null && configJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(configJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final config = CustomMapConfig.fromJson(decoded);
|
||||
if (config != null && await File(config.filePath).exists()) {
|
||||
_customMapConfig = config;
|
||||
} else {
|
||||
_customMapConfig = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_customMapConfig = null;
|
||||
}
|
||||
_isUsingCustomMap =
|
||||
(prefs.getBool(_scopedKey(_customMapModeKey)) ?? false) &&
|
||||
_customMapConfig != null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveCustomMapState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (_customMapConfig == null) {
|
||||
await prefs.remove(_scopedKey(_customMapConfigKey));
|
||||
} else {
|
||||
await prefs.setString(
|
||||
_scopedKey(_customMapConfigKey),
|
||||
jsonEncode(_customMapConfig!.toJson()),
|
||||
);
|
||||
}
|
||||
await prefs.setBool(_scopedKey(_customMapModeKey), _isUsingCustomMap);
|
||||
}
|
||||
|
||||
Future<(int, int)> _decodeImageSize(Uint8List bytes) async {
|
||||
final codec = await ui.instantiateImageCodec(bytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
return (frame.image.width, frame.image.height);
|
||||
}
|
||||
|
||||
String _normalizedExtension(String displayName) {
|
||||
final dotIndex = displayName.lastIndexOf('.');
|
||||
if (dotIndex == -1 || dotIndex == displayName.length - 1) {
|
||||
return 'png';
|
||||
}
|
||||
return displayName.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
Future<void> _deleteFileIfExists(String path) async {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> exportWorkspaceJson() {
|
||||
return {
|
||||
'currentTrail': _currentTrail?.toJson(),
|
||||
'trailHistory': _trailHistory.map((trail) => trail.toJson()).toList(),
|
||||
'importedTrail': _importedTrail?.toJson(),
|
||||
'isTrailVisible': _isTrailVisible,
|
||||
'showCadastralOverlay': _showCadastralOverlay,
|
||||
'showForestRoadsOverlay': _showForestRoadsOverlay,
|
||||
'showHikingTrailsOverlay': _showHikingTrailsOverlay,
|
||||
'showMainRoadsOverlay': _showMainRoadsOverlay,
|
||||
'showHouseNumbersOverlay': _showHouseNumbersOverlay,
|
||||
'showFireHazardZonesOverlay': _showFireHazardZonesOverlay,
|
||||
'showHistoricalFiresOverlay': _showHistoricalFiresOverlay,
|
||||
'showFirebreaksOverlay': _showFirebreaksOverlay,
|
||||
'showKrasFireZonesOverlay': _showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': _showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': _showMunicipalityBordersOverlay,
|
||||
'hideRepeatersOnMap': _hideRepeatersOnMap,
|
||||
};
|
||||
}
|
||||
|
||||
void applyWorkspaceJson(Map<String, dynamic> json) {
|
||||
_currentTrail = json['currentTrail'] is Map<String, dynamic>
|
||||
? LocationTrail.fromJson(json['currentTrail'] as Map<String, dynamic>)
|
||||
: null;
|
||||
_trailHistory
|
||||
..clear()
|
||||
..addAll(
|
||||
(json['trailHistory'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LocationTrail.fromJson),
|
||||
);
|
||||
_importedTrail = json['importedTrail'] is Map<String, dynamic>
|
||||
? LocationTrail.fromJson(json['importedTrail'] as Map<String, dynamic>)
|
||||
: null;
|
||||
_isTrailVisible = json['isTrailVisible'] as bool? ?? true;
|
||||
_showCadastralOverlay = json['showCadastralOverlay'] as bool? ?? false;
|
||||
_showForestRoadsOverlay = json['showForestRoadsOverlay'] as bool? ?? false;
|
||||
_showHikingTrailsOverlay =
|
||||
json['showHikingTrailsOverlay'] as bool? ?? false;
|
||||
_showMainRoadsOverlay = json['showMainRoadsOverlay'] as bool? ?? false;
|
||||
_showHouseNumbersOverlay =
|
||||
json['showHouseNumbersOverlay'] as bool? ?? false;
|
||||
_showFireHazardZonesOverlay =
|
||||
json['showFireHazardZonesOverlay'] as bool? ?? false;
|
||||
_showHistoricalFiresOverlay =
|
||||
json['showHistoricalFiresOverlay'] as bool? ?? false;
|
||||
_showFirebreaksOverlay = json['showFirebreaksOverlay'] as bool? ?? false;
|
||||
_showKrasFireZonesOverlay =
|
||||
json['showKrasFireZonesOverlay'] as bool? ?? false;
|
||||
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
|
||||
_showMunicipalityBordersOverlay =
|
||||
json['showMunicipalityBordersOverlay'] as bool? ?? false;
|
||||
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ typedef DisplayMessageEntry = ({Message message, int occurrenceCount});
|
||||
/// Messages Provider - manages message history and SAR markers
|
||||
class MessagesProvider with ChangeNotifier {
|
||||
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
|
||||
static const Duration _receivedDuplicateWindow = Duration(seconds: 5);
|
||||
|
||||
final List<Message> _messages = [];
|
||||
final Map<String, SarMarker> _sarMarkers = {};
|
||||
@@ -37,6 +38,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
||||
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
||||
String? _storageNamespace;
|
||||
|
||||
// Track pending sent messages by expected ACK/TAG
|
||||
final Map<int, Message> _pendingSentMessages = {};
|
||||
@@ -95,6 +97,10 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||
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;
|
||||
Future<bool> Function({
|
||||
required String messageId,
|
||||
@@ -149,6 +155,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
sarMarkers.where((m) => m.type == SarMarkerType.object).toList();
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
String? get storageNamespace => _storageNamespace;
|
||||
|
||||
String? get targetMessageId => _targetMessageId;
|
||||
|
||||
@@ -311,19 +318,64 @@ class MessagesProvider with ChangeNotifier {
|
||||
Future<void> initialize() async {
|
||||
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 {
|
||||
_cancelAllTimers();
|
||||
_resetInMemoryState();
|
||||
debugPrint('📦 [MessagesProvider] Loading persisted messages...');
|
||||
final storedMessages = await _storageService.loadMessages();
|
||||
final storedMessages = await _storageService.loadMessages(
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
final storedContactLocations = await _storageService
|
||||
.loadMessageContactLocations();
|
||||
.loadMessageContactLocations(namespace: _storageNamespace);
|
||||
final storedReceptionDetails = await _storageService
|
||||
.loadMessageReceptionDetails();
|
||||
.loadMessageReceptionDetails(namespace: _storageNamespace);
|
||||
final storedTransferDetails = await _storageService
|
||||
.loadMessageTransferDetails();
|
||||
.loadMessageTransferDetails(namespace: _storageNamespace);
|
||||
final storedRouteMetadata = await _storageService
|
||||
.loadMessageRouteMetadata();
|
||||
.loadMessageRouteMetadata(namespace: _storageNamespace);
|
||||
final storedRemovedSarMarkerIds = await _storageService
|
||||
.loadRemovedSarMarkerIds();
|
||||
.loadRemovedSarMarkerIds(namespace: _storageNamespace);
|
||||
_messageContactLocations
|
||||
..clear()
|
||||
..addAll(storedContactLocations);
|
||||
@@ -613,6 +665,27 @@ class MessagesProvider with ChangeNotifier {
|
||||
return; // Skip duplicate
|
||||
}
|
||||
|
||||
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
|
||||
if (matchingSentReplayIndex != -1) {
|
||||
final existingId = _messages[matchingSentReplayIndex].id;
|
||||
_clearChannelSendWarning(existingId);
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||
}
|
||||
_messageReceptionDetails[existingId] =
|
||||
MessageReceptionDetails.mergeDuplicate(
|
||||
existing: _messageReceptionDetails[existingId],
|
||||
incoming: receptionDetailsSnapshot,
|
||||
);
|
||||
final existingMessage = _messages[matchingSentReplayIndex];
|
||||
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
|
||||
pathLen: finalMessage.pathLen > 0 ? finalMessage.pathLen : existingMessage.pathLen,
|
||||
pathBytes: finalMessage.pathBytes ?? existingMessage.pathBytes,
|
||||
);
|
||||
_persistMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
_messages.add(finalMessage);
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[finalMessage.id] = contactLocationSnapshot;
|
||||
@@ -659,9 +732,25 @@ class MessagesProvider with ChangeNotifier {
|
||||
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) {
|
||||
for (int index = 0; index < _messages.length; index++) {
|
||||
final existing = _messages[index];
|
||||
if (!_matchesDuplicateScope(existing, message) ||
|
||||
if (existing.isSentMessage ||
|
||||
!_matchesExactDuplicateScope(existing, message) ||
|
||||
existing.text != message.text) {
|
||||
continue;
|
||||
}
|
||||
@@ -678,13 +767,38 @@ class MessagesProvider with ChangeNotifier {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool _matchesDuplicateScope(Message existing, Message message) {
|
||||
int _findLastConversationDuplicateMessageIndex(Message message) {
|
||||
for (int index = _messages.length - 1; index >= 0; index--) {
|
||||
final existing = _messages[index];
|
||||
if (!_isSameConversation(existing, message)) {
|
||||
continue;
|
||||
}
|
||||
if (existing.isSentMessage ||
|
||||
existing.isSystemMessage ||
|
||||
existing.text != message.text) {
|
||||
return -1;
|
||||
}
|
||||
final receivedDelta = existing.receivedAt.difference(message.receivedAt).abs();
|
||||
if (receivedDelta > _receivedDuplicateWindow) {
|
||||
return -1;
|
||||
}
|
||||
return _matchesDuplicateSenderIdentity(existing, message) ? index : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool _matchesExactDuplicateScope(Message existing, Message message) {
|
||||
if (existing.messageType != message.messageType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -692,52 +806,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
final withinChannelRepeatWindow =
|
||||
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
||||
|
||||
final existingSenderKey = existing.senderKeyShort;
|
||||
final incomingSenderKey = message.senderKeyShort;
|
||||
if (existingSenderKey != null &&
|
||||
incomingSenderKey != null &&
|
||||
existingSenderKey == incomingSenderKey &&
|
||||
withinChannelRepeatWindow) {
|
||||
if (existing.senderTimestamp == message.senderTimestamp) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -745,6 +816,85 @@ class MessagesProvider with ChangeNotifier {
|
||||
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;
|
||||
}
|
||||
|
||||
for (int index = 0; index < _messages.length; index++) {
|
||||
final existing = _messages[index];
|
||||
if (!existing.isSentMessage ||
|
||||
!existing.isChannelMessage ||
|
||||
existing.text != message.text) {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Add multiple messages
|
||||
void addMessages(List<Message> messages) {
|
||||
int addedCount = 0;
|
||||
@@ -1051,6 +1201,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
messageReceptionDetails: _messageReceptionDetails,
|
||||
messageTransferDetails: _messageTransferDetails,
|
||||
messageRouteMetadata: _messageRouteMetadata,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1141,9 +1292,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
for (final message in messages) {
|
||||
final occurrenceCount = _messageOccurrenceCount(message);
|
||||
final existingIndex = entries.indexWhere(
|
||||
(entry) =>
|
||||
entry.message.text == message.text &&
|
||||
_matchesDuplicateScope(entry.message, message),
|
||||
(entry) => _shouldCollapseDisplayMessage(entry.message, message),
|
||||
);
|
||||
|
||||
if (existingIndex == -1) {
|
||||
@@ -1161,6 +1310,17 @@ class MessagesProvider with ChangeNotifier {
|
||||
return entries;
|
||||
}
|
||||
|
||||
bool _shouldCollapseDisplayMessage(Message existing, Message message) {
|
||||
if (existing.isSentMessage || message.isSentMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return existing.text == message.text &&
|
||||
(_matchesExactDuplicateScope(existing, message) ||
|
||||
(_isSameConversation(existing, message) &&
|
||||
_matchesDuplicateSenderIdentity(existing, message)));
|
||||
}
|
||||
|
||||
int _messageOccurrenceCount(Message message) =>
|
||||
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||
|
||||
@@ -1199,7 +1359,10 @@ class MessagesProvider with ChangeNotifier {
|
||||
Future<void> removeSarMarker(String id) async {
|
||||
_sarMarkers.remove(id);
|
||||
_removedSarMarkerIds.add(id);
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
await _storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1324,7 +1487,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
unawaited(
|
||||
_storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1348,23 +1516,15 @@ class MessagesProvider with ChangeNotifier {
|
||||
/// Clear all messages
|
||||
void clearMessages() {
|
||||
_cancelAllTimers();
|
||||
_messages.clear();
|
||||
_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();
|
||||
_resetInMemoryState();
|
||||
_retryManager.clearAll();
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
unawaited(
|
||||
_storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1372,13 +1532,29 @@ class MessagesProvider with ChangeNotifier {
|
||||
Future<void> clearSarMarkers() async {
|
||||
_removedSarMarkerIds.addAll(_sarMarkers.keys);
|
||||
_sarMarkers.clear();
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
await _storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all data
|
||||
void clearAll() {
|
||||
_cancelAllTimers();
|
||||
_resetInMemoryState();
|
||||
_retryManager.clearAll();
|
||||
_persistMessages();
|
||||
unawaited(
|
||||
_storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_removedSarMarkerIds.clear();
|
||||
@@ -1393,10 +1569,6 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageAckHistory.clear();
|
||||
_ackHistoryLookup.clear();
|
||||
_completedAckHistory.clear();
|
||||
_retryManager.clearAll();
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int transferCountForSession({
|
||||
@@ -2256,7 +2428,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttempts} for message $messageId',
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttemptsForContact(contact)} for message $messageId',
|
||||
);
|
||||
debugPrint(' Delay: ${delayMs}ms');
|
||||
|
||||
@@ -2439,6 +2611,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
text: message.text,
|
||||
isSarMarker: message.isSarMarker,
|
||||
sarGpsCoordinates: message.sarGpsCoordinates,
|
||||
sarCustomMapPoint: message.sarCustomMapPoint,
|
||||
sarCustomMapId: message.sarCustomMapId,
|
||||
sarNotes: message.sarNotes,
|
||||
sarCustomEmoji: message.sarCustomEmoji,
|
||||
sarColorIndex: message.sarColorIndex,
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
import 'connection_provider.dart';
|
||||
import 'contacts_provider.dart';
|
||||
|
||||
@@ -12,9 +14,23 @@ enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
||||
|
||||
class SensorsProvider with ChangeNotifier {
|
||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||
static const Duration selfAutoRefreshInterval = Duration(seconds: 30);
|
||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
static const String _metricLabelKey = 'sensor_metric_labels';
|
||||
static const String _metricOrderKey = 'sensor_metric_order';
|
||||
static const String _autoRefreshMinutesKey = 'sensor_auto_refresh_minutes';
|
||||
static const List<int> supportedAutoRefreshIntervals = <int>[
|
||||
0,
|
||||
5,
|
||||
15,
|
||||
30,
|
||||
60,
|
||||
360,
|
||||
720,
|
||||
1440,
|
||||
];
|
||||
static const Set<String> _defaultVisibleFields = <String>{
|
||||
'voltage',
|
||||
'battery',
|
||||
@@ -23,6 +39,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
'pressure',
|
||||
'gps',
|
||||
};
|
||||
static const List<String> _defaultMetricOrder = <String>[
|
||||
'voltage',
|
||||
'battery',
|
||||
'temperature',
|
||||
'humidity',
|
||||
'pressure',
|
||||
'gps',
|
||||
];
|
||||
|
||||
final List<String> _watchedSensorKeys = <String>[];
|
||||
final Map<String, SensorRefreshState> _refreshStates =
|
||||
@@ -32,13 +56,22 @@ class SensorsProvider with ChangeNotifier {
|
||||
<String, Set<String>>{};
|
||||
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
||||
<String, Map<String, int>>{};
|
||||
final Map<String, Map<String, String>> _metricLabelsBySensor =
|
||||
<String, Map<String, String>>{};
|
||||
final Map<String, List<String>> _metricOrderBySensor =
|
||||
<String, List<String>>{};
|
||||
final Map<String, int> _autoRefreshMinutesBySensor = <String, int>{};
|
||||
final Map<String, DateTime> _lastRefreshAttemptAt = <String, DateTime>{};
|
||||
bool _isLoaded = false;
|
||||
bool _isRefreshingAll = false;
|
||||
bool _isRunningAutoRefreshTick = false;
|
||||
|
||||
SensorsProvider() {
|
||||
unawaited(_loadWatchedSensors());
|
||||
}
|
||||
|
||||
String _key(String baseKey) => ProfileStorageScope.scopedKey(baseKey);
|
||||
|
||||
List<String> get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys);
|
||||
bool get isLoaded => _isLoaded;
|
||||
bool get isRefreshingAll => _isRefreshingAll;
|
||||
@@ -49,14 +82,26 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> _loadWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
|
||||
final storedSpansJson = prefs.getString(_fieldSpanKey);
|
||||
final stored =
|
||||
prefs.getStringList(_key(_watchedSensorsKey)) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_key(_visibleSensorMetricsKey));
|
||||
final storedSpansJson = prefs.getString(_key(_fieldSpanKey));
|
||||
final storedLabelsJson = prefs.getString(_key(_metricLabelKey));
|
||||
final storedOrderJson = prefs.getString(_key(_metricOrderKey));
|
||||
final storedAutoRefreshJson = prefs.getString(
|
||||
_key(_autoRefreshMinutesKey),
|
||||
);
|
||||
_watchedSensorKeys
|
||||
..clear()
|
||||
..addAll(stored);
|
||||
_refreshStates.clear();
|
||||
_refreshStateUpdatedAt.clear();
|
||||
_visibleFieldsBySensor.clear();
|
||||
_fieldSpansBySensor.clear();
|
||||
_metricLabelsBySensor.clear();
|
||||
_metricOrderBySensor.clear();
|
||||
_autoRefreshMinutesBySensor.clear();
|
||||
_lastRefreshAttemptAt.clear();
|
||||
if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedMetricsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
@@ -72,12 +117,49 @@ class SensorsProvider with ChangeNotifier {
|
||||
.map((key, value) => MapEntry(key, value as int));
|
||||
}
|
||||
}
|
||||
if (storedLabelsJson != null && storedLabelsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedLabelsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricLabelsBySensor[entry.key] =
|
||||
(entry.value as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, value as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (storedOrderJson != null && storedOrderJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedOrderJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricOrderBySensor[entry.key] = (entry.value as List<dynamic>)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
if (storedAutoRefreshJson != null && storedAutoRefreshJson.isNotEmpty) {
|
||||
final decoded =
|
||||
jsonDecode(storedAutoRefreshJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
final minutes = _normalizeAutoRefreshMinutes(
|
||||
(entry.value as num).toInt(),
|
||||
);
|
||||
if (minutes > 0) {
|
||||
_autoRefreshMinutesBySensor[entry.key] = minutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
_autoRefreshMinutesBySensor.removeWhere(
|
||||
(key, _) => !_watchedSensorKeys.contains(key),
|
||||
);
|
||||
for (final key in _watchedSensorKeys) {
|
||||
_visibleFieldsBySensor.putIfAbsent(
|
||||
key,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
_fieldSpansBySensor.putIfAbsent(key, () => <String, int>{});
|
||||
_metricLabelsBySensor.putIfAbsent(key, () => <String, String>{});
|
||||
_metricOrderBySensor.putIfAbsent(
|
||||
key,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading watched sensors: $e');
|
||||
@@ -87,10 +169,15 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_isLoaded = false;
|
||||
await _loadWatchedSensors();
|
||||
}
|
||||
|
||||
Future<void> _persistWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys);
|
||||
await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving watched sensors: $e');
|
||||
}
|
||||
@@ -103,7 +190,10 @@ class SensorsProvider with ChangeNotifier {
|
||||
for (final entry in _visibleFieldsBySensor.entries) {
|
||||
encoded[entry.key] = entry.value.toList();
|
||||
}
|
||||
await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded));
|
||||
await prefs.setString(
|
||||
_key(_visibleSensorMetricsKey),
|
||||
jsonEncode(encoded),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving visible sensor metrics: $e');
|
||||
}
|
||||
@@ -112,16 +202,71 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> _persistFieldSpans() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_fieldSpanKey, jsonEncode(_fieldSpansBySensor));
|
||||
await prefs.setString(
|
||||
_key(_fieldSpanKey),
|
||||
jsonEncode(_fieldSpansBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor field spans: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricLabels() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_metricLabelKey),
|
||||
jsonEncode(_metricLabelsBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric labels: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricOrder() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_metricOrderKey),
|
||||
jsonEncode(_metricOrderBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric order: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistAutoRefreshMinutes() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_autoRefreshMinutesKey),
|
||||
jsonEncode(_autoRefreshMinutesBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor auto refresh minutes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visibleFieldsFor(String publicKeyHex) => Set<String>.unmodifiable(
|
||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
||||
);
|
||||
|
||||
Set<String> effectiveVisibleFieldsFor(
|
||||
String publicKeyHex,
|
||||
Iterable<String> availableFieldKeys,
|
||||
) {
|
||||
final storedVisibleFields =
|
||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields;
|
||||
if (!_shouldAutoIncludeAvailableFields(publicKeyHex, storedVisibleFields)) {
|
||||
return Set<String>.unmodifiable(storedVisibleFields);
|
||||
}
|
||||
|
||||
return Set<String>.unmodifiable(<String>{
|
||||
...storedVisibleFields,
|
||||
...availableFieldKeys,
|
||||
});
|
||||
}
|
||||
|
||||
bool showsField(String publicKeyHex, String fieldKey) =>
|
||||
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
||||
|
||||
@@ -131,6 +276,118 @@ class SensorsProvider with ChangeNotifier {
|
||||
return span == 2 ? 2 : 1;
|
||||
}
|
||||
|
||||
List<String> metricOrderFor(
|
||||
String publicKeyHex,
|
||||
Iterable<String> availableFieldKeys,
|
||||
) {
|
||||
final available = availableFieldKeys.toList();
|
||||
final availableSet = available.toSet();
|
||||
final ordered = <String>[];
|
||||
final seen = <String>{};
|
||||
final stored =
|
||||
_metricOrderBySensor[publicKeyHex] ??
|
||||
List<String>.from(_defaultMetricOrder);
|
||||
|
||||
for (final fieldKey in stored) {
|
||||
if (availableSet.contains(fieldKey) && seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
for (final fieldKey in available) {
|
||||
if (seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
return List<String>.unmodifiable(ordered);
|
||||
}
|
||||
|
||||
Map<String, String> labelOverridesFor(String publicKeyHex) =>
|
||||
Map<String, String>.unmodifiable(
|
||||
_metricLabelsBySensor[publicKeyHex] ?? const <String, String>{},
|
||||
);
|
||||
|
||||
String? labelOverrideFor(String publicKeyHex, String fieldKey) =>
|
||||
_metricLabelsBySensor[publicKeyHex]?[fieldKey];
|
||||
|
||||
int autoRefreshMinutesFor(String publicKeyHex) =>
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] ?? 0;
|
||||
|
||||
Future<void> setAutoRefreshMinutes(String publicKeyHex, int minutes) async {
|
||||
final normalizedMinutes = _normalizeAutoRefreshMinutes(minutes);
|
||||
final currentMinutes = autoRefreshMinutesFor(publicKeyHex);
|
||||
if (currentMinutes == normalizedMinutes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedMinutes == 0) {
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
} else {
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] = normalizedMinutes;
|
||||
}
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static int _normalizeAutoRefreshMinutes(int minutes) {
|
||||
if (minutes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (supportedAutoRefreshIntervals.contains(minutes)) {
|
||||
return minutes;
|
||||
}
|
||||
|
||||
final positiveIntervals = supportedAutoRefreshIntervals.where(
|
||||
(value) => value > 0,
|
||||
);
|
||||
var bestMatch = positiveIntervals.first;
|
||||
var bestDistance = (minutes - bestMatch).abs();
|
||||
|
||||
for (final interval in positiveIntervals.skip(1)) {
|
||||
final distance = (minutes - interval).abs();
|
||||
if (distance < bestDistance) {
|
||||
bestMatch = interval;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
List<String> dueAutoRefreshSensorKeys({DateTime? now}) {
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = <String>[];
|
||||
|
||||
for (final key in _watchedSensorKeys) {
|
||||
final minutes = autoRefreshMinutesFor(key);
|
||||
if (minutes <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lastRefreshAt = _lastRefreshAttemptAt[key];
|
||||
if (lastRefreshAt == null ||
|
||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes)) {
|
||||
dueKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return List<String>.unmodifiable(dueKeys);
|
||||
}
|
||||
|
||||
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(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
@@ -140,8 +397,17 @@ class SensorsProvider with ChangeNotifier {
|
||||
publicKeyHex,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
final metricOrder = _metricOrderBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
var shouldPersistOrder = false;
|
||||
if (visible) {
|
||||
visibleFields.add(fieldKey);
|
||||
if (!metricOrder.contains(fieldKey)) {
|
||||
metricOrder.add(fieldKey);
|
||||
shouldPersistOrder = true;
|
||||
}
|
||||
} else {
|
||||
if (visibleFields.length == 1 && visibleFields.contains(fieldKey)) {
|
||||
return;
|
||||
@@ -149,6 +415,9 @@ class SensorsProvider with ChangeNotifier {
|
||||
visibleFields.remove(fieldKey);
|
||||
}
|
||||
await _persistVisibleMetrics();
|
||||
if (shouldPersistOrder) {
|
||||
await _persistMetricOrder();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -166,11 +435,84 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setMetricLabel(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
String? label,
|
||||
) async {
|
||||
final sensorLabels = _metricLabelsBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => <String, String>{},
|
||||
);
|
||||
final trimmed = label?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
sensorLabels.remove(fieldKey);
|
||||
} else {
|
||||
sensorLabels[fieldKey] = trimmed;
|
||||
}
|
||||
if (sensorLabels.isEmpty) {
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
}
|
||||
await _persistMetricLabels();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> moveMetric(
|
||||
String publicKeyHex, {
|
||||
required List<String> availableFieldKeys,
|
||||
required int oldIndex,
|
||||
required int newIndex,
|
||||
}) async {
|
||||
if (oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= availableFieldKeys.length ||
|
||||
newIndex >= availableFieldKeys.length ||
|
||||
oldIndex == newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final reordered = List<String>.from(
|
||||
metricOrderFor(publicKeyHex, availableFieldKeys),
|
||||
);
|
||||
final fieldKey = reordered.removeAt(oldIndex);
|
||||
reordered.insert(newIndex, fieldKey);
|
||||
|
||||
final storedTail =
|
||||
(_metricOrderBySensor[publicKeyHex] ?? _defaultMetricOrder).where(
|
||||
(key) => !reordered.contains(key),
|
||||
);
|
||||
_metricOrderBySensor[publicKeyHex] = <String>[...reordered, ...storedTail];
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isWatched(String publicKeyHex) =>
|
||||
_watchedSensorKeys.contains(publicKeyHex);
|
||||
|
||||
Contact? selfContact(
|
||||
ContactsProvider contactsProvider,
|
||||
ConnectionProvider connectionProvider,
|
||||
) {
|
||||
final selfKey = connectionProvider.deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final existing = contactsProvider.findContactByKey(
|
||||
Uint8List.fromList(selfKey),
|
||||
);
|
||||
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 {
|
||||
if (!contact.isChat && !contact.isRepeater) {
|
||||
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
||||
return;
|
||||
}
|
||||
if (_watchedSensorKeys.contains(contact.publicKeyHex)) {
|
||||
@@ -178,43 +520,183 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
_watchedSensorKeys.add(contact.publicKeyHex);
|
||||
final initialVisibleFields = _initialVisibleFieldsForContact(contact);
|
||||
await _persistWatchedSensors();
|
||||
_visibleFieldsBySensor[contact.publicKeyHex] = Set<String>.from(
|
||||
_defaultVisibleFields,
|
||||
);
|
||||
_visibleFieldsBySensor[contact.publicKeyHex] = initialVisibleFields;
|
||||
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
||||
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
||||
_metricOrderBySensor[contact.publicKeyHex] = metricOrderFor(
|
||||
contact.publicKeyHex,
|
||||
initialVisibleFields,
|
||||
);
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _shouldAutoIncludeAvailableFields(
|
||||
String publicKeyHex,
|
||||
Set<String> storedVisibleFields,
|
||||
) {
|
||||
if (!setEquals(storedVisibleFields, _defaultVisibleFields)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedOrder = _metricOrderBySensor[publicKeyHex];
|
||||
if (storedOrder != null && !listEquals(storedOrder, _defaultMetricOrder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedLabels = _metricLabelsBySensor[publicKeyHex];
|
||||
if (storedLabels != null && storedLabels.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedSpans = _fieldSpansBySensor[publicKeyHex];
|
||||
if (storedSpans != null &&
|
||||
!(storedSpans.length == 1 && storedSpans['gps'] == 2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<String> _initialVisibleFieldsForContact(Contact contact) {
|
||||
return <String>{..._defaultVisibleFields, ...sensorMetricKeysFor(contact)};
|
||||
}
|
||||
|
||||
Future<void> removeSensor(String publicKeyHex) async {
|
||||
_watchedSensorKeys.remove(publicKeyHex);
|
||||
_refreshStates.remove(publicKeyHex);
|
||||
_refreshStateUpdatedAt.remove(publicKeyHex);
|
||||
_visibleFieldsBySensor.remove(publicKeyHex);
|
||||
_fieldSpansBySensor.remove(publicKeyHex);
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
_metricOrderBySensor.remove(publicKeyHex);
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
_lastRefreshAttemptAt.remove(publicKeyHex);
|
||||
await _persistWatchedSensors();
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
|
||||
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>[
|
||||
...contactsProvider.chatContacts,
|
||||
...contactsProvider.repeaters,
|
||||
...contactsProvider.sensorContacts,
|
||||
];
|
||||
|
||||
// Add "myself" (own device) as a candidate if it has a public key
|
||||
if (connectionProvider != null) {
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!candidates.any((c) => c.publicKeyHex == self.publicKeyHex)) {
|
||||
candidates.insert(0, self);
|
||||
}
|
||||
}
|
||||
|
||||
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
||||
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
List<String> displaySensorKeys({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
if (_watchedSensorKeys.isNotEmpty) {
|
||||
return List<String>.unmodifiable(_watchedSensorKeys);
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self == null) {
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
return <String>[self.publicKeyHex];
|
||||
}
|
||||
|
||||
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({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) 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;
|
||||
}
|
||||
|
||||
@@ -222,7 +704,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
for (final key in _watchedSensorKeys) {
|
||||
for (final key in keysToRefresh) {
|
||||
await refreshSensor(
|
||||
publicKeyHex: key,
|
||||
contactsProvider: contactsProvider,
|
||||
@@ -239,15 +721,20 @@ class SensorsProvider with ChangeNotifier {
|
||||
required String publicKeyHex,
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? requestedAt,
|
||||
}) async {
|
||||
Contact? contact;
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
contact = entry;
|
||||
break;
|
||||
}
|
||||
if (stateFor(publicKeyHex) == SensorRefreshState.refreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||
|
||||
final contact = contactForDisplay(
|
||||
publicKeyHex,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
|
||||
if (contact == null) {
|
||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||
return;
|
||||
@@ -266,6 +753,48 @@ class SensorsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshDueSensors({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? now,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected ||
|
||||
_isRefreshingAll ||
|
||||
_isRunningAutoRefreshTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = dueAutoRefreshSensorKeys(now: refreshTime).toList();
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!dueKeys.contains(self.publicKeyHex) &&
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
void clearExpiredRefreshStates({DateTime? now}) {
|
||||
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
|
||||
final keysToClear = <String>[];
|
||||
@@ -297,4 +826,29 @@ class SensorsProvider with ChangeNotifier {
|
||||
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Contact? _buildSelfCandidate(
|
||||
ConnectionProvider connectionProvider, {
|
||||
ContactTelemetry? telemetry,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final selfKey = deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(selfKey),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
advName: deviceInfo.selfName ?? 'My Device',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: deviceInfo.advLat ?? 0,
|
||||
advLon: deviceInfo.advLon ?? 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
telemetry: telemetry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
@@ -92,6 +93,9 @@ class VoiceProvider with ChangeNotifier {
|
||||
_restorePersistedVoiceData();
|
||||
}
|
||||
|
||||
String _storageKey() =>
|
||||
ProfileStorageScope.scopedKey(_voiceSessionsStorageKey);
|
||||
|
||||
// ── Session accessors ────────────────────────────────────────────────────
|
||||
|
||||
VoiceSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -329,19 +333,22 @@ class VoiceProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearStoredVoiceData() async {
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_playingSessionId = null;
|
||||
await _resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_voiceSessionsStorageKey);
|
||||
await prefs.remove(_storageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to clear stored voice data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _resetInMemoryState();
|
||||
await _restorePersistedVoiceData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -365,7 +372,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_voiceSessionsStorageKey, jsonEncode(payload));
|
||||
await prefs.setString(_storageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to persist voice data: $e');
|
||||
}
|
||||
@@ -374,7 +381,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
Future<void> _restorePersistedVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_voiceSessionsStorageKey);
|
||||
final raw = prefs.getString(_storageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
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
|
||||
void dispose() {
|
||||
_playerEventsSub.cancel();
|
||||
|
||||
394
lib/screens/add_contact_screen.dart
Normal file
394
lib/screens/add_contact_screen.dart
Normal file
@@ -0,0 +1,394 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class AddContactScreen extends StatefulWidget {
|
||||
final String? initialAdvert;
|
||||
|
||||
const AddContactScreen({super.key, this.initialAdvert});
|
||||
|
||||
@override
|
||||
State<AddContactScreen> createState() => _AddContactScreenState();
|
||||
}
|
||||
|
||||
class _AddContactScreenState extends State<AddContactScreen> {
|
||||
final TextEditingController _advertController = TextEditingController();
|
||||
bool _isImporting = false;
|
||||
bool _importSucceeded = false;
|
||||
String? _validationError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialAdvert != null &&
|
||||
widget.initialAdvert!.trim().isNotEmpty) {
|
||||
_advertController.text = widget.initialAdvert!.trim();
|
||||
} else {
|
||||
_loadClipboardIfPresent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_advertController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadClipboardIfPresent() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = clipboardData?.text?.trim();
|
||||
if (!mounted || text == null || text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_normalizeAdvertText(text) == null) {
|
||||
return;
|
||||
}
|
||||
_advertController.text = text;
|
||||
}
|
||||
|
||||
String? _normalizeAdvertText(String value) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = trimmed;
|
||||
if (normalized.startsWith('meshcore://')) {
|
||||
normalized = normalized.substring('meshcore://'.length);
|
||||
}
|
||||
|
||||
normalized = normalized.replaceAll(RegExp(r'\s+'), '');
|
||||
if (normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final isHex = RegExp(r'^[0-9a-fA-F]+$').hasMatch(normalized);
|
||||
if (!isHex || normalized.length.isOdd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
Uint8List _hexToBytes(String hex) {
|
||||
final bytes = <int>[];
|
||||
for (var i = 0; i < hex.length; i += 2) {
|
||||
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
||||
}
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
Future<void> _pasteFromClipboard() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = clipboardData?.text;
|
||||
if (text == null || text.trim().isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_advertController.text = text.trim();
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _importContact() async {
|
||||
final normalized = _normalizeAdvertText(_advertController.text);
|
||||
if (normalized == null) {
|
||||
setState(() {
|
||||
_validationError =
|
||||
'Enter a valid meshcore:// advert or raw hexadecimal contact advert.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final advertBytes = _hexToBytes(normalized);
|
||||
if (advertBytes.length < 98) {
|
||||
setState(() {
|
||||
_validationError =
|
||||
'Advert is too short. Expected exported contact data.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isImporting = true;
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
await connectionProvider.importContactAdvert(advertBytes);
|
||||
final importError = connectionProvider.error;
|
||||
|
||||
if (importError == null) {
|
||||
await connectionProvider.getContacts();
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isImporting = false;
|
||||
});
|
||||
|
||||
if (importError != null) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(importError)));
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)),
|
||||
);
|
||||
setState(() {
|
||||
_importSucceeded = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final normalized = _normalizeAdvertText(_advertController.text);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.addContact)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_add_alt_1_outlined,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Import a shared contact advert',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'Paste a `meshcore://...` link or raw hexadecimal advert. The app will validate it and import the contact into the connected device.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_ImportHintChip(
|
||||
icon: Icons.link_outlined,
|
||||
label: AppLocalizations.of(context)!.acceptsShareLinks,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.code_outlined,
|
||||
label: AppLocalizations.of(context)!.supportsRawHex,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.content_paste_go_outlined,
|
||||
label: AppLocalizations.of(context)!.clipboardfriendly,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Advert payload',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'You can paste the full share link or only the hex payload.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _advertController,
|
||||
minLines: 5,
|
||||
maxLines: 9,
|
||||
onChanged: (_) {
|
||||
if (_validationError != null || _importSucceeded) {
|
||||
setState(() {
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Contact advert',
|
||||
hintText: 'Paste a share link or hex advert',
|
||||
alignLabelWithHint: true,
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _validationError,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
normalized == null
|
||||
? 'No valid advert detected yet'
|
||||
: 'Advert size: ${normalized.length ~/ 2} bytes',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _isImporting ? null : _pasteFromClipboard,
|
||||
icon: Icon(Icons.content_paste_go_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.paste),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: (_isImporting || _importSucceeded)
|
||||
? null
|
||||
: _importContact,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
icon: _isImporting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: _importSucceeded
|
||||
? const Icon(Icons.check_circle_outline)
|
||||
: const Icon(Icons.person_add_alt_1_outlined),
|
||||
label: Text(
|
||||
_isImporting
|
||||
? 'Importing...'
|
||||
: _importSucceeded
|
||||
? 'Imported'
|
||||
: 'Add Contact',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_importSucceeded)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.18),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Contact imported. Paste another advert or edit the current one to import again.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImportHintChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const _ImportHintChip({required this.icon, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: theme.textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -6,7 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/device_info.dart' show ConnectionMode;
|
||||
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
@@ -20,14 +22,17 @@ import 'settings_screen.dart';
|
||||
import 'device_config_screen.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import 'live_traffic_screen.dart';
|
||||
import 'spectrum_scan_screen.dart';
|
||||
import 'profiles_screen.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../widgets/permission_request_dialog.dart';
|
||||
import '../widgets/connection_dialog.dart';
|
||||
import '../utils/battery_display_helper.dart';
|
||||
import '../services/developer_mode_service.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../services/profile_device_key_resolver.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
|
||||
enum _HomeTab { messages, contacts, sensors, map }
|
||||
|
||||
@@ -57,14 +62,15 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
with TickerProviderStateMixin, WidgetsBindingObserver {
|
||||
late TabController _tabController;
|
||||
late final AppProvider _appProvider;
|
||||
late final ConnectionProvider _connectionProvider;
|
||||
int _currentIndex = 0;
|
||||
bool _isMapFullscreen = false;
|
||||
bool _showRxTxIndicators = true;
|
||||
bool _isDeveloperModeEnabled = false;
|
||||
bool _isMapEnabled = true;
|
||||
bool _isContactsEnabled = true;
|
||||
bool _isSensorsEnabled = false;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
String? _lastProfileDeviceKey;
|
||||
|
||||
List<_HomeTab> get _enabledTabs {
|
||||
return [
|
||||
@@ -88,6 +94,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_appProvider = context.read<AppProvider>();
|
||||
_connectionProvider = context.read<ConnectionProvider>();
|
||||
_connectionProvider.addListener(_handleConnectionProviderChanged);
|
||||
_isMapEnabled = _appProvider.isMapEnabled;
|
||||
_isContactsEnabled = _appProvider.isContactsEnabled;
|
||||
_isSensorsEnabled = _appProvider.isSensorsEnabled;
|
||||
@@ -96,7 +104,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
// Initialize synchronously so first build always has a valid controller.
|
||||
_initTabController();
|
||||
_loadRxTxPreference();
|
||||
_loadDeveloperModePreference();
|
||||
MeshMapNodesService.syncInBackgroundIfStale();
|
||||
|
||||
// Show permission dialog after the first frame if needed
|
||||
@@ -105,6 +112,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
_showPermissionDialog();
|
||||
});
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleConnectionProviderChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void _initTabController() {
|
||||
@@ -220,6 +231,36 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void _handleConnectionProviderChanged() {
|
||||
final deviceKey = ProfileDeviceKeyResolver.resolve(
|
||||
deviceInfo: _connectionProvider.deviceInfo,
|
||||
connectionMode: _connectionProvider.connectionMode,
|
||||
);
|
||||
if (_lastProfileDeviceKey == deviceKey) {
|
||||
return;
|
||||
}
|
||||
_lastProfileDeviceKey = deviceKey;
|
||||
if (deviceKey == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.syncActiveProfileForCurrentDevice(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connectionProvider.removeListener(_handleConnectionProviderChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
_appProvider.removeListener(_handleAppProviderChanged);
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _openLiveTraffic(ConnectionProvider provider) {
|
||||
openLiveTrafficScreen(context, provider);
|
||||
}
|
||||
@@ -237,29 +278,15 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showRxTxIndicators = prefs.getBool('show_rx_tx_indicators') ?? true;
|
||||
_showRxTxIndicators =
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
) ??
|
||||
true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperModePreference() async {
|
||||
final isEnabled = await DeveloperModeService.isEnabled();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDeveloperModeEnabled = isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
_appProvider.removeListener(_handleAppProviderChanged);
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showPermissionDialog() {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -355,7 +382,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.flood),
|
||||
subtitle: const Text('Relay through repeaters across the mesh'),
|
||||
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -381,7 +408,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.direct),
|
||||
subtitle: const Text('Nearby only, without repeater flooding'),
|
||||
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -395,45 +422,184 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityBadge({
|
||||
required String label,
|
||||
required int count,
|
||||
required bool isActive,
|
||||
required Color activeColor,
|
||||
bool compact = false,
|
||||
}) {
|
||||
final color = isActive ? activeColor : Colors.grey;
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: compact ? 7 : 8,
|
||||
vertical: compact ? 4 : 5,
|
||||
void _showDeviceInfoSheet(BuildContext context, DeviceInfo deviceInfo) {
|
||||
// Request self telemetry so it's fresh
|
||||
context.read<ConnectionProvider>().requestSelfTelemetry();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: compact ? 6 : 7,
|
||||
height: compact ? 6 : 7,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
SizedBox(width: compact ? 5 : 6),
|
||||
Text(
|
||||
'$label:$count',
|
||||
style: TextStyle(
|
||||
fontSize: compact ? 10 : 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
builder: (context) => Consumer<ContactsProvider>(
|
||||
builder: (context, contactsProvider, child) {
|
||||
final telemetry = contactsProvider.selfTelemetry;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
deviceInfo.selfName ?? deviceInfo.deviceName ?? 'Device',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bluetooth,
|
||||
'BLE Signal',
|
||||
deviceInfo.signalRssi != null
|
||||
? '${deviceInfo.signalRssi} dBm'
|
||||
: 'N/A',
|
||||
),
|
||||
if (deviceInfo.batteryPercent != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
'Battery',
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
),
|
||||
if (deviceInfo.batteryMilliVolts != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bolt,
|
||||
'Voltage',
|
||||
'${(deviceInfo.batteryMilliVolts! / 1000).toStringAsFixed(2)}V',
|
||||
),
|
||||
if (deviceInfo.storageUsedKb != null &&
|
||||
deviceInfo.storageTotalKb != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.storage,
|
||||
'Storage',
|
||||
'${deviceInfo.storageUsedKb} / ${deviceInfo.storageTotalKb} KB',
|
||||
),
|
||||
if (deviceInfo.firmwareVersion != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.system_update,
|
||||
'Firmware',
|
||||
'v${deviceInfo.firmwareVersion}',
|
||||
),
|
||||
if (deviceInfo.radioFreq != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.radio,
|
||||
'Frequency',
|
||||
'${(deviceInfo.radioFreq! / 1000).toStringAsFixed(3)} MHz',
|
||||
),
|
||||
if (deviceInfo.txPower != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.power,
|
||||
'TX Power',
|
||||
'${deviceInfo.txPower} dBm',
|
||||
),
|
||||
if (telemetry != null) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Self Telemetry',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (telemetry.temperature != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.thermostat,
|
||||
'Temperature',
|
||||
'${telemetry.temperature!.toStringAsFixed(1)}°C',
|
||||
),
|
||||
if (telemetry.humidity != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.water_drop,
|
||||
'Humidity',
|
||||
'${telemetry.humidity!.toStringAsFixed(1)}%',
|
||||
),
|
||||
if (telemetry.pressure != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.compress,
|
||||
'Pressure',
|
||||
'${telemetry.pressure!.toStringAsFixed(1)} hPa',
|
||||
),
|
||||
if (telemetry.gpsLocation != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.gps_fixed,
|
||||
'GPS',
|
||||
'${telemetry.gpsLocation!.latitude.toStringAsFixed(5)}, ${telemetry.gpsLocation!.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deviceInfoRow(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniSignalBars({required int activeBars, required Color color}) {
|
||||
final inactive = Colors.grey.withValues(alpha: 0.3);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (var i = 0; i < 5; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 1.5),
|
||||
Container(
|
||||
width: 2.5,
|
||||
height: 4.0 + (i * 2),
|
||||
decoration: BoxDecoration(
|
||||
color: i < activeBars ? color : inactive,
|
||||
borderRadius: BorderRadius.circular(1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactActivityIndicator({
|
||||
required bool rxActive,
|
||||
required bool txActive,
|
||||
@@ -571,12 +737,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _showConnectionDialog(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
Future<void> _showConnectionDialog(BuildContext context) async {
|
||||
await showConnectionDialogFlow(
|
||||
context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const ConnectionDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -590,6 +754,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
|
||||
context.watch<AppProvider>();
|
||||
final activeProfileId = context.select<ProfileManager, String>(
|
||||
(manager) => manager.activeProfileId,
|
||||
);
|
||||
|
||||
final enabledTabs = _enabledTabs;
|
||||
final isMapTabActive = _currentTab == _HomeTab.map;
|
||||
@@ -614,7 +781,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onPressed: () async {
|
||||
await provider.disconnect();
|
||||
},
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
icon: Icon(Icons.power_settings_new),
|
||||
tooltip: AppLocalizations.of(context)!.disconnect,
|
||||
color: Colors.red.shade700,
|
||||
);
|
||||
@@ -626,40 +793,17 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
final items = <PopupMenuEntry<void>>[];
|
||||
|
||||
if (_isDeveloperModeEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const SpectrumScanScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final profilesEnabled = context
|
||||
.read<ProfileManager>()
|
||||
.profilesEnabled;
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.radar_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Live Traffic'),
|
||||
const Icon(Icons.radar_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -690,11 +834,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.router_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Repeaters Map'),
|
||||
const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -751,7 +895,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
@@ -770,12 +914,36 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
_loadRxTxPreference();
|
||||
_loadDeveloperModePreference();
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (profilesEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.layers_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.profiles),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfilesScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
),
|
||||
@@ -802,9 +970,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
return const SensorsTab();
|
||||
return SensorsTab(
|
||||
isActive:
|
||||
_currentTab == _HomeTab.sensors &&
|
||||
_lifecycleState == AppLifecycleState.resumed,
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return MapTab(
|
||||
key: ValueKey<String>('map:$activeProfileId'),
|
||||
onFullscreenChanged: (isFullscreen) {
|
||||
setState(() {
|
||||
_isMapFullscreen = isFullscreen;
|
||||
@@ -861,7 +1034,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return Tab(
|
||||
icon: const Icon(Icons.map),
|
||||
icon: Icon(Icons.map),
|
||||
text: AppLocalizations.of(context)!.map,
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
@@ -886,19 +1059,35 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
||||
|
||||
if (!isConnected) {
|
||||
// Disconnected state: show connect button
|
||||
final buttonLabel = provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
provider.isReconnecting
|
||||
? 'Restoring previous link'
|
||||
: 'No device connected',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: provider.isReconnecting
|
||||
? null
|
||||
: () => _showConnectionDialog(context),
|
||||
@@ -909,22 +1098,19 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.black54,
|
||||
Colors.white70,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth, size: 18),
|
||||
label: Text(
|
||||
provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
: const Icon(Icons.add_link_rounded, size: 18),
|
||||
label: Text(buttonLabel),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -932,7 +1118,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => provider.cancelReconnection(),
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
icon: Icon(Icons.close, size: 20),
|
||||
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
@@ -991,50 +1177,45 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isTcpConnected
|
||||
? Icons.wifi_rounded
|
||||
: Icons.bluetooth_connected_rounded,
|
||||
size: 13,
|
||||
color: signalColor,
|
||||
),
|
||||
if (!isTcpConnected &&
|
||||
deviceInfo.signalRssi != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${deviceInfo.signalRssi}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: signalColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (deviceInfo.batteryPercent != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
_showDeviceInfoSheet(context, deviceInfo),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
isTcpConnected
|
||||
? Icons.wifi_rounded
|
||||
: Icons.bluetooth_connected_rounded,
|
||||
size: 13,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
color: signalColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
if (!isTcpConnected &&
|
||||
deviceInfo.signalRssi != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
_buildMiniSignalBars(
|
||||
activeBars:
|
||||
BatteryDisplayHelper.getSignalBars(
|
||||
deviceInfo.signalRssi!,
|
||||
),
|
||||
color: signalColor,
|
||||
),
|
||||
],
|
||||
if (deviceInfo.batteryPercent != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
size: 13,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -1044,9 +1225,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1143,48 +1324,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isTight
|
||||
? _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
)
|
||||
: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh
|
||||
.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildActivityBadge(
|
||||
label: 'RX',
|
||||
count: provider.rxPacketCount,
|
||||
isActive: provider.rxActivity,
|
||||
activeColor: Colors.green,
|
||||
compact: true,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildActivityBadge(
|
||||
label: 'TX',
|
||||
count: provider.txPacketCount,
|
||||
isActive: provider.txActivity,
|
||||
activeColor: Colors.blue,
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(width: isTight ? 24 : 74),
|
||||
const SizedBox(width: 24),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import 'dart:async';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../models/ble_packet_log.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../services/live_traffic_summary.dart';
|
||||
import '../services/location_tracking_service.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../services/traffic_stats_reporting_service.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
import '../widgets/compact_signal_indicator.dart';
|
||||
import '../widgets/messages/message_trace_sheet.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
T? _maybeProvider<T>(BuildContext context) {
|
||||
try {
|
||||
@@ -25,7 +30,6 @@ T? _maybeProvider<T>(BuildContext context) {
|
||||
|
||||
class LiveTrafficScreen extends StatefulWidget {
|
||||
final List<BlePacketLog> Function() logReader;
|
||||
final int Function()? rxCountReader;
|
||||
final Listenable? refreshListenable;
|
||||
final DateTime Function() now;
|
||||
final VoidCallback? openPacketLogs;
|
||||
@@ -33,7 +37,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
||||
const LiveTrafficScreen({
|
||||
super.key,
|
||||
required this.logReader,
|
||||
this.rxCountReader,
|
||||
this.refreshListenable,
|
||||
DateTime Function()? now,
|
||||
this.openPacketLogs,
|
||||
@@ -47,7 +50,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
||||
return LiveTrafficScreen(
|
||||
key: key,
|
||||
logReader: () => provider.bleService.packetLogs,
|
||||
rxCountReader: () => provider.rxPacketCount,
|
||||
refreshListenable: provider,
|
||||
openPacketLogs: openPacketLogs,
|
||||
);
|
||||
@@ -116,15 +118,17 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logs = widget.logReader();
|
||||
final totalRxDataCount = LiveTrafficSummary.countRxDataLogs(logs);
|
||||
final unfilteredSnapshot = LiveTrafficSummary.fromLogs(
|
||||
widget.logReader(),
|
||||
logs,
|
||||
now: widget.now(),
|
||||
clearedAt: _clearedAt,
|
||||
preferredHashSize: _preferredHashSize,
|
||||
window: _selectedWindow,
|
||||
);
|
||||
final snapshot = LiveTrafficSummary.fromLogs(
|
||||
widget.logReader(),
|
||||
logs,
|
||||
now: widget.now(),
|
||||
clearedAt: _clearedAt,
|
||||
preferredHashSize: _preferredHashSize,
|
||||
@@ -146,7 +150,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Live Traffic'),
|
||||
title: Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
actions: [
|
||||
if (widget.openPacketLogs != null)
|
||||
IconButton(
|
||||
@@ -154,6 +158,16 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
tooltip: 'Open packet logs',
|
||||
icon: const Icon(Icons.list_alt_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _openStatsDashboard,
|
||||
tooltip: 'View public stats',
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showPacketTypeHelpSheet(context),
|
||||
tooltip: 'Packet type help',
|
||||
icon: const Icon(Icons.help_outline),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -175,7 +189,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
children: [
|
||||
_SummaryPanel(
|
||||
snapshot: snapshot,
|
||||
totalRxCount: widget.rxCountReader?.call(),
|
||||
totalRxCount: totalRxDataCount,
|
||||
routeHashCounts: routeHashCounts,
|
||||
onWindowTap: () => _showWindowPicker(context),
|
||||
),
|
||||
@@ -249,6 +263,13 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openStatsDashboard() async {
|
||||
final url = TrafficStatsReportingService.dashboardUri;
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showWindowPicker(BuildContext context) async {
|
||||
final selected = await showModalBottomSheet<Duration>(
|
||||
context: context,
|
||||
@@ -277,6 +298,68 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
_selectedWindow = selected;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showPacketTypeHelpSheet(BuildContext context) {
|
||||
final packetTypes = LiveTrafficEntry.knownPayloadTypes;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Text(
|
||||
'Packet Types',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Descriptions below follow the current MeshCore payload definitions.',
|
||||
style: TextStyle(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (final packetType in packetTypes) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
packetType.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
packetType.description,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryPanel extends StatelessWidget {
|
||||
@@ -304,8 +387,48 @@ class _SummaryPanel extends StatelessWidget {
|
||||
LiveTrafficBusyness.active => 'Active',
|
||||
LiveTrafficBusyness.busy => 'Busy',
|
||||
};
|
||||
final metrics = [
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:rxPackets'),
|
||||
label: AppLocalizations.of(context)!.rxPackets,
|
||||
value: '${snapshot.rxCount}',
|
||||
subtitle: totalRxCount == null
|
||||
? _windowSummaryLabel(snapshot.windowDuration)
|
||||
: 'Device total $totalRxCount',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:rssi'),
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
value: snapshot.latestRssiDbm == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestRssiDbm} dBm',
|
||||
subtitle: snapshot.avgRssiDbm == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:snr'),
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
value: snapshot.latestSnrDb == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||
subtitle: snapshot.avgSnrDb == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:multihop'),
|
||||
label: AppLocalizations.of(context)!.multihop,
|
||||
value: '${snapshot.multiHopCount}',
|
||||
subtitle: routeHashCounts.summaryLabel,
|
||||
footer: snapshot.avgHopCount == null
|
||||
? 'No routes yet'
|
||||
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
||||
),
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -326,17 +449,17 @@ class _SummaryPanel extends StatelessWidget {
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
_SummaryBadge(
|
||||
label: 'Mesh',
|
||||
label: AppLocalizations.of(context)!.mesh,
|
||||
value: busynessLabel,
|
||||
color: busynessColor,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Rate',
|
||||
label: AppLocalizations.of(context)!.rate,
|
||||
value: '${snapshot.packetsPerMinute} pkt/min',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Window',
|
||||
label: AppLocalizations.of(context)!.window,
|
||||
value: _windowLabel(snapshot.windowDuration),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onTap: onWindowTap,
|
||||
@@ -344,44 +467,26 @@ class _SummaryPanel extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
_MetricTile(
|
||||
label: 'RX packets',
|
||||
value: '${snapshot.rxCount}',
|
||||
subtitle: totalRxCount == null
|
||||
? _windowSummaryLabel(snapshot.windowDuration)
|
||||
: 'Device total $totalRxCount',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'RSSI',
|
||||
value: snapshot.latestRssiDbm == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestRssiDbm} dBm',
|
||||
subtitle: snapshot.avgRssiDbm == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'SNR',
|
||||
value: snapshot.latestSnrDb == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||
subtitle: snapshot.avgSnrDb == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'Multi-hop',
|
||||
value: '${snapshot.multiHopCount}',
|
||||
subtitle: routeHashCounts.summaryLabel,
|
||||
footer: snapshot.avgHopCount == null
|
||||
? 'No routes yet'
|
||||
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
||||
),
|
||||
],
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacing = 10.0;
|
||||
const minTileWidth = 150.0;
|
||||
final columns =
|
||||
((constraints.maxWidth + spacing) / (minTileWidth + spacing))
|
||||
.floor()
|
||||
.clamp(1, metrics.length);
|
||||
final tileWidth =
|
||||
(constraints.maxWidth - (spacing * (columns - 1))) / columns;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
for (final metric in metrics)
|
||||
SizedBox(width: tileWidth, child: metric),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -454,7 +559,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
_FilterChip(
|
||||
label: 'All',
|
||||
label: AppLocalizations.of(context)!.all,
|
||||
selected: selectedType == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
@@ -523,12 +628,14 @@ String _windowSummaryLabel(Duration duration) {
|
||||
}
|
||||
|
||||
class _MetricTile extends StatelessWidget {
|
||||
final Key? tileKey;
|
||||
final String label;
|
||||
final String value;
|
||||
final String subtitle;
|
||||
final String? footer;
|
||||
|
||||
const _MetricTile({
|
||||
this.tileKey,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
@@ -538,7 +645,7 @@ class _MetricTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 160,
|
||||
key: tileKey,
|
||||
height: 136,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -651,7 +758,11 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
final accent = isRx ? Colors.green : Colors.blue;
|
||||
final rxInfo = log.logRxDataInfo;
|
||||
final originDistance = _originDistanceLabel(context, entry);
|
||||
final packetDetails = _LiveTrafficPacketDetails.fromEntry(entry);
|
||||
final channelsProvider = _maybeProvider<ChannelsProvider>(context);
|
||||
final packetDetails = _LiveTrafficPacketDetails.fromEntry(
|
||||
entry,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
final signalMetric = SignalMetric.fromRxInfo(rxInfo);
|
||||
|
||||
return Material(
|
||||
@@ -705,7 +816,7 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
if (entry.payloadMeaning != null)
|
||||
Text(
|
||||
entry.payloadMeaning!,
|
||||
maxLines: 1,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
@@ -947,7 +1058,6 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _LiveTrafficPacketDetails {
|
||||
@@ -965,27 +1075,22 @@ class _LiveTrafficPacketDetails {
|
||||
required this.endpointLine,
|
||||
});
|
||||
|
||||
factory _LiveTrafficPacketDetails.fromEntry(LiveTrafficEntry entry) {
|
||||
factory _LiveTrafficPacketDetails.fromEntry(
|
||||
LiveTrafficEntry entry, {
|
||||
ChannelsProvider? channelsProvider,
|
||||
}) {
|
||||
final route = entry.route;
|
||||
final payloadType = route?.payloadType;
|
||||
final parsedPayload = _ParsedTrafficPayload.tryParse(
|
||||
entry.log.rawData,
|
||||
route,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
final title = switch (payloadType) {
|
||||
0x00 => 'FLOOD REQUEST',
|
||||
0x01 => 'FLOOD RESPONSE',
|
||||
0x02 => 'FLOOD TEXT',
|
||||
0x03 => 'FLOOD ACK',
|
||||
0x04 => 'FLOOD ADVERTISEMENT',
|
||||
0x05 => 'FLOOD GROUP_TEXT',
|
||||
0x06 => 'FLOOD GROUP_DATA',
|
||||
0x07 => 'FLOOD ANON_REQUEST',
|
||||
0x08 => 'FLOOD RETURNED_PATH',
|
||||
0x09 => 'FLOOD TRACE_PATH',
|
||||
0x0A => 'FLOOD MULTIPART',
|
||||
0x0B => 'FLOOD CONTROL',
|
||||
_ => entry.payloadLabel.toUpperCase(),
|
||||
0x05 => parsedPayload?.channelDisplayName ?? LiveTrafficEntry.payloadTypeTitle(0x05),
|
||||
0x06 => parsedPayload?.channelDisplayName ?? LiveTrafficEntry.payloadTypeTitle(0x06),
|
||||
null => entry.payloadLabel.toUpperCase(),
|
||||
_ => LiveTrafficEntry.payloadTypeTitle(payloadType),
|
||||
};
|
||||
|
||||
final hopHashes = route?.hopHashes ?? const <String>[];
|
||||
@@ -1006,27 +1111,26 @@ class _LiveTrafficPacketDetails {
|
||||
}
|
||||
|
||||
static String _packetHash(List<int> bytes) {
|
||||
const fnvOffset = 0xcbf29ce484222325;
|
||||
const fnvPrime = 0x100000001b3;
|
||||
const mask = 0xFFFFFFFFFFFFFFFF;
|
||||
var hash = fnvOffset;
|
||||
for (final byte in bytes) {
|
||||
hash ^= byte & 0xFF;
|
||||
hash = (hash * fnvPrime) & mask;
|
||||
}
|
||||
return hash.toRadixString(16).padLeft(16, '0').toUpperCase();
|
||||
final digest = md5.convert(bytes).bytes;
|
||||
return digest
|
||||
.take(8)
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
class _ParsedTrafficPayload {
|
||||
final String? endpointLine;
|
||||
final String? channelDisplayName;
|
||||
|
||||
const _ParsedTrafficPayload({this.endpointLine});
|
||||
const _ParsedTrafficPayload({this.endpointLine, this.channelDisplayName});
|
||||
|
||||
static _ParsedTrafficPayload? tryParse(
|
||||
List<int> rawData,
|
||||
DecodedLogRxRoute? route,
|
||||
) {
|
||||
DecodedLogRxRoute? route, {
|
||||
ChannelsProvider? channelsProvider,
|
||||
}) {
|
||||
if (rawData.length < 5 ||
|
||||
rawData.first != LiveTrafficSummary.logRxDataResponseCode) {
|
||||
return null;
|
||||
@@ -1063,9 +1167,18 @@ class _ParsedTrafficPayload {
|
||||
case 0x05:
|
||||
case 0x06:
|
||||
if (payload.isEmpty) return const _ParsedTrafficPayload();
|
||||
final channelHash = payload.first;
|
||||
final channelHashHex = channelHash
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
final channelDisplayName = channelsProvider
|
||||
?.getUniqueChannelDisplayNameByHashByte(channelHash);
|
||||
return _ParsedTrafficPayload(
|
||||
endpointLine:
|
||||
'Channel Hash: ${payload.first.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
||||
channelDisplayName: channelDisplayName,
|
||||
endpointLine: channelDisplayName == null
|
||||
? 'Channel Hash: $channelHashHex'
|
||||
: 'Channel: $channelDisplayName ($channelHashHex)',
|
||||
);
|
||||
case 0x00:
|
||||
case 0x01:
|
||||
@@ -1106,7 +1219,6 @@ class _PacketInfoLine extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _GeoPoint {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -155,8 +155,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
||||
Clipboard.setData(ClipboardData(text: log.hexData));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Hex data copied to clipboard'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.hexDataCopiedToClipboard),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -184,7 +184,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
setState(() {});
|
||||
if (parentContext.mounted) {
|
||||
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||
const SnackBar(content: Text('Packet logs cleared')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.packetLogsCleared)),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -205,7 +205,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('BLE Packet Logs'),
|
||||
Text(AppLocalizations.of(context)!.blePacketLogs),
|
||||
Text(
|
||||
'${logs.length} packets',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -316,23 +316,23 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Export logs',
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'csv',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.table_chart),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as CSV'),
|
||||
const Icon(Icons.table_chart),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsCsv),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'txt',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.text_snippet),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as Text'),
|
||||
const Icon(Icons.text_snippet),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsText),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -411,8 +411,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
_filterDirection = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear_all),
|
||||
label: const Text('Clear filters'),
|
||||
icon: Icon(Icons.clear_all),
|
||||
label: Text(AppLocalizations.of(context)!.clearFilters),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -519,24 +519,24 @@ class _PacketLogCard extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: isRx ? Icons.call_received : Icons.call_made,
|
||||
label: 'Direction',
|
||||
label: AppLocalizations.of(context)!.direction,
|
||||
value: isRx ? 'RX' : 'TX',
|
||||
accent: directionColor,
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.data_object,
|
||||
label: 'Size',
|
||||
label: AppLocalizations.of(context)!.size,
|
||||
value: '${log.rawData.length} bytes',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Captured',
|
||||
label: AppLocalizations.of(context)!.captured,
|
||||
value: _formatTimestamp(log.timestamp),
|
||||
),
|
||||
if (log.responseCode != null)
|
||||
_FactCard(
|
||||
icon: Icons.sell,
|
||||
label: 'Opcode',
|
||||
label: AppLocalizations.of(context)!.opcode,
|
||||
value: log.opcodeName,
|
||||
),
|
||||
],
|
||||
@@ -565,7 +565,7 @@ class _PacketLogCard extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
if (rxInfo?.rssiDbm != null)
|
||||
_SignalMeter(
|
||||
label: 'RSSI',
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
||||
normalized: _normalizeRssi(
|
||||
rxInfo.rssiDbm!.toDouble(),
|
||||
@@ -573,9 +573,9 @@ class _PacketLogCard extends StatelessWidget {
|
||||
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
||||
),
|
||||
if (rxInfo?.snrDb != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
_SignalMeter(
|
||||
label: 'SNR',
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
valueLabel:
|
||||
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
||||
normalized: _normalizeSnr(rxInfo.snrDb!),
|
||||
@@ -854,17 +854,17 @@ class _RouteSection extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: Icons.route,
|
||||
label: 'Payload',
|
||||
label: AppLocalizations.of(context)!.payload,
|
||||
value: _payloadTypeLabel(route.payloadType),
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.hub,
|
||||
label: 'Hops',
|
||||
label: AppLocalizations.of(context)!.hops,
|
||||
value: '${route.hopCount}',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.tag,
|
||||
label: 'Hash size',
|
||||
label: AppLocalizations.of(context)!.hashSize,
|
||||
value:
|
||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
||||
),
|
||||
|
||||
251
lib/screens/profiles_screen.dart
Normal file
251
lib/screens/profiles_screen.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class ProfilesScreen extends StatelessWidget {
|
||||
const ProfilesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
final profiles = profileManager.visibleProfiles;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.profiles),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.importProfileFromFile();
|
||||
},
|
||||
icon: const Icon(Icons.file_open),
|
||||
tooltip: 'Import profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _createProfile(context),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newProfile),
|
||||
),
|
||||
body: profiles.isEmpty
|
||||
? Center(
|
||||
child: Text(AppLocalizations.of(context)!.enableProfilesToStartManagingThem),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
final isActive =
|
||||
profileManager.activeProfileId == profile.id;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
profile.name,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (isActive)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(AppLocalizations.of(context)!.active),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(_summary(profile)),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.openProfile(profile.id);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.open),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final resolved =
|
||||
profile.id ==
|
||||
ConfigProfile.defaultProfileId
|
||||
? await context
|
||||
.read<
|
||||
ProfileWorkspaceCoordinator
|
||||
>()
|
||||
.snapshotCurrentProfile(
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
)
|
||||
: profile;
|
||||
if (!context.mounted) return;
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.exportProfile(resolved);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.share),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case 'duplicate':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.duplicateProfile(profile);
|
||||
break;
|
||||
case 'rename':
|
||||
await _renameProfile(context, profile);
|
||||
break;
|
||||
case 'delete':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.deleteProfile(profile);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Text(AppLocalizations.of(context)!.duplicate),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
PopupMenuItem(
|
||||
value: 'rename',
|
||||
child: Text(AppLocalizations.of(context)!.rename),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _summary(ConfigProfile profile) {
|
||||
if (profile.isDefault) {
|
||||
return 'Current app state and history.';
|
||||
}
|
||||
|
||||
final sections = <String>[];
|
||||
if (profile.sections.deviceConfig?.isEmpty == false) {
|
||||
sections.add('Device');
|
||||
}
|
||||
if (profile.sections.channels.isNotEmpty) {
|
||||
sections.add('${profile.sections.channels.length} channels');
|
||||
}
|
||||
if (profile.sections.appSettings?.isEmpty == false) {
|
||||
sections.add('App settings');
|
||||
}
|
||||
if (profile.sections.mapWorkspace?.isEmpty == false) {
|
||||
sections.add('Map workspace');
|
||||
}
|
||||
return sections.isEmpty ? 'Empty profile' : sections.join(' | ');
|
||||
}
|
||||
|
||||
Future<void> _createProfile(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.createProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: Text(AppLocalizations.of(context)!.create),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().createProfileFromCurrent(
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _renameProfile(
|
||||
BuildContext context,
|
||||
ConfigProfile profile,
|
||||
) async {
|
||||
final controller = TextEditingController(text: profile.name);
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.renameProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().renameProfile(
|
||||
profile,
|
||||
name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RepeatersMapScreen extends StatefulWidget {
|
||||
const RepeatersMapScreen({super.key});
|
||||
@@ -191,18 +192,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 12),
|
||||
_InfoLine(
|
||||
label: 'Public key',
|
||||
label: AppLocalizations.of(context)!.publicKeyLabel,
|
||||
value: repeater.publicKey,
|
||||
monospace: true,
|
||||
),
|
||||
_InfoLine(
|
||||
label: 'Coordinates',
|
||||
label: AppLocalizations.of(context)!.coordinates,
|
||||
value:
|
||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
_InfoLine(label: 'Source', value: repeater.sourceLabel),
|
||||
_InfoLine(label: AppLocalizations.of(context)!.source, value: repeater.sourceLabel),
|
||||
const SizedBox(height: 12),
|
||||
if (canAdd)
|
||||
SizedBox(
|
||||
@@ -233,8 +234,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Already in contacts'),
|
||||
icon: Icon(Icons.check_circle),
|
||||
label: Text(AppLocalizations.of(context)!.alreadyInContacts),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -253,8 +254,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Connect to a device before adding contacts'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectToADeviceBeforeAddingContacts),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -366,7 +367,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Repeaters Map'),
|
||||
title: Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _isLoading
|
||||
@@ -446,18 +447,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
children: [
|
||||
_LegendRow(
|
||||
color: Color(0xFF7C3AED),
|
||||
label: 'From contacts',
|
||||
color: const Color(0xFF7C3AED),
|
||||
label: AppLocalizations.of(context)!.fromContacts,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(
|
||||
color: Color(0xFFE8681D),
|
||||
label: 'Online only',
|
||||
color: const Color(0xFFE8681D),
|
||||
label: AppLocalizations.of(context)!.onlineOnly,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_LegendRow(color: Color(0xFF1B8F4F), label: 'In both'),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(color: const Color(0xFF1B8F4F), label: AppLocalizations.of(context)!.inBoth),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -240,7 +240,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'import',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
leading: Icon(Icons.download),
|
||||
title: Text(l10n.importFromClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.upload),
|
||||
leading: Icon(Icons.upload),
|
||||
title: Text(l10n.exportToClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -257,7 +257,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.restart_alt),
|
||||
leading: Icon(Icons.restart_alt),
|
||||
title: Text(l10n.resetToDefaults),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -283,14 +283,14 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noTemplates,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tapAddToCreate,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
@@ -317,7 +317,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _addTemplate,
|
||||
icon: const Icon(Icons.add),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(l10n.addTemplate),
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,601 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/device_info.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../widgets/device/spectrum_scan_panel.dart';
|
||||
|
||||
class SpectrumScanScreen extends StatefulWidget {
|
||||
const SpectrumScanScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SpectrumScanScreen> createState() => _SpectrumScanScreenState();
|
||||
}
|
||||
|
||||
class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
||||
static const List<String> _bandwidthOptions = [
|
||||
'7.8 kHz',
|
||||
'10.4 kHz',
|
||||
'15.6 kHz',
|
||||
'20.8 kHz',
|
||||
'31.25 kHz',
|
||||
'41.7 kHz',
|
||||
'62.5 kHz',
|
||||
'125 kHz',
|
||||
'250 kHz',
|
||||
'500 kHz',
|
||||
];
|
||||
|
||||
String _selectedBandwidth = '62.5 kHz';
|
||||
bool _isSpectrumScanRunning = false;
|
||||
bool _rangeInitialized = false;
|
||||
String? _lastRangeSourceKey;
|
||||
late double _scanRangeMinMhz;
|
||||
late double _scanRangeMaxMhz;
|
||||
late RangeValues _scanRangeValues;
|
||||
List<SpectrumScanCandidate> _scanCandidates = const [];
|
||||
int? _selectedScanFrequencyKhz;
|
||||
int _completedScanSectors = 0;
|
||||
int _totalScanSectors = 0;
|
||||
|
||||
List<SpectrumScanCandidate> get _recommendedScanCandidates =>
|
||||
_scanCandidates.take(8).toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final deviceInfo = _deviceInfo;
|
||||
if (deviceInfo.radioBw != null &&
|
||||
deviceInfo.radioBw! >= 0 &&
|
||||
deviceInfo.radioBw! < _bandwidthOptions.length) {
|
||||
_selectedBandwidth = _bandwidthOptions[deviceInfo.radioBw!];
|
||||
}
|
||||
_syncRangeFromDevice(deviceInfo);
|
||||
_selectedScanFrequencyKhz = deviceInfo.radioFreq;
|
||||
}
|
||||
|
||||
DeviceInfo get _deviceInfo => context.read<ConnectionProvider>().deviceInfo;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_syncRangeFromDevice(context.watch<ConnectionProvider>().deviceInfo);
|
||||
}
|
||||
|
||||
void _syncRangeFromDevice(DeviceInfo deviceInfo) {
|
||||
final minKhz = deviceInfo.spectrumScanMinKhz;
|
||||
final maxKhz = deviceInfo.spectrumScanMaxKhz;
|
||||
late double normalizedMinMhz;
|
||||
late double normalizedMaxMhz;
|
||||
late String sourceKey;
|
||||
|
||||
if (minKhz != null && maxKhz != null && maxKhz > minKhz) {
|
||||
final normalized = _normalizeScanRange(
|
||||
minKhz / 1000.0,
|
||||
maxKhz / 1000.0,
|
||||
deviceInfo.radioFreq != null ? deviceInfo.radioFreq! / 1000.0 : null,
|
||||
);
|
||||
normalizedMinMhz = normalized.$1;
|
||||
normalizedMaxMhz = normalized.$2;
|
||||
sourceKey = 'fw:$minKhz:$maxKhz';
|
||||
} else {
|
||||
final normalized = _normalizeScanRange(
|
||||
null,
|
||||
null,
|
||||
deviceInfo.radioFreq != null ? deviceInfo.radioFreq! / 1000.0 : null,
|
||||
);
|
||||
normalizedMinMhz = normalized.$1;
|
||||
normalizedMaxMhz = normalized.$2;
|
||||
sourceKey =
|
||||
'fallback:${deviceInfo.radioFreq != null ? deviceInfo.radioFreq! ~/ 1000 : 869525}';
|
||||
}
|
||||
|
||||
if (_rangeInitialized && _lastRangeSourceKey == sourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scanRangeMinMhz = normalizedMinMhz;
|
||||
_scanRangeMaxMhz = normalizedMaxMhz;
|
||||
|
||||
if (_scanRangeMaxMhz <= _scanRangeMinMhz) {
|
||||
_scanRangeMaxMhz = _scanRangeMinMhz + 0.5;
|
||||
}
|
||||
|
||||
_scanRangeValues = RangeValues(_scanRangeMinMhz, _scanRangeMaxMhz);
|
||||
_rangeInitialized = true;
|
||||
_lastRangeSourceKey = sourceKey;
|
||||
}
|
||||
|
||||
(double, double) _normalizeScanRange(
|
||||
double? minMhz,
|
||||
double? maxMhz,
|
||||
double? centerMhz,
|
||||
) {
|
||||
const hardMinMhz = 800.0;
|
||||
const hardMaxMhz = 950.0;
|
||||
final center = centerMhz ?? 869.525;
|
||||
|
||||
if (minMhz != null && maxMhz != null) {
|
||||
final clampedMin = minMhz.clamp(hardMinMhz, hardMaxMhz);
|
||||
final clampedMax = maxMhz.clamp(hardMinMhz, hardMaxMhz);
|
||||
if (clampedMax > clampedMin) {
|
||||
return (clampedMin, clampedMax);
|
||||
}
|
||||
}
|
||||
|
||||
if (center >= 900.0 && center <= 930.0) {
|
||||
return (902.0, 928.0);
|
||||
}
|
||||
|
||||
return (863.0, 870.0);
|
||||
}
|
||||
|
||||
double _bandwidthToKhz(String bw) {
|
||||
switch (bw) {
|
||||
case '7.8 kHz':
|
||||
return 7.8;
|
||||
case '10.4 kHz':
|
||||
return 10.4;
|
||||
case '15.6 kHz':
|
||||
return 15.6;
|
||||
case '20.8 kHz':
|
||||
return 20.8;
|
||||
case '31.25 kHz':
|
||||
return 31.25;
|
||||
case '41.7 kHz':
|
||||
return 41.7;
|
||||
case '62.5 kHz':
|
||||
return 62.5;
|
||||
case '125 kHz':
|
||||
return 125.0;
|
||||
case '250 kHz':
|
||||
return 250.0;
|
||||
case '500 kHz':
|
||||
return 500.0;
|
||||
default:
|
||||
return 62.5;
|
||||
}
|
||||
}
|
||||
|
||||
String _currentParamProfile(DeviceInfo deviceInfo) {
|
||||
final sf = deviceInfo.radioSf ?? 8;
|
||||
final cr = deviceInfo.radioCr ?? 8;
|
||||
return 'BW $_selectedBandwidth | SF$sf | CR 4/$cr';
|
||||
}
|
||||
|
||||
String _recommendationTitle(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return 'Best candidate';
|
||||
case 1:
|
||||
return 'Alternate';
|
||||
case 2:
|
||||
return 'Fallback';
|
||||
default:
|
||||
return 'Candidate ${index + 1}';
|
||||
}
|
||||
}
|
||||
|
||||
List<int> _possibleBracketFrequenciesKhz() {
|
||||
final bandwidthKhz = _bandwidthToKhz(_selectedBandwidth);
|
||||
final halfBandwidthKhz = bandwidthKhz / 2.0;
|
||||
final startKhz = (_scanRangeValues.start * 1000).round();
|
||||
final stopKhz = (_scanRangeValues.end * 1000).round();
|
||||
final firstCenterKhz = (startKhz + halfBandwidthKhz).round();
|
||||
final lastCenterKhz = (stopKhz - halfBandwidthKhz).round();
|
||||
|
||||
if (lastCenterKhz < firstCenterKhz) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final stepKhz = bandwidthKhz >= 125.0 ? bandwidthKhz.round() : 25;
|
||||
final centers = <int>[];
|
||||
for (
|
||||
var centerKhz = firstCenterKhz;
|
||||
centerKhz <= lastCenterKhz && centers.length < 8;
|
||||
centerKhz += stepKhz
|
||||
) {
|
||||
centers.add(centerKhz);
|
||||
}
|
||||
|
||||
if (centers.isEmpty || centers.last != lastCenterKhz) {
|
||||
centers.add(lastCenterKhz);
|
||||
}
|
||||
|
||||
return centers.toSet().toList()..sort();
|
||||
}
|
||||
|
||||
void _resetDerivedScanResults() {
|
||||
_scanCandidates = const [];
|
||||
_selectedScanFrequencyKhz = null;
|
||||
}
|
||||
|
||||
List<(int startKhz, int stopKhz)> _buildScanSectors(double bandwidthKhz) {
|
||||
final startKhz = (_scanRangeValues.start * 1000).round();
|
||||
final stopKhz = (_scanRangeValues.end * 1000).round();
|
||||
final sectorWidthKhz = (bandwidthKhz * 24).round().clamp(250, 1200);
|
||||
final overlapKhz = bandwidthKhz.round().clamp(8, 500);
|
||||
final sectors = <(int startKhz, int stopKhz)>[];
|
||||
|
||||
var sectorStartKhz = startKhz;
|
||||
while (sectorStartKhz < stopKhz) {
|
||||
final sectorStopKhz = (sectorStartKhz + sectorWidthKhz).clamp(
|
||||
sectorStartKhz + overlapKhz,
|
||||
stopKhz,
|
||||
);
|
||||
sectors.add((sectorStartKhz, sectorStopKhz));
|
||||
if (sectorStopKhz >= stopKhz) {
|
||||
break;
|
||||
}
|
||||
sectorStartKhz = sectorStopKhz - overlapKhz;
|
||||
}
|
||||
|
||||
return sectors;
|
||||
}
|
||||
|
||||
List<SpectrumScanCandidate> _mergeSectorCandidates(
|
||||
Iterable<SpectrumScanCandidate> candidates,
|
||||
) {
|
||||
final byFrequency = <int, SpectrumScanCandidate>{};
|
||||
for (final candidate in candidates) {
|
||||
final existing = byFrequency[candidate.centerFrequencyKhz];
|
||||
if (existing == null ||
|
||||
candidate.occupancyPercent < existing.occupancyPercent ||
|
||||
(candidate.occupancyPercent == existing.occupancyPercent &&
|
||||
candidate.peakRssiDbm < existing.peakRssiDbm) ||
|
||||
(candidate.occupancyPercent == existing.occupancyPercent &&
|
||||
candidate.peakRssiDbm == existing.peakRssiDbm &&
|
||||
candidate.avgRssiDbm < existing.avgRssiDbm)) {
|
||||
byFrequency[candidate.centerFrequencyKhz] = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
final merged = byFrequency.values.toList()
|
||||
..sort((a, b) {
|
||||
final occupancyCompare = a.occupancyPercent.compareTo(
|
||||
b.occupancyPercent,
|
||||
);
|
||||
if (occupancyCompare != 0) return occupancyCompare;
|
||||
final peakCompare = a.peakRssiDbm.compareTo(b.peakRssiDbm);
|
||||
if (peakCompare != 0) return peakCompare;
|
||||
return a.avgRssiDbm.compareTo(b.avgRssiDbm);
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
Future<void> _runSpectrumScan() async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final bandwidthKhz = _bandwidthToKhz(_selectedBandwidth);
|
||||
final sectors = _buildScanSectors(bandwidthKhz);
|
||||
final sectorCandidates = <SpectrumScanCandidate>[];
|
||||
|
||||
setState(() {
|
||||
_isSpectrumScanRunning = true;
|
||||
_resetDerivedScanResults();
|
||||
_completedScanSectors = 0;
|
||||
_totalScanSectors = sectors.length;
|
||||
});
|
||||
|
||||
try {
|
||||
for (var i = 0; i < sectors.length; i++) {
|
||||
final sector = sectors[i];
|
||||
final result = await connectionProvider.scanSpectrum(
|
||||
startFrequencyKhz: sector.$1,
|
||||
stopFrequencyKhz: sector.$2,
|
||||
bandwidthKhz: bandwidthKhz.round(),
|
||||
stepKhz: (bandwidthKhz / 2).round().clamp(1, 1000),
|
||||
dwellMs: 160,
|
||||
thresholdDb: 8,
|
||||
);
|
||||
if (result != null) {
|
||||
sectorCandidates.addAll(result.candidates);
|
||||
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_scanCandidates = mergedCandidates;
|
||||
if (_selectedScanFrequencyKhz == null &&
|
||||
mergedCandidates.isNotEmpty) {
|
||||
_selectedScanFrequencyKhz =
|
||||
mergedCandidates.first.centerFrequencyKhz;
|
||||
}
|
||||
_completedScanSectors = i + 1;
|
||||
});
|
||||
}
|
||||
} else if (mounted) {
|
||||
setState(() {
|
||||
_completedScanSectors = i + 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSpectrumScanRunning = false;
|
||||
if (_completedScanSectors == 0) {
|
||||
_totalScanSectors = sectors.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
||||
if (mergedCandidates.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Spectrum scan returned no candidate frequencies'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_scanCandidates = mergedCandidates;
|
||||
_selectedScanFrequencyKhz = mergedCandidates.first.centerFrequencyKhz;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applySelectedScanFrequency() async {
|
||||
if (_selectedScanFrequencyKhz == null) return;
|
||||
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
|
||||
await connectionProvider.setRadioParams(
|
||||
frequency: _selectedScanFrequencyKhz!,
|
||||
bandwidth: _bandwidthOptions.indexOf(_selectedBandwidth),
|
||||
spreadingFactor: deviceInfo.radioSf ?? 8,
|
||||
codingRate: deviceInfo.radioCr ?? 8,
|
||||
repeat: deviceInfo.clientRepeat,
|
||||
);
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Applied ${(_selectedScanFrequencyKhz! / 1000.0).toStringAsFixed(3)} MHz',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int? _currentPreviewFrequencyKhz(DeviceInfo deviceInfo) {
|
||||
if (_selectedScanFrequencyKhz != null) {
|
||||
return _selectedScanFrequencyKhz;
|
||||
}
|
||||
return deviceInfo.radioFreq;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final deviceInfo = context.watch<ConnectionProvider>().deviceInfo;
|
||||
final theme = Theme.of(context);
|
||||
final possibleBracketFrequencies = _possibleBracketFrequenciesKhz();
|
||||
final recommendedScanCandidates = _recommendedScanCandidates;
|
||||
final recommendationFrequencies = recommendedScanCandidates.isNotEmpty
|
||||
? recommendedScanCandidates
|
||||
.map((candidate) => candidate.centerFrequencyKhz)
|
||||
.toList()
|
||||
: possibleBracketFrequencies;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Spectrum Scan')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _selectedBandwidth,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Bandwidth',
|
||||
border: OutlineInputBorder(),
|
||||
helperText:
|
||||
'Scan and apply frequencies for this bandwidth',
|
||||
),
|
||||
items: _bandwidthOptions.map((value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_selectedBandwidth = value;
|
||||
_resetDerivedScanResults();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
deviceInfo.spectrumScanMinKhz != null &&
|
||||
deviceInfo.spectrumScanMaxKhz != null
|
||||
? 'Firmware scan range: ${_scanRangeMinMhz.toStringAsFixed(3)}-${_scanRangeMaxMhz.toStringAsFixed(3)} MHz'
|
||||
: 'Fallback scan range selected from current band. MeshCore commonly uses EU868 or US915.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (_isSpectrumScanRunning) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Scanning sector ${_completedScanSectors + 1} of $_totalScanSectors. Results update as each sector completes.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
SpectrumScanPanel(
|
||||
theme: theme,
|
||||
scanSupported: deviceInfo.supportsSpectrumScan == true,
|
||||
isRunning: _isSpectrumScanRunning,
|
||||
rangeMinMhz: _scanRangeMinMhz,
|
||||
rangeMaxMhz: _scanRangeMaxMhz,
|
||||
rangeValues: _scanRangeValues,
|
||||
bandwidthKhz: _bandwidthToKhz(_selectedBandwidth),
|
||||
selectedFrequencyKhz: _currentPreviewFrequencyKhz(
|
||||
deviceInfo,
|
||||
),
|
||||
graphCandidates: _scanCandidates,
|
||||
selectableCandidates: recommendedScanCandidates,
|
||||
onRangeChanged: (values) {
|
||||
setState(() {
|
||||
_scanRangeValues = values;
|
||||
_resetDerivedScanResults();
|
||||
});
|
||||
},
|
||||
onCandidateChanged: (value) {
|
||||
setState(() {
|
||||
_selectedScanFrequencyKhz = value;
|
||||
});
|
||||
},
|
||||
onRunScan: _runSpectrumScan,
|
||||
onApplySelected: _applySelectedScanFrequency,
|
||||
),
|
||||
if (recommendationFrequencies.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
_scanCandidates.isNotEmpty
|
||||
? 'Recommended profiles'
|
||||
: 'Possible brackets',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_scanCandidates.isNotEmpty
|
||||
? 'Suggested frequencies from the latest scan with the radio parameters to keep alongside them.'
|
||||
: 'Usable frequency brackets derived from the selected span and bandwidth, even without live scan data.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (
|
||||
var i = 0;
|
||||
i < recommendationFrequencies.length;
|
||||
i++
|
||||
) ...[
|
||||
_RecommendationTile(
|
||||
title: _recommendationTitle(i),
|
||||
frequencyKhz: recommendationFrequencies[i],
|
||||
candidate: recommendedScanCandidates.isNotEmpty
|
||||
? recommendedScanCandidates[i]
|
||||
: null,
|
||||
bandwidthKhz: _bandwidthToKhz(_selectedBandwidth),
|
||||
paramsLabel: _currentParamProfile(deviceInfo),
|
||||
isSelected:
|
||||
_selectedScanFrequencyKhz ==
|
||||
recommendationFrequencies[i],
|
||||
onSelect: () {
|
||||
setState(() {
|
||||
_selectedScanFrequencyKhz =
|
||||
recommendationFrequencies[i];
|
||||
});
|
||||
},
|
||||
),
|
||||
if (i != recommendationFrequencies.length - 1)
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecommendationTile extends StatelessWidget {
|
||||
final String title;
|
||||
final int frequencyKhz;
|
||||
final SpectrumScanCandidate? candidate;
|
||||
final double bandwidthKhz;
|
||||
final String paramsLabel;
|
||||
final bool isSelected;
|
||||
final VoidCallback onSelect;
|
||||
|
||||
const _RecommendationTile({
|
||||
required this.title,
|
||||
required this.frequencyKhz,
|
||||
required this.candidate,
|
||||
required this.bandwidthKhz,
|
||||
required this.paramsLabel,
|
||||
required this.isSelected,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: onSelect,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? scheme.primaryContainer.withValues(alpha: 0.55)
|
||||
: scheme.surfaceContainerHighest.withValues(alpha: 0.35),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isSelected ? scheme.primary : scheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, color: scheme.primary, size: 18),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(frequencyKhz / 1000.0).toStringAsFixed(3)} MHz',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(paramsLabel, style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
candidate != null
|
||||
? 'Occupancy ${candidate!.occupancyPercent}% | Avg ${candidate!.avgRssiDbm} dBm | Peak ${candidate!.peakRssiDbm} dBm'
|
||||
: 'Bracket ${(frequencyKhz / 1000.0 - bandwidthKhz / 2000.0).toStringAsFixed(3)}-${(frequencyKhz / 1000.0 + bandwidthKhz / 2000.0).toStringAsFixed(3)} MHz',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -221,14 +221,13 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
}
|
||||
|
||||
Future<void> _openConnectionDialog() async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const ConnectionDialog(),
|
||||
final connected = await showConnectionDialogFlow(
|
||||
context,
|
||||
offerPostConnectRepeaterDiscovery: true,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
if (connected) {
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
}
|
||||
if (!mounted) return;
|
||||
@@ -331,7 +330,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
if (_currentPage > 0)
|
||||
TextButton.icon(
|
||||
onPressed: _previousPage,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
icon: Icon(Icons.arrow_back),
|
||||
label: Text(l10n.wizardBack),
|
||||
)
|
||||
else
|
||||
@@ -510,7 +509,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isConnected
|
||||
@@ -601,7 +600,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
_selectedPreset.summary,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
...[
|
||||
_FeatureItem(
|
||||
icon: Icons.rule,
|
||||
|
||||
156
lib/services/app_config_snapshot_service.dart
Normal file
156
lib/services/app_config_snapshot_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import 'image_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'route_hash_preferences.dart';
|
||||
import 'voice_bitrate_preferences.dart';
|
||||
|
||||
class AppConfigSnapshotService {
|
||||
Future<AppSettingsProfileSection> capture(AppProvider appProvider) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
return AppSettingsProfileSection(
|
||||
mapEnabled: appProvider.isMapEnabled,
|
||||
contactsEnabled: appProvider.isContactsEnabled,
|
||||
sensorsEnabled: appProvider.isSensorsEnabled,
|
||||
voiceSilenceTrimmingEnabled: appProvider.isVoiceSilenceTrimmingEnabled,
|
||||
voiceBandPassFilterEnabled: appProvider.isVoiceBandPassFilterEnabled,
|
||||
voiceCompressorEnabled: appProvider.isVoiceCompressorEnabled,
|
||||
voiceLimiterEnabled: appProvider.isVoiceLimiterEnabled,
|
||||
voiceAutoGainEnabled: appProvider.isVoiceAutoGainEnabled,
|
||||
voiceEchoCancellationEnabled: appProvider.isVoiceEchoCancellationEnabled,
|
||||
voiceNoiseSuppressionEnabled: appProvider.isVoiceNoiseSuppressionEnabled,
|
||||
messageFontScale: appProvider.messageFontScale,
|
||||
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
||||
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
||||
nearestRelayFallbackEnabled: appProvider.nearestRelayFallbackEnabled,
|
||||
voiceBitrate: await VoiceBitratePreferences.getBitrate(),
|
||||
routeHashSize: await RouteHashPreferences.getHashSize(),
|
||||
imageMaxSize: await ImagePreferences.getMaxSize(),
|
||||
imageCompression: await ImagePreferences.getCompression(),
|
||||
imageGrayscale: await ImagePreferences.getGrayscale(),
|
||||
imageUltraMode: await ImagePreferences.getUltraMode(),
|
||||
showRxTxIndicators:
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
) ??
|
||||
true,
|
||||
fastLocationUpdatesEnabled: locationTracking.fastLocationUpdatesEnabled,
|
||||
fastLocationMovementThresholdMeters:
|
||||
locationTracking.fastLocationMovementThresholdMeters,
|
||||
fastLocationActiveCadenceSeconds:
|
||||
locationTracking.fastLocationActiveCadenceSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
AppSettingsProfileSection? section,
|
||||
AppProvider appProvider,
|
||||
) async {
|
||||
if (section == null || section.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.mapEnabled != null) {
|
||||
await appProvider.toggleMapEnabled(section.mapEnabled!);
|
||||
}
|
||||
if (section.contactsEnabled != null) {
|
||||
await appProvider.toggleContactsEnabled(section.contactsEnabled!);
|
||||
}
|
||||
if (section.sensorsEnabled != null) {
|
||||
await appProvider.toggleSensorsEnabled(section.sensorsEnabled!);
|
||||
}
|
||||
if (section.voiceSilenceTrimmingEnabled != null) {
|
||||
await appProvider.toggleVoiceSilenceTrimmingEnabled(
|
||||
section.voiceSilenceTrimmingEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBandPassFilterEnabled != null) {
|
||||
await appProvider.toggleVoiceBandPassFilterEnabled(
|
||||
section.voiceBandPassFilterEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceCompressorEnabled != null) {
|
||||
await appProvider.toggleVoiceCompressorEnabled(
|
||||
section.voiceCompressorEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceLimiterEnabled != null) {
|
||||
await appProvider.toggleVoiceLimiterEnabled(section.voiceLimiterEnabled!);
|
||||
}
|
||||
if (section.voiceAutoGainEnabled != null) {
|
||||
await appProvider.toggleVoiceAutoGainEnabled(
|
||||
section.voiceAutoGainEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceEchoCancellationEnabled != null) {
|
||||
await appProvider.toggleVoiceEchoCancellationEnabled(
|
||||
section.voiceEchoCancellationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceNoiseSuppressionEnabled != null) {
|
||||
await appProvider.toggleVoiceNoiseSuppressionEnabled(
|
||||
section.voiceNoiseSuppressionEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.messageFontScale != null) {
|
||||
await appProvider.setMessageFontScale(section.messageFontScale!);
|
||||
}
|
||||
if (section.autoRouteRotationEnabled != null) {
|
||||
await appProvider.toggleAutoRouteRotationEnabled(
|
||||
section.autoRouteRotationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.clearPathOnMaxRetry != null) {
|
||||
await appProvider.toggleClearPathOnMaxRetry(section.clearPathOnMaxRetry!);
|
||||
}
|
||||
if (section.nearestRelayFallbackEnabled != null) {
|
||||
await appProvider.toggleNearestRelayFallbackEnabled(
|
||||
section.nearestRelayFallbackEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBitrate != null) {
|
||||
await VoiceBitratePreferences.setBitrate(section.voiceBitrate!);
|
||||
}
|
||||
if (section.routeHashSize != null) {
|
||||
await RouteHashPreferences.setHashSize(section.routeHashSize!);
|
||||
}
|
||||
if (section.imageMaxSize != null) {
|
||||
await ImagePreferences.setMaxSize(section.imageMaxSize!);
|
||||
}
|
||||
if (section.imageCompression != null) {
|
||||
await ImagePreferences.setCompression(section.imageCompression!);
|
||||
}
|
||||
if (section.imageGrayscale != null) {
|
||||
await ImagePreferences.setGrayscale(section.imageGrayscale!);
|
||||
}
|
||||
if (section.imageUltraMode != null) {
|
||||
await ImagePreferences.setUltraMode(section.imageUltraMode!);
|
||||
}
|
||||
if (section.showRxTxIndicators != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
section.showRxTxIndicators!,
|
||||
);
|
||||
}
|
||||
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
if (section.fastLocationUpdatesEnabled != null) {
|
||||
locationTracking.fastLocationUpdatesEnabled =
|
||||
section.fastLocationUpdatesEnabled!;
|
||||
}
|
||||
if (section.fastLocationMovementThresholdMeters != null) {
|
||||
locationTracking.fastLocationMovementThresholdMeters =
|
||||
section.fastLocationMovementThresholdMeters!.clamp(10.0, 1000.0);
|
||||
}
|
||||
if (section.fastLocationActiveCadenceSeconds != null) {
|
||||
locationTracking.fastLocationActiveCadenceSeconds =
|
||||
section.fastLocationActiveCadenceSeconds!.clamp(10, 31);
|
||||
}
|
||||
await locationTracking.saveSettings();
|
||||
await appProvider.reloadProfileScopedSettings();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Background location tracking service for SAR operations
|
||||
/// Tracks user location and sends periodic updates via MeshCore BLE
|
||||
@@ -15,6 +16,11 @@ class BackgroundLocationService {
|
||||
static const String _prefKeyLastLon = 'background_last_lon';
|
||||
|
||||
MeshCoreBleService? _bleService;
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
bool _isInitialized = false;
|
||||
StreamSubscription<Position>? _positionSubscription;
|
||||
|
||||
@@ -71,8 +77,8 @@ class BackgroundLocationService {
|
||||
|
||||
// Save settings
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, true);
|
||||
await prefs.setDouble(_prefKeyDistance, distanceThreshold);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), true);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distanceThreshold);
|
||||
|
||||
// Start listening to position updates
|
||||
Position? lastPosition;
|
||||
@@ -110,8 +116,14 @@ class BackgroundLocationService {
|
||||
lastPosition = position;
|
||||
|
||||
// Save to preferences
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLat),
|
||||
position.latitude,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLon),
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
// Update device's advertised location
|
||||
if (_bleService != null && _bleService!.isConnected) {
|
||||
@@ -161,7 +173,7 @@ class BackgroundLocationService {
|
||||
_positionSubscription = null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, false);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||
debugPrint('✅ [BackgroundLocation] Tracking stopped');
|
||||
}
|
||||
|
||||
@@ -169,13 +181,13 @@ class BackgroundLocationService {
|
||||
/// Note: This will restart tracking with the new threshold
|
||||
Future<void> updateDistanceThreshold(double distance) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyDistance, distance);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distance);
|
||||
debugPrint(
|
||||
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
||||
);
|
||||
|
||||
// Restart tracking if currently enabled
|
||||
final isEnabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
||||
final isEnabled = prefs.getBool(_scopedKey(_prefKeyEnabled)) ?? false;
|
||||
if (isEnabled && _bleService != null) {
|
||||
await stopTracking();
|
||||
await startTracking(distanceThreshold: distance);
|
||||
|
||||
178
lib/services/bthome_met_history.dart
Normal file
178
lib/services/bthome_met_history.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import '../models/contact.dart';
|
||||
|
||||
enum BTHomeMetMeasurement {
|
||||
temperature(1, 'Temperature', '°C'),
|
||||
humidity(2, 'Humidity', '%'),
|
||||
windSpeed(3, 'Wind speed', 'm/s'),
|
||||
gust(4, 'Wind gust', 'm/s'),
|
||||
rain(5, 'Rain', 'mm');
|
||||
|
||||
const BTHomeMetMeasurement(this.id, this.label, this.unit);
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final String unit;
|
||||
|
||||
static BTHomeMetMeasurement? fromId(int id) {
|
||||
for (final value in BTHomeMetMeasurement.values) {
|
||||
if (value.id == id) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryPage {
|
||||
const BTHomeMetHistoryPage({
|
||||
required this.measurement,
|
||||
required this.page,
|
||||
required this.values,
|
||||
});
|
||||
|
||||
final BTHomeMetMeasurement measurement;
|
||||
final int page;
|
||||
final List<double> values;
|
||||
|
||||
double? get latest => values.isEmpty ? null : values.last;
|
||||
double? get minimum => values.isEmpty
|
||||
? null
|
||||
: values.reduce((left, right) => left < right ? left : right);
|
||||
double? get maximum => values.isEmpty
|
||||
? null
|
||||
: values.reduce((left, right) => left > right ? left : right);
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryFormatException implements Exception {
|
||||
const BTHomeMetHistoryFormatException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryParser {
|
||||
static BTHomeMetHistoryPage parse(String text) {
|
||||
final parts = text
|
||||
.trim()
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.toList(growable: false);
|
||||
if (parts.length < 3) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history response is too short.',
|
||||
);
|
||||
}
|
||||
|
||||
final measurementId = int.tryParse(parts[0]);
|
||||
final page = int.tryParse(parts[1]);
|
||||
final count = int.tryParse(parts[2]);
|
||||
if (measurementId == null || page == null || count == null) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history header is invalid.',
|
||||
);
|
||||
}
|
||||
|
||||
final measurement = BTHomeMetMeasurement.fromId(measurementId);
|
||||
if (measurement == null) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'Unsupported MET history measurement id: $measurementId',
|
||||
);
|
||||
}
|
||||
if (count < 0) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history sample count is invalid.',
|
||||
);
|
||||
}
|
||||
if (parts.length != count + 3) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'MET history sample count mismatch: expected $count values, got ${parts.length - 3}.',
|
||||
);
|
||||
}
|
||||
|
||||
final values = <double>[];
|
||||
for (final part in parts.skip(3)) {
|
||||
final value = double.tryParse(part);
|
||||
if (value == null) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'Invalid MET history sample value: $part',
|
||||
);
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
return BTHomeMetHistoryPage(
|
||||
measurement: measurement,
|
||||
page: page,
|
||||
values: List<double>.unmodifiable(values),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTruthyCapabilityValue(Object? value) {
|
||||
if (value is num) {
|
||||
return value > 0;
|
||||
}
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _hasBTHomeMetCapability(Contact? contact) {
|
||||
final extraSensorData = contact?.telemetry?.extraSensorData;
|
||||
if (extraSensorData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// MET history is only enabled when the node advertises an explicit
|
||||
// capability marker on channel 1. Accept a small set of channel-1 marker
|
||||
// keys so the app remains tolerant while firmware-side encoding settles.
|
||||
const capabilityKeys = <String>[
|
||||
'met_capability',
|
||||
'met_capability_1',
|
||||
'generic_sensor_1',
|
||||
'light_level_1',
|
||||
];
|
||||
|
||||
for (final key in capabilityKeys) {
|
||||
if (_isTruthyCapabilityValue(extraSensorData[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<BTHomeMetMeasurement> bTHomeMetMeasurementsForContact(Contact? contact) {
|
||||
final telemetry = contact?.telemetry;
|
||||
if (telemetry == null || !_hasBTHomeMetCapability(contact)) {
|
||||
return const <BTHomeMetMeasurement>[];
|
||||
}
|
||||
|
||||
final measurements = <BTHomeMetMeasurement>[
|
||||
if (telemetry.temperature != null) BTHomeMetMeasurement.temperature,
|
||||
if (telemetry.humidity != null) BTHomeMetMeasurement.humidity,
|
||||
];
|
||||
|
||||
final extraSensorData = telemetry.extraSensorData;
|
||||
if (extraSensorData != null) {
|
||||
if (extraSensorData.keys.any(
|
||||
(key) => key.startsWith('speed_') || key.startsWith('signed_speed_'),
|
||||
)) {
|
||||
measurements.add(BTHomeMetMeasurement.windSpeed);
|
||||
}
|
||||
if (extraSensorData.keys.any((key) => key.startsWith('gust_'))) {
|
||||
measurements.add(BTHomeMetMeasurement.gust);
|
||||
}
|
||||
if (extraSensorData.keys.any((key) => key.startsWith('rain_'))) {
|
||||
measurements.add(BTHomeMetMeasurement.rain);
|
||||
}
|
||||
}
|
||||
|
||||
return List<BTHomeMetMeasurement>.unmodifiable(measurements);
|
||||
}
|
||||
|
||||
bool supportsBTHomeMetHistory(Contact? contact) =>
|
||||
bTHomeMetMeasurementsForContact(contact).isNotEmpty;
|
||||
@@ -5,6 +5,76 @@ import 'package:meshcore_client/meshcore_client.dart';
|
||||
/// Cayenne LPP (Low Power Payload) data parser
|
||||
/// Used for decoding telemetry sensor data from MeshCore devices
|
||||
class CayenneLppParser {
|
||||
static const int _selfTelemetryChannel = 1;
|
||||
static const int _lppGenericSensor = 100;
|
||||
static const int _lppCurrent = 117;
|
||||
static const int _lppFrequency = 118;
|
||||
static const int _lppPercentage = 120;
|
||||
static const int _lppAltitude = 121;
|
||||
static const int _lppConcentration = 125;
|
||||
static const int _lppPower = 128;
|
||||
static const int _lppSpeed = 129;
|
||||
static const int _lppDistance = 130;
|
||||
static const int _lppEnergy = 131;
|
||||
static const int _lppDirection = 132;
|
||||
static const int _lppUnixTime = 133;
|
||||
static const int _lppColour = 135;
|
||||
static const int _lppGust = 137;
|
||||
static const int _lppDewPoint = 138;
|
||||
static const int _lppRain = 139;
|
||||
static const int _lppSwitch = 142;
|
||||
static const int _lppBinaryBool = 143;
|
||||
static const int _lppBinaryPowerSwitch = 144;
|
||||
static const int _lppBinaryOpen = 145;
|
||||
static const int _lppBinaryBatteryLow = 146;
|
||||
static const int _lppBinaryCharging = 147;
|
||||
static const int _lppBinaryCarbonMonoxide = 148;
|
||||
static const int _lppBinaryCold = 149;
|
||||
static const int _lppBinaryConnectivity = 150;
|
||||
static const int _lppBinaryDoor = 151;
|
||||
static const int _lppBinaryGarageDoor = 152;
|
||||
static const int _lppBinaryGas = 153;
|
||||
static const int _lppBinaryHeat = 154;
|
||||
static const int _lppBinaryLight = 155;
|
||||
static const int _lppBinaryLock = 156;
|
||||
static const int _lppBinaryMoisture = 157;
|
||||
static const int _lppBinaryMotion = 158;
|
||||
static const int _lppBinaryMoving = 159;
|
||||
static const int _lppBinaryOccupancy = 160;
|
||||
static const int _lppBinaryPlug = 161;
|
||||
static const int _lppBinaryPresence = 162;
|
||||
static const int _lppBinaryProblem = 163;
|
||||
static const int _lppBinaryRunning = 164;
|
||||
static const int _lppBinarySafety = 165;
|
||||
static const int _lppBinarySmoke = 166;
|
||||
static const int _lppBinarySound = 167;
|
||||
static const int _lppBinaryTamper = 168;
|
||||
static const int _lppBinaryVibration = 169;
|
||||
static const int _lppBinaryWindow = 170;
|
||||
static const int _lppButtonEvent = 171;
|
||||
static const int _lppDimmer = 172;
|
||||
static const int _lppUv = 173;
|
||||
static const int _lppLightLevel = 174;
|
||||
static const int _lppPm25 = 175;
|
||||
static const int _lppPm10 = 176;
|
||||
static const int _lppCo2 = 177;
|
||||
static const int _lppTvoc = 178;
|
||||
static const int _lppRpm = 179;
|
||||
static const int _lppConductivity = 180;
|
||||
static const int _lppRotation = 181;
|
||||
static const int _lppDuration = 182;
|
||||
static const int _lppAcceleration = 183;
|
||||
static const int _lppGyroRate = 184;
|
||||
static const int _lppVolume = 185;
|
||||
static const int _lppFlowRate = 186;
|
||||
static const int _lppVolumeStorage = 187;
|
||||
static const int _lppWater = 188;
|
||||
static const int _lppGasVolume = 189;
|
||||
static const int _lppMass = 190;
|
||||
static const int _lppSignedSpeed = 191;
|
||||
static const int _lppSignedPower = 192;
|
||||
static const int _lppSignedCurrent = 193;
|
||||
|
||||
/// Parse Cayenne LPP data into ContactTelemetry
|
||||
static ContactTelemetry parse(Uint8List data) {
|
||||
debugPrint(' [CayenneLPP] Parsing LPP data...');
|
||||
@@ -18,14 +88,18 @@ class CayenneLppParser {
|
||||
LatLng? gpsLocation;
|
||||
double? batteryPercentage;
|
||||
double? batteryMilliVolts;
|
||||
double? deferredBatteryMilliVolts;
|
||||
double? deferredBatteryPercentage;
|
||||
double? temperature;
|
||||
double? humidity;
|
||||
double? pressure;
|
||||
final extraSensorData = <String, dynamic>{};
|
||||
bool sawNonSelfChannel = false;
|
||||
|
||||
int fieldCount = 0;
|
||||
while (reader.hasRemaining) {
|
||||
if (fieldCount > 0 && _isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
||||
if (fieldCount > 0 &&
|
||||
_isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
||||
debugPrint(
|
||||
' Detected zero-padded telemetry tail, stopping parse at position '
|
||||
'${data.length - reader.remainingBytesCount}',
|
||||
@@ -41,6 +115,9 @@ class CayenneLppParser {
|
||||
|
||||
final channel = reader.readByte();
|
||||
debugPrint(' Channel: $channel');
|
||||
if (channel != _selfTelemetryChannel) {
|
||||
sawNonSelfChannel = true;
|
||||
}
|
||||
|
||||
final type = reader.readByte();
|
||||
debugPrint(
|
||||
@@ -65,14 +142,20 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Analog Input (raw): $rawValue');
|
||||
debugPrint(' Analog Input (volts): ${value}V');
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
// If this is a battery reading
|
||||
if (channel == 0 || channel == 1) {
|
||||
if (_isDedicatedBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else if (_isDeferredBatteryChannel(channel)) {
|
||||
deferredBatteryMilliVolts = value * 1000;
|
||||
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
} else {
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -85,7 +168,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppIlluminanceSensor:
|
||||
final value = reader.readUInt16BE();
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Illuminance: $value lux');
|
||||
extraSensorData['illuminance_$channel'] = value;
|
||||
break;
|
||||
@@ -98,18 +181,36 @@ class CayenneLppParser {
|
||||
|
||||
case MeshCoreConstants.lppTemperatureSensor:
|
||||
final rawValue = reader.readInt16BE();
|
||||
temperature = rawValue / 10.0;
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Temperature (raw): $rawValue');
|
||||
debugPrint(
|
||||
' Temperature: ${temperature.toStringAsFixed(1)}°C',
|
||||
);
|
||||
debugPrint(' Temperature: ${value.toStringAsFixed(1)}°C');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
temperature = value;
|
||||
extraSensorData[_sourceChannelKey('temperature')] = channel;
|
||||
} else {
|
||||
extraSensorData['temperature_$channel'] = value;
|
||||
if (temperature == null) {
|
||||
temperature = value;
|
||||
extraSensorData[_sourceChannelKey('temperature')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppHumiditySensor:
|
||||
final rawValue = reader.readByte();
|
||||
humidity = rawValue / 2.0;
|
||||
final value = rawValue / 2.0;
|
||||
debugPrint(' Humidity (raw): $rawValue');
|
||||
debugPrint(' Humidity: ${humidity.toStringAsFixed(1)}%');
|
||||
debugPrint(' Humidity: ${value.toStringAsFixed(1)}%');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
humidity = value;
|
||||
extraSensorData[_sourceChannelKey('humidity')] = channel;
|
||||
} else {
|
||||
extraSensorData['humidity_$channel'] = value;
|
||||
if (humidity == null) {
|
||||
humidity = value;
|
||||
extraSensorData[_sourceChannelKey('humidity')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
@@ -126,9 +227,19 @@ class CayenneLppParser {
|
||||
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
pressure = rawValue / 10.0;
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Barometer (raw): $rawValue');
|
||||
debugPrint(' Barometer: ${pressure.toStringAsFixed(1)} hPa');
|
||||
debugPrint(' Barometer: ${value.toStringAsFixed(1)} hPa');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
pressure = value;
|
||||
extraSensorData[_sourceChannelKey('pressure')] = channel;
|
||||
} else {
|
||||
extraSensorData['pressure_$channel'] = value;
|
||||
if (pressure == null) {
|
||||
pressure = value;
|
||||
extraSensorData[_sourceChannelKey('pressure')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
@@ -136,12 +247,21 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Voltage (raw): $rawValue');
|
||||
debugPrint(' Voltage: ${value}V');
|
||||
// Treat voltage sensor as battery reading
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
if (_isDedicatedBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else if (_isDeferredBatteryChannel(channel)) {
|
||||
deferredBatteryMilliVolts = value * 1000;
|
||||
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData['voltage_$channel'] = value;
|
||||
} else {
|
||||
extraSensorData['voltage_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
@@ -194,15 +314,328 @@ class CayenneLppParser {
|
||||
}
|
||||
|
||||
gpsLocation = LatLng(lat, lon);
|
||||
extraSensorData[_sourceChannelKey('gps')] = channel;
|
||||
extraSensorData['altitude_$channel'] = alt;
|
||||
break;
|
||||
|
||||
case _lppGenericSensor:
|
||||
final value = _readUInt32BE(reader).toDouble();
|
||||
debugPrint(' Generic Sensor: $value');
|
||||
extraSensorData['generic_sensor_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppCurrent:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Current (raw): $rawValue');
|
||||
debugPrint(' Current: ${value}A');
|
||||
extraSensorData['current_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppFrequency:
|
||||
final value = _readUInt32BE(reader).toDouble();
|
||||
debugPrint(' Frequency: ${value}Hz');
|
||||
extraSensorData['frequency_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPercentage:
|
||||
final value = reader.readByte().toDouble();
|
||||
debugPrint(' Percentage: $value%');
|
||||
if (_isDedicatedBatteryChannel(channel) || _isDeferredBatteryChannel(channel)) {
|
||||
batteryPercentage = value;
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
} else {
|
||||
extraSensorData['percentage_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case _lppAltitude:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue.toDouble();
|
||||
debugPrint(' Altitude: ${value}m');
|
||||
extraSensorData['altitude_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppConcentration:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Concentration: ${value}ppm');
|
||||
extraSensorData['concentration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPower:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Power: ${value}W');
|
||||
extraSensorData['power_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSpeed:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Speed: ${value}m/s');
|
||||
extraSensorData['speed_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDistance:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Distance: ${value}m');
|
||||
extraSensorData['distance_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppEnergy:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Energy: ${value}kWh');
|
||||
extraSensorData['energy_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDirection:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Direction: $value°');
|
||||
extraSensorData['direction_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppUnixTime:
|
||||
final value = _readUInt32BE(reader);
|
||||
debugPrint(' Unix time: $value');
|
||||
extraSensorData['unixtime_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppColour:
|
||||
final red = reader.readByte();
|
||||
final green = reader.readByte();
|
||||
final blue = reader.readByte();
|
||||
debugPrint(' Colour: r=$red, g=$green, b=$blue');
|
||||
extraSensorData['colour_$channel'] = {
|
||||
'r': red,
|
||||
'g': green,
|
||||
'b': blue,
|
||||
};
|
||||
break;
|
||||
|
||||
case _lppGust:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Gust: ${value}m/s');
|
||||
extraSensorData['gust_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDewPoint:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Dew point: ${value.toStringAsFixed(1)}°C');
|
||||
extraSensorData['dew_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRain:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Rain: ${value}mm');
|
||||
extraSensorData['rain_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppBinaryBool:
|
||||
case _lppBinaryPowerSwitch:
|
||||
case _lppBinaryOpen:
|
||||
case _lppBinaryBatteryLow:
|
||||
case _lppBinaryCharging:
|
||||
case _lppBinaryCarbonMonoxide:
|
||||
case _lppBinaryCold:
|
||||
case _lppBinaryConnectivity:
|
||||
case _lppBinaryDoor:
|
||||
case _lppBinaryGarageDoor:
|
||||
case _lppBinaryGas:
|
||||
case _lppBinaryHeat:
|
||||
case _lppBinaryLight:
|
||||
case _lppBinaryLock:
|
||||
case _lppBinaryMoisture:
|
||||
case _lppBinaryMotion:
|
||||
case _lppBinaryMoving:
|
||||
case _lppBinaryOccupancy:
|
||||
case _lppBinaryPlug:
|
||||
case _lppBinaryPresence:
|
||||
case _lppBinaryProblem:
|
||||
case _lppBinaryRunning:
|
||||
case _lppBinarySafety:
|
||||
case _lppBinarySmoke:
|
||||
case _lppBinarySound:
|
||||
case _lppBinaryTamper:
|
||||
case _lppBinaryVibration:
|
||||
case _lppBinaryWindow:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Binary state: $value');
|
||||
extraSensorData['${_binaryMetricKeyForType(type)}_$channel'] =
|
||||
value;
|
||||
break;
|
||||
|
||||
case _lppButtonEvent:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Button event: $value');
|
||||
extraSensorData['button_event_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDimmer:
|
||||
final value = _readInt8(reader);
|
||||
debugPrint(' Dimmer: $value');
|
||||
extraSensorData['dimmer_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppUv:
|
||||
final value = reader.readByte() / 10.0;
|
||||
debugPrint(' UV index: $value');
|
||||
extraSensorData['uv_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppLightLevel:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Light level: $value');
|
||||
extraSensorData['light_level_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPm25:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' PM2.5: $value');
|
||||
extraSensorData['pm25_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPm10:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' PM10: $value');
|
||||
extraSensorData['pm10_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppCo2:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' CO2: $value');
|
||||
extraSensorData['co2_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppTvoc:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' TVOC: $value');
|
||||
extraSensorData['tvoc_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRpm:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' RPM: $value');
|
||||
extraSensorData['rpm_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppConductivity:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Conductivity: $value');
|
||||
extraSensorData['conductivity_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRotation:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Rotation: $value');
|
||||
extraSensorData['rotation_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDuration:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Duration: $value s');
|
||||
extraSensorData['duration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppAcceleration:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000000.0;
|
||||
debugPrint(' Acceleration: $value');
|
||||
extraSensorData['acceleration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppGyroRate:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Gyro rate: $value');
|
||||
extraSensorData['gyro_rate_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppVolume:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Volume: $value');
|
||||
extraSensorData['volume_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppFlowRate:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Flow rate: $value');
|
||||
extraSensorData['flow_rate_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppVolumeStorage:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Storage volume: $value');
|
||||
extraSensorData['volume_storage_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppWater:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Water: $value');
|
||||
extraSensorData['water_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppGasVolume:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Gas volume: $value');
|
||||
extraSensorData['gas_volume_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppMass:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Mass: $value');
|
||||
extraSensorData['mass_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedSpeed:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000000.0;
|
||||
debugPrint(' Signed speed: $value');
|
||||
extraSensorData['signed_speed_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedPower:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Signed power: $value');
|
||||
extraSensorData['signed_power_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedCurrent:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Signed current: $value');
|
||||
extraSensorData['signed_current_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSwitch:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Switch: $value');
|
||||
extraSensorData['switch_$channel'] = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint(
|
||||
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
|
||||
);
|
||||
// Unknown type, skip remaining to avoid parsing errors
|
||||
reader.skip(reader.remainingBytesCount);
|
||||
final size = _payloadSizeForType(type);
|
||||
if (size == null || reader.remainingBytesCount < size) {
|
||||
debugPrint(
|
||||
' ⚠️ Unknown type $type with unsupported size, stopping parse',
|
||||
);
|
||||
reader.skip(reader.remainingBytesCount);
|
||||
break;
|
||||
}
|
||||
debugPrint(' ⚠️ Unknown type $type, skipping $size bytes');
|
||||
reader.skip(size);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -212,6 +645,21 @@ class CayenneLppParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (batteryMilliVolts == null &&
|
||||
batteryPercentage == null &&
|
||||
deferredBatteryMilliVolts != null &&
|
||||
deferredBatteryPercentage != null &&
|
||||
!sawNonSelfChannel) {
|
||||
batteryMilliVolts = deferredBatteryMilliVolts;
|
||||
batteryPercentage = deferredBatteryPercentage;
|
||||
extraSensorData[_sourceChannelKey('battery')] = _selfTelemetryChannel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = _selfTelemetryChannel;
|
||||
debugPrint(
|
||||
' Promoted self-channel voltage to battery: '
|
||||
'${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(' Parsed $fieldCount fields');
|
||||
debugPrint(' ✅ [CayenneLPP] Parsing complete');
|
||||
debugPrint(
|
||||
@@ -258,6 +706,165 @@ class CayenneLppParser {
|
||||
return ((voltage - 3.0) / 1.2) * 100.0;
|
||||
}
|
||||
|
||||
static bool _isDedicatedBatteryChannel(int channel) => channel == 0;
|
||||
|
||||
static bool _isDeferredBatteryChannel(int channel) =>
|
||||
channel == _selfTelemetryChannel;
|
||||
|
||||
static int _readUInt32BE(BufferReader reader) {
|
||||
final bytes = reader.readBytes(4);
|
||||
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
||||
}
|
||||
|
||||
static int _readInt32BE(BufferReader reader) {
|
||||
final value = _readUInt32BE(reader);
|
||||
if ((value & 0x80000000) != 0) {
|
||||
return value - 0x100000000;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int _readInt8(BufferReader reader) {
|
||||
final value = reader.readByte();
|
||||
if ((value & 0x80) != 0) {
|
||||
return value - 0x100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String _sourceChannelKey(String fieldKey) =>
|
||||
'__source_channel:$fieldKey';
|
||||
|
||||
static String _binaryMetricKeyForType(int type) {
|
||||
switch (type) {
|
||||
case _lppBinaryBool:
|
||||
return 'binary_bool';
|
||||
case _lppBinaryPowerSwitch:
|
||||
return 'binary_power_switch';
|
||||
case _lppBinaryOpen:
|
||||
return 'binary_open';
|
||||
case _lppBinaryBatteryLow:
|
||||
return 'binary_battery_low';
|
||||
case _lppBinaryCharging:
|
||||
return 'binary_charging';
|
||||
case _lppBinaryCarbonMonoxide:
|
||||
return 'binary_carbon_monoxide';
|
||||
case _lppBinaryCold:
|
||||
return 'binary_cold';
|
||||
case _lppBinaryConnectivity:
|
||||
return 'binary_connectivity';
|
||||
case _lppBinaryDoor:
|
||||
return 'binary_door';
|
||||
case _lppBinaryGarageDoor:
|
||||
return 'binary_garage_door';
|
||||
case _lppBinaryGas:
|
||||
return 'binary_gas';
|
||||
case _lppBinaryHeat:
|
||||
return 'binary_heat';
|
||||
case _lppBinaryLight:
|
||||
return 'binary_light';
|
||||
case _lppBinaryLock:
|
||||
return 'binary_lock';
|
||||
case _lppBinaryMoisture:
|
||||
return 'binary_moisture';
|
||||
case _lppBinaryMotion:
|
||||
return 'binary_motion';
|
||||
case _lppBinaryMoving:
|
||||
return 'binary_moving';
|
||||
case _lppBinaryOccupancy:
|
||||
return 'binary_occupancy';
|
||||
case _lppBinaryPlug:
|
||||
return 'binary_plug';
|
||||
case _lppBinaryPresence:
|
||||
return 'binary_presence';
|
||||
case _lppBinaryProblem:
|
||||
return 'binary_problem';
|
||||
case _lppBinaryRunning:
|
||||
return 'binary_running';
|
||||
case _lppBinarySafety:
|
||||
return 'binary_safety';
|
||||
case _lppBinarySmoke:
|
||||
return 'binary_smoke';
|
||||
case _lppBinarySound:
|
||||
return 'binary_sound';
|
||||
case _lppBinaryTamper:
|
||||
return 'binary_tamper';
|
||||
case _lppBinaryVibration:
|
||||
return 'binary_vibration';
|
||||
case _lppBinaryWindow:
|
||||
return 'binary_window';
|
||||
}
|
||||
return 'binary_state';
|
||||
}
|
||||
|
||||
static int? _payloadSizeForType(int type) {
|
||||
if (type >= _lppBinaryBool && type <= _lppBinaryWindow) {
|
||||
return 1;
|
||||
}
|
||||
switch (type) {
|
||||
case MeshCoreConstants.lppDigitalInput:
|
||||
case MeshCoreConstants.lppDigitalOutput:
|
||||
case MeshCoreConstants.lppPresenceSensor:
|
||||
case MeshCoreConstants.lppHumiditySensor:
|
||||
case _lppPercentage:
|
||||
case _lppSwitch:
|
||||
case _lppButtonEvent:
|
||||
case _lppDimmer:
|
||||
case _lppUv:
|
||||
case _lppLightLevel:
|
||||
return 1;
|
||||
case MeshCoreConstants.lppAnalogInput:
|
||||
case MeshCoreConstants.lppAnalogOutput:
|
||||
case MeshCoreConstants.lppIlluminanceSensor:
|
||||
case MeshCoreConstants.lppTemperatureSensor:
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
case _lppCurrent:
|
||||
case _lppAltitude:
|
||||
case _lppConcentration:
|
||||
case _lppPower:
|
||||
case _lppSpeed:
|
||||
case _lppDirection:
|
||||
case _lppGust:
|
||||
case _lppDewPoint:
|
||||
case _lppRain:
|
||||
case _lppPm25:
|
||||
case _lppPm10:
|
||||
case _lppCo2:
|
||||
case _lppTvoc:
|
||||
case _lppRpm:
|
||||
case _lppConductivity:
|
||||
case _lppRotation:
|
||||
return 2;
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
return 6;
|
||||
case MeshCoreConstants.lppGps:
|
||||
return 9;
|
||||
case _lppGenericSensor:
|
||||
case _lppFrequency:
|
||||
case _lppDistance:
|
||||
case _lppEnergy:
|
||||
case _lppUnixTime:
|
||||
case _lppDuration:
|
||||
case _lppAcceleration:
|
||||
case _lppGyroRate:
|
||||
case _lppVolume:
|
||||
case _lppFlowRate:
|
||||
case _lppVolumeStorage:
|
||||
case _lppWater:
|
||||
case _lppGasVolume:
|
||||
case _lppMass:
|
||||
case _lppSignedSpeed:
|
||||
case _lppSignedPower:
|
||||
case _lppSignedCurrent:
|
||||
return 4;
|
||||
case _lppColour:
|
||||
return 3;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
|
||||
final start = data.length - remainingBytes;
|
||||
for (int i = start; i < data.length; i++) {
|
||||
|
||||
11
lib/services/compass_support.dart
Normal file
11
lib/services/compass_support.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter/foundation.dart'
|
||||
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||
|
||||
class CompassSupport {
|
||||
const CompassSupport._();
|
||||
|
||||
static bool get isAvailable =>
|
||||
!kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/contact_group.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
@@ -14,8 +15,12 @@ class ContactStorageService {
|
||||
static const int _maxStoredContacts = 500; // Store up to 500 contacts
|
||||
static const int _maxStoredPendingAdverts = 500;
|
||||
|
||||
String _key(String baseKey, {String? namespace}) {
|
||||
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
|
||||
}
|
||||
|
||||
/// Save contacts to persistent storage
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -30,7 +35,10 @@ class ContactStorageService {
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_contactsKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
||||
@@ -42,10 +50,15 @@ class ContactStorageService {
|
||||
|
||||
/// Load contacts from persistent storage
|
||||
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
|
||||
Future<List<Contact>> loadContacts({Uint8List? excludePublicKey}) async {
|
||||
Future<List<Contact>> loadContacts({
|
||||
Uint8List? excludePublicKey,
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
debugPrint('ℹ️ [ContactStorage] No stored contacts found');
|
||||
@@ -84,23 +97,29 @@ class ContactStorageService {
|
||||
}
|
||||
|
||||
/// Clear all stored contacts
|
||||
Future<void> clearContacts() async {
|
||||
Future<void> clearContacts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_contactsKey);
|
||||
await prefs.remove(_key(_contactsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveContactGroups(List<SavedContactGroup> groups) async {
|
||||
Future<void> saveContactGroups(
|
||||
List<SavedContactGroup> groups, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = jsonEncode(
|
||||
groups.map((group) => _contactGroupToJson(group)).toList(),
|
||||
);
|
||||
await prefs.setString(_contactGroupsKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
|
||||
);
|
||||
@@ -109,10 +128,12 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SavedContactGroup>> loadContactGroups() async {
|
||||
Future<List<SavedContactGroup>> loadContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactGroupsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
@@ -128,13 +149,29 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePendingAdverts(List<Map<String, dynamic>> adverts) async {
|
||||
Future<void> clearContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_key(_contactGroupsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all contact groups');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contact groups: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePendingAdverts(
|
||||
List<Map<String, dynamic>> adverts, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final limitedList = adverts.length > _maxStoredPendingAdverts
|
||||
? adverts.sublist(adverts.length - _maxStoredPendingAdverts)
|
||||
: adverts;
|
||||
await prefs.setString(_pendingAdvertsKey, jsonEncode(limitedList));
|
||||
await prefs.setString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
jsonEncode(limitedList),
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} pending adverts to storage',
|
||||
);
|
||||
@@ -143,10 +180,14 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> loadPendingAdverts() async {
|
||||
Future<List<Map<String, dynamic>>> loadPendingAdverts({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_pendingAdvertsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
@@ -159,21 +200,55 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearPendingAdverts() async {
|
||||
Future<void> clearPendingAdverts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_pendingAdvertsKey);
|
||||
await prefs.remove(_key(_pendingAdvertsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored pending adverts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing pending adverts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
// ── Favorites ───────────────────────────────────────────────────────────────
|
||||
|
||||
static const String _favoritesKey = 'contact_favorites';
|
||||
|
||||
Future<void> saveFavorites(
|
||||
Set<String> publicKeyHexes, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
await prefs.setStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
publicKeyHexes.toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>> loadFavorites({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
);
|
||||
return list?.toSet() ?? {};
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
|
||||
140
lib/services/device_config_applicator.dart
Normal file
140
lib/services/device_config_applicator.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
|
||||
class DeviceConfigApplicator {
|
||||
ConfigProfileSections capture({
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
return ConfigProfileSections(
|
||||
deviceConfig: DeviceConfigProfileSection(
|
||||
frequencyKhz: deviceInfo.radioFreq,
|
||||
bandwidth: deviceInfo.radioBw,
|
||||
spreadingFactor: deviceInfo.radioSf,
|
||||
codingRate: deviceInfo.radioCr,
|
||||
repeatEnabled: deviceInfo.clientRepeat,
|
||||
txPower: deviceInfo.txPower,
|
||||
telemetryModes: deviceInfo.telemetryModes,
|
||||
advertLocationPolicy: deviceInfo.advertLocPolicy,
|
||||
multiAcks: deviceInfo.multiAcks,
|
||||
manualAddContacts: deviceInfo.manualAddContacts,
|
||||
autoAddUsers: deviceInfo.autoAddUsers,
|
||||
autoAddRepeaters: deviceInfo.autoAddRepeaters,
|
||||
autoAddRoomServers: deviceInfo.autoAddRoomServers,
|
||||
autoAddSensors: deviceInfo.autoAddSensors,
|
||||
autoAddOverwriteOldest: deviceInfo.autoAddOverwriteOldest,
|
||||
publicLatitude: _decodeAdvertCoordinate(deviceInfo.advLat),
|
||||
publicLongitude: _decodeAdvertCoordinate(deviceInfo.advLon),
|
||||
),
|
||||
channels: channelsProvider.channels,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
ConfigProfile profile, {
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final deviceConfig = profile.sections.deviceConfig;
|
||||
if (deviceConfig != null && !deviceConfig.isEmpty) {
|
||||
if (deviceConfig.txPower != null) {
|
||||
await connectionProvider.setTxPower(deviceConfig.txPower!);
|
||||
}
|
||||
if (deviceConfig.telemetryModes != null ||
|
||||
deviceConfig.advertLocationPolicy != null ||
|
||||
deviceConfig.manualAddContacts != null ||
|
||||
deviceConfig.multiAcks != null) {
|
||||
await connectionProvider.setOtherParams(
|
||||
manualAddContacts: (deviceConfig.manualAddContacts ?? false) ? 1 : 0,
|
||||
telemetryModes: deviceConfig.telemetryModes ?? 0,
|
||||
advertLocationPolicy: deviceConfig.advertLocationPolicy ?? 0,
|
||||
multiAcks: deviceConfig.multiAcks ?? 0,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.autoAddUsers != null &&
|
||||
deviceConfig.autoAddRepeaters != null &&
|
||||
deviceConfig.autoAddRoomServers != null &&
|
||||
deviceConfig.autoAddSensors != null &&
|
||||
deviceConfig.autoAddOverwriteOldest != null) {
|
||||
await connectionProvider.setAutoaddConfig(
|
||||
autoAddUsers: deviceConfig.autoAddUsers!,
|
||||
autoAddRepeaters: deviceConfig.autoAddRepeaters!,
|
||||
autoAddRoomServers: deviceConfig.autoAddRoomServers!,
|
||||
autoAddSensors: deviceConfig.autoAddSensors!,
|
||||
overwriteOldest: deviceConfig.autoAddOverwriteOldest!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.publicLatitude != null &&
|
||||
deviceConfig.publicLongitude != null) {
|
||||
await connectionProvider.setAdvertLatLon(
|
||||
latitude: deviceConfig.publicLatitude!,
|
||||
longitude: deviceConfig.publicLongitude!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.frequencyKhz != null &&
|
||||
deviceConfig.bandwidth != null &&
|
||||
deviceConfig.spreadingFactor != null &&
|
||||
deviceConfig.codingRate != null) {
|
||||
await connectionProvider.setRadioParams(
|
||||
frequency: deviceConfig.frequencyKhz!,
|
||||
bandwidth: deviceConfig.bandwidth!,
|
||||
spreadingFactor: deviceConfig.spreadingFactor!,
|
||||
codingRate: deviceConfig.codingRate!,
|
||||
repeat: deviceConfig.repeatEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _applyChannels(
|
||||
channels: profile.sections.channels,
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
}
|
||||
|
||||
Future<void> _applyChannels({
|
||||
required List<Channel> channels,
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (channels.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final desired = {for (final channel in channels) channel.index: channel};
|
||||
|
||||
for (final channel in channels) {
|
||||
await connectionProvider.setChannelSlot(
|
||||
channelIdx: channel.index,
|
||||
channelName: channel.name,
|
||||
secret: Uint8List.fromList(channel.secret),
|
||||
);
|
||||
channelsProvider.addOrUpdateChannelObject(channel);
|
||||
}
|
||||
|
||||
for (final existing in channelsProvider.channels) {
|
||||
if (existing.index == 0 || desired.containsKey(existing.index)) {
|
||||
continue;
|
||||
}
|
||||
await connectionProvider.deleteChannel(existing.index);
|
||||
channelsProvider.removeChannel(existing.index);
|
||||
}
|
||||
}
|
||||
|
||||
double? _decodeAdvertCoordinate(int? value) {
|
||||
if (value == null || value == 0) {
|
||||
return null;
|
||||
}
|
||||
return value / 1e6;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Stores user-selected image compression settings.
|
||||
class ImagePreferences {
|
||||
@@ -17,44 +18,53 @@ class ImagePreferences {
|
||||
|
||||
static Future<int> getMaxSize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_maxSizeKey) ?? defaultMaxSize;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_maxSizeKey)) ??
|
||||
defaultMaxSize;
|
||||
return supportedSizes.contains(value) ? value : defaultMaxSize;
|
||||
}
|
||||
|
||||
static Future<void> setMaxSize(int size) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_maxSizeKey, size);
|
||||
await prefs.setInt(ProfileStorageScope.scopedKey(_maxSizeKey), size);
|
||||
}
|
||||
|
||||
static Future<int> getCompression() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_qualityKey) ?? defaultQuality;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_qualityKey)) ??
|
||||
defaultQuality;
|
||||
return value.clamp(10, 90);
|
||||
}
|
||||
|
||||
static Future<void> setCompression(int compression) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_qualityKey, compression.clamp(10, 90));
|
||||
await prefs.setInt(
|
||||
ProfileStorageScope.scopedKey(_qualityKey),
|
||||
compression.clamp(10, 90),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getGrayscale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_grayscaleKey) ?? defaultGrayscale;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_grayscaleKey)) ??
|
||||
defaultGrayscale;
|
||||
}
|
||||
|
||||
static Future<void> setGrayscale(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_grayscaleKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_grayscaleKey), value);
|
||||
}
|
||||
|
||||
static Future<bool> getUltraMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_ultraModeKey) ?? defaultUltraMode;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_ultraModeKey)) ??
|
||||
defaultUltraMode;
|
||||
}
|
||||
|
||||
static Future<void> setUltraMode(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_ultraModeKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_ultraModeKey), value);
|
||||
}
|
||||
|
||||
static int effectiveMaxSize(
|
||||
|
||||
@@ -3,12 +3,136 @@ import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
enum LiveTrafficBusyness { quiet, active, busy }
|
||||
|
||||
class LiveTrafficPacketTypeDetails {
|
||||
final int payloadType;
|
||||
final String title;
|
||||
final String label;
|
||||
final String summary;
|
||||
final String description;
|
||||
|
||||
const LiveTrafficPacketTypeDetails({
|
||||
required this.payloadType,
|
||||
required this.title,
|
||||
required this.label,
|
||||
required this.summary,
|
||||
required this.description,
|
||||
});
|
||||
}
|
||||
|
||||
class LiveTrafficEntry {
|
||||
final BlePacketLog log;
|
||||
final DecodedLogRxRoute? route;
|
||||
|
||||
const LiveTrafficEntry({required this.log, required this.route});
|
||||
|
||||
static const List<LiveTrafficPacketTypeDetails> _knownPayloadTypes = [
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x00,
|
||||
title: 'FLOOD REQUEST',
|
||||
label: 'Request',
|
||||
summary: 'Encrypted request to a known peer',
|
||||
description:
|
||||
'Encrypted request to a known peer. The wire payload carries destination and source hashes plus a MAC, and the decrypted body starts with a timestamp followed by application-defined request data such as stats or keepalive requests.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x01,
|
||||
title: 'FLOOD RESPONSE',
|
||||
label: 'Response',
|
||||
summary: 'Encrypted reply to a request',
|
||||
description:
|
||||
'Encrypted reply to a Request or Anonymous request. After decryption, the body is application-defined response data with no single generic response envelope.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x02,
|
||||
title: 'FLOOD TEXT',
|
||||
label: 'Text message',
|
||||
summary: 'Encrypted direct text with timestamp and retry flags',
|
||||
description:
|
||||
'Encrypted direct text message to a known peer. The decrypted body contains a timestamp, a flags and attempt byte, and the message text.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x03,
|
||||
title: 'FLOOD ACK',
|
||||
label: 'Ack',
|
||||
summary: '4-byte acknowledgement for an earlier message',
|
||||
description:
|
||||
'Short acknowledgement proving that a prior message was received. It carries a 4-byte checksum derived from the original message data.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x04,
|
||||
title: 'FLOOD ADVERTISEMENT',
|
||||
label: 'Advertisement',
|
||||
summary: 'Signed node identity broadcast',
|
||||
description:
|
||||
'Signed node advertisement announcing a device identity plus app data such as a name or location. Receivers verify the signature before accepting it.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x05,
|
||||
title: 'FLOOD GROUP_TEXT',
|
||||
label: 'Group text',
|
||||
summary: 'Encrypted channel text matched by channel hash',
|
||||
description:
|
||||
'Encrypted channel text message. It is matched by the first byte of SHA256(channel secret), then decrypted with the channel key. The plaintext is usually in the form "<sender name>: <message body>".',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x06,
|
||||
title: 'FLOOD GROUP_DATA',
|
||||
label: 'Group datagram',
|
||||
summary: 'Encrypted channel data with type and length',
|
||||
description:
|
||||
'Encrypted channel datagram. After channel-hash matching and decryption, the body starts with a 16-bit data type and a 1-byte data length before the application payload.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x07,
|
||||
title: 'FLOOD ANON_REQUEST',
|
||||
label: 'Anonymous request',
|
||||
summary: 'Request using an ephemeral sender key',
|
||||
description:
|
||||
'Encrypted request to a destination hash without using a stored sender identity. The packet includes the sender\'s ephemeral public key so the receiver can derive the shared secret.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x08,
|
||||
title: 'FLOOD RETURNED_PATH',
|
||||
label: 'Returned path',
|
||||
summary:
|
||||
'Return route back to the sender, with optional bundled ACK or response',
|
||||
description:
|
||||
'Path reply sent back to the original author to describe the route a received packet took. MeshCore stores that returned path as the peer\'s direct out-path and can bundle an ACK or response in the same payload.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x09,
|
||||
title: 'FLOOD TRACE_PATH',
|
||||
label: 'Trace path',
|
||||
summary: 'Direct trace that records SNR at each hop',
|
||||
description:
|
||||
'Direct diagnostic packet that walks a supplied path and appends one SNR sample per hop. When it reaches the end of the path, the initiator can inspect hop-by-hop link quality.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x0A,
|
||||
title: 'FLOOD MULTIPART',
|
||||
label: 'Multipart packet',
|
||||
summary: 'Wrapper for one packet in a multipart sequence',
|
||||
description:
|
||||
'Packet wrapper used when a logical message is split into a sequence. Current MeshCore code uses it for multipart ACKs, where the first nibble says how many parts remain.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x0B,
|
||||
title: 'FLOOD CONTROL',
|
||||
label: 'Control packet',
|
||||
summary: 'Discovery or other control data',
|
||||
description:
|
||||
'Control or discovery payload, typically unencrypted. Current documented subtypes are discovery request and response packets used to find nearby nodes and report SNR.',
|
||||
),
|
||||
LiveTrafficPacketTypeDetails(
|
||||
payloadType: 0x0F,
|
||||
title: 'RAW CUSTOM',
|
||||
label: 'Custom packet',
|
||||
summary: 'Application-defined custom packet',
|
||||
description:
|
||||
'Application-defined raw packet bytes for custom encryption or custom payload formats. MeshCore leaves the inner format up to the higher-level application.',
|
||||
),
|
||||
];
|
||||
|
||||
bool get isMultiHop => (route?.hopCount ?? 0) > 1;
|
||||
|
||||
int? get hopCount => route?.hopCount;
|
||||
@@ -37,66 +161,39 @@ class LiveTrafficEntry {
|
||||
.join(' -> ');
|
||||
}
|
||||
|
||||
static String payloadTypeLabel(int payloadType) {
|
||||
switch (payloadType) {
|
||||
case 0x00:
|
||||
return 'Request';
|
||||
case 0x01:
|
||||
return 'Response';
|
||||
case 0x02:
|
||||
return 'Text message';
|
||||
case 0x03:
|
||||
return 'Ack';
|
||||
case 0x04:
|
||||
return 'Advertisement';
|
||||
case 0x05:
|
||||
return 'Group text';
|
||||
case 0x06:
|
||||
return 'Group datagram';
|
||||
case 0x07:
|
||||
return 'Anonymous request';
|
||||
case 0x08:
|
||||
return 'Returned path';
|
||||
case 0x09:
|
||||
return 'Trace path';
|
||||
case 0x0A:
|
||||
return 'Multipart packet';
|
||||
case 0x0B:
|
||||
return 'Control packet';
|
||||
default:
|
||||
return '0x${payloadType.toRadixString(16).padLeft(2, '0')}';
|
||||
static List<LiveTrafficPacketTypeDetails> get knownPayloadTypes =>
|
||||
_knownPayloadTypes;
|
||||
|
||||
static LiveTrafficPacketTypeDetails payloadTypeDetails(int payloadType) {
|
||||
for (final details in _knownPayloadTypes) {
|
||||
if (details.payloadType == payloadType) {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
return LiveTrafficPacketTypeDetails(
|
||||
payloadType: payloadType,
|
||||
title: '0x${payloadType.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
||||
label: '0x${payloadType.toRadixString(16).padLeft(2, '0')}',
|
||||
summary: 'Unknown or application-specific protocol payload',
|
||||
description:
|
||||
'Unknown or application-specific packet type. Check the current MeshCore firmware or app-specific protocol docs for the exact payload format.',
|
||||
);
|
||||
}
|
||||
|
||||
static String payloadTypeLabel(int payloadType) {
|
||||
return payloadTypeDetails(payloadType).label;
|
||||
}
|
||||
|
||||
static String payloadTypeTitle(int payloadType) {
|
||||
return payloadTypeDetails(payloadType).title;
|
||||
}
|
||||
|
||||
static String payloadTypeMeaning(int payloadType) {
|
||||
switch (payloadType) {
|
||||
case 0x00:
|
||||
return 'Request (destination/source hashes + MAC)';
|
||||
case 0x01:
|
||||
return 'Response to Request or Anonymous request';
|
||||
case 0x02:
|
||||
return 'Plain text message';
|
||||
case 0x03:
|
||||
return 'Simple acknowledgement';
|
||||
case 0x04:
|
||||
return 'Node advertisement';
|
||||
case 0x05:
|
||||
return 'Unverified group text message';
|
||||
case 0x06:
|
||||
return 'Unverified group datagram';
|
||||
case 0x07:
|
||||
return 'Generic anonymous request';
|
||||
case 0x08:
|
||||
return 'Returned path payload';
|
||||
case 0x09:
|
||||
return 'Trace path collecting hop SNR';
|
||||
case 0x0A:
|
||||
return 'One packet from a multipart set';
|
||||
case 0x0B:
|
||||
return 'Control or discovery packet';
|
||||
default:
|
||||
return 'protocol payload';
|
||||
}
|
||||
return payloadTypeDetails(payloadType).summary;
|
||||
}
|
||||
|
||||
static String payloadTypeDescription(int payloadType) {
|
||||
return payloadTypeDetails(payloadType).description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +238,18 @@ class LiveTrafficSummary {
|
||||
|
||||
const LiveTrafficSummary._();
|
||||
|
||||
static bool isRxDataLog(BlePacketLog log) {
|
||||
return log.direction == PacketDirection.rx &&
|
||||
log.responseCode == logRxDataResponseCode;
|
||||
}
|
||||
|
||||
static int countRxDataLogs(Iterable<BlePacketLog> logs, {DateTime? since}) {
|
||||
return logs.where((log) {
|
||||
return isRxDataLog(log) &&
|
||||
(since == null || !log.timestamp.isBefore(since));
|
||||
}).length;
|
||||
}
|
||||
|
||||
static LiveTrafficSnapshot fromLogs(
|
||||
Iterable<BlePacketLog> logs, {
|
||||
required DateTime now,
|
||||
@@ -154,15 +263,14 @@ class LiveTrafficSummary {
|
||||
? clearedAt
|
||||
: windowStart;
|
||||
|
||||
final recentLogs = logs
|
||||
.where(
|
||||
(log) =>
|
||||
log.direction == PacketDirection.rx &&
|
||||
log.responseCode == logRxDataResponseCode &&
|
||||
!log.timestamp.isBefore(effectiveStart),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final recentLogs =
|
||||
logs
|
||||
.where(
|
||||
(log) =>
|
||||
isRxDataLog(log) && !log.timestamp.isBefore(effectiveStart),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
final entries = <LiveTrafficEntry>[];
|
||||
for (final log in recentLogs) {
|
||||
@@ -215,7 +323,9 @@ class LiveTrafficSummary {
|
||||
}
|
||||
}
|
||||
|
||||
final visibleEntries = filteredEntries.reversed.take(maxVisibleEntries).toList();
|
||||
final visibleEntries = filteredEntries.reversed
|
||||
.take(maxVisibleEntries)
|
||||
.toList();
|
||||
const txCount = 0;
|
||||
final totalCount = rxCount;
|
||||
final packetsPerMinute =
|
||||
@@ -233,7 +343,9 @@ class LiveTrafficSummary {
|
||||
avgRssiDbm: rssiCount == 0 ? null : rssiSum / rssiCount,
|
||||
latestRssiDbm: latestRssiDbm,
|
||||
multiHopCount: multiHopCount,
|
||||
avgHopCount: hopCountSamples == 0 ? null : hopCountTotal / hopCountSamples,
|
||||
avgHopCount: hopCountSamples == 0
|
||||
? null
|
||||
: hopCountTotal / hopCountSamples,
|
||||
visibleEntries: visibleEntries,
|
||||
busyness: _busynessForPacketsPerMinute(packetsPerMinute),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Centralized location tracking service for MeshCore SAR
|
||||
///
|
||||
@@ -19,6 +20,14 @@ import 'package:meshcore_client/meshcore_client.dart';
|
||||
/// - MeshCore mesh network integration
|
||||
/// - Real-time position updates via callbacks
|
||||
class LocationTrackingService {
|
||||
static const double _defaultFastLocationMovementThresholdMeters = 10.0;
|
||||
static const double _minFastLocationMovementThresholdMeters = 10.0;
|
||||
static const int _defaultFastLocationActiveCadenceSeconds = 10;
|
||||
static const int _minFastLocationActiveCadenceSeconds = 10;
|
||||
static const int _maxFastLocationActiveCadenceSeconds = 31;
|
||||
static const Duration _fastLocationMinimumUpdateInterval = Duration(
|
||||
seconds: 31,
|
||||
);
|
||||
// ============================================================================
|
||||
// Singleton Pattern
|
||||
// ============================================================================
|
||||
@@ -48,6 +57,11 @@ class LocationTrackingService {
|
||||
'fast_location_movement_threshold_meters';
|
||||
static const String _prefKeyFastActiveCadence =
|
||||
'fast_location_active_cadence_seconds';
|
||||
static const String _prefKeyFastChannelIdx = 'fast_location_channel_idx';
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Properties
|
||||
@@ -69,10 +83,15 @@ class LocationTrackingService {
|
||||
bool fastLocationUpdatesEnabled = false;
|
||||
|
||||
/// Distance threshold for fast GPS updates
|
||||
double fastLocationMovementThresholdMeters = 10.0;
|
||||
double fastLocationMovementThresholdMeters =
|
||||
_defaultFastLocationMovementThresholdMeters;
|
||||
|
||||
/// Cadence for active-use fast GPS updates
|
||||
int fastLocationActiveCadenceSeconds = 10;
|
||||
int fastLocationActiveCadenceSeconds =
|
||||
_defaultFastLocationActiveCadenceSeconds;
|
||||
|
||||
/// Target channel index for fast GPS updates; null means disabled/unset.
|
||||
int? fastLocationChannelIdx;
|
||||
|
||||
// ============================================================================
|
||||
// State Properties
|
||||
@@ -399,7 +418,7 @@ class LocationTrackingService {
|
||||
|
||||
// Save disabled state
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, false);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||
|
||||
debugPrint('✅ [LocationTracking] Tracking stopped');
|
||||
}
|
||||
@@ -477,8 +496,8 @@ class LocationTrackingService {
|
||||
|
||||
// Save to preferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyLastLat), position.latitude);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyLastLon), position.longitude);
|
||||
|
||||
debugPrint('✅ [LocationTracking] Initial position set without broadcast');
|
||||
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
|
||||
@@ -513,18 +532,30 @@ class LocationTrackingService {
|
||||
}
|
||||
|
||||
Future<void> updateFastLocationMovementThreshold(double meters) async {
|
||||
fastLocationMovementThresholdMeters = meters.clamp(1.0, 1000.0);
|
||||
fastLocationMovementThresholdMeters = meters.clamp(
|
||||
_minFastLocationMovementThresholdMeters,
|
||||
1000.0,
|
||||
);
|
||||
await saveSettings();
|
||||
}
|
||||
|
||||
Future<void> updateFastLocationActiveCadenceSeconds(int seconds) async {
|
||||
fastLocationActiveCadenceSeconds = seconds.clamp(5, 60);
|
||||
fastLocationActiveCadenceSeconds = seconds.clamp(
|
||||
_minFastLocationActiveCadenceSeconds,
|
||||
_maxFastLocationActiveCadenceSeconds,
|
||||
);
|
||||
await saveSettings();
|
||||
_refreshFastLocationTimer();
|
||||
}
|
||||
|
||||
Future<void> updateFastLocationChannelIdx(int? channelIdx) async {
|
||||
fastLocationChannelIdx = channelIdx;
|
||||
await saveSettings();
|
||||
_refreshFastLocationTimer();
|
||||
}
|
||||
|
||||
void _evaluateFastLocationMovement(Position position) {
|
||||
if (!fastLocationUpdatesEnabled) return;
|
||||
if (!fastLocationUpdatesEnabled || fastLocationChannelIdx == null) return;
|
||||
final previous = _lastFastLocationSentPosition;
|
||||
if (previous == null) {
|
||||
_emitFastLocationUpdate(position, reason: 'initial');
|
||||
@@ -547,6 +578,7 @@ class LocationTrackingService {
|
||||
_fastLocationTimer = null;
|
||||
if (!isTracking ||
|
||||
!fastLocationUpdatesEnabled ||
|
||||
fastLocationChannelIdx == null ||
|
||||
!_isFastLocationActiveUse) {
|
||||
return;
|
||||
}
|
||||
@@ -562,11 +594,16 @@ class LocationTrackingService {
|
||||
}
|
||||
|
||||
void _emitFastLocationUpdate(Position position, {required String reason}) {
|
||||
if (!fastLocationUpdatesEnabled) return;
|
||||
if (!fastLocationUpdatesEnabled || fastLocationChannelIdx == null) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final previous = _lastFastLocationSentPosition;
|
||||
final previousTime = _lastFastLocationSentAt;
|
||||
if (previousTime != null &&
|
||||
now.difference(previousTime) < _fastLocationMinimumUpdateInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previous != null && previousTime != null) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
previous.latitude,
|
||||
@@ -646,17 +683,27 @@ class LocationTrackingService {
|
||||
Future<void> loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
minDistanceMeters = prefs.getDouble(_prefKeyMinDistance) ?? 5.0;
|
||||
maxDistanceMeters = prefs.getDouble(_prefKeyMaxDistance) ?? 100.0;
|
||||
minTimeIntervalSeconds = prefs.getInt(_prefKeyMinTimeInterval) ?? 30;
|
||||
gpsUpdateDistance = prefs.getDouble(_prefKeyGpsUpdateDistance) ?? 10.0;
|
||||
minDistanceMeters = prefs.getDouble(_scopedKey(_prefKeyMinDistance)) ?? 5.0;
|
||||
maxDistanceMeters =
|
||||
prefs.getDouble(_scopedKey(_prefKeyMaxDistance)) ?? 100.0;
|
||||
minTimeIntervalSeconds =
|
||||
prefs.getInt(_scopedKey(_prefKeyMinTimeInterval)) ?? 30;
|
||||
gpsUpdateDistance =
|
||||
prefs.getDouble(_scopedKey(_prefKeyGpsUpdateDistance)) ?? 10.0;
|
||||
fastLocationUpdatesEnabled =
|
||||
prefs.getBool(_prefKeyFastLocationEnabled) ?? false;
|
||||
prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false;
|
||||
fastLocationMovementThresholdMeters =
|
||||
(prefs.getDouble(_prefKeyFastMovementThreshold) ?? gpsUpdateDistance)
|
||||
.clamp(1.0, 1000.0);
|
||||
(prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ??
|
||||
_defaultFastLocationMovementThresholdMeters)
|
||||
.clamp(_minFastLocationMovementThresholdMeters, 1000.0);
|
||||
fastLocationActiveCadenceSeconds =
|
||||
(prefs.getInt(_prefKeyFastActiveCadence) ?? 10).clamp(5, 60);
|
||||
(prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ??
|
||||
_defaultFastLocationActiveCadenceSeconds)
|
||||
.clamp(
|
||||
_minFastLocationActiveCadenceSeconds,
|
||||
_maxFastLocationActiveCadenceSeconds,
|
||||
);
|
||||
fastLocationChannelIdx = prefs.getInt(_scopedKey(_prefKeyFastChannelIdx));
|
||||
|
||||
debugPrint('✅ [LocationTracking] Settings loaded');
|
||||
debugPrint(' Min distance: ${minDistanceMeters}m');
|
||||
@@ -668,29 +715,42 @@ class LocationTrackingService {
|
||||
' Fast movement threshold: ${fastLocationMovementThresholdMeters}m',
|
||||
);
|
||||
debugPrint(' Fast active cadence: ${fastLocationActiveCadenceSeconds}s');
|
||||
debugPrint(' Fast channel idx: ${fastLocationChannelIdx ?? "unset"}');
|
||||
}
|
||||
|
||||
/// Save settings to SharedPreferences
|
||||
Future<void> saveSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setDouble(_prefKeyMinDistance, minDistanceMeters);
|
||||
await prefs.setDouble(_prefKeyMaxDistance, maxDistanceMeters);
|
||||
await prefs.setInt(_prefKeyMinTimeInterval, minTimeIntervalSeconds);
|
||||
await prefs.setDouble(_prefKeyGpsUpdateDistance, gpsUpdateDistance);
|
||||
await prefs.setBool(_prefKeyEnabled, isTracking);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyMinDistance), minDistanceMeters);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyMaxDistance), maxDistanceMeters);
|
||||
await prefs.setInt(
|
||||
_scopedKey(_prefKeyMinTimeInterval),
|
||||
minTimeIntervalSeconds,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyGpsUpdateDistance),
|
||||
gpsUpdateDistance,
|
||||
);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), isTracking);
|
||||
await prefs.setBool(
|
||||
_prefKeyFastLocationEnabled,
|
||||
_scopedKey(_prefKeyFastLocationEnabled),
|
||||
fastLocationUpdatesEnabled,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_prefKeyFastMovementThreshold,
|
||||
_scopedKey(_prefKeyFastMovementThreshold),
|
||||
fastLocationMovementThresholdMeters,
|
||||
);
|
||||
await prefs.setInt(
|
||||
_prefKeyFastActiveCadence,
|
||||
_scopedKey(_prefKeyFastActiveCadence),
|
||||
fastLocationActiveCadenceSeconds,
|
||||
);
|
||||
final channelKey = _scopedKey(_prefKeyFastChannelIdx);
|
||||
if (fastLocationChannelIdx == null) {
|
||||
await prefs.remove(channelKey);
|
||||
} else {
|
||||
await prefs.setInt(channelKey, fastLocationChannelIdx!);
|
||||
}
|
||||
|
||||
debugPrint('✅ [LocationTracking] Settings saved');
|
||||
}
|
||||
|
||||
@@ -41,90 +41,103 @@ class MapMarkerService {
|
||||
Function(Contact)? onTap,
|
||||
double mapRotation = 0,
|
||||
Position? userPosition,
|
||||
Map<String, LatLng>? estimatedLocations,
|
||||
}) {
|
||||
return contacts.map((contact) {
|
||||
final location = contact.displayLocation;
|
||||
if (location == null) return null;
|
||||
return contacts
|
||||
.map((contact) {
|
||||
final location = contact.displayLocation ??
|
||||
estimatedLocations?[contact.publicKeyHex];
|
||||
if (location == null) return null;
|
||||
|
||||
return Marker(
|
||||
point: location,
|
||||
width: 80,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
child: Transform.rotate(
|
||||
angle: -mapRotation * pi / 180,
|
||||
child: GestureDetector(
|
||||
onTap: onTap != null ? () => onTap(contact) : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Location update time indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: getLocationAgeColor(contact),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.timeSinceLocationUpdate,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Marker icon
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BoxShape.rectangle
|
||||
: BoxShape.circle,
|
||||
borderRadius: contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BorderRadius.circular(14)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
return Marker(
|
||||
point: location,
|
||||
width: 80,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
child: Transform.rotate(
|
||||
angle: -mapRotation * pi / 180,
|
||||
child: GestureDetector(
|
||||
onTap: onTap != null ? () => onTap(contact) : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Location update time indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getLocationAgeColor(contact),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.timeSinceLocationUpdate,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ContactAvatar(contact: contact, radius: 16),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Name label (without emoji)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.displayName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Marker icon
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape:
|
||||
contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BoxShape.rectangle
|
||||
: BoxShape.circle,
|
||||
borderRadius:
|
||||
contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BorderRadius.circular(14)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ContactAvatar(contact: contact, radius: 16),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Name label (without emoji)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.displayName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).whereType<Marker>().toList();
|
||||
);
|
||||
})
|
||||
.whereType<Marker>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Generate markers for SAR events.
|
||||
@@ -141,10 +154,11 @@ class MapMarkerService {
|
||||
required BuildContext context,
|
||||
Function(SarMarker)? onTap,
|
||||
double mapRotation = 0,
|
||||
LatLng Function(LatLng point)? pointTransformer,
|
||||
}) {
|
||||
return sarMarkers.map((marker) {
|
||||
return Marker(
|
||||
point: marker.location,
|
||||
point: pointTransformer?.call(marker.location) ?? marker.location,
|
||||
width: 90,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
@@ -157,7 +171,10 @@ class MapMarkerService {
|
||||
children: [
|
||||
// Time ago label
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getSarMarkerColor(marker.type),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
@@ -188,7 +205,7 @@ class MapMarkerService {
|
||||
),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Text(
|
||||
marker.emoji, // Use custom emoji if available
|
||||
marker.emoji, // Use custom emoji if available
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
@@ -196,13 +213,17 @@ class MapMarkerService {
|
||||
// Type label
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 90),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
marker.displayName, // Uses notes if available, otherwise type.displayName
|
||||
marker
|
||||
.displayName, // Uses notes if available, otherwise type.displayName
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
@@ -269,7 +290,8 @@ class MapMarkerService {
|
||||
final dLat = (lat2 - lat1) * pi / 180;
|
||||
final dLon = (lon2 - lon1) * pi / 180;
|
||||
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
final a =
|
||||
sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(lat1 * pi / 180) *
|
||||
cos(lat2 * pi / 180) *
|
||||
sin(dLon / 2) *
|
||||
@@ -299,8 +321,8 @@ class MapMarkerService {
|
||||
final lat2Rad = lat2 * pi / 180;
|
||||
|
||||
final y = sin(dLon) * cos(lat2Rad);
|
||||
final x = cos(lat1Rad) * sin(lat2Rad) -
|
||||
sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
||||
final x =
|
||||
cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
||||
|
||||
final bearing = atan2(y, x) * 180 / pi;
|
||||
return (bearing + 360) % 360;
|
||||
@@ -368,6 +390,8 @@ class MapMarkerService {
|
||||
return Colors.deepPurple; // Purple for repeaters
|
||||
case ContactType.room:
|
||||
return Colors.teal; // Teal for rooms
|
||||
case ContactType.sensor:
|
||||
return Colors.green; // Green for sensors
|
||||
case ContactType.channel:
|
||||
return Colors.orange; // Orange for channels
|
||||
case ContactType.none:
|
||||
@@ -389,6 +413,8 @@ class MapMarkerService {
|
||||
return Icons.router; // Router icon for repeaters
|
||||
case ContactType.room:
|
||||
return Icons.forum; // Forum/chat icon for rooms
|
||||
case ContactType.sensor:
|
||||
return Icons.sensors; // Sensors icon for sensor nodes
|
||||
case ContactType.channel:
|
||||
return Icons.public; // Public icon for channels
|
||||
case ContactType.none:
|
||||
@@ -466,7 +492,8 @@ class MapMarkerService {
|
||||
}
|
||||
|
||||
if (allPoints.isEmpty) {
|
||||
return defaultCenter ?? const LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||
return defaultCenter ??
|
||||
const LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||
}
|
||||
|
||||
double lat = 0, lng = 0;
|
||||
|
||||
99
lib/services/map_workspace_snapshot_service.dart
Normal file
99
lib/services/map_workspace_snapshot_service.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/drawing_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class MapWorkspaceSnapshotService {
|
||||
Future<MapWorkspaceProfileSection> capture({
|
||||
required MapProvider mapProvider,
|
||||
required DrawingProvider drawingProvider,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return MapWorkspaceProfileSection(
|
||||
mapPrefs: {
|
||||
'map_rotate_with_heading':
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
|
||||
) ??
|
||||
false,
|
||||
'map_show_debug_info':
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('map_show_debug_info'),
|
||||
) ??
|
||||
false,
|
||||
'map_fullscreen':
|
||||
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
|
||||
false,
|
||||
'map_last_latitude': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_latitude'),
|
||||
),
|
||||
'map_last_longitude': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_longitude'),
|
||||
),
|
||||
'map_last_zoom': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_zoom'),
|
||||
),
|
||||
'map_last_layer_type': prefs.getInt(
|
||||
ProfileStorageScope.scopedKey('map_last_layer_type'),
|
||||
),
|
||||
'map_gps_update_distance': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_gps_update_distance'),
|
||||
),
|
||||
'background_tracking_enabled': prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('background_tracking_enabled'),
|
||||
),
|
||||
},
|
||||
drawings: drawingProvider.exportDrawingsJson(),
|
||||
currentTrail: mapProvider.currentTrail?.toJson(),
|
||||
trailHistory: mapProvider.trailHistory
|
||||
.map((trail) => trail.toJson())
|
||||
.toList(),
|
||||
importedTrail: mapProvider.importedTrail?.toJson(),
|
||||
isTrailVisible: mapProvider.isTrailVisible,
|
||||
showCadastralOverlay: mapProvider.showCadastralOverlay,
|
||||
showForestRoadsOverlay: mapProvider.showForestRoadsOverlay,
|
||||
showHikingTrailsOverlay: mapProvider.showHikingTrailsOverlay,
|
||||
showMainRoadsOverlay: mapProvider.showMainRoadsOverlay,
|
||||
showHouseNumbersOverlay: mapProvider.showHouseNumbersOverlay,
|
||||
showFireHazardZonesOverlay: mapProvider.showFireHazardZonesOverlay,
|
||||
showHistoricalFiresOverlay: mapProvider.showHistoricalFiresOverlay,
|
||||
showFirebreaksOverlay: mapProvider.showFirebreaksOverlay,
|
||||
showKrasFireZonesOverlay: mapProvider.showKrasFireZonesOverlay,
|
||||
showPlaceNamesOverlay: mapProvider.showPlaceNamesOverlay,
|
||||
showMunicipalityBordersOverlay:
|
||||
mapProvider.showMunicipalityBordersOverlay,
|
||||
hideRepeatersOnMap: mapProvider.hideRepeatersOnMap,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
MapWorkspaceProfileSection? section, {
|
||||
required MapProvider mapProvider,
|
||||
required DrawingProvider drawingProvider,
|
||||
}) async {
|
||||
if (section == null || section.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final mapPrefs = section.mapPrefs ?? const <String, dynamic>{};
|
||||
for (final entry in mapPrefs.entries) {
|
||||
final key = ProfileStorageScope.scopedKey(entry.key);
|
||||
final value = entry.value;
|
||||
if (value == null) {
|
||||
await prefs.remove(key);
|
||||
} else if (value is bool) {
|
||||
await prefs.setBool(key, value);
|
||||
} else if (value is int) {
|
||||
await prefs.setInt(key, value);
|
||||
} else if (value is double) {
|
||||
await prefs.setDouble(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
mapProvider.applyWorkspaceJson(section.toJson());
|
||||
await drawingProvider.replaceDrawingsFromJson(section.drawings);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,14 @@ class MeshMapNode {
|
||||
required this.updatedAtMs,
|
||||
});
|
||||
|
||||
bool get hasValidCoordinates =>
|
||||
latitude >= -90 &&
|
||||
latitude <= 90 &&
|
||||
longitude >= -180 &&
|
||||
longitude <= 180 &&
|
||||
latitude != 0 &&
|
||||
longitude != 0;
|
||||
|
||||
factory MeshMapNode.fromJson(Map<String, dynamic> json) {
|
||||
return MeshMapNode(
|
||||
type: (json['type'] as num?)?.toInt() ?? 0,
|
||||
@@ -77,9 +85,7 @@ class MeshMapNodesService {
|
||||
final nodes = nodesRaw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(MeshMapNode.fromJson)
|
||||
.where(
|
||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
||||
)
|
||||
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||
.toList();
|
||||
|
||||
await _storeCache(nodes, cachedAt: now);
|
||||
@@ -116,9 +122,7 @@ class MeshMapNodesService {
|
||||
final nodes = decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(MeshMapNode.fromJson)
|
||||
.where(
|
||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
||||
)
|
||||
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||
.toList();
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = cachedAt;
|
||||
|
||||
@@ -5,6 +5,12 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
class MessageDestinationPreferences {
|
||||
static const String _destinationTypeKey = 'message_destination_type';
|
||||
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
||||
static const String _lockedDestinationEnabledKey =
|
||||
'message_locked_destination_enabled';
|
||||
static const String _lockedDestinationTypeKey =
|
||||
'message_locked_destination_type';
|
||||
static const String _lockedRecipientPublicKeyKey =
|
||||
'message_locked_recipient_public_key';
|
||||
|
||||
/// Destination types
|
||||
static const String destinationTypeAll = 'all';
|
||||
@@ -12,6 +18,10 @@ class MessageDestinationPreferences {
|
||||
static const String destinationTypeContact = 'contact';
|
||||
static const String destinationTypeRoom = 'room';
|
||||
|
||||
static bool isLockableDestinationType(String type) {
|
||||
return type == destinationTypeChannel || type == destinationTypeRoom;
|
||||
}
|
||||
|
||||
/// Get the saved destination configuration
|
||||
/// Returns a map with 'type' and optional 'publicKey'
|
||||
/// Returns null if no preference is saved (defaults to public channel)
|
||||
@@ -53,6 +63,53 @@ class MessageDestinationPreferences {
|
||||
await prefs.remove(_recipientPublicKeyKey);
|
||||
}
|
||||
|
||||
/// Get the saved locked destination configuration.
|
||||
/// Returns null when the lock is disabled.
|
||||
static Future<Map<String, String>?> getLockedDestination() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final isEnabled = prefs.getBool(_lockedDestinationEnabledKey) ?? false;
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final savedType =
|
||||
prefs.getString(_lockedDestinationTypeKey) ?? destinationTypeChannel;
|
||||
final type = isLockableDestinationType(savedType)
|
||||
? savedType
|
||||
: destinationTypeChannel;
|
||||
final publicKey = prefs.getString(_lockedRecipientPublicKeyKey);
|
||||
|
||||
return {'type': type, 'publicKey': ?publicKey};
|
||||
}
|
||||
|
||||
static Future<void> setLockedDestination({
|
||||
required bool enabled,
|
||||
String type = destinationTypeChannel,
|
||||
String? recipientPublicKey,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setBool(_lockedDestinationEnabledKey, enabled);
|
||||
|
||||
if (!enabled) {
|
||||
await prefs.remove(_lockedDestinationTypeKey);
|
||||
await prefs.remove(_lockedRecipientPublicKeyKey);
|
||||
return;
|
||||
}
|
||||
|
||||
final sanitizedType = isLockableDestinationType(type)
|
||||
? type
|
||||
: destinationTypeChannel;
|
||||
await prefs.setString(_lockedDestinationTypeKey, sanitizedType);
|
||||
|
||||
if (recipientPublicKey != null) {
|
||||
await prefs.setString(_lockedRecipientPublicKeyKey, recipientPublicKey);
|
||||
} else {
|
||||
await prefs.remove(_lockedRecipientPublicKeyKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get display name for destination type
|
||||
static String getDestinationTypeName(String type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../models/message_contact_location.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../models/message_transfer_details.dart';
|
||||
import '../models/message_route_metadata.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting messages to local storage
|
||||
@@ -24,6 +25,10 @@ class MessageStorageService {
|
||||
static const String _legacyPathBytesKey = 'storedPathBytes';
|
||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
||||
|
||||
String _key(String baseKey, {String? namespace}) {
|
||||
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
|
||||
}
|
||||
|
||||
/// Save messages to persistent storage
|
||||
Future<void> saveMessages(
|
||||
List<Message> messages, {
|
||||
@@ -31,6 +36,7 @@ class MessageStorageService {
|
||||
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
||||
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
||||
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -52,7 +58,7 @@ class MessageStorageService {
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_messagesKey, jsonString);
|
||||
final messagesKey = _key(_messagesKey, namespace: namespace);
|
||||
final retainedMessageIds = limitedList
|
||||
.map((entry) => entry['id'] as String)
|
||||
.toSet();
|
||||
@@ -80,22 +86,41 @@ class MessageStorageService {
|
||||
routeMetadataJson[entry.key] = entry.value.toJson();
|
||||
}
|
||||
}
|
||||
await prefs.setString(
|
||||
final contactLocationsKey = _key(
|
||||
_messageContactLocationsKey,
|
||||
jsonEncode(locationJson),
|
||||
namespace: namespace,
|
||||
);
|
||||
await prefs.setString(
|
||||
final receptionDetailsKey = _key(
|
||||
_messageReceptionDetailsKey,
|
||||
jsonEncode(receptionJson),
|
||||
namespace: namespace,
|
||||
);
|
||||
await prefs.setString(
|
||||
final transferDetailsKey = _key(
|
||||
_messageTransferDetailsKey,
|
||||
jsonEncode(transferJson),
|
||||
namespace: namespace,
|
||||
);
|
||||
await prefs.setString(
|
||||
final routeMetadataKey = _key(
|
||||
_messageRouteMetadataKey,
|
||||
jsonEncode(routeMetadataJson),
|
||||
namespace: namespace,
|
||||
);
|
||||
final locationJsonString = jsonEncode(locationJson);
|
||||
final receptionJsonString = jsonEncode(receptionJson);
|
||||
final transferJsonString = jsonEncode(transferJson);
|
||||
final routeMetadataJsonString = jsonEncode(routeMetadataJson);
|
||||
final hasChanges =
|
||||
prefs.getString(messagesKey) != jsonString ||
|
||||
prefs.getString(contactLocationsKey) != locationJsonString ||
|
||||
prefs.getString(receptionDetailsKey) != receptionJsonString ||
|
||||
prefs.getString(transferDetailsKey) != transferJsonString ||
|
||||
prefs.getString(routeMetadataKey) != routeMetadataJsonString;
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prefs.setString(messagesKey, jsonString);
|
||||
await prefs.setString(contactLocationsKey, locationJsonString);
|
||||
await prefs.setString(receptionDetailsKey, receptionJsonString);
|
||||
await prefs.setString(transferDetailsKey, transferJsonString);
|
||||
await prefs.setString(routeMetadataKey, routeMetadataJsonString);
|
||||
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Saved ${limitedList.length} messages to storage',
|
||||
@@ -105,11 +130,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageContactLocation>>
|
||||
loadMessageContactLocations() async {
|
||||
Future<Map<String, MessageContactLocation>> loadMessageContactLocations({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageContactLocationsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageContactLocationsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -135,11 +163,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageReceptionDetails>>
|
||||
loadMessageReceptionDetails() async {
|
||||
Future<Map<String, MessageReceptionDetails>> loadMessageReceptionDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageReceptionDetailsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||
);
|
||||
final result = <String, MessageReceptionDetails>{};
|
||||
if (jsonString != null && jsonString.isNotEmpty) {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
@@ -155,12 +186,16 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
final embeddedReceptionDetails = await _loadEmbeddedReceptionDetails();
|
||||
final embeddedReceptionDetails = await _loadEmbeddedReceptionDetails(
|
||||
namespace: namespace,
|
||||
);
|
||||
embeddedReceptionDetails.forEach((messageId, snapshot) {
|
||||
result.putIfAbsent(messageId, () => snapshot);
|
||||
});
|
||||
|
||||
final fallbackPathBytes = await _loadLegacyPathBytesFromMessages();
|
||||
final fallbackPathBytes = await _loadLegacyPathBytesFromMessages(
|
||||
namespace: namespace,
|
||||
);
|
||||
fallbackPathBytes.forEach((messageId, pathBytes) {
|
||||
result.putIfAbsent(
|
||||
messageId,
|
||||
@@ -177,11 +212,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageTransferDetails>>
|
||||
loadMessageTransferDetails() async {
|
||||
Future<Map<String, MessageTransferDetails>> loadMessageTransferDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageTransferDetailsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -207,10 +245,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata() async {
|
||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageRouteMetadataKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageRouteMetadataKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -234,10 +276,12 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Load messages from persistent storage
|
||||
Future<List<Message>> loadMessages() async {
|
||||
Future<List<Message>> loadMessages({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
debugPrint('ℹ️ [MessageStorage] No stored messages found');
|
||||
@@ -262,25 +306,35 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Clear all stored messages
|
||||
Future<void> clearMessages() async {
|
||||
Future<void> clearMessages({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_messagesKey);
|
||||
await prefs.remove(_messageContactLocationsKey);
|
||||
await prefs.remove(_messageReceptionDetailsKey);
|
||||
await prefs.remove(_messageTransferDetailsKey);
|
||||
await prefs.remove(_messageRouteMetadataKey);
|
||||
await prefs.remove(_removedSarMarkerIdsKey);
|
||||
await prefs.remove(_key(_messagesKey, namespace: namespace));
|
||||
await prefs.remove(
|
||||
_key(_messageContactLocationsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(
|
||||
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(
|
||||
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(_key(_messageRouteMetadataKey, namespace: namespace));
|
||||
await prefs.remove(_key(_removedSarMarkerIdsKey, namespace: namespace));
|
||||
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error clearing messages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>> loadRemovedSarMarkerIds() async {
|
||||
Future<Set<String>> loadRemovedSarMarkerIds({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final ids = prefs.getStringList(_removedSarMarkerIdsKey) ?? const [];
|
||||
final ids =
|
||||
prefs.getStringList(
|
||||
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||
) ??
|
||||
const [];
|
||||
return ids.toSet();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error loading removed SAR marker IDs: $e');
|
||||
@@ -288,10 +342,16 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveRemovedSarMarkerIds(Set<String> ids) async {
|
||||
Future<void> saveRemovedSarMarkerIds(
|
||||
Set<String> ids, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(_removedSarMarkerIdsKey, ids.toList()..sort());
|
||||
await prefs.setStringList(
|
||||
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||
ids.toList()..sort(),
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Saved ${ids.length} removed SAR marker IDs',
|
||||
);
|
||||
@@ -301,10 +361,12 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
@@ -346,6 +408,9 @@ class MessageStorageService {
|
||||
'isSarMarker': message.isSarMarker,
|
||||
'sarGpsLat': message.sarGpsCoordinates?.latitude,
|
||||
'sarGpsLon': message.sarGpsCoordinates?.longitude,
|
||||
'sarCustomMapLat': message.sarCustomMapPoint?.latitude,
|
||||
'sarCustomMapLon': message.sarCustomMapPoint?.longitude,
|
||||
'sarCustomMapId': message.sarCustomMapId,
|
||||
'sarNotes': message.sarNotes,
|
||||
'sarCustomEmoji': message.sarCustomEmoji,
|
||||
'sarColorIndex': message.sarColorIndex,
|
||||
@@ -398,10 +463,13 @@ class MessageStorageService {
|
||||
};
|
||||
}
|
||||
|
||||
Future<Map<String, MessageReceptionDetails>>
|
||||
_loadEmbeddedReceptionDetails() async {
|
||||
Future<Map<String, MessageReceptionDetails>> _loadEmbeddedReceptionDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -424,9 +492,13 @@ class MessageStorageService {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<String, List<int>>> _loadLegacyPathBytesFromMessages() async {
|
||||
Future<Map<String, List<int>>> _loadLegacyPathBytesFromMessages({
|
||||
String? namespace,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -482,8 +554,19 @@ class MessageStorageService {
|
||||
isSarMarker: json['isSarMarker'] as bool? ?? false,
|
||||
sarGpsCoordinates:
|
||||
json['sarGpsLat'] != null && json['sarGpsLon'] != null
|
||||
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
|
||||
? LatLng(
|
||||
(json['sarGpsLat'] as num).toDouble(),
|
||||
(json['sarGpsLon'] as num).toDouble(),
|
||||
)
|
||||
: null,
|
||||
sarCustomMapPoint:
|
||||
json['sarCustomMapLat'] != null && json['sarCustomMapLon'] != null
|
||||
? LatLng(
|
||||
(json['sarCustomMapLat'] as num).toDouble(),
|
||||
(json['sarCustomMapLon'] as num).toDouble(),
|
||||
)
|
||||
: null,
|
||||
sarCustomMapId: json['sarCustomMapId'] as String?,
|
||||
sarNotes: json['sarNotes'] as String?,
|
||||
sarCustomEmoji: json['sarCustomEmoji'] as String?,
|
||||
sarColorIndex: json['sarColorIndex'] as int?,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class MessagingRoutePreferences {
|
||||
static const bool defaultAutoRouteRotationEnabled = false;
|
||||
@@ -14,33 +15,49 @@ class MessagingRoutePreferences {
|
||||
|
||||
static Future<bool> getAutoRouteRotationEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_autoRouteRotationKey) ??
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||
) ??
|
||||
defaultAutoRouteRotationEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setAutoRouteRotationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_autoRouteRotationKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getClearPathOnMaxRetry() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_clearPathOnMaxRetryKey) ?? defaultClearPathOnMaxRetry;
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||
) ??
|
||||
defaultClearPathOnMaxRetry;
|
||||
}
|
||||
|
||||
static Future<void> setClearPathOnMaxRetry(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getNearestRelayFallbackEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_nearestRelayFallbackKey) ??
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_nearestRelayFallbackKey),
|
||||
) ??
|
||||
defaultNearestRelayFallbackEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_nearestRelayFallbackKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_nearestRelayFallbackKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class NotificationService {
|
||||
FlutterLocalNotificationsPlugin();
|
||||
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
|
||||
static const String _prefSarEnabled = 'notifications_sar_enabled';
|
||||
static const String _prefDiscoveryEnabled = 'notifications_discovery_enabled';
|
||||
static const String _prefUpdatesEnabled = 'notifications_updates_enabled';
|
||||
static const String _prefMuteForeground = 'notifications_mute_foreground';
|
||||
|
||||
@@ -23,6 +24,7 @@ class NotificationService {
|
||||
bool _permissionGranted = false;
|
||||
bool _messageNotificationsEnabled = true;
|
||||
bool _sarNotificationsEnabled = true;
|
||||
bool _discoveryNotificationsEnabled = true;
|
||||
bool _updateNotificationsEnabled = true;
|
||||
bool _muteForegroundNotifications = true;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
@@ -63,6 +65,7 @@ class NotificationService {
|
||||
|
||||
bool get messageNotificationsEnabled => _messageNotificationsEnabled;
|
||||
bool get sarNotificationsEnabled => _sarNotificationsEnabled;
|
||||
bool get discoveryNotificationsEnabled => _discoveryNotificationsEnabled;
|
||||
bool get updateNotificationsEnabled => _updateNotificationsEnabled;
|
||||
bool get muteForegroundNotifications => _muteForegroundNotifications;
|
||||
bool get isAppInForeground => _lifecycleState == AppLifecycleState.resumed;
|
||||
@@ -175,6 +178,8 @@ class NotificationService {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_messageNotificationsEnabled = prefs.getBool(_prefMessagesEnabled) ?? true;
|
||||
_sarNotificationsEnabled = prefs.getBool(_prefSarEnabled) ?? true;
|
||||
_discoveryNotificationsEnabled =
|
||||
prefs.getBool(_prefDiscoveryEnabled) ?? true;
|
||||
_updateNotificationsEnabled = prefs.getBool(_prefUpdatesEnabled) ?? true;
|
||||
_muteForegroundNotifications = prefs.getBool(_prefMuteForeground) ?? true;
|
||||
}
|
||||
@@ -191,6 +196,12 @@ class NotificationService {
|
||||
await prefs.setBool(_prefSarEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setDiscoveryNotificationsEnabled(bool value) async {
|
||||
_discoveryNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefDiscoveryEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setUpdateNotificationsEnabled(bool value) async {
|
||||
_updateNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -854,7 +865,7 @@ class NotificationService {
|
||||
}) async {
|
||||
if (!_isInitialized) return false;
|
||||
if (!_permissionGranted) return false;
|
||||
if (!_messageNotificationsEnabled) return false;
|
||||
if (!_discoveryNotificationsEnabled) return false;
|
||||
if (_shouldSuppressForegroundNotifications()) return false;
|
||||
|
||||
final shortKey = contactKey.length > 12
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
@@ -9,35 +10,60 @@ import '../models/path_selection.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
class PathHistoryService {
|
||||
static const String _storageKey = 'contact_path_history_v1';
|
||||
static const String _storageKey = 'contact_path_history_v2';
|
||||
static const String _suppressedRouteStorageKey =
|
||||
'contact_path_history_suppressed_routes_v1';
|
||||
static const int _maxDirectPaths = 20;
|
||||
static const int _topRotationCount = 3;
|
||||
|
||||
final Map<String, ContactPathHistory> _cache = {};
|
||||
final Map<String, String> _suppressedCurrentRoutes = {};
|
||||
bool _isLoaded = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isLoaded) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
final suppressedRaw = prefs.getString(_suppressedRouteStorageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
_isLoaded = true;
|
||||
return;
|
||||
if (suppressedRaw == null || suppressedRaw.isEmpty) {
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is Map<String, dynamic>) {
|
||||
_cache[entry.key] = ContactPathHistory.fromJson(entry.key, value);
|
||||
if (raw != null && raw.isNotEmpty) {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is Map<String, dynamic>) {
|
||||
_cache[entry.key] = ContactPathHistory.fromJson(entry.key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('⚠️ [PathHistoryService] Failed to load history: $error');
|
||||
}
|
||||
try {
|
||||
if (suppressedRaw != null && suppressedRaw.isNotEmpty) {
|
||||
final decoded = jsonDecode(suppressedRaw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is String && value.isNotEmpty) {
|
||||
_suppressedCurrentRoutes[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'⚠️ [PathHistoryService] Failed to load suppressed routes: $error',
|
||||
);
|
||||
}
|
||||
_isLoaded = true;
|
||||
}
|
||||
|
||||
@@ -49,6 +75,9 @@ class PathHistoryService {
|
||||
|
||||
final history = _historyFor(contact.publicKeyHex);
|
||||
final signature = _signature(contact.routePathBytes);
|
||||
if (_suppressedCurrentRoutes[contact.publicKeyHex] == signature) {
|
||||
return;
|
||||
}
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: contact.routePathBytes.toList(),
|
||||
@@ -59,6 +88,11 @@ class PathHistoryService {
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
lastUsedAt: DateTime.now(),
|
||||
lastSucceededAt: existing?.lastSucceededAt,
|
||||
senderLatitude: existing?.senderLatitude,
|
||||
senderLongitude: existing?.senderLongitude,
|
||||
recipientLatitude: existing?.recipientLatitude,
|
||||
recipientLongitude: existing?.recipientLongitude,
|
||||
);
|
||||
|
||||
await _saveHistory(
|
||||
@@ -94,6 +128,10 @@ class PathHistoryService {
|
||||
final signature = normalizedPathBytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
_clearSuppressedRoute(
|
||||
contactPublicKeyHex,
|
||||
signature: signature,
|
||||
);
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: normalizedPathBytes,
|
||||
@@ -104,6 +142,11 @@ class PathHistoryService {
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
lastUsedAt: DateTime.now(),
|
||||
lastSucceededAt: existing?.lastSucceededAt,
|
||||
senderLatitude: existing?.senderLatitude,
|
||||
senderLongitude: existing?.senderLongitude,
|
||||
recipientLatitude: existing?.recipientLatitude,
|
||||
recipientLongitude: existing?.recipientLongitude,
|
||||
);
|
||||
|
||||
await _saveHistory(
|
||||
@@ -172,6 +215,10 @@ class PathHistoryService {
|
||||
PathSelection selection, {
|
||||
required bool success,
|
||||
int? roundTripTimeMs,
|
||||
double? senderLatitude,
|
||||
double? senderLongitude,
|
||||
double? recipientLatitude,
|
||||
double? recipientLongitude,
|
||||
}) async {
|
||||
await initialize();
|
||||
final history = _historyFor(contactPublicKeyHex);
|
||||
@@ -194,18 +241,31 @@ class PathHistoryService {
|
||||
}
|
||||
|
||||
final signature = _signature(selection.pathBytes);
|
||||
_clearSuppressedRoute(
|
||||
contactPublicKeyHex,
|
||||
signature: signature,
|
||||
);
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: selection.pathBytes.toList(),
|
||||
hopCount: selection.hopCount,
|
||||
hashSize: selection.hashSize,
|
||||
source: existing?.source ?? PathRecordSource.learned,
|
||||
source: success
|
||||
? PathRecordSource.learned
|
||||
: existing?.source ?? PathRecordSource.learned,
|
||||
successCount: (existing?.successCount ?? 0) + (success ? 1 : 0),
|
||||
failureCount: (existing?.failureCount ?? 0) + (success ? 0 : 1),
|
||||
lastRoundTripTimeMs: success
|
||||
? (roundTripTimeMs ?? existing?.lastRoundTripTimeMs ?? 0)
|
||||
: (existing?.lastRoundTripTimeMs ?? 0),
|
||||
lastUsedAt: DateTime.now(),
|
||||
lastSucceededAt: success ? DateTime.now() : existing?.lastSucceededAt,
|
||||
senderLatitude: success ? senderLatitude : existing?.senderLatitude,
|
||||
senderLongitude: success ? senderLongitude : existing?.senderLongitude,
|
||||
recipientLatitude:
|
||||
success ? recipientLatitude : existing?.recipientLatitude,
|
||||
recipientLongitude:
|
||||
success ? recipientLongitude : existing?.recipientLongitude,
|
||||
);
|
||||
await _saveHistory(
|
||||
contactPublicKeyHex,
|
||||
@@ -215,11 +275,79 @@ class PathHistoryService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<PathSelection?> getLastSuccessfulDirectSelection(
|
||||
Contact contact, {
|
||||
String? excludeSignature,
|
||||
double? senderLatitude,
|
||||
double? senderLongitude,
|
||||
double? recipientLatitude,
|
||||
double? recipientLongitude,
|
||||
}) async {
|
||||
await initialize();
|
||||
final history = _historyFor(contact.publicKeyHex);
|
||||
final ranked = history.directPaths
|
||||
.where(
|
||||
(record) =>
|
||||
record.successCount > 0 &&
|
||||
record.lastSucceededAt != null &&
|
||||
record.signature != excludeSignature,
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) {
|
||||
final locationCompare = _compareLocationFit(
|
||||
a,
|
||||
b,
|
||||
senderLatitude: senderLatitude,
|
||||
senderLongitude: senderLongitude,
|
||||
recipientLatitude: recipientLatitude,
|
||||
recipientLongitude: recipientLongitude,
|
||||
);
|
||||
if (locationCompare != 0) return locationCompare;
|
||||
final succeededCompare = b.lastSucceededAt!.compareTo(
|
||||
a.lastSucceededAt!,
|
||||
);
|
||||
if (succeededCompare != 0) return succeededCompare;
|
||||
return _comparePathRecords(a, b);
|
||||
});
|
||||
|
||||
if (ranked.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final record = ranked.first;
|
||||
return PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList(record.pathBytes),
|
||||
hopCount: record.hopCount,
|
||||
hashSize: record.hashSize,
|
||||
);
|
||||
}
|
||||
|
||||
ContactPathHistory historyFor(String contactPublicKeyHex) {
|
||||
return _cache[contactPublicKeyHex] ??
|
||||
ContactPathHistory.empty(contactPublicKeyHex);
|
||||
}
|
||||
|
||||
Future<void> clearHistoryFor(String contactPublicKeyHex) async {
|
||||
await initialize();
|
||||
_cache.remove(contactPublicKeyHex);
|
||||
_suppressedCurrentRoutes.remove(contactPublicKeyHex);
|
||||
await _persistState();
|
||||
}
|
||||
|
||||
Future<void> clearHistoryForContact(Contact contact) async {
|
||||
await initialize();
|
||||
_cache.remove(contact.publicKeyHex);
|
||||
if (contact.routeHasPath && contact.routeHopCount > 0) {
|
||||
_suppressedCurrentRoutes[contact.publicKeyHex] = _signature(
|
||||
contact.routePathBytes,
|
||||
);
|
||||
} else {
|
||||
_suppressedCurrentRoutes.remove(contact.publicKeyHex);
|
||||
}
|
||||
await _persistState();
|
||||
}
|
||||
|
||||
ContactPathHistory _historyFor(String contactPublicKeyHex) {
|
||||
return _cache.putIfAbsent(
|
||||
contactPublicKeyHex,
|
||||
@@ -232,12 +360,34 @@ class PathHistoryService {
|
||||
ContactPathHistory history,
|
||||
) async {
|
||||
_cache[contactPublicKeyHex] = history;
|
||||
await _persistState();
|
||||
}
|
||||
|
||||
void _clearSuppressedRoute(String contactPublicKeyHex, {String? signature}) {
|
||||
final suppressedSignature = _suppressedCurrentRoutes[contactPublicKeyHex];
|
||||
if (suppressedSignature == null) {
|
||||
return;
|
||||
}
|
||||
if (signature == null || suppressedSignature == signature) {
|
||||
_suppressedCurrentRoutes.remove(contactPublicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final payload = <String, dynamic>{};
|
||||
for (final entry in _cache.entries) {
|
||||
payload[entry.key] = entry.value.toJson();
|
||||
}
|
||||
final suppressedPayload = <String, dynamic>{};
|
||||
for (final entry in _suppressedCurrentRoutes.entries) {
|
||||
suppressedPayload[entry.key] = entry.value;
|
||||
}
|
||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
||||
await prefs.setString(
|
||||
_suppressedRouteStorageKey,
|
||||
jsonEncode(suppressedPayload),
|
||||
);
|
||||
}
|
||||
|
||||
List<PathRecord> _upsertDirectPath(
|
||||
@@ -279,4 +429,68 @@ class PathHistoryService {
|
||||
|
||||
String _signature(Uint8List bytes) =>
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
int _compareLocationFit(
|
||||
PathRecord a,
|
||||
PathRecord b, {
|
||||
required double? senderLatitude,
|
||||
required double? senderLongitude,
|
||||
required double? recipientLatitude,
|
||||
required double? recipientLongitude,
|
||||
}) {
|
||||
final aDistance = _locationDistanceScore(
|
||||
a,
|
||||
senderLatitude: senderLatitude,
|
||||
senderLongitude: senderLongitude,
|
||||
recipientLatitude: recipientLatitude,
|
||||
recipientLongitude: recipientLongitude,
|
||||
);
|
||||
final bDistance = _locationDistanceScore(
|
||||
b,
|
||||
senderLatitude: senderLatitude,
|
||||
senderLongitude: senderLongitude,
|
||||
recipientLatitude: recipientLatitude,
|
||||
recipientLongitude: recipientLongitude,
|
||||
);
|
||||
return aDistance.compareTo(bDistance);
|
||||
}
|
||||
|
||||
double _locationDistanceScore(
|
||||
PathRecord record, {
|
||||
required double? senderLatitude,
|
||||
required double? senderLongitude,
|
||||
required double? recipientLatitude,
|
||||
required double? recipientLongitude,
|
||||
}) {
|
||||
var total = 0.0;
|
||||
var matched = false;
|
||||
|
||||
if (senderLatitude != null &&
|
||||
senderLongitude != null &&
|
||||
record.senderLatitude != null &&
|
||||
record.senderLongitude != null) {
|
||||
matched = true;
|
||||
total += Geolocator.distanceBetween(
|
||||
senderLatitude,
|
||||
senderLongitude,
|
||||
record.senderLatitude!,
|
||||
record.senderLongitude!,
|
||||
);
|
||||
}
|
||||
|
||||
if (recipientLatitude != null &&
|
||||
recipientLongitude != null &&
|
||||
record.recipientLatitude != null &&
|
||||
record.recipientLongitude != null) {
|
||||
matched = true;
|
||||
total += Geolocator.distanceBetween(
|
||||
recipientLatitude,
|
||||
recipientLongitude,
|
||||
record.recipientLatitude!,
|
||||
record.recipientLongitude!,
|
||||
);
|
||||
}
|
||||
|
||||
return matched ? total : double.infinity;
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/services/profile_device_key_resolver.dart
Normal file
21
lib/services/profile_device_key_resolver.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import '../models/device_info.dart';
|
||||
|
||||
class ProfileDeviceKeyResolver {
|
||||
static String? resolve({
|
||||
required DeviceInfo deviceInfo,
|
||||
required ConnectionMode connectionMode,
|
||||
}) {
|
||||
// Always prefer the public key — it's the only truly stable identifier.
|
||||
// Don't fall back to deviceId to avoid creating duplicate profiles when
|
||||
// publicKey arrives late (after initial connection but before DeviceInfo).
|
||||
final publicKey = deviceInfo.publicKey;
|
||||
if (publicKey == null || publicKey.isEmpty) {
|
||||
return null; // Wait until publicKey is available
|
||||
}
|
||||
|
||||
final hex = publicKey
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
return hex.isNotEmpty ? 'pk:$hex' : null;
|
||||
}
|
||||
}
|
||||
185
lib/services/profile_manager.dart
Normal file
185
lib/services/profile_manager.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class ProfileManager with ChangeNotifier {
|
||||
static const String _profilesKey = 'profiles_library';
|
||||
static const String activeProfileIdKey = 'profiles_active_profile_id';
|
||||
static const String _deviceProfileDefaultsKey =
|
||||
'profiles_device_active_profile_ids';
|
||||
static const String _transferHistoryKey = 'profiles_transfer_history';
|
||||
|
||||
final List<ConfigProfile> _customProfiles = <ConfigProfile>[];
|
||||
final List<ProfileTransferRecord> _transferHistory =
|
||||
<ProfileTransferRecord>[];
|
||||
final Map<String, String> _deviceProfileDefaults = <String, String>{};
|
||||
bool _isInitialized = false;
|
||||
bool _profilesEnabled = false;
|
||||
String _activeProfileId = ConfigProfile.defaultProfileId;
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get profilesEnabled => _profilesEnabled;
|
||||
String get activeProfileId => _activeProfileId;
|
||||
List<ConfigProfile> get customProfiles => List.unmodifiable(_customProfiles);
|
||||
List<ProfileTransferRecord> get transferHistory =>
|
||||
List.unmodifiable(_transferHistory);
|
||||
|
||||
List<ConfigProfile> get visibleProfiles => [
|
||||
if (_profilesEnabled) ConfigProfile.defaultProfile(),
|
||||
..._customProfiles,
|
||||
];
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
_activeProfileId =
|
||||
prefs.getString(activeProfileIdKey) ?? ConfigProfile.defaultProfileId;
|
||||
|
||||
final deviceDefaultsJson = prefs.getString(_deviceProfileDefaultsKey);
|
||||
if (deviceDefaultsJson != null && deviceDefaultsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(deviceDefaultsJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
_deviceProfileDefaults
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.map((key, value) => MapEntry(key, value?.toString() ?? ''))
|
||||
..removeWhere((key, value) => value.isEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final profilesJson = prefs.getString(_profilesKey);
|
||||
if (profilesJson != null && profilesJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(profilesJson) as List<dynamic>;
|
||||
_customProfiles
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(ConfigProfile.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
final historyJson = prefs.getString(_transferHistoryKey);
|
||||
if (historyJson != null && historyJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(historyJson) as List<dynamic>;
|
||||
_transferHistory
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(
|
||||
ProfileTransferRecord.fromJson,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ConfigProfile? getProfile(String id) {
|
||||
if (id == ConfigProfile.defaultProfileId) {
|
||||
return ConfigProfile.defaultProfile();
|
||||
}
|
||||
for (final profile in _customProfiles) {
|
||||
if (profile.id == id) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> setProfilesEnabled(bool enabled) async {
|
||||
_profilesEnabled = enabled;
|
||||
await ProfilesFeatureService.setEnabled(enabled);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileId(String id) async {
|
||||
await setActiveProfileIdForDevice(id);
|
||||
}
|
||||
|
||||
String profileIdForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return _activeProfileId;
|
||||
}
|
||||
return _deviceProfileDefaults[deviceKey] ?? ConfigProfile.defaultProfileId;
|
||||
}
|
||||
|
||||
bool hasProfileForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return _deviceProfileDefaults.containsKey(deviceKey);
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileIdForDevice(
|
||||
String id, {
|
||||
String? deviceKey,
|
||||
}) async {
|
||||
_activeProfileId = id;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(activeProfileIdKey, id);
|
||||
if (deviceKey != null && deviceKey.isNotEmpty) {
|
||||
_deviceProfileDefaults[deviceKey] = id;
|
||||
await prefs.setString(
|
||||
_deviceProfileDefaultsKey,
|
||||
jsonEncode(_deviceProfileDefaults),
|
||||
);
|
||||
}
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> upsertProfile(ConfigProfile profile) async {
|
||||
final index = _customProfiles.indexWhere((item) => item.id == profile.id);
|
||||
if (index == -1) {
|
||||
_customProfiles.add(profile);
|
||||
} else {
|
||||
_customProfiles[index] = profile;
|
||||
}
|
||||
_customProfiles.sort((a, b) => a.name.compareTo(b.name));
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteProfile(String id) async {
|
||||
_customProfiles.removeWhere((profile) => profile.id == id);
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> recordTransfer(ProfileTransferRecord record) async {
|
||||
_transferHistory.insert(0, record);
|
||||
if (_transferHistory.length > 100) {
|
||||
_transferHistory.removeRange(100, _transferHistory.length);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_transferHistoryKey,
|
||||
jsonEncode(_transferHistory.map((item) => item.toJson()).toList()),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistProfiles() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_profilesKey,
|
||||
jsonEncode(_customProfiles.map((profile) => profile.toJson()).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/services/profile_transport.dart
Normal file
10
lib/services/profile_transport.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
abstract class ProfileTransport {
|
||||
String get label;
|
||||
}
|
||||
|
||||
class FileProfileTransport implements ProfileTransport {
|
||||
const FileProfileTransport();
|
||||
|
||||
@override
|
||||
String get label => 'File';
|
||||
}
|
||||
499
lib/services/profile_workspace_coordinator.dart
Normal file
499
lib/services/profile_workspace_coordinator.dart
Normal file
@@ -0,0 +1,499 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/drawing_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../providers/voice_provider.dart';
|
||||
import '../providers/image_provider.dart' as ip;
|
||||
import 'app_config_snapshot_service.dart';
|
||||
import 'contact_storage_service.dart';
|
||||
import 'device_config_applicator.dart';
|
||||
import 'message_storage_service.dart';
|
||||
import 'profile_manager.dart';
|
||||
import 'profile_device_key_resolver.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'map_workspace_snapshot_service.dart';
|
||||
|
||||
class ProfileWorkspaceCoordinator {
|
||||
ProfileWorkspaceCoordinator({
|
||||
required this.profileManager,
|
||||
required this.connectionProvider,
|
||||
required this.contactsProvider,
|
||||
required this.messagesProvider,
|
||||
required this.sensorsProvider,
|
||||
required this.mapProvider,
|
||||
required this.drawingProvider,
|
||||
required this.channelsProvider,
|
||||
required this.voiceProvider,
|
||||
required this.imageProvider,
|
||||
required this.appProvider,
|
||||
AppConfigSnapshotService? appConfigSnapshotService,
|
||||
MapWorkspaceSnapshotService? mapWorkspaceSnapshotService,
|
||||
DeviceConfigApplicator? deviceConfigApplicator,
|
||||
MessageStorageService? messageStorageService,
|
||||
ContactStorageService? contactStorageService,
|
||||
}) : _appConfigSnapshotService =
|
||||
appConfigSnapshotService ?? AppConfigSnapshotService(),
|
||||
_mapWorkspaceSnapshotService =
|
||||
mapWorkspaceSnapshotService ?? MapWorkspaceSnapshotService(),
|
||||
_deviceConfigApplicator =
|
||||
deviceConfigApplicator ?? DeviceConfigApplicator(),
|
||||
_messageStorageService =
|
||||
messageStorageService ?? MessageStorageService(),
|
||||
_contactStorageService =
|
||||
contactStorageService ?? ContactStorageService();
|
||||
|
||||
final ProfileManager profileManager;
|
||||
final ConnectionProvider connectionProvider;
|
||||
final ContactsProvider contactsProvider;
|
||||
final MessagesProvider messagesProvider;
|
||||
final SensorsProvider sensorsProvider;
|
||||
final MapProvider mapProvider;
|
||||
final DrawingProvider drawingProvider;
|
||||
final ChannelsProvider channelsProvider;
|
||||
final VoiceProvider voiceProvider;
|
||||
final ip.ImageProvider imageProvider;
|
||||
final AppProvider appProvider;
|
||||
final AppConfigSnapshotService _appConfigSnapshotService;
|
||||
final MapWorkspaceSnapshotService _mapWorkspaceSnapshotService;
|
||||
final DeviceConfigApplicator _deviceConfigApplicator;
|
||||
final MessageStorageService _messageStorageService;
|
||||
final ContactStorageService _contactStorageService;
|
||||
Future<void>? _deviceProfileSyncFuture;
|
||||
|
||||
Future<void> setProfilesEnabled(bool enabled) async {
|
||||
final wasEnabled = profileManager.profilesEnabled;
|
||||
if (wasEnabled && !enabled) {
|
||||
await _saveActiveCustomProfileSnapshot();
|
||||
} else {
|
||||
await _persistCurrentState();
|
||||
}
|
||||
await profileManager.setProfilesEnabled(enabled);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: enabled,
|
||||
activeProfileId: enabled ? profileManager.activeProfileId : 'default',
|
||||
);
|
||||
if (enabled) {
|
||||
await _ensureProfileForCurrentDevice();
|
||||
if (wasEnabled) {
|
||||
await openProfile(profileManager.activeProfileId);
|
||||
} else {
|
||||
await _switchRuntimeScope(profileManager.activeProfileId);
|
||||
final profile = await resolveProfile(profileManager.activeProfileId);
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _switchRuntimeScope('default');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConfigProfile> snapshotCurrentProfile({
|
||||
required String id,
|
||||
required String name,
|
||||
String? notes,
|
||||
}) async {
|
||||
final deviceSections = _deviceConfigApplicator.capture(
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
final appSettings = await _appConfigSnapshotService.capture(appProvider);
|
||||
final mapWorkspace = await _mapWorkspaceSnapshotService.capture(
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
return ConfigProfile(
|
||||
id: id,
|
||||
name: name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
notes: notes,
|
||||
sections: ConfigProfileSections(
|
||||
deviceConfig: deviceSections.deviceConfig,
|
||||
channels: deviceSections.channels,
|
||||
appSettings: appSettings,
|
||||
mapWorkspace: mapWorkspace,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> createProfileFromCurrent({
|
||||
required String name,
|
||||
String? notes,
|
||||
}) async {
|
||||
final profileId = 'profile_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final profile = await snapshotCurrentProfile(
|
||||
id: profileId,
|
||||
name: name,
|
||||
notes: notes,
|
||||
);
|
||||
await _initializeEmptyStorageNamespace(profileId);
|
||||
await profileManager.upsertProfile(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
Future<ConfigProfile> duplicateProfile(ConfigProfile source) async {
|
||||
final duplicate = await snapshotCurrentProfile(
|
||||
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '${source.name} Copy',
|
||||
notes: source.notes,
|
||||
);
|
||||
final sourceNamespace = source.id == ConfigProfile.defaultProfileId
|
||||
? null
|
||||
: source.id;
|
||||
await _copyStorageNamespace(sourceNamespace, duplicate.id);
|
||||
await profileManager.upsertProfile(
|
||||
duplicate.copyWith(
|
||||
sections: source.id == profileManager.activeProfileId
|
||||
? duplicate.sections
|
||||
: source.sections,
|
||||
),
|
||||
);
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
Future<void> renameProfile(ConfigProfile profile, String name) async {
|
||||
await profileManager.upsertProfile(
|
||||
profile.copyWith(name: name, updatedAt: DateTime.now()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteProfile(ConfigProfile profile) async {
|
||||
if (profile.isDefault) return;
|
||||
if (profile.id == profileManager.activeProfileId) {
|
||||
await openProfile(ConfigProfile.defaultProfileId);
|
||||
}
|
||||
await _messageStorageService.clearMessages(namespace: profile.id);
|
||||
await _contactStorageService.clearContacts(namespace: profile.id);
|
||||
await _contactStorageService.clearContactGroups(namespace: profile.id);
|
||||
await _contactStorageService.clearPendingAdverts(namespace: profile.id);
|
||||
await profileManager.deleteProfile(profile.id);
|
||||
}
|
||||
|
||||
Future<void> openProfile(String profileId) async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
await _saveActiveCustomProfileSnapshot();
|
||||
await connectionProvider.disconnect();
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
profileId,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
await _switchRuntimeScope(profileId);
|
||||
|
||||
final profile = await resolveProfile(profileId);
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> applyProfile(String profileId) async {
|
||||
await openProfile(profileId);
|
||||
final profile = await resolveProfile(profileId);
|
||||
await _deviceConfigApplicator.apply(
|
||||
profile,
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> resolveProfile(String profileId) async {
|
||||
if (profileId == ConfigProfile.defaultProfileId) {
|
||||
return snapshotCurrentProfile(id: profileId, name: 'Default');
|
||||
}
|
||||
return profileManager.getProfile(profileId) ??
|
||||
await snapshotCurrentProfile(id: profileId, name: 'Unknown');
|
||||
}
|
||||
|
||||
Future<void> exportProfile(ConfigProfile profile) async {
|
||||
final resolved = profile.id == ConfigProfile.defaultProfileId
|
||||
? await snapshotCurrentProfile(id: profile.id, name: profile.name)
|
||||
: profile;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File(
|
||||
'${tempDir.path}/${resolved.name.replaceAll(' ', '_').toLowerCase()}_${resolved.id}.meshcore_profile.json',
|
||||
);
|
||||
await file.writeAsString(
|
||||
const JsonEncoder.withIndent(' ').convert(resolved.toJson()),
|
||||
);
|
||||
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
||||
await profileManager.recordTransfer(
|
||||
ProfileTransferRecord(
|
||||
profileId: resolved.id,
|
||||
direction: 'export',
|
||||
timestamp: DateTime.now(),
|
||||
detail: 'Shared as file',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile?> importProfileFromFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
withData: true,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final file = result.files.first;
|
||||
if (file.bytes == null) return null;
|
||||
|
||||
final decoded = jsonDecode(utf8.decode(file.bytes!));
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final imported = ConfigProfile.fromJson(decoded).copyWith(
|
||||
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
await profileManager.upsertProfile(imported);
|
||||
await profileManager.recordTransfer(
|
||||
ProfileTransferRecord(
|
||||
profileId: imported.id,
|
||||
direction: 'import',
|
||||
timestamp: DateTime.now(),
|
||||
detail: file.name,
|
||||
),
|
||||
);
|
||||
return imported;
|
||||
}
|
||||
|
||||
Future<void> syncActiveProfileForCurrentDevice() async {
|
||||
if (!profileManager.profilesEnabled) {
|
||||
return;
|
||||
}
|
||||
final inFlightSync = _deviceProfileSyncFuture;
|
||||
if (inFlightSync != null) {
|
||||
await inFlightSync;
|
||||
return;
|
||||
}
|
||||
|
||||
final syncFuture = _syncActiveProfileForCurrentDeviceInternal();
|
||||
_deviceProfileSyncFuture = syncFuture;
|
||||
try {
|
||||
await syncFuture;
|
||||
} finally {
|
||||
if (identical(_deviceProfileSyncFuture, syncFuture)) {
|
||||
_deviceProfileSyncFuture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncActiveProfileForCurrentDeviceInternal() async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
if (deviceKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final profile = await _ensureProfileForCurrentDevice();
|
||||
final targetProfileId = profile.id;
|
||||
if (targetProfileId == profileManager.activeProfileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _persistCurrentState();
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
targetProfileId,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
await _switchRuntimeScope(targetProfileId);
|
||||
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> _ensureProfileForCurrentDevice() async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
final targetProfileId = profileManager.profileIdForDevice(deviceKey);
|
||||
final existingProfile = profileManager.getProfile(targetProfileId);
|
||||
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||
existingProfile != null) {
|
||||
return existingProfile;
|
||||
}
|
||||
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||
targetProfileId == ConfigProfile.defaultProfileId) {
|
||||
return ConfigProfile.defaultProfile();
|
||||
}
|
||||
if (deviceKey == null) {
|
||||
return await resolveProfile(profileManager.activeProfileId);
|
||||
}
|
||||
|
||||
final profile = await createProfileFromCurrent(
|
||||
name: _buildDeviceProfileName(),
|
||||
);
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
profile.id,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
return profile;
|
||||
}
|
||||
|
||||
String _buildDeviceProfileName() {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final name = deviceInfo.selfName?.trim();
|
||||
if (name != null && name.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(name)}';
|
||||
}
|
||||
|
||||
final displayName = deviceInfo.displayName?.trim();
|
||||
if (displayName != null && displayName.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(displayName)}';
|
||||
}
|
||||
|
||||
final deviceId = deviceInfo.deviceId?.trim();
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(deviceId)}';
|
||||
}
|
||||
|
||||
return 'Device Profile';
|
||||
}
|
||||
|
||||
String _sanitizeProfileLabel(String value) {
|
||||
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
}
|
||||
|
||||
String? get _currentDeviceProfileKey => ProfileDeviceKeyResolver.resolve(
|
||||
deviceInfo: connectionProvider.deviceInfo,
|
||||
connectionMode: connectionProvider.connectionMode,
|
||||
);
|
||||
|
||||
Future<void> _switchRuntimeScope(String profileId) async {
|
||||
final runtimeProfilesEnabled = profileManager.profilesEnabled;
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: runtimeProfilesEnabled,
|
||||
activeProfileId: profileId,
|
||||
);
|
||||
await messagesProvider.reloadFromStorage(
|
||||
namespace: ProfileStorageScope.effectiveNamespace,
|
||||
);
|
||||
await contactsProvider.reloadFromStorage(
|
||||
namespace: ProfileStorageScope.effectiveNamespace,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
await sensorsProvider.reloadProfileScopedState();
|
||||
await drawingProvider.reloadProfileScopedState();
|
||||
await mapProvider.reloadProfileScopedState();
|
||||
await voiceProvider.reloadProfileScopedState();
|
||||
await imageProvider.reloadProfileScopedState();
|
||||
await appProvider.reloadProfileScopedSettings();
|
||||
}
|
||||
|
||||
Future<void> _persistCurrentState() async {
|
||||
await messagesProvider.persistNow();
|
||||
await contactsProvider.persistNow();
|
||||
}
|
||||
|
||||
Future<void> _saveActiveCustomProfileSnapshot() async {
|
||||
if (profileManager.activeProfileId == ConfigProfile.defaultProfileId) {
|
||||
await _persistCurrentState();
|
||||
return;
|
||||
}
|
||||
final current = profileManager.getProfile(profileManager.activeProfileId);
|
||||
if (current == null) {
|
||||
await _persistCurrentState();
|
||||
return;
|
||||
}
|
||||
final snapshot = await snapshotCurrentProfile(
|
||||
id: current.id,
|
||||
name: current.name,
|
||||
notes: current.notes,
|
||||
);
|
||||
await profileManager.upsertProfile(snapshot);
|
||||
await _persistCurrentState();
|
||||
}
|
||||
|
||||
Future<void> _copyStorageNamespace(
|
||||
String? sourceNamespace,
|
||||
String targetNamespace,
|
||||
) async {
|
||||
final messages = await _messageStorageService.loadMessages(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final contactLocations = await _messageStorageService
|
||||
.loadMessageContactLocations(namespace: sourceNamespace);
|
||||
final receptionDetails = await _messageStorageService
|
||||
.loadMessageReceptionDetails(namespace: sourceNamespace);
|
||||
final transferDetails = await _messageStorageService
|
||||
.loadMessageTransferDetails(namespace: sourceNamespace);
|
||||
final routeMetadata = await _messageStorageService.loadMessageRouteMetadata(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final removedIds = await _messageStorageService.loadRemovedSarMarkerIds(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
await _messageStorageService.saveMessages(
|
||||
messages,
|
||||
messageContactLocations: contactLocations,
|
||||
messageReceptionDetails: receptionDetails,
|
||||
messageTransferDetails: transferDetails,
|
||||
messageRouteMetadata: routeMetadata,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _messageStorageService.saveRemovedSarMarkerIds(
|
||||
removedIds,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
|
||||
final contacts = await _contactStorageService.loadContacts(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final groups = await _contactStorageService.loadContactGroups(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final pending = await _contactStorageService.loadPendingAdverts(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
await _contactStorageService.saveContacts(
|
||||
contacts,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _contactStorageService.saveContactGroups(
|
||||
groups,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _contactStorageService.savePendingAdverts(
|
||||
pending,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initializeEmptyStorageNamespace(String namespace) async {
|
||||
await _messageStorageService.clearMessages(namespace: namespace);
|
||||
await _contactStorageService.clearContacts(namespace: namespace);
|
||||
await _contactStorageService.clearContactGroups(namespace: namespace);
|
||||
await _contactStorageService.clearPendingAdverts(namespace: namespace);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user