Compare commits

..

98 Commits

Author SHA1 Message Date
Janez T
c299349b6a chore: Update changelog #0 2026-03-22 16:02:17 +01:00
Janez T
ab05f13de9 fix: Remove contact trails from map #123 2026-03-22 15:48:55 +01:00
Janez T
0e61dc5bf1 fix: Revert Darwin BLE override 2026-03-22 13:57:14 +01:00
Janez T
d45924d7b4 fix: Vendor Darwin BLE override 2026-03-22 13:56:23 +01:00
Janez T
a24e699e22 fix: Refresh MeshCore lock #31
ref: dz0ny/meshcore-sar#31
2026-03-22 09:49:16 +01:00
Janez T
7486837ad0 fix: Compact recipient selector #0 2026-03-22 09:37:53 +01:00
Janez T
059125c714 fix: Restore telemetry refresh behavior
ref:
2026-03-22 09:19:33 +01:00
Janez T
9279ff2158 chore: Bump iOS build number #123 2026-03-21 20:45:09 +01:00
Janez T
dc297b0b9f fix: Tighten device settings layout 2026-03-21 20:44:29 +01:00
Janez T
2d24481aba feat: Add MET history isolation 2026-03-21 19:22:43 +01:00
Janez T
20a1df94eb fix: Support MeshCore message links #123 2026-03-21 18:36:56 +01:00
Janez T
5cc81bef65 feat: Show self sensor default 2026-03-20 19:08:24 +01:00
Janez T
6145462555 chore: Bump iOS build #123 2026-03-19 21:39:51 +01:00
Janez T
721fb7e7a2 feat: Add serial compass support #0 2026-03-19 21:38:54 +01:00
Janez T
5a58e45e13 fix: Add contacts test providers 2026-03-19 14:28:10 +01:00
Janez T
de4258c0a4 fix: Resolve DeviceInfo import clash 2026-03-19 14:18:48 +01:00
Janez T
d8cb35c595 fix: Simplify device settings styling #123 2026-03-19 14:18:29 +01:00
Janez T
8da69705ee fix: Align contact sorting labels #0 2026-03-19 14:06:05 +01:00
Janez T
e474168ba8 feat: Show self telemetry in device info sheet
Tapping signal/battery in header now also shows device's own telemetry
(temperature, humidity, pressure, GPS) when available. Triggers a self
telemetry request on sheet open. Data comes from the device contact's
telemetry field.
2026-03-19 13:52:15 +01:00
Janez T
03dba8b3ce feat: Device info sheet on signal/battery tap
Tapping the signal bars or battery indicator in the header now shows
a bottom sheet with device details: BLE signal (dBm), battery %,
voltage, storage usage, firmware version, frequency, and TX power.

Fixes #22 (partial — device telemetry display)
2026-03-19 11:07:08 +01:00
Janez T
bc9e37bd22 fix: Count RX data and speed up Flutter builds #123 2026-03-19 10:33:20 +01:00
Janez T
a32d5d2c85 fix: Show irradiance values #123 2026-03-19 09:24:53 +01:00
Janez T
e683ab933f fix: Show original poster name in room messages, not room server name
Room servers forward messages with the original author's 4-byte public
key prefix (roomPostAuthorPrefix). Was ignoring this — all room messages
showed the room server's name as sender.

Now resolves the actual poster's name from roomPostAuthorPrefix by
matching against the contacts list. Falls back to room server name if
the author isn't a known contact.

Also fixes findContactByPrefix to accept 4-byte prefixes (was rejecting
anything shorter than 6 bytes).

Fixes #22
2026-03-19 09:20:22 +01:00
Janez T
f286b0afd1 fix: Show signal chips on double tap 2026-03-18 16:59:02 +01:00
Janez T
68ec72534d fix: Sensor tiles — move channel label to bottom-right, smaller
Channel chip (ch2) was inline with the label, stealing horizontal
space and causing label truncation ('Temper...'). Now positioned as
a tiny 9px label anchored bottom-right of the tile, giving the full
width to the metric label and value.
2026-03-18 16:58:48 +01:00
Janez T
6af8b62d9b fix: Show message signal chips by default #123 2026-03-18 16:38:19 +01:00
Janez T
01fbb7de6c fix: Profile duplication on device connect
DeviceKey resolver was falling back to deviceId when publicKey wasn't
yet available during early connection. This created two different keys
(id:xxx then pk:yyy) for the same device, causing a new profile to be
created each time.

Now returns null until publicKey is available, so profile sync waits
for the stable identifier.
2026-03-18 14:45:55 +01:00
Janez T
9303d24b69 fix: Remove duplicate sensor cards for temperature, humidity, pressure
Core telemetry fields (temperature, humidity, pressure, voltage) were
shown twice: once from the ContactTelemetry top-level fields and again
from extraSensorData channel-keyed entries (e.g. temperature_2).

Now skips extraSensorData keys that are already represented by core
telemetry cards.
2026-03-18 14:37:06 +01:00
Janez T
140088fe2f fix: Improve message signal chip behavior 2026-03-18 14:29:33 +01:00
Janez T
67d8e1c906 fix: Deduplicate telemetry from 0x8B + 0x8C double delivery
When firmware sends telemetry via both pushTelemetryResponse (0x8B)
and pushBinaryResponse (0x8C), the same LPP data was parsed and
applied twice, causing duplicate sensor entries.

Added hash-based dedup: same payload for same contact within 2s is
skipped. Also added minimum length check for binary responses.
2026-03-18 14:24:38 +01:00
Janez T
29b75a3155 feat: Save profile defaults per device 2026-03-18 13:34:50 +01:00
Janez T
87cb890877 chore: Linter fixes for USB serial transport 2026-03-17 14:43:47 +01:00
Janez T
095458ece4 feat: USB serial companion support (Android)
- Add MeshCoreSerialService to meshcore_client (0x3C/0x3E framing)
- Add usb_serial package for Android USB OTG
- Add UsbSerialTransport wrapper for Android
- Add USB tab to connection dialog (scan, select, connect)
- Add ConnectionMode.usb + connectSerial/disconnectSerial
- Web Serial stub (not yet implemented)
- Fix message limits: DM 156→150, Channel 127→160 (firmware MAX_TEXT_LEN=160)
2026-03-17 13:46:50 +01:00
Janez T
5659242672 fix: Remove stale analyze warnings #123
ref:
2026-03-17 11:26:59 +01:00
Janez T
3da4ba8fb2 fix: Remove local client override #123 2026-03-17 11:24:01 +01:00
Janez T
89dc1f2862 chore: Bump version to 2026.0317.1+33 2026-03-17 11:22:23 +01:00
Janez T
251932bb47 fix: Use first hop of received path (near sender), not outbound path
Received message path: Sender → hop1 (near sender) → ... → Us
Outbound path: Us → hop1 (near us) → ... → Contact

Was using outbound path which placed contacts near OUR repeater.
Now uses first hop of the received message path — the repeater
nearest to the sender. Only triggers on received messages, not sends.
2026-03-17 11:19:37 +01:00
Janez T
b48139964a feat: RSSI trilateration from multiple repeaters
When a contact has no GPS, estimate their position from RSSI
observations through multiple repeaters:

- 1 repeater: offset by RSSI distance in deterministic direction
- 2 repeaters: weighted midpoint between circle intersections
- 3+ repeaters: weighted centroid with inverse-square weighting
  (closer repeater observations dominate)

Observations stored per-repeater (latest wins), max 8 per contact,
expire after 30 minutes. Each incoming message updates the estimate.
2026-03-17 11:15:51 +01:00
Janez T
d29b85feb8 feat: Estimate contact location from RSSI + last-hop repeater
For contacts without GPS, estimate their position by:
1. Finding the first-hop repeater from their routing path
2. Estimating distance from RSSI using log-distance path loss model
3. Placing them at a deterministic offset from the repeater

Uses LoRa outdoor path loss exponent (n=3.0) with reference RSSI
-30dBm at 1m. Distance clamped to 10m-50km range. Bearing derived
from contact public key hash for stable positioning.
2026-03-17 11:08:56 +01:00
Janez T
c01ef7a059 fix: DM retry stuck — reduce jitter, fix flood-only retry count
- maxRetryAttemptsFloodOnly: 1→0 (was causing an extra retry for
  flood contacts that the official app doesn't do)
- isLastAttempt: fix off-by-one (retryAttempt already incremented
  when called from _scheduleRetry)
- Reduce ACK timeout jitter from 1-8s to 0.5-2s to avoid long waits
  that make the UI appear stuck
2026-03-17 10:48:53 +01:00
Janez T
fa2b586ab7 i18n: Add 247 missing translation keys across all 13 languages
Replace hardcoded English strings in 32 Dart source files with l10n
references. Covers settings labels, UI actions, map/SAR features,
device config, notifications, profiles, spectrum scan, and status
messages for sl, de, hr, pl, es, fr, it, el, ru, tr, uk, zh, pt.
2026-03-17 10:36:58 +01:00
Janez T
bbd5d108cb fix: Show neighbours in bottom sheet instead of dialog 2026-03-17 10:20:28 +01:00
Janez T
e17f24a6bc feat: View Neighbours option for repeater contacts
Sends 'neighbors' text command to the repeater and displays the
response as a list with resolved names and SNR values.
2026-03-17 10:16:11 +01:00
Janez T
372224a62c feat: Share contact as meshcore:// URL via clipboard 2026-03-17 10:07:02 +01:00
Janez T
e4c8709ee2 chore: Point meshcore_client to main branch 2026-03-17 09:54:16 +01:00
Janez T
3c469b62d0 chore: Update meshcore_client ref after rebase 2026-03-17 09:53:14 +01:00
Janez T
1e97ba9831 feat: Favourites support using firmware flags bit 0
- Contact.isFavourite reads bit 0 of firmware flags field
- Toggle favourite via contact action sheet (writes to device)
- Favourites section shown first in contacts tab
- Path size options corrected to [1, 2, 3] bytes
2026-03-17 09:44:38 +01:00
Janez T
6187067017 fix: Pass selfPoint to _routeCandidates after signature change 2026-03-17 09:44:38 +01:00
Janez T
cd165d2372 feat: Add GPS toggle and node sorting 2026-03-17 09:44:38 +01:00
Janez T
59f6df4260 feat: GPS module toggle in device settings 2026-03-17 09:44:38 +01:00
Janez T
464fc5e715 feat: Per-contact path size selector in route editor 2026-03-17 09:44:38 +01:00
Janez T
a76ee4f8f6 fix: Profile coordinator null safety and data loss 2026-03-17 09:44:38 +01:00
Janez T
c884f81ca5 fix: Channel message dedup uses senderTimestamp as primary key 2026-03-16 22:17:52 +01:00
Janez T
993563f059 fix: Prevent invalid map bounds #123 2026-03-16 22:16:36 +01:00
Janez T
cfd233d38e fix: Remove relay labels, add signal bars #123 2026-03-16 22:01:41 +01:00
Janez T
9b14ff4e1a Remove relay info from contact card 2026-03-16 21:54:53 +01:00
Janez T
84cd8eec00 Refine contact refresh flow 2026-03-16 21:52:46 +01:00
Janez T
434d3df53e Fix BottomSheet ancestor lookup 2026-03-16 21:46:37 +01:00
Janez T
1fe260b95e feat: Match official app's advert import and discovery flow
- pushAdvert (0x80): import advert into firmware via cmdGetAdvertPath
  before fetching contact, so name/type/location are available
- pushNewAdvert (0x8A): fire-and-forget importReceivedAdvert to store
  contact in firmware (data already in push payload)
- controlData (0x8E) discovery: no firmware import (matches official app),
  just lookup existing contacts and show generic labels for unknown nodes
- Update meshcore_client git ref to bcbeff0 (burst sync + advert import)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:19:40 +01:00
Janez T
d3d53995a6 Add wakelock plus initialization 2026-03-16 20:38:26 +01:00
Janez T
efbddc8831 Add wakelock plus package 2026-03-16 20:37:04 +01:00
Janez T
0e3acfa4ca Ensure new profile starts empty 2026-03-16 14:04:07 +01:00
Janez T
450b8382e8 Add custom cave map mode 2026-03-16 11:30:53 +01:00
Janez T
420690d683 Enable profiles switch UI 2026-03-16 11:25:51 +01:00
Janez T
fedd7b9a2b Add custom cave map mode 2026-03-16 10:50:44 +01:00
Janez T
c82e0c3452 Update iOS project version to 118 2026-03-16 10:02:43 +01:00
Janez T
a55cfffa59 Redesign contact import screen 2026-03-15 21:02:17 +01:00
Janez T
2bb5b353bd Fix channel refresh telemetry how 2026-03-15 21:00:02 +01:00
Janez T
991421d097 Fix telemetry merge bug 2026-03-15 20:58:12 +01:00
Janez Troha
1dcd85f178 Merge pull request #26 from dz0ny/feat/sensor-telemetry-preview
Feat/sensor telemetry preview
2026-03-15 20:14:31 +01:00
Janez T
5d2f61749e Update sensor parsing layout 2026-03-15 20:14:01 +01:00
Janez T
69377d90a4 Update auto-refresh and UI 2026-03-15 11:15:19 +01:00
Janez T
3e559a4f4e Enforce theme colors on switches 2026-03-15 10:56:47 +01:00
Janez T
a124e3d9f2 Improve sensor settings UI 2026-03-15 10:48:48 +01:00
Janez T
3bf4971279 Update sensor preview interactions 2026-03-15 10:43:37 +01:00
Janez T
e355f28ca9 Update sensor customization view 2026-03-15 10:40:16 +01:00
Janez Troha
7a9be3eb76 Merge pull request #25 from dz0ny/feat/sensor-telemetry-preview
Feat/sensor telemetry preview
2026-03-15 10:34:36 +01:00
Janez T
650f3c66aa Show values in sensor fields 2026-03-15 10:22:53 +01:00
Janez T
df993cd176 Enhance sensor telemetry cards 2026-03-15 10:10:22 +01:00
Janez T
feb3145854 Add sensor contact preview sheet 2026-03-15 09:36:31 +01:00
Janez T
da40acf950 chore: Update Meshcore client pin
ref:
2026-03-15 09:31:18 +01:00
Janez T
66eb32bf44 Fix BLE contact add failure 2026-03-15 09:24:41 +01:00
Janez T
c2e0fa952e Fix contact add failure 2026-03-15 09:19:59 +01:00
Janez T
4557fb4b9e feat: Add Sensor telemetry alerts
ref:
2026-03-15 09:13:32 +01:00
Janez T
c05de2cf0a Update iOS project version 2026-03-14 21:42:05 +01:00
Janez Troha
c5145c2fca Merge pull request #24 from dz0ny/feat/update-changelog-highlights
Add weekly changelog highlights
2026-03-14 21:36:10 +01:00
Janez T
e502bc23b7 Update changelog to drop LPCNet 2026-03-14 21:35:57 +01:00
Janez T
62bf33c9d1 fix: Replace web unsafe hash
ref:
2026-03-14 21:34:57 +01:00
Janez T
7be1460ef6 Update changelog highlights 2026-03-14 21:32:41 +01:00
Janez T
30c273c656 Fix connection provider auto-add bug 2026-03-14 21:30:23 +01:00
Janez T
5712feba53 Reduce composer mention padding 2026-03-14 21:10:22 +01:00
Janez T
6de12225fc feat: Finish discovery flow
ref:
2026-03-14 21:04:23 +01:00
Janez T
6e9e9e397d Fix RX advert data parsing 2026-03-14 16:34:31 +01:00
Janez T
a3147e9d7c Summarize repo changes 2026-03-14 10:39:27 +01:00
Janez T
5e1912d9f2 Remove artificial UI delays 2026-03-14 10:24:50 +01:00
Janez T
b42328bd82 Ensure contact sync waits 2026-03-14 10:24:14 +01:00
Janez T
1b801a10ef Remove hex logs and keep path data 2026-03-14 10:07:11 +01:00
Janez T
06632ee32d Update log display handling 2026-03-14 10:01:17 +01:00
201 changed files with 46074 additions and 7972 deletions

View File

@@ -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

View File

@@ -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

1
.gitignore vendored
View File

@@ -28,6 +28,7 @@ migrate_working_dir/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
pubspec_overrides.yaml
.pub-cache/
.pub/
/build/

25
CHANGELOG.md Normal file
View File

@@ -0,0 +1,25 @@
# Changelog
## 2026-03-15 to 2026-03-22
- Device connectivity and routing: added Android USB serial companion support, serial compass support, GPS module controls, per-contact path size selection, and favourites support via firmware flags.
- Location and map intelligence: added RSSI-based repeater trilateration and contact location estimates, isolated MET history, matched the official advert import/discovery flow, and refreshed map presentation by removing contact trails.
- Telemetry and device info: surfaced self telemetry and sensor defaults, opened device info from signal and battery taps, improved signal chip behavior, fixed duplicate telemetry delivery, and restored telemetry refresh behavior.
- Contacts and messaging: added meshcore:// contact sharing and repeater neighbour views, supported MeshCore message links, fixed DM retry behavior, corrected room poster naming, and compacted the recipient selector.
- UI polish and platform upkeep: improved device settings layout, tightened sensor and contact presentation, expanded localization coverage, and refreshed platform dependencies and build numbers.
## Key PRs
- [#26](https://github.com/dz0ny/meshcore-sar/pull/26) Merge pull request #26 from `dz0ny/feat/sensor-telemetry-preview`
## 2026-03-08 to 2026-03-14
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.
- Messaging and channels: fixed channel sync, deduplication, overwrite and deletion issues, cleared message input after send, and added delayed-message warning tracking.
- Voice and transport: improved voice playback quality, improved scan and advert parsing, and kept route path data visible in logs.
- Maps and field operations: enabled iOS background GPS updates, added GPX trail import/export, introduced a fullscreen repeater map, and added live mesh traffic views.
- Localization and UI polish: added multilingual localization coverage, updated locale strings, fixed onboarding layout issues, and refined channel and contacts presentation.
## Key PRs
- [#16](https://github.com/dz0ny/meshcore-sar/pull/16) Merge pull request #16 from `MGJ520/main`

View File

@@ -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

View File

@@ -489,7 +489,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 115;
CURRENT_PROJECT_VERSION = 124;
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 = 124;
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 = 124;
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 = 124;
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 = 124;
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 = 124;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>115</string>
<string>124</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -5,24 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.001473">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.569758">
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.144858">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="95.271262">
<testcase classname="fastlane.lanes" name="2: build_app" time="98.142592">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="91.795184">
<failure message="/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/actions/actions_helper.rb:67:in &apos;Fastlane::Actions.execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:255:in &apos;block in Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:229:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:229:in &apos;Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:157:in &apos;Fastlane::Runner#trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/fast_file.rb:159:in &apos;Fastlane::FastFile#method_missing&apos;&#10;Fastfile:23:in &apos;block (2 levels) in Fastlane::FastFile#parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/lane.rb:41:in &apos;Fastlane::Lane#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:49:in &apos;block in Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:45:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:45:in &apos;Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/lane_manager.rb:46:in &apos;Fastlane::LaneManager.cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/command_line_handler.rb:34:in &apos;Fastlane::CommandLineHandler.handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:110:in &apos;block (2 levels) in Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in &apos;Commander::Command#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in &apos;Commander::Command#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in &apos;Commander::Runner#run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in &apos;Commander::Runner#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in &apos;Commander::Delegates#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:363:in &apos;Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:43:in &apos;Fastlane::CommandsGenerator.start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/cli_tools_distributor.rb:132:in &apos;Fastlane::CLIToolsDistributor.take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/bin/fastlane:23:in &apos;&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/ruby/4.0.1/lib/ruby/4.0.0/rubygems.rb:303:in &apos;Kernel#load&apos;&#10;/opt/homebrew/Cellar/ruby/4.0.1/lib/ruby/4.0.0/rubygems.rb:303:in &apos;Gem.activate_and_load_bin_path&apos;&#10;/opt/homebrew/Cellar/fastlane/2.231.1/libexec/bin/fastlane:25:in &apos;&lt;main&gt;&apos;&#10;&#10;Invalid username and password combination. Used &apos;hey@dz0n.dev&apos; as the username." />
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="157.12775">
</testcase>

View File

@@ -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."
}

View File

@@ -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": "Δεν υπάρχουν επαφές συσκευής."
}

View File

@@ -2228,80 +2228,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"
@@ -2542,5 +2517,258 @@
"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."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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

View File

@@ -1929,4 +1929,773 @@ 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.';
}

View File

@@ -1936,4 +1936,768 @@ 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 => 'Δεν υπάρχουν επαφές συσκευής.';
}

View File

@@ -1902,4 +1902,764 @@ 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.';
}

View File

@@ -1932,4 +1932,774 @@ 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.';
}

View File

@@ -1936,4 +1936,774 @@ 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.';
}

View File

@@ -1912,4 +1912,767 @@ 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.';
}

View File

@@ -1927,4 +1927,769 @@ 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.';
}

View File

@@ -1924,4 +1924,769 @@ 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.';
}

View File

@@ -1931,4 +1931,773 @@ 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.';
}

View File

@@ -1921,4 +1921,768 @@ 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 => 'Нет контактов устройства для очистки.';
}

View File

@@ -1912,4 +1912,767 @@ 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.';
}

View File

@@ -1914,4 +1914,763 @@ 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 => '';
@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ı';
@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.';
}

View File

@@ -1927,4 +1927,773 @@ 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 =>
'Немає контактів пристрою для очищення.';
}

View File

@@ -1827,4 +1827,745 @@ 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 => '没有设备联系人可清除。';
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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": "Нет контактов устройства для очистки."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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": "Немає контактів пристрою для очищення."
}

View File

@@ -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": "没有设备联系人可清除。"
}

View File

@@ -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,14 +24,20 @@ import 'services/voice_player_service.dart';
import 'services/notification_service.dart';
import 'services/locale_preferences.dart';
import 'services/mesh_map_nodes_service.dart';
import 'services/profile_manager.dart';
import 'services/profile_workspace_coordinator.dart';
import 'services/profiles_feature_service.dart';
import 'services/update_checker_service.dart';
import 'services/wizard_preferences.dart';
import 'screens/discovery_screen.dart';
import 'screens/home_screen.dart';
import 'screens/welcome_wizard_screen.dart';
import 'theme/app_theme.dart';
import 'l10n/app_localizations.dart';
void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await WakelockPlus.enable();
runApp(const MeshCoreSarApp());
}
@@ -42,11 +49,13 @@ class MeshCoreSarApp extends StatefulWidget {
}
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
AppThemeMode _themeMode = AppThemeMode.system;
Locale? _locale;
bool _isInitialized = false;
bool _shouldShowPermissionDialog = false;
bool _wizardCompleted = true; // Will be updated in _initializeApp()
String? _pendingNotificationPayload;
@override
void initState() {
@@ -55,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();
@@ -66,6 +83,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
// Set up notification tap handler for update notifications
NotificationService().onNotificationTapped = _handleNotificationTap;
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
// Check if we need to request location permissions
await _checkLocationPermissions();
@@ -81,6 +99,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
_wizardCompleted = wizardCompleted;
_isInitialized = true;
});
if (_pendingNotificationPayload != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final payload = _pendingNotificationPayload;
_pendingNotificationPayload = null;
_handleNotificationTap(payload);
});
}
}
/// Handle notification tap
@@ -89,10 +115,27 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
debugPrint('[Main] Notification tapped: $payload');
if (!_wizardCompleted) {
_pendingNotificationPayload = payload;
return;
}
// Handle update notification tap
if (payload.startsWith('update:')) {
final downloadUrl = payload.substring(7); // Remove 'update:' prefix
_launchUpdateDownload(downloadUrl);
return;
}
if (payload.startsWith('discovery:')) {
final navigator = _navigatorKey.currentState;
if (navigator == null) {
_pendingNotificationPayload = payload;
return;
}
navigator.push(
MaterialPageRoute(builder: (context) => const DiscoveryScreen()),
);
}
// SAR and message notifications handled by their respective providers
}
@@ -242,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(
@@ -294,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(),
);
@@ -304,6 +387,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
builder: (context) {
final systemBrightness = MediaQuery.platformBrightnessOf(context);
final materialApp = MaterialApp(
navigatorKey: _navigatorKey,
key: ValueKey<String?>(
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
),

View File

@@ -57,9 +57,7 @@ class Channel {
} else {
// Normal channel: require explicit secret
if (explicitSecret == null || explicitSecret.length != 16) {
throw ArgumentError(
'Normal channels require a 16-byte secret',
);
throw ArgumentError('Normal channels require a 16-byte secret');
}
return Channel(
index: index,
@@ -78,6 +76,18 @@ class Channel {
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
static bool isHashChannelName(String channelName) {
return channelName.trim().startsWith('#');
}
static String pskBase64ForHashChannelName(String channelName) {
final normalized = channelName.trim();
if (!isHashChannelName(normalized)) {
throw ArgumentError('Only #channels can export derived psk_base64');
}
return base64.encode(_generateHashChannelSecret(normalized));
}
/// Create the default public channel (channel 0)
/// Uses the well-known pre-shared key from MeshCore
factory Channel.publicChannel() {
@@ -85,8 +95,22 @@ class Channel {
index: 0,
name: 'Public Channel',
secret: Uint8List.fromList([
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
0x8b,
0x33,
0x87,
0xe9,
0xc5,
0xcd,
0xea,
0x6a,
0xc9,
0xe5,
0xed,
0xba,
0xa1,
0x15,
0xcd,
0x72,
]),
flags: null,
);
@@ -95,6 +119,9 @@ class Channel {
/// Check if this is a hash-based channel (name starts with '#')
bool get isHashChannel => name.startsWith('#');
/// Base64-encoded PSK for sharing with firmware CLI and related tooling.
String get pskBase64 => base64.encode(secret);
/// Display name for the channel
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
String get displayName {
@@ -131,12 +158,7 @@ class Channel {
}
/// Create a copy with modified fields
Channel copyWith({
int? index,
String? name,
Uint8List? secret,
int? flags,
}) {
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
return Channel(
index: index ?? this.index,
name: name ?? this.name,

View 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,
);
}
}

View File

@@ -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;

View 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']),
);
}
}

View File

@@ -14,11 +14,11 @@ enum ConnectionMode {
/// Direct BLE connection to MeshCore device (default)
ble,
/// Act as SSE server - share BLE device with multiple clients
sseServer,
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
tcp,
/// USB serial connection (Android OTG or Web Serial API)
usb,
}
extension ConnectionModeExtension on ConnectionMode {
@@ -26,10 +26,10 @@ extension ConnectionModeExtension on ConnectionMode {
switch (this) {
case ConnectionMode.ble:
return 'Direct (BLE)';
case ConnectionMode.sseServer:
return 'Share Device (Server)';
case ConnectionMode.tcp:
return 'Direct (WiFi)';
case ConnectionMode.usb:
return 'Direct (Serial)';
}
}
@@ -37,10 +37,10 @@ extension ConnectionModeExtension on ConnectionMode {
switch (this) {
case ConnectionMode.ble:
return 'Direct BLE connection to MeshCore device';
case ConnectionMode.sseServer:
return 'Share BLE device with multiple clients over network';
case ConnectionMode.tcp:
return 'Direct WiFi/TCP connection to MeshCore device';
case ConnectionMode.usb:
return 'Direct serial connection to MeshCore device';
}
}
}
@@ -66,6 +66,12 @@ class DeviceInfo {
final int? advLat;
final int? advLon;
final bool? manualAddContacts;
final bool? autoAddUsers;
final bool? autoAddRepeaters;
final bool? autoAddRoomServers;
final bool? autoAddSensors;
final bool? autoAddOverwriteOldest;
final int? autoAddMaxHops;
final int? radioFreq;
final int? radioBw;
final int? radioSf;
@@ -90,6 +96,7 @@ class DeviceInfo {
// Repeat mode (firmware v9+)
final bool? clientRepeat;
final int? pathHashMode;
final bool? supportsSpectrumScan;
final int? spectrumScanMinKhz;
final int? spectrumScanMaxKhz;
@@ -113,6 +120,12 @@ class DeviceInfo {
this.advLat,
this.advLon,
this.manualAddContacts,
this.autoAddUsers,
this.autoAddRepeaters,
this.autoAddRoomServers,
this.autoAddSensors,
this.autoAddOverwriteOldest,
this.autoAddMaxHops,
this.radioFreq,
this.radioBw,
this.radioSf,
@@ -129,6 +142,7 @@ class DeviceInfo {
this.manufacturerModel,
this.semanticVersion,
this.clientRepeat,
this.pathHashMode,
this.supportsSpectrumScan,
this.spectrumScanMinKhz,
this.spectrumScanMaxKhz,
@@ -239,6 +253,12 @@ class DeviceInfo {
int? advLat,
int? advLon,
bool? manualAddContacts,
bool? autoAddUsers,
bool? autoAddRepeaters,
bool? autoAddRoomServers,
bool? autoAddSensors,
bool? autoAddOverwriteOldest,
int? autoAddMaxHops,
int? radioFreq,
int? radioBw,
int? radioSf,
@@ -255,6 +275,7 @@ class DeviceInfo {
String? manufacturerModel,
String? semanticVersion,
bool? clientRepeat,
int? pathHashMode,
bool? supportsSpectrumScan,
int? spectrumScanMinKhz,
int? spectrumScanMaxKhz,
@@ -278,6 +299,13 @@ class DeviceInfo {
advLat: advLat ?? this.advLat,
advLon: advLon ?? this.advLon,
manualAddContacts: manualAddContacts ?? this.manualAddContacts,
autoAddUsers: autoAddUsers ?? this.autoAddUsers,
autoAddRepeaters: autoAddRepeaters ?? this.autoAddRepeaters,
autoAddRoomServers: autoAddRoomServers ?? this.autoAddRoomServers,
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
autoAddOverwriteOldest:
autoAddOverwriteOldest ?? this.autoAddOverwriteOldest,
autoAddMaxHops: autoAddMaxHops ?? this.autoAddMaxHops,
radioFreq: radioFreq ?? this.radioFreq,
radioBw: radioBw ?? this.radioBw,
radioSf: radioSf ?? this.radioSf,
@@ -294,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,

View 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,
);
}
}

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -30,6 +30,7 @@ class MessageReceptionDetails {
final int? senderToReceiptMs;
final int? estimatedTransmitMs;
final int? postTransmitDelayMs;
final int receivedCopies;
const MessageReceptionDetails({
required this.capturedAt,
@@ -40,6 +41,7 @@ class MessageReceptionDetails {
this.senderToReceiptMs,
this.estimatedTransmitMs,
this.postTransmitDelayMs,
this.receivedCopies = 1,
});
String? get pathBytesHex => pathBytes == null || pathBytes!.isEmpty
@@ -56,9 +58,60 @@ class MessageReceptionDetails {
'senderToReceiptMs': senderToReceiptMs,
'estimatedTransmitMs': estimatedTransmitMs,
'postTransmitDelayMs': postTransmitDelayMs,
'receivedCopies': receivedCopies,
};
}
MessageReceptionDetails copyWith({
DateTime? capturedAt,
DateTime? packetLoggedAt,
int? rssiDbm,
double? snrDb,
List<int>? pathBytes,
int? senderToReceiptMs,
int? estimatedTransmitMs,
int? postTransmitDelayMs,
int? receivedCopies,
}) {
return MessageReceptionDetails(
capturedAt: capturedAt ?? this.capturedAt,
packetLoggedAt: packetLoggedAt ?? this.packetLoggedAt,
rssiDbm: rssiDbm ?? this.rssiDbm,
snrDb: snrDb ?? this.snrDb,
pathBytes: pathBytes ?? this.pathBytes,
senderToReceiptMs: senderToReceiptMs ?? this.senderToReceiptMs,
estimatedTransmitMs: estimatedTransmitMs ?? this.estimatedTransmitMs,
postTransmitDelayMs: postTransmitDelayMs ?? this.postTransmitDelayMs,
receivedCopies: receivedCopies ?? this.receivedCopies,
);
}
static MessageReceptionDetails mergeDuplicate({
MessageReceptionDetails? existing,
MessageReceptionDetails? incoming,
}) {
final base =
existing ??
incoming ??
MessageReceptionDetails(capturedAt: DateTime.now());
return base.copyWith(
capturedAt: incoming?.capturedAt ?? existing?.capturedAt,
packetLoggedAt: incoming?.packetLoggedAt ?? existing?.packetLoggedAt,
rssiDbm: incoming?.rssiDbm ?? existing?.rssiDbm,
snrDb: incoming?.snrDb ?? existing?.snrDb,
pathBytes: incoming?.pathBytes ?? existing?.pathBytes,
senderToReceiptMs:
incoming?.senderToReceiptMs ?? existing?.senderToReceiptMs,
estimatedTransmitMs:
incoming?.estimatedTransmitMs ?? existing?.estimatedTransmitMs,
postTransmitDelayMs:
incoming?.postTransmitDelayMs ?? existing?.postTransmitDelayMs,
receivedCopies:
(existing?.receivedCopies ?? 1) + (incoming?.receivedCopies ?? 1),
);
}
static MessageReceptionDetails? fromJson(Map<String, dynamic> json) {
final capturedAtMillis = json['capturedAtMillis'];
if (capturedAtMillis is! int) {
@@ -81,6 +134,7 @@ class MessageReceptionDetails {
senderToReceiptMs: json['senderToReceiptMs'] as int?,
estimatedTransmitMs: json['estimatedTransmitMs'] as int?,
postTransmitDelayMs: json['postTransmitDelayMs'] as int?,
receivedCopies: json['receivedCopies'] as int? ?? 1,
);
}
}

View File

@@ -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,
);
}

View File

@@ -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

View File

@@ -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];
@@ -54,12 +55,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 +97,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

File diff suppressed because it is too large Load Diff

View File

@@ -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
@@ -22,6 +25,7 @@ class DrawingProvider with ChangeNotifier {
// Completed drawings
final List<MapDrawing> _drawings = [];
bool _isInitialized = false;
// In-progress drawing
MapDrawing? _currentDrawing;
@@ -32,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);
@@ -47,12 +57,24 @@ class DrawingProvider with ChangeNotifier {
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
}
visibleDrawings = visibleDrawings.where((drawing) {
if (drawing.coordinateSpace != _activeCoordinateSpace) {
return false;
}
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
return drawing.mapId != null && drawing.mapId == _activeMapId;
}
return true;
});
return List.unmodifiable(visibleDrawings.toList());
}
MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
bool get isDrawing => _drawingMode != DrawingMode.none;
bool get isInitialized => _isInitialized;
LatLng? get measurementPoint1 => _measurementPoint1;
LatLng? get measurementPoint2 => _measurementPoint2;
double? get measuredDistance => _measuredDistance;
@@ -61,6 +83,36 @@ class DrawingProvider with ChangeNotifier {
Future<void> initialize() async {
await _loadPreferences();
await _loadDrawings();
_isInitialized = true;
}
Future<void> reloadProfileScopedState() async {
await _loadPreferences();
await _loadDrawings();
notifyListeners();
}
void setMapContext({
required MapCoordinateSpace coordinateSpace,
String? mapId,
double? metersPerPixel,
}) {
final changed =
coordinateSpace != _activeCoordinateSpace ||
mapId != _activeMapId ||
metersPerPixel != _activeMetersPerPixel;
_activeCoordinateSpace = coordinateSpace;
_activeMapId = mapId;
_activeMetersPerPixel = metersPerPixel;
if (_measurementPoint1 != null && _measurementPoint2 != null) {
_measuredDistance = _calculateDistance(
_measurementPoint1!,
_measurementPoint2!,
);
}
if (changed) {
notifyListeners();
}
}
/// Set drawing mode
@@ -95,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
@@ -134,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);
@@ -177,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
? _rectangleStartPoint!.longitude
: endPoint.longitude,
),
coordinateSpace: _activeCoordinateSpace,
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
? _activeMapId
: null,
);
notifyListeners();
}
@@ -213,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);
@@ -234,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);
@@ -243,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);
}
@@ -299,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');
}
@@ -309,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();
@@ -336,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) {
@@ -373,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(
@@ -387,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}
return drawing;
@@ -401,9 +489,27 @@ class DrawingProvider with ChangeNotifier {
}
}
List<Map<String, dynamic>> exportDrawingsJson() {
return _drawings.map((drawing) => drawing.toJson()).toList();
}
Future<void> replaceDrawingsFromJson(
List<Map<String, dynamic>> jsonList,
) async {
_drawings
..clear()
..addAll(jsonList.map(MapDrawing.fromJson).whereType<MapDrawing>());
await _saveDrawings();
notifyListeners();
}
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
}
/// Get all unshared drawings (local drawings not yet sent)
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
@@ -425,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(
@@ -439,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
isShared: true,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}
@@ -466,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(
@@ -480,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: !drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data';
/// Message delivery tracking helper
@@ -30,6 +31,9 @@ class MessageDeliveryTracker {
/// Map of ACK tag to timestamp for timeout cleanup
final Map<int, DateTime> _ackTagTimestamps = {};
/// Completer signalled when a pending ACK slot is freed (delivery or removal).
Completer<void>? _slotFreedCompleter;
/// Track a pending message ID before sending
///
/// This is called BEFORE sending the message. When RESP_CODE_SENT
@@ -112,6 +116,7 @@ class MessageDeliveryTracker {
_messageIdToAckTag.remove(messageId);
}
_ackTagTimestamps.remove(ackCode);
_notifySlotFreed();
}
/// Remove ACK tag mapping by message ID
@@ -122,6 +127,7 @@ class MessageDeliveryTracker {
if (ackTag != null) {
_ackTagToMessageId.remove(ackTag);
_ackTagTimestamps.remove(ackTag);
_notifySlotFreed();
}
_pendingMessageIds.remove(messageId);
final emptyKeys = <String>[];
@@ -166,6 +172,7 @@ class MessageDeliveryTracker {
_ackTagToMessageId.clear();
_messageIdToAckTag.clear();
_ackTagTimestamps.clear();
_notifySlotFreed();
}
/// Get count of pending ACK tags
@@ -179,6 +186,27 @@ class MessageDeliveryTracker {
/// Returns true if >= 7 pending ACKs (stay under firmware limit of 8)
bool get shouldRateLimit => pendingCount >= 7;
/// Wait until a pending ACK slot is freed, or [timeout] elapses.
///
/// Returns immediately if not at the rate limit.
Future<void> waitForSlot({
Duration timeout = const Duration(milliseconds: 500),
}) async {
if (!shouldRateLimit) return;
_slotFreedCompleter ??= Completer<void>();
await _slotFreedCompleter!.future.timeout(
timeout,
onTimeout: () {},
);
}
void _notifySlotFreed() {
if (_slotFreedCompleter != null && !_slotFreedCompleter!.isCompleted) {
_slotFreedCompleter!.complete();
}
_slotFreedCompleter = null;
}
/// Get oldest pending ACK timestamp (for debugging)
DateTime? get oldestPendingTimestamp {
if (_ackTagTimestamps.isEmpty) return null;

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math' as math;
import '../../models/message.dart';
import '../../models/contact.dart';
@@ -19,10 +20,19 @@ class MessageRetryManager {
final Map<String, DateTime> _lastRetryTimes = {};
final Map<String, int> _pathFailureStreaks = {};
static const int maxRetryAttempts = 4;
/// Max retry attempts when the contact has a known path.
/// Official MeshCore app uses 5 (with auto-retry) or 3 (without).
static const int maxRetryAttemptsWithPath = 5;
/// No retries for flood-only contacts (no known path).
/// Value 0 means: don't retry at all, go straight to fallback/fail.
static const int maxRetryAttemptsFloodOnly = 0;
@Deprecated('Use maxRetryAttemptsForContact instead')
static const int maxRetryAttempts = maxRetryAttemptsWithPath;
// Retry backoff values in milliseconds.
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
static const List<int> _retryDelays = [1000, 2000, 4000, 8000, 8000];
static const int _defaultLoRaSf = 10;
static const int _defaultLoRaCr = 5;
static const int _defaultLoRaBwHz = 250000;
@@ -38,32 +48,57 @@ class MessageRetryManager {
return _retryDelays[attempt];
}
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
/// provide one or returns an invalid value.
static final math.Random _rng = math.Random();
/// Calculate a delivery-ACK timeout with random jitter.
///
/// Matches the official MeshCore app: `suggestedTimeout + random(1-8s)`.
/// The jitter prevents collision when multiple messages are in flight.
int calculateAckTimeoutMs({
required String text,
required Contact? contact,
int? suggestedTimeoutMs,
}) {
int baseTimeout;
if (suggestedTimeoutMs != null && suggestedTimeoutMs > 0) {
return suggestedTimeoutMs;
baseTimeout = suggestedTimeoutMs;
} else {
final payloadBytes = utf8.encode(text).length;
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
final hopCount = contact?.routeHasPath == true
? contact!.routeHopCount
: -1;
if (hopCount < 0) {
baseTimeout = ((airtimeMs * 10) + 4000).clamp(10000, 30000);
} else {
baseTimeout = ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
}
}
final payloadBytes = utf8.encode(text).length;
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
final hopCount = contact?.routeHasPath == true
? contact!.routeHopCount
: -1;
// Add random jitter: 500-2000ms to avoid collision
final jitterMs = 500 + _rng.nextInt(1501);
return baseTimeout + jitterMs;
}
if (hopCount < 0) {
return ((airtimeMs * 10) + 4000).clamp(10000, 30000);
}
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
/// Max attempts for a given contact based on whether it has a known path.
static int maxRetryAttemptsForContact(Contact? contact) {
final hasPath = contact?.routeHasPath ?? false;
return hasPath ? maxRetryAttemptsWithPath : maxRetryAttemptsFloodOnly;
}
bool canRetry(Message message, Contact contact) {
return message.retryAttempt < maxRetryAttempts;
return message.retryAttempt < maxRetryAttemptsForContact(contact);
}
/// Whether this is the last retry attempt.
/// When true, the caller should reset the path to force flood mode.
///
/// Note: called AFTER retryAttempt has been incremented, so we compare
/// directly against maxAttempts (not +1).
bool isLastAttempt(Message message, Contact contact) {
final maxAttempts = maxRetryAttemptsForContact(contact);
return maxAttempts > 1 && message.retryAttempt >= maxAttempts;
}
/// Track a retry attempt for a message

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -18,6 +18,8 @@ import '../utils/image_message_parser.dart';
import '../l10n/app_localizations.dart';
import 'helpers/message_retry_manager.dart';
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);
@@ -28,11 +30,14 @@ class MessagesProvider with ChangeNotifier {
final MessageStorageService _storageService = MessageStorageService();
final NotificationService _notificationService = NotificationService();
bool _isInitialized = false;
bool _isPersisting = false;
bool _persistRequested = false;
AppLocalizations? _localizations;
final Map<String, MessageContactLocation> _messageContactLocations = {};
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 = {};
@@ -91,6 +96,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,
@@ -145,6 +154,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;
@@ -307,19 +317,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);
@@ -549,6 +604,15 @@ class MessagesProvider with ChangeNotifier {
}
}
final incomingPathBytes = receptionDetailsSnapshot?.pathBytes;
if (incomingPathBytes != null &&
incomingPathBytes.isNotEmpty &&
finalMessage.pathBytes == null) {
finalMessage = finalMessage.copyWith(
pathBytes: Uint8List.fromList(incomingPathBytes),
);
}
// Debug: Check if message is SAR
if (message.text.startsWith('S:')) {
debugPrint(
@@ -580,8 +644,21 @@ class MessagesProvider with ChangeNotifier {
if (contactLocationSnapshot != null) {
_messageContactLocations[existingId] = contactLocationSnapshot;
}
_messageReceptionDetails[existingId] =
MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
incoming: receptionDetailsSnapshot,
);
if (receptionDetailsSnapshot != null) {
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
final existingMessage = _messages[duplicateIndex];
final duplicatePathBytes = receptionDetailsSnapshot.pathBytes;
if (existingMessage.pathBytes == null &&
duplicatePathBytes != null &&
duplicatePathBytes.isNotEmpty) {
_messages[duplicateIndex] = existingMessage.copyWith(
pathBytes: Uint8List.fromList(duplicatePathBytes),
);
}
}
_persistMessages();
return; // Skip duplicate
@@ -658,7 +735,17 @@ class MessagesProvider with ChangeNotifier {
}
if (message.isContactMessage) {
return existing.senderKeyShort == message.senderKeyShort;
// Match by sender key + sender timestamp (matches official app's DB
// uniqueness: contactPublicKey + senderTimestamp + text + txtType).
if (existing.senderKeyShort == message.senderKeyShort &&
existing.senderTimestamp == message.senderTimestamp) {
return true;
}
// Fallback dedup when retransmits surface as separate inbound rows
// without a stable timestamp/message id, but still carry the same
// visible sender identity and payload.
return _matchesDuplicateSenderIdentity(existing, message);
}
if (message.isChannelMessage) {
@@ -666,56 +753,41 @@ class MessagesProvider with ChangeNotifier {
return false;
}
// Primary dedup: same senderTimestamp = same message from mesh repeats.
// Matches official app's DB uniqueness: (channelSecret, senderTimestamp, text).
if (existing.senderTimestamp == message.senderTimestamp) {
return true;
}
// Secondary: catch near-duplicate repeats within a 30s window
// (clock drift between nodes).
final withinChannelRepeatWindow =
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
final existingSenderKey = existing.senderKeyShort;
final incomingSenderKey = message.senderKeyShort;
if (existingSenderKey != null &&
incomingSenderKey != null &&
existingSenderKey == incomingSenderKey &&
withinChannelRepeatWindow) {
return true;
if (!withinChannelRepeatWindow) {
return false;
}
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 true;
}
if (!withinChannelRepeatWindow) {
return false;
}
// Mirror meshcore-open behavior for self-authored channel messages:
// zero-hop self replays are dropped earlier in AppProvider, while
// routed self replays should fold into the local sent bubble when the
// public sender name matches within a short window.
if (existing.isSentMessage &&
!message.isSentMessage &&
existingSenderName == incomingSenderName) {
return true;
}
return true;
}
return false;
return _matchesDuplicateSenderIdentity(existing, message);
}
return true;
// System messages and other types: never deduplicate by scope alone.
return false;
}
bool _matchesDuplicateSenderIdentity(Message existing, Message message) {
final existingSenderKey = existing.senderKeyShort;
final incomingSenderKey = message.senderKeyShort;
if (existingSenderKey != null &&
incomingSenderKey != null &&
existingSenderKey == incomingSenderKey) {
return true;
}
final existingSenderName = _normalizedResolvedSenderName(existing);
final incomingSenderName = _normalizedResolvedSenderName(message);
return existingSenderName != null &&
incomingSenderName != null &&
existingSenderName == incomingSenderName;
}
/// Add multiple messages
@@ -725,10 +797,42 @@ class MessagesProvider with ChangeNotifier {
for (final message in messages) {
// Always enhance message with SAR parser to detect SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);
var enhancedMessage = SarMessageParser.enhanceMessage(message);
// Check if it's a drawing message (D:...) and not already marked
if (DrawingMessageParser.isDrawingMessage(enhancedMessage.text) &&
!enhancedMessage.isDrawing) {
final drawing = DrawingMessageParser.parseDrawingMessage(
enhancedMessage.text,
senderName: enhancedMessage.senderName,
messageId: enhancedMessage.id,
);
enhancedMessage = enhancedMessage.copyWith(
isDrawing: true,
drawingId: drawing?.id,
);
}
// Check if it's a voice message and not already marked
if (!enhancedMessage.isVoice) {
final envelope = VoiceEnvelope.tryParseText(enhancedMessage.text);
if (envelope != null) {
enhancedMessage = enhancedMessage.copyWith(
isVoice: true,
voiceId: envelope.sessionId,
);
}
}
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
// Check for duplicates
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
final duplicateIndex = _findDuplicateMessageIndex(enhancedMessage);
if (duplicateIndex != -1) {
final existingId = _messages[duplicateIndex].id;
_messageReceptionDetails[existingId] =
MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
);
duplicateCount++;
continue; // Skip duplicate
}
@@ -973,18 +1077,32 @@ class MessagesProvider with ChangeNotifier {
return '${mib.toStringAsFixed(mib >= 10 ? 0 : 1)} MB';
}
/// Persist messages to storage (async, non-blocking)
/// Persist messages to storage (async, non-blocking, coalescing).
///
/// Multiple rapid calls are coalesced into a single write to avoid
/// overlapping serialization and redundant SharedPreferences writes.
Future<void> _persistMessages() async {
_persistRequested = true;
if (_isPersisting) {
return; // A write is in flight; it will pick up our changes.
}
_isPersisting = true;
try {
await _storageService.saveMessages(
_messages,
messageContactLocations: _messageContactLocations,
messageReceptionDetails: _messageReceptionDetails,
messageTransferDetails: _messageTransferDetails,
messageRouteMetadata: _messageRouteMetadata,
);
while (_persistRequested) {
_persistRequested = false;
await _storageService.saveMessages(
_messages,
messageContactLocations: _messageContactLocations,
messageReceptionDetails: _messageReceptionDetails,
messageTransferDetails: _messageTransferDetails,
messageRouteMetadata: _messageRouteMetadata,
namespace: _storageNamespace,
);
}
} catch (e) {
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
} finally {
_isPersisting = false;
}
}
@@ -1028,6 +1146,7 @@ class MessagesProvider with ChangeNotifier {
for (final messageId in messageIdsToRemove) {
_timeoutTimers[messageId]?.cancel();
_timeoutTimers.remove(messageId);
_clearChannelSendWarning(messageId);
_messageContactMap.remove(messageId);
_groupedMessageMapping.remove(messageId);
_messageContactLocations.remove(messageId);
@@ -1062,6 +1181,35 @@ class MessagesProvider with ChangeNotifier {
return sorted.take(count).toList();
}
List<DisplayMessageEntry> buildDisplayMessages(Iterable<Message> messages) {
final entries = <DisplayMessageEntry>[];
for (final message in messages) {
final occurrenceCount = _messageOccurrenceCount(message);
final existingIndex = entries.indexWhere(
(entry) =>
entry.message.text == message.text &&
_matchesDuplicateScope(entry.message, message),
);
if (existingIndex == -1) {
entries.add((message: message, occurrenceCount: occurrenceCount));
continue;
}
final existingEntry = entries[existingIndex];
entries[existingIndex] = (
message: existingEntry.message,
occurrenceCount: existingEntry.occurrenceCount + occurrenceCount,
);
}
return entries;
}
int _messageOccurrenceCount(Message message) =>
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
/// Get messages from last N hours
List<Message> getMessagesSince(Duration duration) {
final cutoff = DateTime.now().subtract(duration);
@@ -1097,7 +1245,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();
}
@@ -1222,7 +1373,12 @@ class MessagesProvider with ChangeNotifier {
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
}
@@ -1245,20 +1401,16 @@ class MessagesProvider with ChangeNotifier {
/// Clear all messages
void clearMessages() {
for (final timer in _channelEchoWarningTimers.values) {
timer.cancel();
}
_channelEchoWarningTimers.clear();
_channelEchoWarningMessageIds.clear();
_messages.clear();
_sarMarkers.clear();
_removedSarMarkerIds.clear();
_messageContactLocations.clear();
_messageReceptionDetails.clear();
_messageTransferDetails.clear();
_messageRouteMetadata.clear();
_cancelAllTimers();
_resetInMemoryState();
_retryManager.clearAll();
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
@@ -1266,17 +1418,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() {
for (final timer in _channelEchoWarningTimers.values) {
timer.cancel();
}
_channelEchoWarningTimers.clear();
_channelEchoWarningMessageIds.clear();
_cancelAllTimers();
_resetInMemoryState();
_retryManager.clearAll();
_persistMessages();
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
void _resetInMemoryState() {
_messages.clear();
_sarMarkers.clear();
_removedSarMarkerIds.clear();
@@ -1284,9 +1448,13 @@ class MessagesProvider with ChangeNotifier {
_messageReceptionDetails.clear();
_messageTransferDetails.clear();
_messageRouteMetadata.clear();
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
notifyListeners();
_pendingSentMessages.clear();
_messageContactMap.clear();
_groupedMessageMapping.clear();
_ackTagToRecipients.clear();
_messageAckHistory.clear();
_ackHistoryLookup.clear();
_completedAckHistory.clear();
}
int transferCountForSession({
@@ -1964,7 +2132,7 @@ class MessagesProvider with ChangeNotifier {
debugPrint(' Message index in list: $index');
if (index != -1) {
final updatedMessage = message.copyWith(
final updatedMessage = _messages[index].copyWith(
deliveryStatus: MessageDeliveryStatus.delivered,
roundTripTimeMs: roundTripTimeMs,
deliveredAt: DateTime.now(),
@@ -2146,7 +2314,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');
@@ -2186,6 +2354,17 @@ class MessagesProvider with ChangeNotifier {
return;
}
// On the last attempt, reset the path to force flood mode
// (matches official MeshCore app behaviour)
if (_retryManager.isLastAttempt(currentMessage, contact)) {
debugPrint(
'🔄 [MessagesProvider] Last attempt — resetting path to flood for $messageId',
);
if (resetPathBeforeLastRetryCallback != null) {
await resetPathBeforeLastRetryCallback!(contact);
}
}
if (sendMessageCallback != null) {
final queued = await sendMessageCallback!(
contactPublicKey: contact.publicKey,
@@ -2323,11 +2502,14 @@ class MessagesProvider with ChangeNotifier {
senderPublicKeyPrefix: message.senderPublicKeyPrefix,
channelIdx: message.channelIdx,
pathLen: message.pathLen,
pathBytes: message.pathBytes,
textType: message.textType,
senderTimestamp: message.senderTimestamp,
text: message.text,
isSarMarker: message.isSarMarker,
sarGpsCoordinates: message.sarGpsCoordinates,
sarCustomMapPoint: message.sarCustomMapPoint,
sarCustomMapId: message.sarCustomMapId,
sarNotes: message.sarNotes,
sarCustomEmoji: message.sarCustomEmoji,
sarColorIndex: message.sarColorIndex,
@@ -2410,7 +2592,19 @@ class MessagesProvider with ChangeNotifier {
@override
void dispose() {
// Cancel all pending timeout timers
_cancelAllTimers();
_completedAckHistory.clear();
_messageAckHistory.clear();
_ackHistoryLookup.clear();
// Clear retry manager
_retryManager.clearAll();
super.dispose();
}
/// Cancel all pending timers (timeout + echo warning).
void _cancelAllTimers() {
for (final timer in _timeoutTimers.values) {
timer.cancel();
}
@@ -2420,14 +2614,6 @@ class MessagesProvider with ChangeNotifier {
}
_channelEchoWarningTimers.clear();
_channelEchoWarningMessageIds.clear();
_completedAckHistory.clear();
_messageAckHistory.clear();
_ackHistoryLookup.clear();
// Clear retry manager
_retryManager.clearAll();
super.dispose();
}
void _rememberCompletedAck(int ackCode) {

View File

@@ -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 int selfAutoRefreshMinutes = 1;
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 _isRefreshDue(
String publicKeyHex, {
required int minutes,
required DateTime refreshTime,
}) {
if (minutes <= 0) {
return false;
}
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
return lastRefreshAt == null ||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes);
}
Future<void> toggleMetric(
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) &&
_isRefreshDue(
self.publicKeyHex,
minutes: selfAutoRefreshMinutes,
refreshTime: refreshTime,
)) {
dueKeys.insert(0, self.publicKeyHex);
}
if (dueKeys.isEmpty) {
return;
}
_isRunningAutoRefreshTick = true;
try {
for (final key in dueKeys) {
await refreshSensor(
publicKeyHex: key,
contactsProvider: contactsProvider,
connectionProvider: connectionProvider,
requestedAt: refreshTime,
);
}
} finally {
_isRunningAutoRefreshTick = false;
}
}
void clearExpiredRefreshStates({DateTime? now}) {
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,
);
}
}

View File

@@ -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();

View 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

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@@ -6,12 +8,13 @@ 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';
import 'messages_tab.dart';
import 'contacts_tab.dart';
import 'discovery_screen.dart';
import 'sensors_tab.dart';
import 'map_tab.dart';
import 'repeaters_map_screen.dart';
@@ -19,6 +22,7 @@ import 'settings_screen.dart';
import 'device_config_screen.dart';
import 'packet_log_screen.dart';
import 'live_traffic_screen.dart';
import 'profiles_screen.dart';
import 'spectrum_scan_screen.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
@@ -27,6 +31,10 @@ 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 }
@@ -56,6 +64,7 @@ class _HomeScreenState extends State<HomeScreen>
with TickerProviderStateMixin, WidgetsBindingObserver {
late TabController _tabController;
late final AppProvider _appProvider;
late final ConnectionProvider _connectionProvider;
int _currentIndex = 0;
bool _isMapFullscreen = false;
bool _showRxTxIndicators = true;
@@ -64,6 +73,7 @@ class _HomeScreenState extends State<HomeScreen>
bool _isContactsEnabled = true;
bool _isSensorsEnabled = false;
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
String? _lastProfileDeviceKey;
List<_HomeTab> get _enabledTabs {
return [
@@ -87,6 +97,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;
@@ -104,6 +116,10 @@ class _HomeScreenState extends State<HomeScreen>
_showPermissionDialog();
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleConnectionProviderChanged();
});
}
void _initTabController() {
@@ -219,6 +235,36 @@ class _HomeScreenState extends State<HomeScreen>
);
}
void _handleConnectionProviderChanged() {
final deviceKey = ProfileDeviceKeyResolver.resolve(
deviceInfo: _connectionProvider.deviceInfo,
connectionMode: _connectionProvider.connectionMode,
);
if (_lastProfileDeviceKey == deviceKey) {
return;
}
_lastProfileDeviceKey = deviceKey;
if (deviceKey == null || !mounted) {
return;
}
unawaited(
context
.read<ProfileWorkspaceCoordinator>()
.syncActiveProfileForCurrentDevice(),
);
}
@override
void dispose() {
_connectionProvider.removeListener(_handleConnectionProviderChanged);
WidgetsBinding.instance.removeObserver(this);
_appProvider.setFastLocationUiActive(false);
_appProvider.removeListener(_handleAppProviderChanged);
_tabController.removeListener(_onTabChanged);
_tabController.dispose();
super.dispose();
}
void _openLiveTraffic(ConnectionProvider provider) {
openLiveTrafficScreen(context, provider);
}
@@ -236,7 +282,11 @@ 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;
});
}
}
@@ -249,16 +299,6 @@ class _HomeScreenState extends State<HomeScreen>
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_appProvider.setFastLocationUiActive(false);
_appProvider.removeListener(_handleAppProviderChanged);
_tabController.removeListener(_onTabChanged);
_tabController.dispose();
super.dispose();
}
void _showPermissionDialog() {
if (!mounted) return;
@@ -354,7 +394,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),
@@ -380,7 +420,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),
@@ -394,45 +434,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,
@@ -589,6 +768,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;
@@ -613,7 +795,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,
);
@@ -625,6 +807,9 @@ class _HomeScreenState extends State<HomeScreen>
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
final items = <PopupMenuEntry<void>>[];
final profilesEnabled = context
.read<ProfileManager>()
.profilesEnabled;
if (_isDeveloperModeEnabled) {
items.add(
@@ -632,8 +817,8 @@ class _HomeScreenState extends State<HomeScreen>
child: Row(
children: [
const Icon(Icons.radar),
const SizedBox(width: 8),
const Text('Spectrum Scan'),
SizedBox(width: 8),
Text(AppLocalizations.of(context)!.spectrumScan),
],
),
onTap: () {
@@ -654,11 +839,11 @@ class _HomeScreenState extends State<HomeScreen>
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: () {
@@ -689,11 +874,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: () {
@@ -716,8 +901,41 @@ class _HomeScreenState extends State<HomeScreen>
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.settings),
const Icon(Icons.person_search),
const SizedBox(width: 8),
Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) {
final pendingCount =
contactsProvider.pendingAdverts.length;
return Text(
pendingCount > 0
? 'Discovery ($pendingCount)'
: 'Discovery',
);
},
),
],
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
);
});
},
),
);
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.settings),
SizedBox(width: 8),
Text(AppLocalizations.of(context)!.settings),
],
),
@@ -742,6 +960,31 @@ class _HomeScreenState extends State<HomeScreen>
),
);
if (profilesEnabled) {
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.layers_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.profiles),
],
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => const ProfilesScreen(),
),
);
});
},
),
);
}
return items;
},
),
@@ -768,9 +1011,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;
@@ -827,7 +1075,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:
@@ -852,19 +1100,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),
@@ -875,22 +1139,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),
),
),
),
@@ -898,7 +1159,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,
@@ -957,50 +1218,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,
@@ -1010,9 +1266,9 @@ class _HomeScreenState extends State<HomeScreen>
),
),
),
),
],
],
],
),
),
],
),
@@ -1109,48 +1365,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),
],
);
},

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:provider/provider.dart';
@@ -11,7 +12,10 @@ import '../services/live_traffic_summary.dart';
import '../services/location_tracking_service.dart';
import '../services/route_hash_preferences.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 {
@@ -23,7 +27,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;
@@ -31,7 +34,6 @@ class LiveTrafficScreen extends StatefulWidget {
const LiveTrafficScreen({
super.key,
required this.logReader,
this.rxCountReader,
this.refreshListenable,
DateTime Function()? now,
this.openPacketLogs,
@@ -45,7 +47,6 @@ class LiveTrafficScreen extends StatefulWidget {
return LiveTrafficScreen(
key: key,
logReader: () => provider.bleService.packetLogs,
rxCountReader: () => provider.rxPacketCount,
refreshListenable: provider,
openPacketLogs: openPacketLogs,
);
@@ -114,15 +115,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,
@@ -134,16 +137,17 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
final routeHashCounts = _RouteHashCounts.fromContacts(
contactsProvider?.contacts ?? const <Contact>[],
);
final packetTypes = unfilteredSnapshot.visibleEntries
.map((entry) => entry.payloadLabel)
.toSet()
.toList()
..sort();
final packetTypes =
unfilteredSnapshot.visibleEntries
.map((entry) => entry.payloadLabel)
.toSet()
.toList()
..sort();
final filteredEntries = snapshot.visibleEntries;
return Scaffold(
appBar: AppBar(
title: const Text('Live Traffic'),
title: Text(AppLocalizations.of(context)!.liveTraffic),
actions: [
if (widget.openPacketLogs != null)
IconButton(
@@ -172,7 +176,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
children: [
_SummaryPanel(
snapshot: snapshot,
totalRxCount: widget.rxCountReader?.call(),
totalRxCount: totalRxDataCount,
routeHashCounts: routeHashCounts,
onWindowTap: () => _showWindowPicker(context),
),
@@ -202,7 +206,9 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
Icon(
Icons.radar_rounded,
size: 64,
color: theme.colorScheme.primary.withValues(alpha: 0.45),
color: theme.colorScheme.primary.withValues(
alpha: 0.45,
),
),
const SizedBox(height: 16),
const Text(
@@ -218,7 +224,9 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
? 'This view only shows in-memory traffic while it is active.'
: 'Try a different packet type or switch back to All.',
textAlign: TextAlign.center,
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
@@ -297,8 +305,48 @@ class _SummaryPanel extends StatelessWidget {
LiveTrafficBusyness.active => 'Active',
LiveTrafficBusyness.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(
@@ -319,17 +367,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,
@@ -337,44 +385,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),
],
);
},
),
],
),
@@ -447,7 +477,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
child: Row(
children: [
_FilterChip(
label: 'All',
label: AppLocalizations.of(context)!.all,
selected: selectedType == null,
onTap: () => onSelected(null),
),
@@ -488,7 +518,9 @@ class _FilterChip extends StatelessWidget {
decoration: BoxDecoration(
color: color.withValues(alpha: selected ? 0.14 : 0.08),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color.withValues(alpha: selected ? 0.45 : 0.22)),
border: Border.all(
color: color.withValues(alpha: selected ? 0.45 : 0.22),
),
),
child: Text(
label,
@@ -514,12 +546,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,
@@ -529,7 +563,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(
@@ -641,103 +675,103 @@ class _LiveTrafficCard extends StatelessWidget {
final isRx = log.direction == PacketDirection.rx;
final accent = isRx ? Colors.green : Colors.blue;
final rxInfo = log.logRxDataInfo;
final routePreview = _resolvedRoutePreview(context, entry);
final originDistance = _originDistanceLabel(context, entry);
final packetDetails = _LiveTrafficPacketDetails.fromEntry(entry);
final signalMetric = SignalMetric.fromRxInfo(rxInfo);
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showPacketBytesSheet(context, log.rawData),
onLongPress: () => _showTraceSheet(context, entry),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: accent.withValues(alpha: 0.25)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: accent.withValues(alpha: 0.25)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(999),
),
child: Text(
isRx ? 'RX' : 'TX',
style: TextStyle(
color: accent,
fontWeight: FontWeight.w800,
fontSize: 12,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.payloadLabel,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(999),
),
child: Text(
isRx ? 'RX' : 'TX',
style: TextStyle(
color: accent,
fontWeight: FontWeight.w800,
fontSize: 12,
),
),
if (entry.payloadMeaning != null)
Text(
entry.payloadMeaning!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
packetDetails.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
if (entry.payloadMeaning != null)
Text(
entry.payloadMeaning!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
if (signalMetric != null) ...[
const SizedBox(width: 12),
CompactSignalIndicator(metric: signalMetric),
] else
Text(
_timeAgo(log.timestamp, now),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
],
),
Text(
_timeAgo(log.timestamp, now),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 10),
_PacketInfoLine(
text:
'${_formatClock(log.timestamp)} • Size: ${log.rawData.length} bytes',
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_PacketMetaChip(
label: '${log.rawData.length} bytes',
onTap: () => _showPacketBytesSheet(context, log.rawData),
),
if (rxInfo?.rssiDbm != null)
_PacketMetaChip(label: 'RSSI ${rxInfo!.rssiDbm} dBm'),
if (rxInfo?.snrDb != null)
_PacketMetaChip(
label: 'SNR ${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
),
if (entry.hopCount != null)
_PacketMetaChip(label: '${entry.hopCount} hops'),
if (entry.isMultiHop)
const _PacketMetaChip(label: 'MULTI-HOP', emphasized: true),
_PacketInfoLine(text: 'Hash: ${packetDetails.packetHashHex}'),
if (packetDetails.pathLine != null)
_PacketInfoLine(text: packetDetails.pathLine!),
if (packetDetails.pathHashLine != null)
_PacketInfoLine(text: packetDetails.pathHashLine!),
if (packetDetails.endpointLine != null)
_PacketInfoLine(text: packetDetails.endpointLine!),
if (originDistance != null)
_PacketMetaChip(label: 'Origin $originDistance'),
_PacketInfoLine(text: 'Origin: $originDistance'),
],
),
const SizedBox(height: 10),
Text(
routePreview,
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
@@ -748,36 +782,11 @@ class _LiveTrafficCard extends StatelessWidget {
return '${diff.inMinutes}m ago';
}
static String _resolvedRoutePreview(
BuildContext context,
LiveTrafficEntry entry,
) {
final route = entry.route;
if (route == null || route.hopHashes.isEmpty) {
return entry.routePreview;
}
final contactsProvider = _maybeProvider<ContactsProvider>(context);
final connectionProvider = _maybeProvider<ConnectionProvider>(context);
if (contactsProvider == null && connectionProvider == null) {
return entry.routePreview;
}
final ownLatLng = _ownLatLng(connectionProvider);
final resolvedLabels = route.hopHashes.map((hashHex) {
final resolved = LogRxRouteDecoder.resolveHash(
hashHex,
contacts: contactsProvider?.contacts ?? const <Contact>[],
ownPublicKey: connectionProvider?.deviceInfo.publicKey,
ownName:
connectionProvider?.deviceInfo.selfName ??
connectionProvider?.deviceInfo.displayName,
ownLatitude: ownLatLng?.latitude,
ownLongitude: ownLatLng?.longitude,
);
return _compactNodeLabel(resolved);
}).toList();
return resolvedLabels.join(' -> ');
static String _formatClock(DateTime timestamp) {
final hour = timestamp.hour.toString().padLeft(2, '0');
final minute = timestamp.minute.toString().padLeft(2, '0');
final second = timestamp.second.toString().padLeft(2, '0');
return '$hour:$minute:$second';
}
static String? _originDistanceLabel(
@@ -856,11 +865,16 @@ class _LiveTrafficCard extends StatelessWidget {
final hex = chunk
.map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(' ');
hexLines.add('${offset.toRadixString(16).padLeft(4, '0').toUpperCase()}: $hex');
hexLines.add(
'${offset.toRadixString(16).padLeft(4, '0').toUpperCase()}: $hex',
);
}
final ascii = data
.map((byte) => (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.')
.map(
(byte) =>
(byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.',
)
.join();
return showModalBottomSheet<void>(
@@ -886,10 +900,7 @@ class _LiveTrafficCard extends StatelessWidget {
style: TextStyle(color: scheme.onSurfaceVariant),
),
const SizedBox(height: 16),
Text(
'Hex',
style: Theme.of(context).textTheme.titleSmall,
),
Text('Hex', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -909,10 +920,7 @@ class _LiveTrafficCard extends StatelessWidget {
),
),
const SizedBox(height: 16),
Text(
'ASCII',
style: Theme.of(context).textTheme.titleSmall,
),
Text('ASCII', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -940,14 +948,182 @@ class _LiveTrafficCard extends StatelessWidget {
);
}
static String _compactNodeLabel(ResolvedNodeHash node) {
if (node.isOwnNode) {
return node.label;
static Future<void> _showTraceSheet(
BuildContext context,
LiveTrafficEntry entry,
) {
final route = entry.route;
if (route == null || route.pathBytes.isEmpty) {
return Future.value();
}
if (node.matchCount > 0) {
return node.label;
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => MessageTraceSheet.packetPath(
packetPath: route.pathBytes,
descriptionOverride:
'Relay path from packet path bytes (${route.hopHashes.length} hop${route.hopHashes.length == 1 ? '' : 's'})',
noRelayMatchTextOverride:
'No named nodes could be matched for this packet path.',
),
);
}
}
class _LiveTrafficPacketDetails {
final String title;
final String packetHashHex;
final String? pathLine;
final String? pathHashLine;
final String? endpointLine;
const _LiveTrafficPacketDetails({
required this.title,
required this.packetHashHex,
required this.pathLine,
required this.pathHashLine,
required this.endpointLine,
});
factory _LiveTrafficPacketDetails.fromEntry(LiveTrafficEntry entry) {
final route = entry.route;
final payloadType = route?.payloadType;
final parsedPayload = _ParsedTrafficPayload.tryParse(
entry.log.rawData,
route,
);
final title = switch (payloadType) {
0x00 => 'FLOOD REQUEST',
0x01 => 'FLOOD RESPONSE',
0x02 => 'FLOOD TEXT',
0x03 => 'FLOOD ACK',
0x04 => 'FLOOD ADVERTISEMENT',
0x05 => 'FLOOD GROUP_TEXT',
0x06 => 'FLOOD GROUP_DATA',
0x07 => 'FLOOD ANON_REQUEST',
0x08 => 'FLOOD RETURNED_PATH',
0x09 => 'FLOOD TRACE_PATH',
0x0A => 'FLOOD MULTIPART',
0x0B => 'FLOOD CONTROL',
_ => entry.payloadLabel.toUpperCase(),
};
final hopHashes = route?.hopHashes ?? const <String>[];
final pathLine = hopHashes.isEmpty
? null
: 'Path: ${hopHashes.length} hop${hopHashes.length == 1 ? '' : 's'} [${hopHashes.join(',')}]';
final pathHashLine = route == null
? null
: 'Path Hashes: ${route.hashSize}-byte per hop';
return _LiveTrafficPacketDetails(
title: title,
packetHashHex: _packetHash(entry.log.rawData),
pathLine: pathLine,
pathHashLine: pathHashLine,
endpointLine: parsedPayload?.endpointLine,
);
}
static String _packetHash(List<int> bytes) {
final digest = md5.convert(bytes).bytes;
return digest
.take(8)
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
}
class _ParsedTrafficPayload {
final String? endpointLine;
const _ParsedTrafficPayload({this.endpointLine});
static _ParsedTrafficPayload? tryParse(
List<int> rawData,
DecodedLogRxRoute? route,
) {
if (rawData.length < 5 ||
rawData.first != LiveTrafficSummary.logRxDataResponseCode) {
return null;
}
return node.hexLabel;
final rawPacketData = rawData.sublist(3);
if (rawPacketData.length < 2) return null;
final header = rawPacketData[0];
final routeType = header & 0x03;
final payloadType = (header >> 2) & 0x0F;
var index = 1;
if (routeType == 0x00 || routeType == 0x03) {
if (rawPacketData.length < index + 5) return null;
index += 4;
}
if (rawPacketData.length <= index) return null;
final pathDescriptor = rawPacketData[index++];
final pathByteLen =
LogRxRouteDecoder.descriptorByteLength(pathDescriptor) ??
(pathDescriptor == 0xFF ? 0 : null);
if (pathByteLen == null || rawPacketData.length < index + pathByteLen) {
return null;
}
index += pathByteLen;
final payload = rawPacketData.sublist(index);
final senderHash = route?.hopHashes.isNotEmpty == true
? route!.hopHashes.first
: null;
switch (payloadType) {
case 0x05:
case 0x06:
if (payload.isEmpty) return const _ParsedTrafficPayload();
return _ParsedTrafficPayload(
endpointLine:
'Channel Hash: ${payload.first.toRadixString(16).padLeft(2, '0').toUpperCase()}',
);
case 0x00:
case 0x01:
case 0x07:
if (payload.isEmpty) return const _ParsedTrafficPayload();
final destinationHash = payload.first
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return _ParsedTrafficPayload(
endpointLine: senderHash == null
? 'To: <$destinationHash>'
: 'From: <${senderHash.toUpperCase()}> To: <$destinationHash>',
);
default:
return const _ParsedTrafficPayload();
}
}
}
class _PacketInfoLine extends StatelessWidget {
final String text;
const _PacketInfoLine({required this.text});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
text,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
}
@@ -958,44 +1134,6 @@ class _GeoPoint {
const _GeoPoint(this.latitude, this.longitude);
}
class _PacketMetaChip extends StatelessWidget {
final String label;
final bool emphasized;
final VoidCallback? onTap;
const _PacketMetaChip({
required this.label,
this.emphasized = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final color = emphasized ? scheme.primary : scheme.outline;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: emphasized ? 0.12 : 0.08),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color.withValues(alpha: 0.22)),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
),
),
);
}
}
void openLiveTrafficScreen(BuildContext context, ConnectionProvider provider) {
Navigator.push(
context,

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ import '../services/voice_recorder_service.dart';
import '../services/voice_codec_service.dart';
import '../utils/toast_logger.dart';
import '../utils/key_comparison.dart';
import '../utils/contact_sorting.dart';
import '../utils/voice_message_parser.dart';
import '../utils/image_message_parser.dart';
import '../utils/tictactoe_message_parser.dart';
@@ -47,6 +48,110 @@ class MessagesTab extends StatefulWidget {
State<MessagesTab> createState() => _MessagesTabState();
}
class _ComposerActionTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final Color color;
final bool enabled;
final VoidCallback? onTap;
const _ComposerActionTile({
required this.icon,
required this.title,
required this.subtitle,
required this.color,
this.enabled = true,
this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final effectiveColor = enabled
? color
: colorScheme.onSurfaceVariant.withValues(alpha: 0.45);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: enabled ? onTap : null,
child: Ink(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
enabled
? colorScheme.surfaceContainerLow
: colorScheme.surfaceContainerHighest,
enabled
? effectiveColor.withValues(alpha: 0.08)
: colorScheme.surfaceContainerHigh,
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: enabled
? effectiveColor.withValues(alpha: 0.16)
: colorScheme.outlineVariant.withValues(alpha: 0.16),
),
boxShadow: [
BoxShadow(
color: effectiveColor.withValues(alpha: enabled ? 0.08 : 0.03),
blurRadius: 14,
offset: const Offset(0, 8),
),
],
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: effectiveColor.withValues(alpha: enabled ? 0.14 : 0.10),
borderRadius: BorderRadius.circular(14),
),
alignment: Alignment.center,
child: Icon(icon, color: effectiveColor, size: 20),
),
const SizedBox(height: 8),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
letterSpacing: -0.2,
color: enabled
? colorScheme.onSurface
: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
subtitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.15,
),
),
],
),
),
),
),
);
}
}
class _MessagesTabState extends State<MessagesTab> {
static const int _maxContactMessageBytes = 156;
static const int _maxChannelMessageBytes = 127;
@@ -61,9 +166,10 @@ class _MessagesTabState extends State<MessagesTab> {
Timer? _highlightTimer; // Timer for clearing message highlight
Timer? _channelReadTimer;
String? _pendingChannelReadKey;
TextEditingValue _lastComposerValue = const TextEditingValue();
bool _isMentionPickerOpen = false;
bool _suppressMentionTrigger = false;
TextRange? _activeMentionRange;
String _mentionQuery = '';
List<Contact> _mentionSuggestions = const [];
// Message destination state
String _destinationType =
@@ -92,8 +198,8 @@ class _MessagesTabState extends State<MessagesTab> {
@override
void initState() {
super.initState();
_lastComposerValue = _textController.value;
_textController.addListener(_handleComposerChanged);
_focusNode.addListener(_handleFocusChanged);
// Load saved message destination
_loadSavedDestination();
_loadVoiceSettings();
@@ -126,6 +232,7 @@ class _MessagesTabState extends State<MessagesTab> {
_channelReadTimer?.cancel();
_voiceStreamSub?.cancel();
_voiceRecorder.dispose();
_focusNode.removeListener(_handleFocusChanged);
_textController.dispose();
_focusNode.dispose();
_scrollController.dispose();
@@ -161,9 +268,13 @@ class _MessagesTabState extends State<MessagesTab> {
void _scrollToMessage(String messageId) {
final messagesProvider = context.read<MessagesProvider>();
final messages = _getFilteredMessages(messagesProvider);
final messages = messagesProvider.buildDisplayMessages(
_getFilteredMessages(messagesProvider),
);
final messageIndex = messages.indexWhere((m) => m.id == messageId);
final messageIndex = messages.indexWhere(
(entry) => entry.message.id == messageId,
);
if (messageIndex != -1 && _scrollController.hasClients) {
// Calculate position - accounting for reverse list
@@ -195,25 +306,13 @@ class _MessagesTabState extends State<MessagesTab> {
}
void _handleComposerChanged() {
final previousValue = _lastComposerValue;
final currentValue = _textController.value;
_lastComposerValue = currentValue;
_updateCharacterCount();
if (_suppressMentionTrigger || _isMentionPickerOpen) {
if (_suppressMentionTrigger) {
return;
}
final mentionTriggerRange = _getMentionTriggerRange(
previousValue: previousValue,
currentValue: currentValue,
);
if (mentionTriggerRange == null) {
return;
}
unawaited(_showMentionSelectorForRange(mentionTriggerRange));
_updateMentionSuggestions(_textController.value);
}
void _updateCharacterCount() {
@@ -222,43 +321,112 @@ class _MessagesTabState extends State<MessagesTab> {
});
}
TextRange? _getMentionTriggerRange({
required TextEditingValue previousValue,
required TextEditingValue currentValue,
}) {
if (!previousValue.selection.isValid ||
!currentValue.selection.isValid ||
!previousValue.selection.isCollapsed ||
!currentValue.selection.isCollapsed) {
void _handleFocusChanged() {
if (!_focusNode.hasFocus) {
_clearMentionSuggestions();
return;
}
if (_suppressMentionTrigger) {
return;
}
_updateMentionSuggestions(_textController.value);
}
void _updateMentionSuggestions(TextEditingValue value) {
final triggerRange = _getMentionTriggerRange(value);
if (triggerRange == null) {
_clearMentionSuggestions();
return;
}
final query = value.text.substring(
triggerRange.start + 1,
triggerRange.end,
);
final contacts = _buildMentionSuggestions(query);
if (!mounted) {
return;
}
setState(() {
_activeMentionRange = triggerRange;
_mentionQuery = query;
_mentionSuggestions = contacts;
});
}
void _clearMentionSuggestions() {
if (_activeMentionRange == null &&
_mentionQuery.isEmpty &&
_mentionSuggestions.isEmpty) {
return;
}
setState(() {
_activeMentionRange = null;
_mentionQuery = '';
_mentionSuggestions = const [];
});
}
TextRange? _getMentionTriggerRange(TextEditingValue value) {
if (!value.selection.isValid || !value.selection.isCollapsed) {
return null;
}
final previousOffset = previousValue.selection.baseOffset;
final currentOffset = currentValue.selection.baseOffset;
if (previousOffset < 0 || currentOffset < 0) {
final cursorOffset = value.selection.baseOffset;
if (cursorOffset <= 0) {
return null;
}
if (currentValue.text.length != previousValue.text.length + 1 ||
currentOffset != previousOffset + 1) {
final text = value.text;
final triggerStart = text.lastIndexOf('@', cursorOffset - 1);
if (triggerStart == -1) {
return null;
}
if (currentValue.text.substring(0, previousOffset) !=
previousValue.text.substring(0, previousOffset)) {
final leadingChar = triggerStart > 0 ? text[triggerStart - 1] : null;
if (leadingChar != null && !RegExp(r'[\s\(\[\{]').hasMatch(leadingChar)) {
return null;
}
if (currentValue.text.substring(currentOffset) !=
previousValue.text.substring(previousOffset)) {
final query = text.substring(triggerStart + 1, cursorOffset);
if (query.contains(RegExp(r'[\s@\[\]\n\r]'))) {
return null;
}
if (currentValue.text[previousOffset] != '@') {
return null;
}
return TextRange(start: triggerStart, end: cursorOffset);
}
return TextRange(start: previousOffset, end: currentOffset);
List<Contact> _buildMentionSuggestions(String query) {
final contactsProvider = context.read<ContactsProvider>();
final normalizedQuery = query.trim().toLowerCase();
final contacts =
contactsProvider.chatContacts.where((contact) {
if (normalizedQuery.isEmpty) return true;
return contact.displayName.toLowerCase().contains(normalizedQuery);
}).toList()..sort((a, b) {
final primary = compareContactsByFavouriteThenLastSeen(a, b);
if (primary != 0) {
return primary;
}
final aName = a.displayName.toLowerCase();
final bName = b.displayName.toLowerCase();
final aStarts =
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
final bStarts =
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
if (aStarts != bStarts) {
return aStarts ? -1 : 1;
}
return compareContactsByDisplayName(a, b);
});
return contacts.take(8).toList(growable: false);
}
int get _maxMessageBytes =>
@@ -345,16 +513,33 @@ class _MessagesTabState extends State<MessagesTab> {
final contactsProvider = context.read<ContactsProvider>();
final messagesProvider = context.read<MessagesProvider>();
// Filter contacts by type
final contacts = contactsProvider.contacts
.where((c) => c.type == ContactType.chat)
.toList();
final rooms = contactsProvider.contacts
.where((c) => c.type == ContactType.room)
.toList();
final channels = contactsProvider.contacts
.where((c) => c.type == ContactType.channel)
.toList();
final contacts = List<Contact>.from(contactsProvider.chatContacts)
..sort((a, b) {
final primary = compareContactsByFavouriteThenLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
final rooms = List<Contact>.from(contactsProvider.rooms)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
final channels = List<Contact>.from(contactsProvider.channels)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
showModalBottomSheet(
context: context,
@@ -378,56 +563,6 @@ class _MessagesTabState extends State<MessagesTab> {
);
}
Future<void> _showMentionSelectorForRange(TextRange triggerRange) async {
final contactsProvider = context.read<ContactsProvider>();
final contacts = contactsProvider.contacts
.where((contact) => contact.type == ContactType.chat)
.toList();
if (contacts.isEmpty || !mounted) {
return;
}
_isMentionPickerOpen = true;
Contact? selectedContact;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => RecipientSelectorSheet(
contacts: contacts,
rooms: const [],
channels: const [],
unreadCount: 0,
unreadCountsByPublicKey: {
for (final contact in contacts) contact.publicKeyHex: 0,
},
currentDestinationType: null,
currentRecipientPublicKey: null,
showAllOption: false,
onSelect: (_, recipient) {
selectedContact = recipient;
},
),
);
_isMentionPickerOpen = false;
if (!mounted) {
return;
}
if (selectedContact != null) {
_insertReplyMention(
selectedContact!.displayName,
replacementRange: triggerRange,
);
}
_focusNode.requestFocus();
}
/// Handle recipient selection
Future<void> _onRecipientSelected(String type, Contact? recipient) async {
setState(() {
@@ -489,15 +624,23 @@ class _MessagesTabState extends State<MessagesTab> {
selection: TextSelection.collapsed(offset: nextOffset),
composing: TextRange.empty,
);
_lastComposerValue = _textController.value;
_suppressMentionTrigger = false;
_clearMentionSuggestions();
_enforceMessageByteLimit();
}
void _selectMention(Contact contact) {
_insertReplyMention(
contact.displayName,
replacementRange: _activeMentionRange,
);
_focusNode.requestFocus();
}
Future<void> _replyToMessage(Message message) async {
final l10n = AppLocalizations.of(context)!;
final contactsProvider = context.read<ContactsProvider>();
Contact? senderContact;
String? senderDisplayName;
String destinationType;
Contact? recipient;
@@ -513,7 +656,10 @@ class _MessagesTabState extends State<MessagesTab> {
}).firstOrNull;
if (recipient == null) {
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
ToastLogger.error(
context,
AppLocalizations.of(context)!.cannotReplyContactNotFound,
);
return;
}
}
@@ -522,28 +668,54 @@ class _MessagesTabState extends State<MessagesTab> {
if (senderPrefix != null && senderPrefix.length >= 6) {
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
}
senderDisplayName =
senderContact?.displayName ?? message.senderName?.trim();
} else {
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix == null || senderPrefix.length < 6) {
ToastLogger.error(context, l10n.cannotReplySenderMissing);
return;
}
final roomRecipient = message.recipientPublicKey == null
? null
: contactsProvider.findContactByKey(message.recipientPublicKey!);
recipient = contactsProvider.findContactByPrefix(senderPrefix);
if (recipient == null) {
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
return;
}
if (roomRecipient?.isRoom == true) {
destinationType = MessageDestinationPreferences.destinationTypeRoom;
recipient = roomRecipient;
destinationType = recipient.isRoom
? MessageDestinationPreferences.destinationTypeRoom
: MessageDestinationPreferences.destinationTypeContact;
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix != null && senderPrefix.length >= 6) {
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
}
senderDisplayName =
senderContact?.displayName ?? message.senderName?.trim();
} else {
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix == null || senderPrefix.length < 6) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.cannotReplySenderMissing,
);
return;
}
recipient = contactsProvider.findContactByPrefix(senderPrefix);
if (recipient == null) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.cannotReplyContactNotFound,
);
return;
}
destinationType = recipient.isRoom
? MessageDestinationPreferences.destinationTypeRoom
: MessageDestinationPreferences.destinationTypeContact;
}
}
await _onRecipientSelected(destinationType, recipient);
if (!mounted) return;
if (message.isChannelMessage && senderContact != null) {
_insertReplyMention(senderContact.displayName);
if ((message.isChannelMessage || recipient?.isRoom == true) &&
senderDisplayName != null &&
senderDisplayName.isNotEmpty) {
_insertReplyMention(senderDisplayName);
}
_focusNode.requestFocus();
}
@@ -599,7 +771,6 @@ class _MessagesTabState extends State<MessagesTab> {
}
_textController.clear();
_focusNode.unfocus();
try {
// Check destination type and send accordingly
@@ -1334,7 +1505,7 @@ class _MessagesTabState extends State<MessagesTab> {
final decision = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Send to Public Channel?'),
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
content: Text(
'You are about to send $mediaType to the Public Channel. '
'This is not advised because everyone on the mesh may receive it. '
@@ -1347,7 +1518,7 @@ class _MessagesTabState extends State<MessagesTab> {
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Send anyway'),
child: Text(AppLocalizations.of(context)!.sendAnyway),
),
],
),
@@ -1401,81 +1572,207 @@ class _MessagesTabState extends State<MessagesTab> {
void _showComposerActions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
final theme = Theme.of(sheetContext);
final colorScheme = theme.colorScheme;
final l10n = AppLocalizations.of(context)!;
Future<void> runAction(Future<void> Function() action) async {
await _runAfterSheetDismissal(sheetContext, action);
}
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.search),
title: const Text('Search messages'),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
_showFilteredMessageSearch();
});
},
),
ListTile(
leading: const Icon(Icons.add_location_alt),
title: Text(AppLocalizations.of(context)!.sendSarMarker),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
_showSarDialog();
});
},
),
if (_voiceSupported)
ListTile(
enabled: !_isSendingVoice,
leading: Icon(_isRecording ? Icons.stop : Icons.mic),
title: Text(_isRecording ? 'Stop recording' : 'Record voice'),
onTap: _isSendingVoice
? null
: () async {
await _runAfterSheetDismissal(sheetContext, () async {
if (_isRecording) {
await _stopAndSendVoice();
} else {
await _startVoiceRecording();
}
});
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(sheetContext).size.height * 0.72,
),
child: Container(
margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
colorScheme.surface,
colorScheme.surfaceContainerLow,
],
),
ListTile(
enabled: !_isSendingImage,
leading: const Icon(Icons.photo_library),
title: const Text('Send image from gallery'),
onTap: _isSendingImage
? null
: () async {
await _runAfterSheetDismissal(sheetContext, () async {
await _pickAndSendImage(source: ImageSource.gallery);
});
borderRadius: const BorderRadius.vertical(
top: Radius.circular(32),
bottom: Radius.circular(28),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.14),
blurRadius: 28,
offset: const Offset(0, 10),
),
],
),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(999),
),
),
),
const SizedBox(height: 14),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'More actions',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -0.4,
),
),
const SizedBox(height: 4),
Text(
'Search, share, or start something from this chat.',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: colorScheme.outlineVariant.withValues(
alpha: 0.18,
),
),
),
child: IconButton(
onPressed: () => Navigator.pop(sheetContext),
tooltip: l10n.close,
icon: const Icon(Icons.close_rounded),
),
),
],
),
const SizedBox(height: 18),
LayoutBuilder(
builder: (context, constraints) {
final actions = <Widget>[
_ComposerActionTile(
icon: Icons.search_rounded,
title: l10n.searchMessages,
subtitle: 'Find text in the current conversation',
color: const Color(0xFF2B6CB0),
onTap: () => runAction(() async {
_showFilteredMessageSearch();
}),
),
_ComposerActionTile(
icon: Icons.add_location_alt_rounded,
title: l10n.sendSarMarker,
subtitle: 'Share a marker with coordinates',
color: const Color(0xFFB45309),
onTap: () => runAction(() async {
_showSarDialog();
}),
),
if (_voiceSupported)
_ComposerActionTile(
icon: _isRecording
? Icons.stop_rounded
: Icons.mic_rounded,
title: _isRecording
? 'Stop recording'
: 'Record voice',
subtitle: _isSendingVoice
? 'Voice message is sending'
: _isRecording
? 'Finish and send your clip'
: 'Capture and send a voice note',
color: const Color(0xFF7C3AED),
enabled: !_isSendingVoice,
onTap: !_isSendingVoice
? () => runAction(() async {
if (_isRecording) {
await _stopAndSendVoice();
} else {
await _startVoiceRecording();
}
})
: null,
),
_ComposerActionTile(
icon: Icons.photo_library_rounded,
title: l10n.sendImageFromGallery,
subtitle: 'Choose an image from your library',
color: const Color(0xFF0F766E),
enabled: !_isSendingImage,
onTap: !_isSendingImage
? () => runAction(() async {
await _pickAndSendImage(
source: ImageSource.gallery,
);
})
: null,
),
_ComposerActionTile(
icon: Icons.camera_alt_rounded,
title: l10n.takePhoto,
subtitle: 'Capture something right now',
color: const Color(0xFF2563EB),
enabled: !_isSendingImage,
onTap: !_isSendingImage
? () => runAction(() async {
await _pickAndSendImage(
source: ImageSource.camera,
);
})
: null,
),
_ComposerActionTile(
icon: Icons.grid_3x3_rounded,
title: l10n.startTictactoe,
subtitle: l10n.dmOnly,
color: const Color(0xFFBE185D),
onTap: () => runAction(() async {
await _startTicTacToeGame();
}),
),
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: actions.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
mainAxisExtent: 126,
),
itemBuilder: (context, index) => actions[index],
);
},
),
],
),
),
ListTile(
enabled: !_isSendingImage,
leading: const Icon(Icons.camera_alt),
title: const Text('Take photo'),
onTap: _isSendingImage
? null
: () async {
await _runAfterSheetDismissal(sheetContext, () async {
await _pickAndSendImage(source: ImageSource.camera);
});
},
),
ListTile(
leading: const Icon(Icons.grid_3x3),
title: const Text('Start Tic-Tac-Toe'),
subtitle: const Text('DM only'),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
await _startTicTacToeGame();
});
},
),
],
),
),
);
},
@@ -1978,12 +2275,19 @@ class _MessagesTabState extends State<MessagesTab> {
void _handleMessageTap(Message message) {
if (widget.onNavigateToMap == null) return;
if (message.isSarMarker && message.sarGpsCoordinates != null) {
if (message.isSarMarker) {
final mapProvider = context.read<MapProvider>();
mapProvider.navigateToLocation(
location: message.sarGpsCoordinates!,
zoom: 15.0,
);
final marker = message.toSarMarker();
if (marker == null) {
return;
}
final error = mapProvider.navigateToSarMarker(marker);
if (error != null && mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error)));
return;
}
widget.onNavigateToMap?.call();
return;
}
@@ -1992,7 +2296,16 @@ class _MessagesTabState extends State<MessagesTab> {
debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
final mapProvider = context.read<MapProvider>();
final drawingProvider = context.read<DrawingProvider>();
mapProvider.navigateToDrawing(message.drawingId!, drawingProvider);
final error = mapProvider.navigateToDrawing(
message.drawingId!,
drawingProvider,
);
if (error != null && mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error)));
return;
}
widget.onNavigateToMap?.call();
}
}
@@ -2002,7 +2315,9 @@ class _MessagesTabState extends State<MessagesTab> {
return Consumer<MessagesProvider>(
builder: (context, messagesProvider, child) {
_syncChannelAutoReadTimer(messagesProvider);
final messages = _getFilteredMessages(messagesProvider);
final messages = messagesProvider.buildDisplayMessages(
_getFilteredMessages(messagesProvider),
);
final bottomInset = MediaQuery.of(context).viewPadding.bottom;
final composerBottomPadding = bottomInset > 0 ? 2.0 : 10.0;
@@ -2040,6 +2355,9 @@ class _MessagesTabState extends State<MessagesTab> {
bottomPadding: composerBottomPadding,
destinationLabel: _getDestinationLabel(),
destinationAvatar: _buildDestinationAvatar(context),
mentionSuggestions: _mentionSuggestions,
mentionQuery: _mentionQuery,
onMentionSelected: _selectMention,
onShowComposerActions: _showComposerActions,
onShowRecipientSelector: _showRecipientSelector,
onStartVoiceRecording: _startVoiceRecording,

View File

@@ -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!),
@@ -777,13 +777,7 @@ class _DecodedRouteSection extends StatelessWidget {
),
)
.toList();
final originalSender = resolvedPath.isEmpty ? null : resolvedPath.first;
return _RouteSection(
route: decodedRoute,
path: resolvedPath,
originalSender: originalSender,
);
return _RouteSection(route: decodedRoute, path: resolvedPath);
},
);
}
@@ -827,13 +821,8 @@ class _DecodedRouteSection extends StatelessWidget {
class _RouteSection extends StatelessWidget {
final DecodedLogRxRoute route;
final List<ResolvedNodeHash> path;
final ResolvedNodeHash? originalSender;
const _RouteSection({
required this.route,
required this.path,
required this.originalSender,
});
const _RouteSection({required this.route, required this.path});
@override
Widget build(BuildContext context) {
@@ -865,26 +854,20 @@ class _RouteSection extends StatelessWidget {
children: [
_FactCard(
icon: Icons.route,
label: 'Payload',
label: AppLocalizations.of(context)!.payload,
value: _payloadTypeLabel(route.payloadType),
),
_FactCard(
icon: Icons.hub,
label: 'Hops',
label: AppLocalizations.of(context)!.hops,
value: '${route.hopCount}',
),
_FactCard(
icon: Icons.tag,
label: 'Hash size',
label: AppLocalizations.of(context)!.hashSize,
value:
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
),
if (originalSender != null)
_FactCard(
icon: Icons.person_pin_circle,
label: 'Original sender',
value: _nodeLabel(originalSender!),
),
],
),
const SizedBox(height: 12),

View 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,
);
}
}

View File

@@ -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),
],
),
),

View File

@@ -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

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../models/device_info.dart';
import '../providers/connection_provider.dart';
import '../widgets/device/spectrum_scan_panel.dart';
import '../l10n/app_localizations.dart';
class SpectrumScanScreen extends StatefulWidget {
const SpectrumScanScreen({super.key});
@@ -323,8 +324,8 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
if (mergedCandidates.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Spectrum scan returned no candidate frequencies'),
SnackBar(
content: Text(AppLocalizations.of(context)!.spectrumScanReturnedNoCandidateFrequencies),
backgroundColor: Colors.red,
),
);
@@ -383,7 +384,7 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
: possibleBracketFrequencies;
return Scaffold(
appBar: AppBar(title: const Text('Spectrum Scan')),
appBar: AppBar(title: Text(AppLocalizations.of(context)!.spectrumScan)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [

View File

@@ -331,7 +331,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 +510,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
SizedBox(width: 12),
Expanded(
child: Text(
isConnected
@@ -601,7 +601,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,

View File

@@ -0,0 +1,156 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../models/config_profile.dart';
import '../providers/app_provider.dart';
import 'image_preferences.dart';
import 'profiles_feature_service.dart';
import 'route_hash_preferences.dart';
import 'voice_bitrate_preferences.dart';
class AppConfigSnapshotService {
Future<AppSettingsProfileSection> capture(AppProvider appProvider) async {
final prefs = await SharedPreferences.getInstance();
final locationTracking = appProvider.locationTrackingService;
return AppSettingsProfileSection(
mapEnabled: appProvider.isMapEnabled,
contactsEnabled: appProvider.isContactsEnabled,
sensorsEnabled: appProvider.isSensorsEnabled,
voiceSilenceTrimmingEnabled: appProvider.isVoiceSilenceTrimmingEnabled,
voiceBandPassFilterEnabled: appProvider.isVoiceBandPassFilterEnabled,
voiceCompressorEnabled: appProvider.isVoiceCompressorEnabled,
voiceLimiterEnabled: appProvider.isVoiceLimiterEnabled,
voiceAutoGainEnabled: appProvider.isVoiceAutoGainEnabled,
voiceEchoCancellationEnabled: appProvider.isVoiceEchoCancellationEnabled,
voiceNoiseSuppressionEnabled: appProvider.isVoiceNoiseSuppressionEnabled,
messageFontScale: appProvider.messageFontScale,
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
nearestRelayFallbackEnabled: appProvider.nearestRelayFallbackEnabled,
voiceBitrate: await VoiceBitratePreferences.getBitrate(),
routeHashSize: await RouteHashPreferences.getHashSize(),
imageMaxSize: await ImagePreferences.getMaxSize(),
imageCompression: await ImagePreferences.getCompression(),
imageGrayscale: await ImagePreferences.getGrayscale(),
imageUltraMode: await ImagePreferences.getUltraMode(),
showRxTxIndicators:
prefs.getBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
) ??
true,
fastLocationUpdatesEnabled: locationTracking.fastLocationUpdatesEnabled,
fastLocationMovementThresholdMeters:
locationTracking.fastLocationMovementThresholdMeters,
fastLocationActiveCadenceSeconds:
locationTracking.fastLocationActiveCadenceSeconds,
);
}
Future<void> apply(
AppSettingsProfileSection? section,
AppProvider appProvider,
) async {
if (section == null || section.isEmpty) {
return;
}
if (section.mapEnabled != null) {
await appProvider.toggleMapEnabled(section.mapEnabled!);
}
if (section.contactsEnabled != null) {
await appProvider.toggleContactsEnabled(section.contactsEnabled!);
}
if (section.sensorsEnabled != null) {
await appProvider.toggleSensorsEnabled(section.sensorsEnabled!);
}
if (section.voiceSilenceTrimmingEnabled != null) {
await appProvider.toggleVoiceSilenceTrimmingEnabled(
section.voiceSilenceTrimmingEnabled!,
);
}
if (section.voiceBandPassFilterEnabled != null) {
await appProvider.toggleVoiceBandPassFilterEnabled(
section.voiceBandPassFilterEnabled!,
);
}
if (section.voiceCompressorEnabled != null) {
await appProvider.toggleVoiceCompressorEnabled(
section.voiceCompressorEnabled!,
);
}
if (section.voiceLimiterEnabled != null) {
await appProvider.toggleVoiceLimiterEnabled(section.voiceLimiterEnabled!);
}
if (section.voiceAutoGainEnabled != null) {
await appProvider.toggleVoiceAutoGainEnabled(
section.voiceAutoGainEnabled!,
);
}
if (section.voiceEchoCancellationEnabled != null) {
await appProvider.toggleVoiceEchoCancellationEnabled(
section.voiceEchoCancellationEnabled!,
);
}
if (section.voiceNoiseSuppressionEnabled != null) {
await appProvider.toggleVoiceNoiseSuppressionEnabled(
section.voiceNoiseSuppressionEnabled!,
);
}
if (section.messageFontScale != null) {
await appProvider.setMessageFontScale(section.messageFontScale!);
}
if (section.autoRouteRotationEnabled != null) {
await appProvider.toggleAutoRouteRotationEnabled(
section.autoRouteRotationEnabled!,
);
}
if (section.clearPathOnMaxRetry != null) {
await appProvider.toggleClearPathOnMaxRetry(section.clearPathOnMaxRetry!);
}
if (section.nearestRelayFallbackEnabled != null) {
await appProvider.toggleNearestRelayFallbackEnabled(
section.nearestRelayFallbackEnabled!,
);
}
if (section.voiceBitrate != null) {
await VoiceBitratePreferences.setBitrate(section.voiceBitrate!);
}
if (section.routeHashSize != null) {
await RouteHashPreferences.setHashSize(section.routeHashSize!);
}
if (section.imageMaxSize != null) {
await ImagePreferences.setMaxSize(section.imageMaxSize!);
}
if (section.imageCompression != null) {
await ImagePreferences.setCompression(section.imageCompression!);
}
if (section.imageGrayscale != null) {
await ImagePreferences.setGrayscale(section.imageGrayscale!);
}
if (section.imageUltraMode != null) {
await ImagePreferences.setUltraMode(section.imageUltraMode!);
}
if (section.showRxTxIndicators != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
section.showRxTxIndicators!,
);
}
final locationTracking = appProvider.locationTrackingService;
if (section.fastLocationUpdatesEnabled != null) {
locationTracking.fastLocationUpdatesEnabled =
section.fastLocationUpdatesEnabled!;
}
if (section.fastLocationMovementThresholdMeters != null) {
locationTracking.fastLocationMovementThresholdMeters =
section.fastLocationMovementThresholdMeters!;
}
if (section.fastLocationActiveCadenceSeconds != null) {
locationTracking.fastLocationActiveCadenceSeconds =
section.fastLocationActiveCadenceSeconds!;
}
await locationTracking.saveSettings();
await appProvider.reloadProfileScopedSettings();
}
}

View File

@@ -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);

View 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;

View File

@@ -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++) {

View File

@@ -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';
@@ -10,10 +11,16 @@ import 'package:latlong2/latlong.dart';
class ContactStorageService {
static const String _contactsKey = 'stored_contacts';
static const String _contactGroupsKey = 'stored_contact_groups';
static const String _pendingAdvertsKey = 'stored_pending_adverts';
static const int _maxStoredContacts = 500; // Store up to 500 contacts
static const int _maxStoredPendingAdverts = 500;
String _key(String baseKey, {String? namespace}) {
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
}
/// Save contacts to persistent storage
Future<void> saveContacts(List<Contact> contacts) async {
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -28,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',
@@ -40,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');
@@ -82,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',
);
@@ -107,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 [];
}
@@ -126,11 +149,106 @@ class ContactStorageService {
}
}
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats() async {
Future<void> clearContactGroups({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_contactsKey);
await prefs.remove(_key(_contactGroupsKey, namespace: namespace));
debugPrint('✅ [ContactStorage] Cleared all contact groups');
} catch (e) {
debugPrint('❌ [ContactStorage] Error clearing contact groups: $e');
}
}
Future<void> savePendingAdverts(
List<Map<String, dynamic>> adverts, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final limitedList = adverts.length > _maxStoredPendingAdverts
? adverts.sublist(adverts.length - _maxStoredPendingAdverts)
: adverts;
await prefs.setString(
_key(_pendingAdvertsKey, namespace: namespace),
jsonEncode(limitedList),
);
debugPrint(
'✅ [ContactStorage] Saved ${limitedList.length} pending adverts to storage',
);
} catch (e) {
debugPrint('❌ [ContactStorage] Error saving pending adverts: $e');
}
}
Future<List<Map<String, dynamic>>> loadPendingAdverts({
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(
_key(_pendingAdvertsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return const [];
}
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList.whereType<Map<String, dynamic>>().toList();
} catch (e) {
debugPrint('❌ [ContactStorage] Error loading pending adverts: $e');
return const [];
}
}
Future<void> clearPendingAdverts({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key(_pendingAdvertsKey, namespace: namespace));
debugPrint('✅ [ContactStorage] Cleared all stored pending adverts');
} catch (e) {
debugPrint('❌ [ContactStorage] Error clearing pending adverts: $e');
}
}
// ── Favorites ───────────────────────────────────────────────────────────────
static const String _favoritesKey = 'contact_favorites';
Future<void> saveFavorites(
Set<String> publicKeyHexes, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_key(_favoritesKey, namespace: namespace),
publicKeyHexes.toList(),
);
} catch (e) {
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
}
}
Future<Set<String>> loadFavorites({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(
_key(_favoritesKey, namespace: namespace),
);
return list?.toSet() ?? {};
} catch (e) {
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
return {};
}
}
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(
_key(_contactsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
@@ -167,6 +285,15 @@ class ContactStorageService {
'telemetry': contact.telemetry != null
? _telemetryToJson(contact.telemetry!)
: null,
'advertHistory': contact.advertHistory
.map(
(point) => {
'lat': point.location.latitude,
'lon': point.location.longitude,
'tsMillis': point.timestamp.millisecondsSinceEpoch,
},
)
.toList(),
};
}
@@ -190,6 +317,7 @@ class ContactStorageService {
telemetry: json['telemetry'] != null
? _telemetryFromJson(json['telemetry'] as Map<String, dynamic>)
: null,
advertHistory: _advertHistoryFromJson(json['advertHistory']),
);
} catch (e) {
debugPrint('❌ [ContactStorage] Error parsing contact from JSON: $e');
@@ -242,6 +370,25 @@ class ContactStorageService {
}
}
List<AdvertLocation> _advertHistoryFromJson(dynamic json) {
if (json is! List) return [];
final result = <AdvertLocation>[];
for (final item in json) {
if (item is! Map<String, dynamic>) continue;
final lat = item['lat'];
final lon = item['lon'];
final tsMillis = item['tsMillis'];
if (lat is! num || lon is! num || tsMillis is! int) continue;
result.add(
AdvertLocation(
location: LatLng(lat.toDouble(), lon.toDouble()),
timestamp: DateTime.fromMillisecondsSinceEpoch(tsMillis),
),
);
}
return result;
}
Map<String, dynamic> _contactGroupToJson(SavedContactGroup group) {
return {
'id': group.id,

View 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;
}
}

View File

@@ -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(

View File

@@ -141,6 +141,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 +166,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 +226,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 +246,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),
);

View File

@@ -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
///
@@ -49,6 +50,10 @@ class LocationTrackingService {
static const String _prefKeyFastActiveCadence =
'fast_location_active_cadence_seconds';
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
}
// ============================================================================
// Configuration Properties
// ============================================================================
@@ -399,7 +404,7 @@ class LocationTrackingService {
// Save disabled state
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefKeyEnabled, false);
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
debugPrint('✅ [LocationTracking] Tracking stopped');
}
@@ -477,8 +482,8 @@ class LocationTrackingService {
// Save to preferences
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefKeyLastLat, position.latitude);
await prefs.setDouble(_prefKeyLastLon, position.longitude);
await prefs.setDouble(_scopedKey(_prefKeyLastLat), position.latitude);
await prefs.setDouble(_scopedKey(_prefKeyLastLon), position.longitude);
debugPrint('✅ [LocationTracking] Initial position set without broadcast');
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
@@ -646,17 +651,24 @@ class LocationTrackingService {
Future<void> loadSettings() async {
final prefs = await SharedPreferences.getInstance();
minDistanceMeters = prefs.getDouble(_prefKeyMinDistance) ?? 5.0;
maxDistanceMeters = prefs.getDouble(_prefKeyMaxDistance) ?? 100.0;
minTimeIntervalSeconds = prefs.getInt(_prefKeyMinTimeInterval) ?? 30;
gpsUpdateDistance = prefs.getDouble(_prefKeyGpsUpdateDistance) ?? 10.0;
minDistanceMeters = prefs.getDouble(_scopedKey(_prefKeyMinDistance)) ?? 5.0;
maxDistanceMeters =
prefs.getDouble(_scopedKey(_prefKeyMaxDistance)) ?? 100.0;
minTimeIntervalSeconds =
prefs.getInt(_scopedKey(_prefKeyMinTimeInterval)) ?? 30;
gpsUpdateDistance =
prefs.getDouble(_scopedKey(_prefKeyGpsUpdateDistance)) ?? 10.0;
fastLocationUpdatesEnabled =
prefs.getBool(_prefKeyFastLocationEnabled) ?? false;
prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false;
fastLocationMovementThresholdMeters =
(prefs.getDouble(_prefKeyFastMovementThreshold) ?? gpsUpdateDistance)
(prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ??
gpsUpdateDistance)
.clamp(1.0, 1000.0);
fastLocationActiveCadenceSeconds =
(prefs.getInt(_prefKeyFastActiveCadence) ?? 10).clamp(5, 60);
(prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ?? 10).clamp(
5,
60,
);
debugPrint('✅ [LocationTracking] Settings loaded');
debugPrint(' Min distance: ${minDistanceMeters}m');
@@ -674,21 +686,27 @@ class LocationTrackingService {
Future<void> saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefKeyMinDistance, minDistanceMeters);
await prefs.setDouble(_prefKeyMaxDistance, maxDistanceMeters);
await prefs.setInt(_prefKeyMinTimeInterval, minTimeIntervalSeconds);
await prefs.setDouble(_prefKeyGpsUpdateDistance, gpsUpdateDistance);
await prefs.setBool(_prefKeyEnabled, isTracking);
await prefs.setDouble(_scopedKey(_prefKeyMinDistance), minDistanceMeters);
await prefs.setDouble(_scopedKey(_prefKeyMaxDistance), maxDistanceMeters);
await prefs.setInt(
_scopedKey(_prefKeyMinTimeInterval),
minTimeIntervalSeconds,
);
await prefs.setDouble(
_scopedKey(_prefKeyGpsUpdateDistance),
gpsUpdateDistance,
);
await prefs.setBool(_scopedKey(_prefKeyEnabled), isTracking);
await prefs.setBool(
_prefKeyFastLocationEnabled,
_scopedKey(_prefKeyFastLocationEnabled),
fastLocationUpdatesEnabled,
);
await prefs.setDouble(
_prefKeyFastMovementThreshold,
_scopedKey(_prefKeyFastMovementThreshold),
fastLocationMovementThresholdMeters,
);
await prefs.setInt(
_prefKeyFastActiveCadence,
_scopedKey(_prefKeyFastActiveCadence),
fastLocationActiveCadenceSeconds,
);

View File

@@ -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;

View 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);
}
}

View File

@@ -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;

View File

@@ -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,10 @@ class MessageStorageService {
: jsonList;
final jsonString = jsonEncode(limitedList);
await prefs.setString(_messagesKey, jsonString);
await prefs.setString(
_key(_messagesKey, namespace: namespace),
jsonString,
);
final retainedMessageIds = limitedList
.map((entry) => entry['id'] as String)
.toSet();
@@ -81,19 +90,19 @@ class MessageStorageService {
}
}
await prefs.setString(
_messageContactLocationsKey,
_key(_messageContactLocationsKey, namespace: namespace),
jsonEncode(locationJson),
);
await prefs.setString(
_messageReceptionDetailsKey,
_key(_messageReceptionDetailsKey, namespace: namespace),
jsonEncode(receptionJson),
);
await prefs.setString(
_messageTransferDetailsKey,
_key(_messageTransferDetailsKey, namespace: namespace),
jsonEncode(transferJson),
);
await prefs.setString(
_messageRouteMetadataKey,
_key(_messageRouteMetadataKey, namespace: namespace),
jsonEncode(routeMetadataJson),
);
@@ -105,11 +114,14 @@ class MessageStorageService {
}
}
Future<Map<String, MessageContactLocation>>
loadMessageContactLocations() async {
Future<Map<String, MessageContactLocation>> loadMessageContactLocations({
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_messageContactLocationsKey);
final jsonString = prefs.getString(
_key(_messageContactLocationsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return const {};
}
@@ -135,11 +147,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 +170,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 +196,14 @@ class MessageStorageService {
}
}
Future<Map<String, MessageTransferDetails>>
loadMessageTransferDetails() async {
Future<Map<String, MessageTransferDetails>> loadMessageTransferDetails({
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_messageTransferDetailsKey);
final jsonString = prefs.getString(
_key(_messageTransferDetailsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return const {};
}
@@ -207,10 +229,14 @@ class MessageStorageService {
}
}
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata() async {
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata({
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_messageRouteMetadataKey);
final jsonString = prefs.getString(
_key(_messageRouteMetadataKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return const {};
}
@@ -234,10 +260,12 @@ class MessageStorageService {
}
/// Load messages from persistent storage
Future<List<Message>> loadMessages() async {
Future<List<Message>> loadMessages({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_messagesKey);
final jsonString = prefs.getString(
_key(_messagesKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
debugPrint(' [MessageStorage] No stored messages found');
@@ -262,25 +290,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 +326,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 +345,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};
@@ -329,6 +375,8 @@ class MessageStorageService {
Message message, {
MessageReceptionDetails? receptionDetails,
}) {
final persistedPathBytes =
message.pathBytes ?? _pathBytesFromSnapshot(receptionDetails);
return {
'id': message.id,
'messageType': message.messageType.name,
@@ -337,12 +385,16 @@ class MessageStorageService {
: null,
'channelIdx': message.channelIdx,
'pathLen': message.pathLen,
'pathBytes': persistedPathBytes?.toList(),
'textType': message.textType.value,
'senderTimestamp': message.senderTimestamp,
'text': message.text,
'isSarMarker': message.isSarMarker,
'sarGpsLat': message.sarGpsCoordinates?.latitude,
'sarGpsLon': message.sarGpsCoordinates?.longitude,
'sarCustomMapLat': message.sarCustomMapPoint?.latitude,
'sarCustomMapLon': message.sarCustomMapPoint?.longitude,
'sarCustomMapId': message.sarCustomMapId,
'sarNotes': message.sarNotes,
'sarCustomEmoji': message.sarCustomEmoji,
'sarColorIndex': message.sarColorIndex,
@@ -395,10 +447,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 {};
}
@@ -421,9 +476,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 {};
}
@@ -465,14 +524,33 @@ class MessageStorageService {
: null,
channelIdx: json['channelIdx'] as int?,
pathLen: json['pathLen'] as int,
pathBytes: json['pathBytes'] is List
? Uint8List.fromList(
(json['pathBytes'] as List<dynamic>)
.whereType<num>()
.map((b) => b.toInt())
.toList(),
)
: null,
textType: MessageTextType.fromValue(json['textType'] as int),
senderTimestamp: json['senderTimestamp'] as int,
text: json['text'] as String,
isSarMarker: json['isSarMarker'] as bool? ?? false,
sarGpsCoordinates:
json['sarGpsLat'] != null && json['sarGpsLon'] != null
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
? LatLng(
(json['sarGpsLat'] as num).toDouble(),
(json['sarGpsLon'] as num).toDouble(),
)
: null,
sarCustomMapPoint:
json['sarCustomMapLat'] != null && json['sarCustomMapLon'] != null
? LatLng(
(json['sarCustomMapLat'] as num).toDouble(),
(json['sarCustomMapLon'] as num).toDouble(),
)
: null,
sarCustomMapId: json['sarCustomMapId'] as String?,
sarNotes: json['sarNotes'] as String?,
sarCustomEmoji: json['sarCustomEmoji'] as String?,
sarColorIndex: json['sarColorIndex'] as int?,
@@ -562,4 +640,12 @@ class MessageStorageService {
return null;
}
}
Uint8List? _pathBytesFromSnapshot(MessageReceptionDetails? receptionDetails) {
final pathBytes = receptionDetails?.pathBytes;
if (pathBytes == null || pathBytes.isEmpty) {
return null;
}
return Uint8List.fromList(pathBytes);
}
}

View File

@@ -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,
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import '../models/sar_marker.dart';
import '../l10n/app_localizations.dart';
@@ -13,14 +14,28 @@ class NotificationService {
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
static const String _prefSarEnabled = 'notifications_sar_enabled';
static const String _prefDiscoveryEnabled = 'notifications_discovery_enabled';
static const String _prefUpdatesEnabled = 'notifications_updates_enabled';
static const String _prefMuteForeground = 'notifications_mute_foreground';
bool _isInitialized = false;
bool _permissionGranted = false;
bool _messageNotificationsEnabled = true;
bool _sarNotificationsEnabled = true;
bool _discoveryNotificationsEnabled = true;
bool _updateNotificationsEnabled = true;
bool _muteForegroundNotifications = true;
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
String? _launchPayload;
// Notification IDs
static const int _sarNotificationId = 1000;
static const int _messageNotificationId = 2000;
static const int _updateNotificationId = 3000;
static const int _batteryNotificationId = 4000;
static const int _discoveryNotificationId = 5000;
// Notification channels
static const String _urgentChannelId = 'sar_urgent';
@@ -38,12 +53,32 @@ class NotificationService {
static const String _updateChannelDescription =
'Notifications for available app updates';
static const String _batteryChannelId = 'battery_alerts';
static const String _batteryChannelName = 'Battery Alerts';
static const String _batteryChannelDescription =
'Notifications when a device or contact battery is low';
static const String _discoveryChannelId = 'discovery_alerts';
static const String _discoveryChannelName = 'Discovery Alerts';
static const String _discoveryChannelDescription =
'Notifications when new contacts are discovered';
bool get messageNotificationsEnabled => _messageNotificationsEnabled;
bool get sarNotificationsEnabled => _sarNotificationsEnabled;
bool get discoveryNotificationsEnabled => _discoveryNotificationsEnabled;
bool get updateNotificationsEnabled => _updateNotificationsEnabled;
bool get muteForegroundNotifications => _muteForegroundNotifications;
bool get isAppInForeground => _lifecycleState == AppLifecycleState.resumed;
/// Initialize notification service
Future<void> initialize() async {
if (_isInitialized) return;
try {
debugPrint('📬 [NotificationService] Initializing...');
WidgetsBinding.instance.addObserver(_LifecycleObserver(this));
_lifecycleState =
WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed;
// Initialize timezone data
tz.initializeTimeZones();
@@ -72,8 +107,15 @@ class NotificationService {
onDidReceiveNotificationResponse: _onNotificationResponse,
);
final launchDetails = await _notificationsPlugin
.getNotificationAppLaunchDetails();
if (launchDetails?.didNotificationLaunchApp ?? false) {
_launchPayload = launchDetails?.notificationResponse?.payload;
}
// Request permissions
await _requestPermissions();
await _loadPreferences();
// Create notification channels (Android)
await _createNotificationChannels();
@@ -132,6 +174,54 @@ class NotificationService {
}
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
_messageNotificationsEnabled = prefs.getBool(_prefMessagesEnabled) ?? true;
_sarNotificationsEnabled = prefs.getBool(_prefSarEnabled) ?? true;
_discoveryNotificationsEnabled =
prefs.getBool(_prefDiscoveryEnabled) ?? true;
_updateNotificationsEnabled = prefs.getBool(_prefUpdatesEnabled) ?? true;
_muteForegroundNotifications = prefs.getBool(_prefMuteForeground) ?? true;
}
Future<void> setMessageNotificationsEnabled(bool value) async {
_messageNotificationsEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefMessagesEnabled, value);
}
Future<void> setSarNotificationsEnabled(bool value) async {
_sarNotificationsEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefSarEnabled, value);
}
Future<void> setDiscoveryNotificationsEnabled(bool value) async {
_discoveryNotificationsEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefDiscoveryEnabled, value);
}
Future<void> setUpdateNotificationsEnabled(bool value) async {
_updateNotificationsEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefUpdatesEnabled, value);
}
Future<void> setMuteForegroundNotifications(bool value) async {
_muteForegroundNotifications = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefMuteForeground, value);
}
void handleAppLifecycleStateChanged(AppLifecycleState state) {
_lifecycleState = state;
}
bool _shouldSuppressForegroundNotifications() {
return _muteForegroundNotifications && isAppInForeground;
}
/// Create notification channels for Android
Future<void> _createNotificationChannels() async {
try {
@@ -177,9 +267,31 @@ class NotificationService {
showBadge: true,
);
const batteryChannel = AndroidNotificationChannel(
_batteryChannelId,
_batteryChannelName,
description: _batteryChannelDescription,
importance: Importance.high,
playSound: true,
enableVibration: true,
showBadge: true,
);
const discoveryChannel = AndroidNotificationChannel(
_discoveryChannelId,
_discoveryChannelName,
description: _discoveryChannelDescription,
importance: Importance.high,
playSound: true,
enableVibration: true,
showBadge: true,
);
await androidPlugin.createNotificationChannel(urgentChannel);
await androidPlugin.createNotificationChannel(messagesChannel);
await androidPlugin.createNotificationChannel(updateChannel);
await androidPlugin.createNotificationChannel(batteryChannel);
await androidPlugin.createNotificationChannel(discoveryChannel);
debugPrint('✅ [NotificationService] Created notification channels');
} catch (e) {
debugPrint('⚠️ [NotificationService] Error creating channels: $e');
@@ -201,6 +313,12 @@ class NotificationService {
}
}
String? consumeLaunchPayload() {
final payload = _launchPayload;
_launchPayload = null;
return payload;
}
/// Show urgent notification for SAR marker
Future<void> showSarNotification({
required SarMarkerType type,
@@ -222,6 +340,16 @@ class NotificationService {
);
return;
}
if (!_sarNotificationsEnabled) {
debugPrint(' [NotificationService] SAR notifications disabled');
return;
}
if (_shouldSuppressForegroundNotifications()) {
debugPrint(
' [NotificationService] App in foreground, skipping SAR notification',
);
return;
}
try {
// Generate unique notification ID based on timestamp
@@ -394,6 +522,16 @@ class NotificationService {
);
return;
}
if (!_messageNotificationsEnabled) {
debugPrint(' [NotificationService] Message notifications disabled');
return;
}
if (_shouldSuppressForegroundNotifications()) {
debugPrint(
' [NotificationService] App in foreground, skipping message notification',
);
return;
}
try {
// Generate unique notification ID based on timestamp
@@ -547,6 +685,16 @@ class NotificationService {
);
return;
}
if (!_updateNotificationsEnabled) {
debugPrint(' [NotificationService] Update notifications disabled');
return;
}
if (_shouldSuppressForegroundNotifications()) {
debugPrint(
' [NotificationService] App in foreground, skipping update notification',
);
return;
}
try {
// Build notification title and body
@@ -617,4 +765,169 @@ class NotificationService {
);
}
}
Future<bool> showLowBatteryNotification({
required String nodeId,
required String nodeName,
required double batteryPercent,
required bool isCurrentDevice,
}) async {
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
);
return false;
}
if (!_permissionGranted) {
debugPrint(
'⚠️ [NotificationService] Permission not granted, skipping notification',
);
return false;
}
if (!_messageNotificationsEnabled) {
debugPrint(' [NotificationService] Message notifications disabled');
return false;
}
if (_shouldSuppressForegroundNotifications()) {
debugPrint(
' [NotificationService] App in foreground, skipping low battery notification',
);
return false;
}
final roundedPercent = batteryPercent.round().clamp(0, 100);
final title = isCurrentDevice
? 'Device battery low'
: 'Contact battery low';
final body = isCurrentDevice
? '$nodeName battery is at $roundedPercent%.'
: '$nodeName is at $roundedPercent% battery.';
final notificationId =
_batteryNotificationId + ((nodeId.hashCode & 0x7fffffff) % 1000);
try {
final androidDetails = AndroidNotificationDetails(
_batteryChannelId,
_batteryChannelName,
channelDescription: _batteryChannelDescription,
importance: Importance.high,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: '$roundedPercent%',
),
);
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
threadIdentifier: 'battery_alerts',
subtitle: '$roundedPercent%',
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
await _notificationsPlugin.show(
id: notificationId,
title: title,
body: body,
notificationDetails: notificationDetails,
payload: 'battery:$nodeId',
);
debugPrint(
'✅ [NotificationService] Showed low battery notification for $nodeName ($roundedPercent%)',
);
return true;
} catch (e) {
debugPrint(
'❌ [NotificationService] Error showing low battery notification: $e',
);
return false;
}
}
Future<bool> showContactDiscoveredNotification({
required String contactKey,
String? contactName,
}) async {
if (!_isInitialized) return false;
if (!_permissionGranted) return false;
if (!_discoveryNotificationsEnabled) return false;
if (_shouldSuppressForegroundNotifications()) return false;
final shortKey = contactKey.length > 12
? contactKey.substring(0, 12).toUpperCase()
: contactKey.toUpperCase();
final title = 'New contact discovered';
final resolvedName = contactName?.trim();
final body = resolvedName != null && resolvedName.isNotEmpty
? '$resolvedName is available in Discovery.'
: 'New contact $shortKey is available in Discovery.';
final notificationId =
_discoveryNotificationId + ((contactKey.hashCode & 0x7fffffff) % 1000);
try {
final androidDetails = AndroidNotificationDetails(
_discoveryChannelId,
_discoveryChannelName,
channelDescription: _discoveryChannelDescription,
importance: Importance.high,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
);
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
threadIdentifier: 'discovery_alerts',
);
await _notificationsPlugin.show(
id: notificationId,
title: title,
body: body,
notificationDetails: NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
),
payload: 'discovery:$contactKey',
);
return true;
} catch (e) {
debugPrint(
'❌ [NotificationService] Error showing discovery notification: $e',
);
return false;
}
}
}
class _LifecycleObserver with WidgetsBindingObserver {
_LifecycleObserver(this._service);
final NotificationService _service;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_service.handleAppLifecycleStateChanged(state);
}
}

View 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;
}
}

View 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()),
);
}
}

View File

@@ -0,0 +1,10 @@
abstract class ProfileTransport {
String get label;
}
class FileProfileTransport implements ProfileTransport {
const FileProfileTransport();
@override
String get label => 'File';
}

View 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);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:shared_preferences/shared_preferences.dart';
class ProfilesFeatureService {
static const String enabledKey = 'profiles_enabled';
static Future<bool> isEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(enabledKey) ?? true;
}
static Future<void> setEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(enabledKey, enabled);
}
}
class ProfileStorageScope {
static bool _profilesEnabled = true;
static String _activeProfileId = 'default';
static Future<void> bootstrap({
required bool profilesEnabled,
required String activeProfileId,
}) async {
_profilesEnabled = profilesEnabled;
_activeProfileId = activeProfileId;
}
static void setScope({
required bool profilesEnabled,
required String activeProfileId,
}) {
_profilesEnabled = profilesEnabled;
_activeProfileId = activeProfileId;
}
static bool get profilesEnabled => _profilesEnabled;
static String get activeProfileId => _activeProfileId;
static String? get effectiveNamespace {
if (!_profilesEnabled || _activeProfileId == 'default') {
return null;
}
return _activeProfileId;
}
static String scopedKey(String baseKey, {String? namespace}) {
final scope = namespace ?? effectiveNamespace;
if (scope == null || scope.isEmpty) {
return baseKey;
}
return 'profile.$scope.$baseKey';
}
}

View File

@@ -0,0 +1,48 @@
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import '../models/contact.dart';
class RelayCandidateSorter {
const RelayCandidateSorter();
List<Contact> sortByDistanceFromSelf(
List<Contact> contacts, {
required LatLng? selfPoint,
}) {
final sorted = List<Contact>.from(contacts);
sorted.sort((a, b) {
if (selfPoint != null) {
final distanceCompare = _distanceFrom(
selfPoint,
a,
).compareTo(_distanceFrom(selfPoint, b));
if (distanceCompare != 0) {
return distanceCompare;
}
}
final nameCompare = a.displayName.compareTo(b.displayName);
if (nameCompare != 0) {
return nameCompare;
}
return a.publicKeyHex.compareTo(b.publicKeyHex);
});
return sorted;
}
double _distanceFrom(LatLng selfPoint, Contact contact) {
final location = contact.displayLocation;
if (location == null) {
return double.infinity;
}
return Geolocator.distanceBetween(
selfPoint.latitude,
selfPoint.longitude,
location.latitude,
location.longitude,
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'profiles_feature_service.dart';
class RouteHashPreferences {
static const String _hashSizeKey = 'route_hash_size';
@@ -6,21 +7,28 @@ class RouteHashPreferences {
static Future<int> getHashSize() async {
final prefs = await SharedPreferences.getInstance();
final value = prefs.getInt(_hashSizeKey) ?? defaultHashSize;
final value =
prefs.getInt(ProfileStorageScope.scopedKey(_hashSizeKey)) ??
defaultHashSize;
return _normalize(value);
}
static Future<void> setHashSize(int value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_hashSizeKey, _normalize(value));
await prefs.setInt(
ProfileStorageScope.scopedKey(_hashSizeKey),
_normalize(value),
);
}
static int normalizeSync(int value) => _normalize(value);
/// Valid hash sizes per the MeshCore protocol.
/// The path encoding uses 2 bits: 00=1B, 01=2B, 10=3B, 11=4B.
static const List<int> supportedSizes = [1, 2, 3];
static int _normalize(int value) {
if (value < 1 || value > 3) {
return defaultHashSize;
}
return value;
if (supportedSizes.contains(value)) return value;
return defaultHashSize;
}
}

View File

@@ -0,0 +1,50 @@
import 'package:meshcore_client/meshcore_client.dart';
import 'serial_transport_stub.dart'
if (dart.library.io) 'serial_transport_io.dart'
if (dart.library.js_interop) 'serial_transport_web.dart';
const int kSerialBaudRate = 115200;
class SerialDeviceInfo {
final String id;
final String title;
final String subtitle;
final Object handle;
const SerialDeviceInfo({
required this.id,
required this.title,
required this.subtitle,
required this.handle,
});
}
class SerialConnection {
final MeshCoreSerialService service;
final String deviceId;
final String deviceName;
final Future<void> Function() disconnect;
const SerialConnection({
required this.service,
required this.deviceId,
required this.deviceName,
required this.disconnect,
});
}
abstract class SerialTransport {
bool get isSupported;
bool get canRequestDevice;
String get actionLabel;
String get emptyStateTitle;
String get unsupportedMessage;
Future<List<SerialDeviceInfo>> listDevices();
Future<SerialDeviceInfo?> requestDevice();
Future<SerialConnection> connect(SerialDeviceInfo device);
void dispose();
}
SerialTransport createSerialTransport() => createSerialTransportImpl();

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