Compare commits

...

48 Commits

Author SHA1 Message Date
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 Troha
7a9be3eb76 Merge pull request #25 from dz0ny/feat/sensor-telemetry-preview
Feat/sensor telemetry preview
2026-03-15 10:34:36 +01:00
139 changed files with 26580 additions and 3190 deletions

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/

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 = 117;
CURRENT_PROJECT_VERSION = 119;
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 = 117;
CURRENT_PROJECT_VERSION = 119;
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 = 117;
CURRENT_PROJECT_VERSION = 119;
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 = 117;
CURRENT_PROJECT_VERSION = 119;
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 = 117;
CURRENT_PROJECT_VERSION = 119;
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 = 117;
CURRENT_PROJECT_VERSION = 119;
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>117</string>
<string>119</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00103">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.554978">
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.372199">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="100.17557">
<testcase classname="fastlane.lanes" name="2: build_app" time="121.394124">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="264.49426">
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="365.414242">
</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,6 +24,9 @@ import 'services/voice_player_service.dart';
import 'services/notification_service.dart';
import 'services/locale_preferences.dart';
import 'services/mesh_map_nodes_service.dart';
import 'services/profile_manager.dart';
import 'services/profile_workspace_coordinator.dart';
import 'services/profiles_feature_service.dart';
import 'services/update_checker_service.dart';
import 'services/wizard_preferences.dart';
import 'screens/discovery_screen.dart';
@@ -31,7 +35,9 @@ import 'screens/welcome_wizard_screen.dart';
import 'theme/app_theme.dart';
import 'l10n/app_localizations.dart';
void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await WakelockPlus.enable();
runApp(const MeshCoreSarApp());
}
@@ -58,6 +64,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
}
Future<void> _initializeApp() async {
final prefs = await SharedPreferences.getInstance();
final profilesEnabled = await ProfilesFeatureService.isEnabled();
final activeProfileId =
prefs.getString(ProfileManager.activeProfileIdKey) ?? 'default';
await ProfileStorageScope.bootstrap(
profilesEnabled: profilesEnabled,
activeProfileId: activeProfileId,
);
await _loadThemePreference();
await _loadLocalePreference();
@@ -271,6 +285,13 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
),
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
ChangeNotifierProvider(create: (_) => SensorsProvider()),
ChangeNotifierProvider(
create: (_) {
final manager = ProfileManager();
manager.initialize();
return manager;
},
),
// Voice provider (packet reassembly + playback)
ChangeNotifierProvider(
@@ -323,6 +344,37 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
imageProvider: context.read<ip.ImageProvider>(),
),
),
ProxyProvider6<
ProfileManager,
AppProvider,
ConnectionProvider,
ContactsProvider,
MessagesProvider,
MapProvider,
ProfileWorkspaceCoordinator
>(
update:
(
context,
profileManager,
appProvider,
connectionProvider,
contactsProvider,
messagesProvider,
mapProvider,
previous,
) => ProfileWorkspaceCoordinator(
profileManager: profileManager,
connectionProvider: connectionProvider,
contactsProvider: contactsProvider,
messagesProvider: messagesProvider,
sensorsProvider: context.read<SensorsProvider>(),
mapProvider: mapProvider,
drawingProvider: context.read<DrawingProvider>(),
channelsProvider: context.read<ChannelsProvider>(),
appProvider: appProvider,
),
),
],
child: _buildMaterialApp(),
);

View File

@@ -0,0 +1,528 @@
import 'package:flutter/foundation.dart';
import 'channel.dart';
@immutable
class DeviceConfigProfileSection {
final int? frequencyKhz;
final int? bandwidth;
final int? spreadingFactor;
final int? codingRate;
final bool? repeatEnabled;
final int? txPower;
final int? telemetryModes;
final int? advertLocationPolicy;
final int? multiAcks;
final bool? manualAddContacts;
final bool? autoAddUsers;
final bool? autoAddRepeaters;
final bool? autoAddRoomServers;
final bool? autoAddSensors;
final bool? autoAddOverwriteOldest;
final double? publicLatitude;
final double? publicLongitude;
const DeviceConfigProfileSection({
this.frequencyKhz,
this.bandwidth,
this.spreadingFactor,
this.codingRate,
this.repeatEnabled,
this.txPower,
this.telemetryModes,
this.advertLocationPolicy,
this.multiAcks,
this.manualAddContacts,
this.autoAddUsers,
this.autoAddRepeaters,
this.autoAddRoomServers,
this.autoAddSensors,
this.autoAddOverwriteOldest,
this.publicLatitude,
this.publicLongitude,
});
bool get isEmpty =>
frequencyKhz == null &&
bandwidth == null &&
spreadingFactor == null &&
codingRate == null &&
repeatEnabled == null &&
txPower == null &&
telemetryModes == null &&
advertLocationPolicy == null &&
multiAcks == null &&
manualAddContacts == null &&
autoAddUsers == null &&
autoAddRepeaters == null &&
autoAddRoomServers == null &&
autoAddSensors == null &&
autoAddOverwriteOldest == null &&
publicLatitude == null &&
publicLongitude == null;
Map<String, dynamic> toJson() => {
'frequencyKhz': frequencyKhz,
'bandwidth': bandwidth,
'spreadingFactor': spreadingFactor,
'codingRate': codingRate,
'repeatEnabled': repeatEnabled,
'txPower': txPower,
'telemetryModes': telemetryModes,
'advertLocationPolicy': advertLocationPolicy,
'multiAcks': multiAcks,
'manualAddContacts': manualAddContacts,
'autoAddUsers': autoAddUsers,
'autoAddRepeaters': autoAddRepeaters,
'autoAddRoomServers': autoAddRoomServers,
'autoAddSensors': autoAddSensors,
'autoAddOverwriteOldest': autoAddOverwriteOldest,
'publicLatitude': publicLatitude,
'publicLongitude': publicLongitude,
};
factory DeviceConfigProfileSection.fromJson(Map<String, dynamic> json) {
return DeviceConfigProfileSection(
frequencyKhz: json['frequencyKhz'] as int?,
bandwidth: json['bandwidth'] as int?,
spreadingFactor: json['spreadingFactor'] as int?,
codingRate: json['codingRate'] as int?,
repeatEnabled: json['repeatEnabled'] as bool?,
txPower: json['txPower'] as int?,
telemetryModes: json['telemetryModes'] as int?,
advertLocationPolicy: json['advertLocationPolicy'] as int?,
multiAcks: json['multiAcks'] as int?,
manualAddContacts: json['manualAddContacts'] as bool?,
autoAddUsers: json['autoAddUsers'] as bool?,
autoAddRepeaters: json['autoAddRepeaters'] as bool?,
autoAddRoomServers: json['autoAddRoomServers'] as bool?,
autoAddSensors: json['autoAddSensors'] as bool?,
autoAddOverwriteOldest: json['autoAddOverwriteOldest'] as bool?,
publicLatitude: (json['publicLatitude'] as num?)?.toDouble(),
publicLongitude: (json['publicLongitude'] as num?)?.toDouble(),
);
}
}
@immutable
class AppSettingsProfileSection {
final bool? mapEnabled;
final bool? contactsEnabled;
final bool? sensorsEnabled;
final bool? voiceSilenceTrimmingEnabled;
final bool? voiceBandPassFilterEnabled;
final bool? voiceCompressorEnabled;
final bool? voiceLimiterEnabled;
final bool? voiceAutoGainEnabled;
final bool? voiceEchoCancellationEnabled;
final bool? voiceNoiseSuppressionEnabled;
final double? messageFontScale;
final bool? autoRouteRotationEnabled;
final bool? clearPathOnMaxRetry;
final bool? nearestRelayFallbackEnabled;
final int? voiceBitrate;
final int? routeHashSize;
final int? imageMaxSize;
final int? imageCompression;
final bool? imageGrayscale;
final bool? imageUltraMode;
final bool? showRxTxIndicators;
final bool? fastLocationUpdatesEnabled;
final double? fastLocationMovementThresholdMeters;
final int? fastLocationActiveCadenceSeconds;
const AppSettingsProfileSection({
this.mapEnabled,
this.contactsEnabled,
this.sensorsEnabled,
this.voiceSilenceTrimmingEnabled,
this.voiceBandPassFilterEnabled,
this.voiceCompressorEnabled,
this.voiceLimiterEnabled,
this.voiceAutoGainEnabled,
this.voiceEchoCancellationEnabled,
this.voiceNoiseSuppressionEnabled,
this.messageFontScale,
this.autoRouteRotationEnabled,
this.clearPathOnMaxRetry,
this.nearestRelayFallbackEnabled,
this.voiceBitrate,
this.routeHashSize,
this.imageMaxSize,
this.imageCompression,
this.imageGrayscale,
this.imageUltraMode,
this.showRxTxIndicators,
this.fastLocationUpdatesEnabled,
this.fastLocationMovementThresholdMeters,
this.fastLocationActiveCadenceSeconds,
});
bool get isEmpty =>
mapEnabled == null &&
contactsEnabled == null &&
sensorsEnabled == null &&
voiceSilenceTrimmingEnabled == null &&
voiceBandPassFilterEnabled == null &&
voiceCompressorEnabled == null &&
voiceLimiterEnabled == null &&
voiceAutoGainEnabled == null &&
voiceEchoCancellationEnabled == null &&
voiceNoiseSuppressionEnabled == null &&
messageFontScale == null &&
autoRouteRotationEnabled == null &&
clearPathOnMaxRetry == null &&
nearestRelayFallbackEnabled == null &&
voiceBitrate == null &&
routeHashSize == null &&
imageMaxSize == null &&
imageCompression == null &&
imageGrayscale == null &&
imageUltraMode == null &&
showRxTxIndicators == null &&
fastLocationUpdatesEnabled == null &&
fastLocationMovementThresholdMeters == null &&
fastLocationActiveCadenceSeconds == null;
Map<String, dynamic> toJson() => {
'mapEnabled': mapEnabled,
'contactsEnabled': contactsEnabled,
'sensorsEnabled': sensorsEnabled,
'voiceSilenceTrimmingEnabled': voiceSilenceTrimmingEnabled,
'voiceBandPassFilterEnabled': voiceBandPassFilterEnabled,
'voiceCompressorEnabled': voiceCompressorEnabled,
'voiceLimiterEnabled': voiceLimiterEnabled,
'voiceAutoGainEnabled': voiceAutoGainEnabled,
'voiceEchoCancellationEnabled': voiceEchoCancellationEnabled,
'voiceNoiseSuppressionEnabled': voiceNoiseSuppressionEnabled,
'messageFontScale': messageFontScale,
'autoRouteRotationEnabled': autoRouteRotationEnabled,
'clearPathOnMaxRetry': clearPathOnMaxRetry,
'nearestRelayFallbackEnabled': nearestRelayFallbackEnabled,
'voiceBitrate': voiceBitrate,
'routeHashSize': routeHashSize,
'imageMaxSize': imageMaxSize,
'imageCompression': imageCompression,
'imageGrayscale': imageGrayscale,
'imageUltraMode': imageUltraMode,
'showRxTxIndicators': showRxTxIndicators,
'fastLocationUpdatesEnabled': fastLocationUpdatesEnabled,
'fastLocationMovementThresholdMeters': fastLocationMovementThresholdMeters,
'fastLocationActiveCadenceSeconds': fastLocationActiveCadenceSeconds,
};
factory AppSettingsProfileSection.fromJson(Map<String, dynamic> json) {
return AppSettingsProfileSection(
mapEnabled: json['mapEnabled'] as bool?,
contactsEnabled: json['contactsEnabled'] as bool?,
sensorsEnabled: json['sensorsEnabled'] as bool?,
voiceSilenceTrimmingEnabled: json['voiceSilenceTrimmingEnabled'] as bool?,
voiceBandPassFilterEnabled: json['voiceBandPassFilterEnabled'] as bool?,
voiceCompressorEnabled: json['voiceCompressorEnabled'] as bool?,
voiceLimiterEnabled: json['voiceLimiterEnabled'] as bool?,
voiceAutoGainEnabled: json['voiceAutoGainEnabled'] as bool?,
voiceEchoCancellationEnabled:
json['voiceEchoCancellationEnabled'] as bool?,
voiceNoiseSuppressionEnabled:
json['voiceNoiseSuppressionEnabled'] as bool?,
messageFontScale: (json['messageFontScale'] as num?)?.toDouble(),
autoRouteRotationEnabled: json['autoRouteRotationEnabled'] as bool?,
clearPathOnMaxRetry: json['clearPathOnMaxRetry'] as bool?,
nearestRelayFallbackEnabled: json['nearestRelayFallbackEnabled'] as bool?,
voiceBitrate: json['voiceBitrate'] as int?,
routeHashSize: json['routeHashSize'] as int?,
imageMaxSize: json['imageMaxSize'] as int?,
imageCompression: json['imageCompression'] as int?,
imageGrayscale: json['imageGrayscale'] as bool?,
imageUltraMode: json['imageUltraMode'] as bool?,
showRxTxIndicators: json['showRxTxIndicators'] as bool?,
fastLocationUpdatesEnabled: json['fastLocationUpdatesEnabled'] as bool?,
fastLocationMovementThresholdMeters:
(json['fastLocationMovementThresholdMeters'] as num?)?.toDouble(),
fastLocationActiveCadenceSeconds:
json['fastLocationActiveCadenceSeconds'] as int?,
);
}
}
@immutable
class MapWorkspaceProfileSection {
final Map<String, dynamic>? mapPrefs;
final List<Map<String, dynamic>> drawings;
final Map<String, dynamic>? currentTrail;
final List<Map<String, dynamic>> trailHistory;
final Map<String, dynamic>? importedTrail;
final bool? isTrailVisible;
final bool? showCadastralOverlay;
final bool? showForestRoadsOverlay;
final bool? showHikingTrailsOverlay;
final bool? showMainRoadsOverlay;
final bool? showHouseNumbersOverlay;
final bool? showFireHazardZonesOverlay;
final bool? showHistoricalFiresOverlay;
final bool? showFirebreaksOverlay;
final bool? showKrasFireZonesOverlay;
final bool? showPlaceNamesOverlay;
final bool? showMunicipalityBordersOverlay;
final bool? showAllContactTrails;
final bool? hideRepeatersOnMap;
const MapWorkspaceProfileSection({
this.mapPrefs,
this.drawings = const [],
this.currentTrail,
this.trailHistory = const [],
this.importedTrail,
this.isTrailVisible,
this.showCadastralOverlay,
this.showForestRoadsOverlay,
this.showHikingTrailsOverlay,
this.showMainRoadsOverlay,
this.showHouseNumbersOverlay,
this.showFireHazardZonesOverlay,
this.showHistoricalFiresOverlay,
this.showFirebreaksOverlay,
this.showKrasFireZonesOverlay,
this.showPlaceNamesOverlay,
this.showMunicipalityBordersOverlay,
this.showAllContactTrails,
this.hideRepeatersOnMap,
});
bool get isEmpty =>
mapPrefs == null &&
drawings.isEmpty &&
currentTrail == null &&
trailHistory.isEmpty &&
importedTrail == null &&
isTrailVisible == null &&
showCadastralOverlay == null &&
showForestRoadsOverlay == null &&
showHikingTrailsOverlay == null &&
showMainRoadsOverlay == null &&
showHouseNumbersOverlay == null &&
showFireHazardZonesOverlay == null &&
showHistoricalFiresOverlay == null &&
showFirebreaksOverlay == null &&
showKrasFireZonesOverlay == null &&
showPlaceNamesOverlay == null &&
showMunicipalityBordersOverlay == null &&
showAllContactTrails == null &&
hideRepeatersOnMap == null;
Map<String, dynamic> toJson() => {
'mapPrefs': mapPrefs,
'drawings': drawings,
'currentTrail': currentTrail,
'trailHistory': trailHistory,
'importedTrail': importedTrail,
'isTrailVisible': isTrailVisible,
'showCadastralOverlay': showCadastralOverlay,
'showForestRoadsOverlay': showForestRoadsOverlay,
'showHikingTrailsOverlay': showHikingTrailsOverlay,
'showMainRoadsOverlay': showMainRoadsOverlay,
'showHouseNumbersOverlay': showHouseNumbersOverlay,
'showFireHazardZonesOverlay': showFireHazardZonesOverlay,
'showHistoricalFiresOverlay': showHistoricalFiresOverlay,
'showFirebreaksOverlay': showFirebreaksOverlay,
'showKrasFireZonesOverlay': showKrasFireZonesOverlay,
'showPlaceNamesOverlay': showPlaceNamesOverlay,
'showMunicipalityBordersOverlay': showMunicipalityBordersOverlay,
'showAllContactTrails': showAllContactTrails,
'hideRepeatersOnMap': hideRepeatersOnMap,
};
factory MapWorkspaceProfileSection.fromJson(Map<String, dynamic> json) {
List<Map<String, dynamic>> decodeList(String key) {
final raw = json[key] as List<dynamic>? ?? const [];
return raw.whereType<Map<String, dynamic>>().toList();
}
return MapWorkspaceProfileSection(
mapPrefs: json['mapPrefs'] as Map<String, dynamic>?,
drawings: decodeList('drawings'),
currentTrail: json['currentTrail'] as Map<String, dynamic>?,
trailHistory: decodeList('trailHistory'),
importedTrail: json['importedTrail'] as Map<String, dynamic>?,
isTrailVisible: json['isTrailVisible'] as bool?,
showCadastralOverlay: json['showCadastralOverlay'] as bool?,
showForestRoadsOverlay: json['showForestRoadsOverlay'] as bool?,
showHikingTrailsOverlay: json['showHikingTrailsOverlay'] as bool?,
showMainRoadsOverlay: json['showMainRoadsOverlay'] as bool?,
showHouseNumbersOverlay: json['showHouseNumbersOverlay'] as bool?,
showFireHazardZonesOverlay: json['showFireHazardZonesOverlay'] as bool?,
showHistoricalFiresOverlay: json['showHistoricalFiresOverlay'] as bool?,
showFirebreaksOverlay: json['showFirebreaksOverlay'] as bool?,
showKrasFireZonesOverlay: json['showKrasFireZonesOverlay'] as bool?,
showPlaceNamesOverlay: json['showPlaceNamesOverlay'] as bool?,
showMunicipalityBordersOverlay:
json['showMunicipalityBordersOverlay'] as bool?,
showAllContactTrails: json['showAllContactTrails'] as bool?,
hideRepeatersOnMap: json['hideRepeatersOnMap'] as bool?,
);
}
}
@immutable
class ConfigProfileSections {
final DeviceConfigProfileSection? deviceConfig;
final AppSettingsProfileSection? appSettings;
final MapWorkspaceProfileSection? mapWorkspace;
final List<Channel> channels;
const ConfigProfileSections({
this.deviceConfig,
this.appSettings,
this.mapWorkspace,
this.channels = const [],
});
bool get isEmpty =>
(deviceConfig == null || deviceConfig!.isEmpty) &&
(appSettings == null || appSettings!.isEmpty) &&
(mapWorkspace == null || mapWorkspace!.isEmpty) &&
channels.isEmpty;
Map<String, dynamic> toJson() => {
'deviceConfig': deviceConfig?.toJson(),
'appSettings': appSettings?.toJson(),
'mapWorkspace': mapWorkspace?.toJson(),
'channels': channels.map((channel) => channel.toJson()).toList(),
};
factory ConfigProfileSections.fromJson(Map<String, dynamic> json) {
final rawChannels = json['channels'] as List<dynamic>? ?? const [];
return ConfigProfileSections(
deviceConfig: json['deviceConfig'] is Map<String, dynamic>
? DeviceConfigProfileSection.fromJson(
json['deviceConfig'] as Map<String, dynamic>,
)
: null,
appSettings: json['appSettings'] is Map<String, dynamic>
? AppSettingsProfileSection.fromJson(
json['appSettings'] as Map<String, dynamic>,
)
: null,
mapWorkspace: json['mapWorkspace'] is Map<String, dynamic>
? MapWorkspaceProfileSection.fromJson(
json['mapWorkspace'] as Map<String, dynamic>,
)
: null,
channels: rawChannels
.whereType<Map<String, dynamic>>()
.map(Channel.fromJson)
.toList(),
);
}
}
@immutable
class ConfigProfile {
static const String defaultProfileId = 'default';
final String id;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
final String? notes;
final ConfigProfileSections sections;
const ConfigProfile({
required this.id,
required this.name,
required this.createdAt,
required this.updatedAt,
required this.sections,
this.notes,
});
bool get isDefault => id == defaultProfileId;
factory ConfigProfile.defaultProfile() {
final now = DateTime.now();
return ConfigProfile(
id: defaultProfileId,
name: 'Default',
createdAt: now,
updatedAt: now,
sections: const ConfigProfileSections(),
);
}
ConfigProfile copyWith({
String? id,
String? name,
DateTime? createdAt,
DateTime? updatedAt,
String? notes,
ConfigProfileSections? sections,
}) {
return ConfigProfile(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
notes: notes ?? this.notes,
sections: sections ?? this.sections,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'notes': notes,
'sections': sections.toJson(),
};
factory ConfigProfile.fromJson(Map<String, dynamic> json) {
return ConfigProfile(
id: json['id'] as String,
name: json['name'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
notes: json['notes'] as String?,
sections: ConfigProfileSections.fromJson(
json['sections'] as Map<String, dynamic>? ?? const {},
),
);
}
}
@immutable
class ProfileDiff {
final List<String> changedSections;
const ProfileDiff({required this.changedSections});
}
@immutable
class ProfileTransferRecord {
final String profileId;
final String direction;
final DateTime timestamp;
final String detail;
const ProfileTransferRecord({
required this.profileId,
required this.direction,
required this.timestamp,
required this.detail,
});
Map<String, dynamic> toJson() => {
'profileId': profileId,
'direction': direction,
'timestamp': timestamp.toIso8601String(),
'detail': detail,
};
factory ProfileTransferRecord.fromJson(Map<String, dynamic> json) {
return ProfileTransferRecord(
profileId: json['profileId'] as String,
direction: json['direction'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
detail: json['detail'] as String,
);
}
}

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

@@ -16,6 +16,9 @@ enum ConnectionMode {
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
tcp,
/// USB serial connection (Android OTG or Web Serial API)
usb,
}
extension ConnectionModeExtension on ConnectionMode {
@@ -25,6 +28,8 @@ extension ConnectionModeExtension on ConnectionMode {
return 'Direct (BLE)';
case ConnectionMode.tcp:
return 'Direct (WiFi)';
case ConnectionMode.usb:
return 'Direct (USB)';
}
}
@@ -34,6 +39,8 @@ extension ConnectionModeExtension on ConnectionMode {
return 'Direct BLE connection to MeshCore device';
case ConnectionMode.tcp:
return 'Direct WiFi/TCP connection to MeshCore device';
case ConnectionMode.usb:
return 'USB serial connection to MeshCore device';
}
}
}

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

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

@@ -18,6 +18,8 @@ import '../services/messaging_route_preferences.dart';
import '../services/nearest_router_selector.dart';
import '../services/packet_capture_storage_service.dart';
import '../services/path_history_service.dart';
import '../utils/rssi_location_estimator.dart';
import '../services/profiles_feature_service.dart';
import '../services/route_hash_preferences.dart';
import '../services/notification_service.dart';
import '../models/contact.dart';
@@ -253,6 +255,10 @@ class AppProvider with ChangeNotifier {
_isInitialized = true;
}
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
}
void _startPacketCapturePersistence() {
_packetCaptureFlushTimer?.cancel();
_packetCaptureFlushTimer = Timer.periodic(const Duration(seconds: 2), (_) {
@@ -516,7 +522,7 @@ class AppProvider with ChangeNotifier {
Future<void> _loadMapEnabled() async {
try {
final prefs = await SharedPreferences.getInstance();
_isMapEnabled = prefs.getBool('map_enabled') ?? true;
_isMapEnabled = prefs.getBool(_scopedKey('map_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading map enabled setting: $e');
@@ -528,7 +534,7 @@ class AppProvider with ChangeNotifier {
try {
_isMapEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_enabled', enabled);
await prefs.setBool(_scopedKey('map_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving map enabled setting: $e');
@@ -539,7 +545,8 @@ class AppProvider with ChangeNotifier {
Future<void> _loadContactsEnabled() async {
try {
final prefs = await SharedPreferences.getInstance();
_isContactsEnabled = prefs.getBool('contacts_enabled') ?? true;
_isContactsEnabled =
prefs.getBool(_scopedKey('contacts_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading contacts enabled setting: $e');
@@ -551,7 +558,7 @@ class AppProvider with ChangeNotifier {
try {
_isContactsEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('contacts_enabled', enabled);
await prefs.setBool(_scopedKey('contacts_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving contacts enabled setting: $e');
@@ -562,7 +569,7 @@ class AppProvider with ChangeNotifier {
Future<void> _loadSensorsEnabled() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSensorsEnabled = prefs.getBool('sensors_enabled') ?? true;
_isSensorsEnabled = prefs.getBool(_scopedKey('sensors_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading sensors enabled setting: $e');
@@ -574,7 +581,7 @@ class AppProvider with ChangeNotifier {
try {
_isSensorsEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('sensors_enabled', enabled);
await prefs.setBool(_scopedKey('sensors_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving sensors enabled setting: $e');
@@ -586,7 +593,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceSilenceTrimmingEnabled =
prefs.getBool('voice_silence_trimming_enabled') ?? true;
prefs.getBool(_scopedKey('voice_silence_trimming_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice silence trimming setting: $e');
@@ -598,7 +605,10 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceSilenceTrimmingEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_silence_trimming_enabled', enabled);
await prefs.setBool(
_scopedKey('voice_silence_trimming_enabled'),
enabled,
);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice silence trimming setting: $e');
@@ -610,7 +620,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceBandPassFilterEnabled =
prefs.getBool('voice_band_pass_filter_enabled') ?? true;
prefs.getBool(_scopedKey('voice_band_pass_filter_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice band-pass filter setting: $e');
@@ -622,7 +632,10 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceBandPassFilterEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_band_pass_filter_enabled', enabled);
await prefs.setBool(
_scopedKey('voice_band_pass_filter_enabled'),
enabled,
);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice band-pass filter setting: $e');
@@ -634,7 +647,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceCompressorEnabled =
prefs.getBool('voice_compressor_enabled') ?? true;
prefs.getBool(_scopedKey('voice_compressor_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice compressor setting: $e');
@@ -646,7 +659,7 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceCompressorEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_compressor_enabled', enabled);
await prefs.setBool(_scopedKey('voice_compressor_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice compressor setting: $e');
@@ -657,7 +670,8 @@ class AppProvider with ChangeNotifier {
Future<void> _loadVoiceLimiterEnabled() async {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceLimiterEnabled = prefs.getBool('voice_limiter_enabled') ?? true;
_isVoiceLimiterEnabled =
prefs.getBool(_scopedKey('voice_limiter_enabled')) ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice limiter setting: $e');
@@ -669,7 +683,7 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceLimiterEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_limiter_enabled', enabled);
await prefs.setBool(_scopedKey('voice_limiter_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice limiter setting: $e');
@@ -680,7 +694,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceAutoGainEnabled =
prefs.getBool('voice_auto_gain_enabled') ?? false;
prefs.getBool(_scopedKey('voice_auto_gain_enabled')) ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice auto gain setting: $e');
@@ -691,7 +705,7 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceAutoGainEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_auto_gain_enabled', enabled);
await prefs.setBool(_scopedKey('voice_auto_gain_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice auto gain setting: $e');
@@ -702,7 +716,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceEchoCancellationEnabled =
prefs.getBool('voice_echo_cancellation_enabled') ?? false;
prefs.getBool(_scopedKey('voice_echo_cancellation_enabled')) ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice echo cancellation setting: $e');
@@ -713,7 +727,10 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceEchoCancellationEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_echo_cancellation_enabled', enabled);
await prefs.setBool(
_scopedKey('voice_echo_cancellation_enabled'),
enabled,
);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice echo cancellation setting: $e');
@@ -724,7 +741,7 @@ class AppProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
_isVoiceNoiseSuppressionEnabled =
prefs.getBool('voice_noise_suppression_enabled') ?? false;
prefs.getBool(_scopedKey('voice_noise_suppression_enabled')) ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading voice noise suppression setting: $e');
@@ -735,7 +752,10 @@ class AppProvider with ChangeNotifier {
try {
_isVoiceNoiseSuppressionEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('voice_noise_suppression_enabled', enabled);
await prefs.setBool(
_scopedKey('voice_noise_suppression_enabled'),
enabled,
);
notifyListeners();
} catch (e) {
debugPrint('Error saving voice noise suppression setting: $e');
@@ -745,10 +765,11 @@ class AppProvider with ChangeNotifier {
Future<void> _loadMessageFontScale() async {
try {
final prefs = await SharedPreferences.getInstance();
_messageFontScale = (prefs.getDouble('message_font_scale') ?? 1.0).clamp(
0.85,
1.4,
);
_messageFontScale =
(prefs.getDouble(_scopedKey('message_font_scale')) ?? 1.0).clamp(
0.85,
1.4,
);
notifyListeners();
} catch (e) {
debugPrint('Error loading message font scale setting: $e');
@@ -759,7 +780,10 @@ class AppProvider with ChangeNotifier {
try {
_messageFontScale = scale.clamp(0.85, 1.4);
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('message_font_scale', _messageFontScale);
await prefs.setDouble(
_scopedKey('message_font_scale'),
_messageFontScale,
);
notifyListeners();
} catch (e) {
debugPrint('Error saving message font scale setting: $e');
@@ -905,6 +929,14 @@ class AppProvider with ChangeNotifier {
source == ContactReceiveSource.preview;
if (isAdvertSource) {
// For pushNewAdvert (0x8A): contact data is already in the push, but
// we also import into firmware so subsequent getContact calls work.
// Matches the official app which calls cmdGetAdvertPath for all adverts.
if (source == ContactReceiveSource.advert) {
unawaited(
connectionProvider.importReceivedAdvert(contact.publicKey),
);
}
final isNewPendingAdvert = contactsProvider
.addOrUpdatePendingAdvertContact(
contact,
@@ -1144,6 +1176,17 @@ class AppProvider with ChangeNotifier {
);
}
// Estimate location for contacts without GPS using received path
if (senderContact != null &&
senderContact.displayLocation == null &&
receivedPathBytes != null &&
receivedPathBytes.isNotEmpty) {
_estimateContactLocationFromReceivedPath(
contact: senderContact,
receivedPathBytes: receivedPathBytes,
);
}
// Check if message is a drawing broadcast
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
debugPrint('🎨 [AppProvider] Drawing message received, parsing...');
@@ -1295,11 +1338,14 @@ class AppProvider with ChangeNotifier {
return;
}
debugPrint(
'📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry',
'📊 [AppProvider] Binary response (0x8C tag=$tag) received',
);
// Binary response tag 0 = telemetry data (Cayenne LPP format)
// Other tags may be used for different data types in the future
contactsProvider.updateTelemetry(publicKeyPrefix, responseData);
// Binary responses carry Cayenne LPP telemetry data.
// The data starts with a channel byte — valid LPP always has at least
// 3 bytes (channel + type + value). Skip clearly non-telemetry payloads.
if (responseData.length >= 3) {
contactsProvider.updateTelemetry(publicKeyPrefix, responseData);
}
};
// When raw binary data is received (PUSH_CODE_RAW_DATA 0x84)
@@ -1582,62 +1628,26 @@ class AppProvider with ChangeNotifier {
debugPrint(
'🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
);
// Trigger a single contact fetch to get the updated path information
// This is much more efficient than fetching all contacts
// This happens asynchronously to avoid blocking the event handler
Future.delayed(const Duration(milliseconds: 100), () {
if (connectionProvider.deviceInfo.isConnected) {
connectionProvider.getContact(publicKey);
}
});
if (connectionProvider.deviceInfo.isConnected) {
unawaited(connectionProvider.getContact(publicKey));
}
};
// When an advertisement is received (PUSH_CODE_ADVERT 0x80)
// This may be sent by the radio for existing contacts instead of PUSH_CODE_NEW_ADVERT (0x8A)
//
// Official app flow:
// 1. importReceivedAdvert(key) — import advert into firmware contact table
// 2. getContact(key) — check if contact existed before (for new-notification)
// 3. syncContact(key) — fetch from device + upsert into DB
// 4. getContact(key) — get fresh contact with name/type
// 5. upsert into discovered_contacts
// 6. if was new → notify with name + type
connectionProvider.onAdvertReceived = (publicKey) {
debugPrint(
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
);
unawaited(_retainAdvertRxPath(publicKey));
// Check if this is an existing contact that might have updated location
final contact = contactsProvider.findContactByKey(publicKey);
if (contact != null) {
debugPrint(
' Existing contact "${contact.advName}" - fetching updated contact info (optimized)',
);
// Trigger a single contact fetch to get the updated contact information
// This is much more efficient than fetching all contacts
Future.delayed(const Duration(milliseconds: 100), () {
if (connectionProvider.deviceInfo.isConnected) {
connectionProvider.getContact(publicKey);
}
});
} else {
final isNewPendingAdvert = contactsProvider.addPendingAdvert(
publicKey,
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
debugPrint(
' Unknown contact - added to pending adverts list and waiting for manual resolution',
);
if (isNewPendingAdvert) {
final keyHex = publicKey
.take(6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
unawaited(
_notificationService.showContactDiscoveredNotification(
contactKey: keyHex,
contactName: contactsProvider
.pendingAdvertByKey(publicKey)
?.advName,
),
);
}
if (contactsProvider.shouldEnrichPendingAdvert(publicKey)) {
unawaited(connectionProvider.previewContact(publicKey));
}
}
unawaited(_handlePushAdvert(publicKey));
};
// When firmware deletes a contact due to contacts table overflow (PUSH_CODE_CONTACT_DELETED 0x8F)
@@ -1723,6 +1733,16 @@ class AppProvider with ChangeNotifier {
connectionProvider.resolveContactForDmCallback = (contactPublicKey) {
return contactsProvider.findContactByKey(contactPublicKey);
};
// Reset path before the last retry attempt to force flood mode
messagesProvider.resetPathBeforeLastRetryCallback = (contact) async {
if (connectionProvider.deviceInfo.isConnected) {
debugPrint(
'🔄 [AppProvider] Resetting path for ${contact.advName} before last retry (flood fallback)',
);
await connectionProvider.resetPath(contact.publicKey);
}
};
messagesProvider.onFinalRouterFallbackCallback =
({required messageId, required contact, required message}) async {
return _sendWithFinalNearestRouterFallback(
@@ -1986,11 +2006,9 @@ class AppProvider with ChangeNotifier {
contactsProvider.resetContactRouteLocal(latestContact.publicKey);
if (connectionProvider.deviceInfo.isConnected) {
await connectionProvider.resetPath(latestContact.publicKey);
Future.delayed(const Duration(milliseconds: 150), () {
if (connectionProvider.deviceInfo.isConnected) {
connectionProvider.getContact(latestContact.publicKey);
}
});
if (connectionProvider.deviceInfo.isConnected) {
await connectionProvider.getContact(latestContact.publicKey);
}
}
}
}
@@ -2000,6 +2018,56 @@ class AppProvider with ChangeNotifier {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
/// Estimate contact location from the received message path.
///
/// When we receive a message, the path bytes describe how it traveled:
/// Sender → first_hop (near sender) → ... → last_hop (near us) → Us
///
/// The FIRST hop in the received path is the repeater nearest to the sender.
/// We don't have the actual RSSI between sender and that repeater, so we
/// use a conservative estimate to place them within typical LoRa range.
void _estimateContactLocationFromReceivedPath({
required Contact contact,
required List<int> receivedPathBytes,
}) {
if (receivedPathBytes.isEmpty) return;
// Infer hash size from the contact's known path encoding, or default to 1
final hashSize = contact.routeHasPath ? contact.routeHashSize : 1;
if (receivedPathBytes.length < hashSize) return;
// First hop in received path = repeater nearest to sender
final firstHopHash = receivedPathBytes
.sublist(0, hashSize)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toLowerCase();
Contact? nearSenderRepeater;
for (final c in contactsProvider.contacts) {
if (c.publicKeyHex.toLowerCase().startsWith(firstHopHash) &&
c.displayLocation != null) {
nearSenderRepeater = c;
break;
}
}
if (nearSenderRepeater == null) return;
// We don't have the RSSI between sender and their repeater.
// Use -70 dBm as conservative estimate (~300m in open terrain).
const int estimatedRssi = -70;
contactsProvider.addRssiObservation(
contactPublicKeyHex: contact.publicKeyHex,
contactPublicKey: contact.publicKey,
observation: RssiObservation(
repeaterLocation: nearSenderRepeater.displayLocation!,
rssiDbm: estimatedRssi,
observedAt: DateTime.now(),
),
);
}
Future<void> _learnPathFromPublicMessage({
required Contact contact,
required List<int> pathBytes,
@@ -2048,6 +2116,40 @@ class AppProvider with ChangeNotifier {
);
}
/// Handle pushAdvert (0x80) — matches the official MeshCore app flow:
///
/// 1. importReceivedAdvert(key) — import advert into firmware contact table
/// 2. Check if contact was already known locally (for new-notification)
/// 3. getContact(key) — fetch full contact from device (now stored after import)
/// → flows through onContactReceivedDetailed which upserts + notifies
Future<void> _handlePushAdvert(Uint8List publicKey) async {
// Step 1: Import the advert into firmware's contact table.
// This tells the firmware to store the contact from its received-advert
// buffer, making subsequent getContact return name/type/location.
await connectionProvider.importReceivedAdvert(publicKey);
// Step 2: Check if this was an already-known contact before sync
final wasKnown = contactsProvider.findContactByKey(publicKey) != null;
// Step 3: Fetch the full contact from device (import made it available).
// This flows through onContactReceivedDetailed, which handles:
// - upserting into contacts or pending adverts
// - showing discovery notification for new contacts
if (connectionProvider.deviceInfo.isConnected) {
await connectionProvider.getContact(publicKey);
}
// For existing contacts, the getContact above already updated them.
// For new contacts from adverts, onContactReceivedDetailed (source=advert)
// already added to pending adverts and showed notification.
// Nothing else to do — the callback pipeline handles everything.
if (wasKnown) {
debugPrint(
' [pushAdvert] Existing contact refreshed',
);
}
}
Future<void> _requestRepeaterStatus(Uint8List publicKey) async {
if (!connectionProvider.deviceInfo.isConnected) {
return;
@@ -2600,6 +2702,24 @@ class AppProvider with ChangeNotifier {
locationTrackingService.setFastLocationActiveUse(isActive);
}
Future<void> reloadProfileScopedSettings() async {
await Future.wait([
_loadMapEnabled(),
_loadContactsEnabled(),
_loadSensorsEnabled(),
_loadVoiceSilenceTrimmingEnabled(),
_loadVoiceBandPassFilterEnabled(),
_loadVoiceCompressorEnabled(),
_loadVoiceLimiterEnabled(),
_loadVoiceAutoGainEnabled(),
_loadVoiceEchoCancellationEnabled(),
_loadVoiceNoiseSuppressionEnabled(),
_loadMessageFontScale(),
_loadMessagingRouteSettings(),
locationTrackingService.loadSettings(),
]);
}
Future<void> _sendFastLocationUpdate(
dynamic position, {
required String reason,
@@ -3612,6 +3732,12 @@ class AppProvider with ChangeNotifier {
'pathLen=$pathLen snr=$snrRaw rssi=$rssiDbm new=$isNewPendingAdvert',
);
// Official app flow for controlData discovery:
// - Try getContact from firmware. If not found → skip.
// - If found → update path, request region names for repeaters.
// - Discovery list shows generic labels ("Repeater XX:YY") for unknown nodes.
// - Names appear later when the node's advert arrives via pushAdvert/pushNewAdvert.
if (isNewPendingAdvert) {
final keyHex = publicKey
.take(6)
@@ -3630,6 +3756,8 @@ class AppProvider with ChangeNotifier {
unawaited(_maybeRequestRepeaterOwnerInfo(publicKey));
}
// Try to enrich from firmware's existing contacts (no import — the
// controlData discovery response doesn't store an advert in firmware).
if (contactsProvider.shouldEnrichPendingAdvert(publicKey)) {
unawaited(connectionProvider.previewContact(publicKey));
}

View File

@@ -72,15 +72,21 @@ class ConnectionProvider with ChangeNotifier {
static const int _controlTypeNodeDiscoverReq = 0x80;
final MeshCoreBleService _bleService = MeshCoreBleService();
MeshCoreTcpService? _tcpService;
MeshCoreSerialService? _serialService;
/// Expose BLE service for background location tracking
MeshCoreBleService get bleService => _bleService;
/// Active service — BLE or TCP depending on current mode
MeshCoreServiceBase get _activeService =>
(_connectionMode == ConnectionMode.tcp && _tcpService != null)
? _tcpService!
: _bleService;
/// Active service — BLE, TCP, or USB depending on current mode
MeshCoreServiceBase get _activeService {
if (_connectionMode == ConnectionMode.tcp && _tcpService != null) {
return _tcpService!;
}
if (_connectionMode == ConnectionMode.usb && _serialService != null) {
return _serialService!;
}
return _bleService;
}
/// Current connection mode
ConnectionMode _connectionMode = ConnectionMode.ble;
@@ -218,6 +224,77 @@ class ConnectionProvider with ChangeNotifier {
_wireServiceCallbacks(_bleService);
}
Future<void> _prepareForConnectionSwitch(ConnectionMode nextMode) async {
if (_isScanning) {
debugPrint('🔵 [Provider] Stopping active scan before connect()');
await stopScan();
}
await _disconnectInactiveTransports(nextMode);
_error = null;
_supportsAutoaddConfig = null;
_resetSyncState();
}
Future<void> _disconnectInactiveTransports(ConnectionMode activeMode) async {
if (activeMode != ConnectionMode.ble && _bleService.isConnected) {
await _bleService.disconnect();
}
if (activeMode != ConnectionMode.tcp && _tcpService != null) {
await _disposeTcpService();
}
if (activeMode != ConnectionMode.usb && _serialService != null) {
_disposeSerialService();
}
}
Future<void> _disposeTcpService() async {
final service = _tcpService;
if (service == null) {
return;
}
_tcpService = null;
await service.disconnect();
service.dispose();
}
void _disposeSerialService() {
_serialService?.markDisconnected();
_serialService?.dispose();
_serialService = null;
}
void _beginConnectionAttempt({
required ConnectionMode mode,
required String deviceId,
required String deviceName,
String? tcpHost,
}) {
_connectionMode = mode;
_tcpHost = mode == ConnectionMode.tcp ? tcpHost : null;
_deviceInfo = _deviceInfo.copyWith(
deviceId: deviceId,
deviceName: deviceName,
connectionState: ConnectionState.connecting,
);
notifyListeners();
}
void _resetConnectionSession({ConnectionMode nextMode = ConnectionMode.ble}) {
_tcpHost = nextMode == ConnectionMode.tcp ? _tcpHost : null;
_connectionMode = nextMode;
_supportsAutoaddConfig = null;
_resetSyncState();
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
_roomLoginManager.clearRoomLoginStates();
_pingTracker.clearAll();
_pendingSendOperations.clear();
_messageDeliveryTracker.clearTracking();
notifyListeners();
}
/// Wire all shared event callbacks onto [service].
/// Called for both BLE and TCP services so the provider handles events
/// identically regardless of transport.
@@ -623,27 +700,15 @@ class ConnectionProvider with ChangeNotifier {
debugPrint(
'🔵 [Provider] connect() called for device: ${device.platformName}',
);
if (_isScanning) {
debugPrint('🔵 [Provider] Stopping active scan before connect()');
await stopScan();
}
// Ensure we route commands to BLE, not a stale TCP service.
_connectionMode = ConnectionMode.ble;
_deviceInfo = _deviceInfo.copyWith(
await _prepareForConnectionSwitch(ConnectionMode.ble);
_beginConnectionAttempt(
mode: ConnectionMode.ble,
deviceId: device.remoteId.toString(),
deviceName: device.platformName.isNotEmpty
? device.platformName
: 'Unknown',
connectionState: ConnectionState.connecting,
);
_error = null;
_supportsAutoaddConfig = null;
_resetSyncState();
debugPrint('✅ [Provider] Device info updated to connecting state');
notifyListeners();
debugPrint('🔵 [Provider] Calling BLE service connect()...');
final success = await _bleService.connect(device);
@@ -663,23 +728,18 @@ class ConnectionProvider with ChangeNotifier {
/// Connect to a MeshCore device over TCP/WiFi (port 5000)
Future<bool> connectTcp(String host, int port) async {
debugPrint('🌐 [Provider] connectTcp() $host:$port');
_tcpHost = host;
_deviceInfo = _deviceInfo.copyWith(
deviceId: '$host:$port',
deviceName: host,
connectionState: ConnectionState.connecting,
);
_error = null;
_supportsAutoaddConfig = null;
notifyListeners();
await _prepareForConnectionSwitch(ConnectionMode.tcp);
// Create fresh TCP service and wire its callbacks
_tcpService?.dispose();
await _disposeTcpService();
_tcpService = MeshCoreTcpService();
_wireServiceCallbacks(_tcpService!);
_connectionMode = ConnectionMode.tcp;
_beginConnectionAttempt(
mode: ConnectionMode.tcp,
deviceId: '$host:$port',
deviceName: host,
tcpHost: host,
);
final success = await _tcpService!.connect(host, port);
if (!success) {
@@ -693,21 +753,43 @@ class ConnectionProvider with ChangeNotifier {
/// Disconnect from TCP/WiFi device
Future<void> disconnectTcp() async {
if (_tcpService != null) {
await _tcpService!.disconnect();
_tcpService!.dispose();
_tcpService = null;
await _disposeTcpService();
_resetConnectionSession();
}
/// Connect via USB serial using a pre-configured [MeshCoreSerialService].
///
/// The caller is responsible for opening the serial port and wiring
/// [service.writeRaw] + [service.feedRawBytes] before calling this.
/// After this call succeeds, [service.markConnected()] has already run.
Future<bool> connectSerial(MeshCoreSerialService service) async {
debugPrint('🔌 [Provider] connectSerial()');
await _prepareForConnectionSwitch(ConnectionMode.usb);
_disposeSerialService();
_serialService = service;
_wireServiceCallbacks(_serialService!);
_beginConnectionAttempt(
mode: ConnectionMode.usb,
deviceId: 'usb',
deviceName: 'USB Companion',
);
// markConnected() should already have been called by the transport.
// If it hasn't, the service won't be connected yet.
if (!service.isConnected) {
_deviceInfo = _deviceInfo.copyWith(
connectionState: ConnectionState.error,
);
notifyListeners();
return false;
}
_tcpHost = null;
_connectionMode = ConnectionMode.ble;
_supportsAutoaddConfig = null;
_resetSyncState();
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
_roomLoginManager.clearRoomLoginStates();
_pingTracker.clearAll();
_pendingSendOperations.clear();
_messageDeliveryTracker.clearTracking();
notifyListeners();
return true;
}
/// Disconnect from USB serial device.
Future<void> disconnectSerial() async {
_disposeSerialService();
_resetConnectionSession();
}
/// Disconnect from device
@@ -721,17 +803,13 @@ class ConnectionProvider with ChangeNotifier {
await disconnectTcp();
return;
}
if (_connectionMode == ConnectionMode.usb) {
await disconnectSerial();
return;
}
await _bleService.disconnect();
_supportsAutoaddConfig = null;
_resetSyncState();
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
_roomLoginManager.clearRoomLoginStates();
_pingTracker.clearAll();
_pendingSendOperations.clear();
_messageDeliveryTracker.clearTracking();
notifyListeners();
_resetConnectionSession();
}
/// Reset message sync state so the next connect/reconnect can sync cleanly.
@@ -864,6 +942,23 @@ class ConnectionProvider with ChangeNotifier {
}
}
/// Import a received advert by public key, then fetch the full contact.
///
/// This tells the firmware to store the advert it received, making the
/// contact available for [getContact]/[previewContact] with full details
/// (name, type, location, etc.).
Future<void> importReceivedAdvert(Uint8List publicKey) async {
if (!_activeService.isConnected) return;
try {
await _activeService.importReceivedAdvert(publicKey);
} catch (e) {
debugPrint(
'⚠️ [Provider] importReceivedAdvert failed for ${_publicKeyToHex(publicKey)}: $e',
);
}
}
Future<void> previewContact(Uint8List publicKey) async {
if (!_activeService.isConnected) {
return;
@@ -1257,6 +1352,10 @@ class ConnectionProvider with ChangeNotifier {
channelName: channelName,
secret: secretBytes,
);
// Reconcile local state from the device after the write completes,
// matching the delete flow so channel/contact lists refresh immediately.
await _activeService.getChannel(slotIdx);
} catch (e) {
if (_isChannelRefreshTimeoutError(e)) {
debugPrint(
@@ -1321,6 +1420,31 @@ class ConnectionProvider with ChangeNotifier {
}
}
Future<void> setChannelSlot({
required int channelIdx,
required String channelName,
required Uint8List secret,
}) async {
if (!_activeService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _activeService.setChannel(
channelIdx: channelIdx,
channelName: channelName,
secret: secret,
);
await _activeService.getChannel(channelIdx);
} catch (e) {
_error = 'Failed to set channel: $e';
notifyListeners();
rethrow;
}
}
/// Generate secret for hash channel using SHA256
/// Same algorithm as Channel model for consistency
/// Python equivalent: hashlib.sha256(channel_name.encode()).digest()[0:16]
@@ -2155,7 +2279,46 @@ class ConnectionProvider with ChangeNotifier {
}
}
/// Request fresh device info (triggers SelfInfo response)
/// Export a contact as a meshcore:// share URL.
/// Pass null to export self.
Future<String?> exportContactUrl(Uint8List? publicKey) async {
if (!_activeService.isConnected) return null;
try {
final frame = await _activeService.exportContact(publicKey);
final hex = frame.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return 'meshcore://$hex';
} catch (e) {
debugPrint('⚠️ [Provider] exportContact failed: $e');
_error = 'Failed to export contact: $e';
notifyListeners();
return null;
}
}
/// Read custom variables from device (GPS mode, sensor settings, etc.)
Future<Map<String, String>> getCustomVars() async {
if (!_activeService.isConnected) {
return {};
}
try {
return await _activeService.getCustomVars();
} catch (e) {
debugPrint('⚠️ [Provider] getCustomVars failed: $e');
return {};
}
}
/// Set a custom variable on the device
Future<void> setCustomVar(String key, String value) async {
if (!_activeService.isConnected) return;
try {
await _activeService.setCustomVar(key, value);
} catch (e) {
_error = 'Failed to set $key: $e';
notifyListeners();
}
}
Future<void> refreshDeviceInfo() async {
if (_isSpectrumScanActive) return;
if (!_activeService.isConnected) {

View File

@@ -8,6 +8,7 @@ import '../models/message_contact_location.dart';
import '../services/cayenne_lpp_parser.dart';
import '../services/contact_storage_service.dart';
import '../utils/fast_gps_packet.dart';
import '../utils/rssi_location_estimator.dart';
import '../utils/key_comparison.dart';
class PendingAdvert {
@@ -128,12 +129,15 @@ class ContactsProvider with ChangeNotifier {
final Map<String, Contact> _contacts = {};
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
final Map<String, PendingAdvert> _pendingAdverts = {};
final Map<String, LatLng> _estimatedLocations = {};
final Map<String, List<RssiObservation>> _rssiObservations = {};
final ContactStorageService _storageService = ContactStorageService();
bool _isInitialized = false;
bool _isPersisting = false;
bool _persistRequested = false;
bool _isPersistingPendingAdverts = false;
bool _persistPendingAdvertsRequested = false;
String? _storageNamespace;
// Add default public channel on initialization
ContactsProvider() {
@@ -141,19 +145,74 @@ class ContactsProvider with ChangeNotifier {
}
bool get isInitialized => _isInitialized;
String? get storageNamespace => _storageNamespace;
/// Initialize and load persisted contacts at app startup
/// This loads contacts without filtering, allowing offline viewing
Future<void> initializeEarly() async {
if (_isInitialized) return;
await _loadFromStorage();
}
Future<void> reloadFromStorage({
String? namespace,
Uint8List? devicePublicKey,
}) async {
_storageNamespace = namespace;
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
}
Future<void> persistNow() async {
await _storageService.saveContacts(
_contactsForStorage(),
namespace: _storageNamespace,
);
await _storageService.saveContactGroups(
_savedContactGroups,
namespace: _storageNamespace,
);
await _storageService.savePendingAdverts(
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
namespace: _storageNamespace,
);
}
Future<void> cloneCurrentStorageTo(String? namespace) async {
await _storageService.saveContacts(
_contactsForStorage(),
namespace: namespace,
);
await _storageService.saveContactGroups(
_savedContactGroups,
namespace: namespace,
);
await _storageService.savePendingAdverts(
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
namespace: namespace,
);
}
Future<void> _loadFromStorage({
bool force = false,
Uint8List? devicePublicKey,
}) async {
if (_isInitialized && !force) return;
try {
_resetInMemoryState();
debugPrint(
'📦 [ContactsProvider] Early loading persisted contacts (no filtering)...',
);
final storedContacts = await _storageService.loadContacts();
final storedGroups = await _storageService.loadContactGroups();
final storedPendingAdverts = await _storageService.loadPendingAdverts();
final storedContacts = await _storageService.loadContacts(
namespace: _storageNamespace,
);
final storedGroups = await _storageService.loadContactGroups(
namespace: _storageNamespace,
);
final storedPendingAdverts = await _storageService.loadPendingAdverts(
namespace: _storageNamespace,
);
// Add stored contacts (excluding any with all-zeros public key)
const publicChannelKey =
@@ -170,7 +229,10 @@ class ContactsProvider with ChangeNotifier {
_savedContactGroups
..clear()
..addAll(storedGroups);
_restorePendingAdverts(storedPendingAdverts);
_restorePendingAdverts(
storedPendingAdverts,
devicePublicKey: devicePublicKey,
);
debugPrint(
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
);
@@ -197,45 +259,9 @@ class ContactsProvider with ChangeNotifier {
return;
}
try {
debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
final storedContacts = await _storageService.loadContacts(
excludePublicKey: devicePublicKey,
);
final storedGroups = await _storageService.loadContactGroups();
final storedPendingAdverts = await _storageService.loadPendingAdverts();
// Add stored contacts (excluding any with all-zeros public key)
const publicChannelKey =
'0000000000000000000000000000000000000000000000000000000000000000';
for (final contact in storedContacts) {
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
if (contact.publicKeyHex == publicChannelKey) {
continue;
}
_contacts[contact.publicKeyHex] = contact;
}
_isInitialized = true;
_savedContactGroups
..clear()
..addAll(storedGroups);
_restorePendingAdverts(
storedPendingAdverts,
devicePublicKey: devicePublicKey,
);
debugPrint(
'✅ [ContactsProvider] Loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
);
// Ensure public channel exists after loading
_ensurePublicChannelExists();
notifyListeners();
} catch (e) {
debugPrint('❌ [ContactsProvider] Error initializing: $e');
_isInitialized = true; // Mark as initialized even on error
_ensurePublicChannelExists();
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
if (devicePublicKey != null) {
_removeSelfContact(devicePublicKey);
}
}
@@ -287,7 +313,10 @@ class ContactsProvider with ChangeNotifier {
try {
while (_persistRequested) {
_persistRequested = false;
await _storageService.saveContacts(_contactsForStorage());
await _storageService.saveContacts(
_contactsForStorage(),
namespace: _storageNamespace,
);
}
} catch (e) {
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
@@ -305,6 +334,7 @@ class ContactsProvider with ChangeNotifier {
_persistPendingAdvertsRequested = false;
await _storageService.savePendingAdverts(
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
namespace: _storageNamespace,
);
}
} catch (e) {
@@ -315,6 +345,8 @@ class ContactsProvider with ChangeNotifier {
}
List<Contact> get contacts => _contacts.values.toList();
List<Contact> get favouriteContacts =>
_contacts.values.where((c) => c.isFavourite).toList();
List<SavedContactGroup> get savedContactGroups =>
List<SavedContactGroup>.from(_savedContactGroups)
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
@@ -436,7 +468,10 @@ class ContactsProvider with ChangeNotifier {
Future<void> _persistSavedGroups() async {
try {
await _storageService.saveContactGroups(_savedContactGroups);
await _storageService.saveContactGroups(
_savedContactGroups,
namespace: _storageNamespace,
);
} catch (e) {
debugPrint('❌ [ContactsProvider] Error persisting contact groups: $e');
}
@@ -469,13 +504,71 @@ class ContactsProvider with ChangeNotifier {
..sort(_sortByLastSeen);
}
/// All estimated locations (for passing to map marker service).
Map<String, LatLng> get estimatedLocations =>
Map.unmodifiable(_estimatedLocations);
/// Get estimated location for a contact (RSSI-based, near last-hop repeater).
LatLng? estimatedLocationFor(String publicKeyHex) =>
_estimatedLocations[publicKeyHex];
/// Set an estimated location for a contact that has no GPS.
void setEstimatedLocation(String publicKeyHex, LatLng location) {
_estimatedLocations[publicKeyHex] = location;
notifyListeners();
}
/// Record an RSSI observation and re-trilaterate the contact's position.
///
/// Keeps up to 5 observations per unique repeater (latest wins).
/// With multiple repeaters, trilateration produces a better estimate.
void addRssiObservation({
required String contactPublicKeyHex,
required List<int> contactPublicKey,
required RssiObservation observation,
}) {
final observations = _rssiObservations.putIfAbsent(
contactPublicKeyHex,
() => [],
);
// Replace existing observation from the same repeater, or add new
final repeaterKey =
'${observation.repeaterLocation.latitude},${observation.repeaterLocation.longitude}';
observations.removeWhere((o) =>
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
repeaterKey);
observations.add(observation);
// Keep at most 8 observations (most recent per repeater)
if (observations.length > 8) {
observations.sort((a, b) => b.observedAt.compareTo(a.observedAt));
observations.removeRange(8, observations.length);
}
final estimated = RssiLocationEstimator.trilaterate(
observations: observations,
contactPublicKey: contactPublicKey,
);
if (estimated != null) {
_estimatedLocations[contactPublicKeyHex] = estimated;
notifyListeners();
}
}
/// Effective location: own GPS/advert > estimated from RSSI.
LatLng? effectiveLocationFor(Contact contact) {
return contact.displayLocation ?? _estimatedLocations[contact.publicKeyHex];
}
/// Get contacts with location (for map display)
/// Includes contacts with estimated locations from RSSI.
List<Contact> get contactsWithLocation =>
contacts.where((c) => c.displayLocation != null).toList();
contacts.where((c) => effectiveLocationFor(c) != null).toList();
/// Get chat contacts with location (team members on map)
List<Contact> get chatContactsWithLocation =>
chatContacts.where((c) => c.displayLocation != null).toList();
chatContacts.where((c) => effectiveLocationFor(c) != null).toList();
MessageContactLocation? buildMessageContactLocationSnapshot(
Contact contact, {
@@ -556,7 +649,12 @@ class ContactsProvider with ChangeNotifier {
);
_contacts[contact.publicKeyHex] = updatedContact;
_pendingAdverts.remove(contact.publicKeyHex);
// Keep repeaters and sensors in pending adverts so they remain visible
// in the discovery list (with a checkmark). Remove others.
if (contact.type != ContactType.repeater &&
contact.type != ContactType.sensor) {
_pendingAdverts.remove(contact.publicKeyHex);
}
debugPrint(
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
);
@@ -585,7 +683,10 @@ class ContactsProvider with ChangeNotifier {
incomingContact: contact,
existingContact: existingContact,
);
_pendingAdverts.remove(contact.publicKeyHex);
if (contact.type != ContactType.repeater &&
contact.type != ContactType.sensor) {
_pendingAdverts.remove(contact.publicKeyHex);
}
}
if (excluded > 0) {
debugPrint(
@@ -825,6 +926,10 @@ class ContactsProvider with ChangeNotifier {
}
/// Update contact telemetry
// Dedup: track last telemetry data hash per contact to avoid processing
// the same payload twice (0x8B + 0x8C can both fire for the same data).
final Map<String, (int, DateTime)> _lastTelemetryHash = {};
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
debugPrint(
@@ -832,6 +937,21 @@ class ContactsProvider with ChangeNotifier {
);
debugPrint(' LPP data size: ${lppData.length} bytes');
// Deduplicate: same data for same contact within 2 seconds = skip
final prefixHex = publicKeyPrefix
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final dataHash = lppData.fold<int>(0, (h, b) => h * 31 + b);
final now = DateTime.now();
final last = _lastTelemetryHash[prefixHex];
if (last != null &&
last.$1 == dataHash &&
now.difference(last.$2).inSeconds < 2) {
debugPrint(' ⏭️ Duplicate telemetry payload, skipping');
return;
}
_lastTelemetryHash[prefixHex] = (dataHash, now);
// Find contact by public key prefix
final contact = _findContactByPrefix(publicKeyPrefix);
if (contact == null) {
@@ -875,10 +995,19 @@ class ContactsProvider with ChangeNotifier {
telemetry.gpsLocation,
)) {
debugPrint(
' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps',
' ⚠️ Incoming telemetry GPS is invalid/missing; keeping last known contact location as fallback: $incomingGps',
);
}
telemetry = mergedTelemetry;
telemetry = ContactTelemetry(
gpsLocation: telemetry.gpsLocation,
batteryPercentage: mergedTelemetry.batteryPercentage,
batteryMilliVolts: mergedTelemetry.batteryMilliVolts,
temperature: mergedTelemetry.temperature,
timestamp: mergedTelemetry.timestamp,
humidity: mergedTelemetry.humidity,
pressure: mergedTelemetry.pressure,
extraSensorData: telemetry.extraSensorData,
);
}
// Update contact with new telemetry AND last seen time
@@ -993,10 +1122,10 @@ class ContactsProvider with ChangeNotifier {
// last known reading for any field that is omitted in the incoming update.
final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation);
final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation);
final mergedExtraSensorData = <String, dynamic>{
...?existingTelemetry?.extraSensorData,
...?incomingTelemetry.extraSensorData,
};
final mergedExtraSensorData = _mergeExtraSensorData(
existingTelemetry?.extraSensorData,
incomingTelemetry.extraSensorData,
);
return ContactTelemetry(
gpsLocation: incomingGps ?? previousGps,
@@ -1017,6 +1146,49 @@ class ContactsProvider with ChangeNotifier {
);
}
Map<String, dynamic> _mergeExtraSensorData(
Map<String, dynamic>? existingExtraSensorData,
Map<String, dynamic>? incomingExtraSensorData,
) {
final merged = <String, dynamic>{...?existingExtraSensorData};
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
return merged;
}
final incomingMetricFamilies = incomingExtraSensorData.keys
.map(_telemetryMetricFamilyForKey)
.whereType<String>()
.toSet();
if (incomingMetricFamilies.isNotEmpty) {
merged.removeWhere((key, _) {
final family = _telemetryMetricFamilyForKey(key);
return family != null && incomingMetricFamilies.contains(family);
});
}
merged.addAll(incomingExtraSensorData);
return merged;
}
String? _telemetryMetricFamilyForKey(String key) {
const sourcePrefix = '__source_channel:';
if (key.startsWith(sourcePrefix)) {
return key.substring(sourcePrefix.length);
}
final separatorIndex = key.lastIndexOf('_');
if (separatorIndex <= 0 || separatorIndex == key.length - 1) {
return key;
}
final suffix = key.substring(separatorIndex + 1);
if (int.tryParse(suffix) == null) {
return key;
}
return key.substring(0, separatorIndex);
}
int _coordinateToAdvertMicrodegrees(double coordinate) {
return (coordinate * 1e6).round();
}
@@ -1390,11 +1562,7 @@ class ContactsProvider with ChangeNotifier {
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats() async {
return await _storageService.getStorageStats();
}
Future<void> persistNow() async {
await _storageService.saveContacts(_contactsForStorage());
return await _storageService.getStorageStats(namespace: _storageNamespace);
}
Future<void> clearPendingAdverts() async {
@@ -1402,7 +1570,7 @@ class ContactsProvider with ChangeNotifier {
return;
}
_pendingAdverts.clear();
await _storageService.clearPendingAdverts();
await _storageService.clearPendingAdverts(namespace: _storageNamespace);
notifyListeners();
}
@@ -1465,6 +1633,12 @@ class ContactsProvider with ChangeNotifier {
}
}
void _resetInMemoryState() {
_contacts.clear();
_savedContactGroups.clear();
_pendingAdverts.clear();
}
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
return {
'publicKey': base64Encode(advert.publicKey),

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
@@ -33,12 +36,18 @@ class DrawingProvider with ChangeNotifier {
LatLng? _measurementPoint1;
LatLng? _measurementPoint2;
double? _measuredDistance; // in meters
MapCoordinateSpace _activeCoordinateSpace = MapCoordinateSpace.geo;
String? _activeMapId;
double? _activeMetersPerPixel;
// Getters
DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor;
bool get showReceivedDrawings => _showReceivedDrawings;
bool get showSarMarkers => _showSarMarkers;
MapCoordinateSpace get activeCoordinateSpace => _activeCoordinateSpace;
String? get activeMapId => _activeMapId;
double? get activeMetersPerPixel => _activeMetersPerPixel;
List<MapDrawing> get drawings {
// Filter out hidden drawings first
var visibleDrawings = _drawings.where((d) => !d.isHidden);
@@ -48,8 +57,19 @@ class DrawingProvider with ChangeNotifier {
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
}
visibleDrawings = visibleDrawings.where((drawing) {
if (drawing.coordinateSpace != _activeCoordinateSpace) {
return false;
}
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
return drawing.mapId != null && drawing.mapId == _activeMapId;
}
return true;
});
return List.unmodifiable(visibleDrawings.toList());
}
MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
@@ -66,6 +86,35 @@ class DrawingProvider with ChangeNotifier {
_isInitialized = true;
}
Future<void> reloadProfileScopedState() async {
await _loadPreferences();
await _loadDrawings();
notifyListeners();
}
void setMapContext({
required MapCoordinateSpace coordinateSpace,
String? mapId,
double? metersPerPixel,
}) {
final changed =
coordinateSpace != _activeCoordinateSpace ||
mapId != _activeMapId ||
metersPerPixel != _activeMetersPerPixel;
_activeCoordinateSpace = coordinateSpace;
_activeMapId = mapId;
_activeMetersPerPixel = metersPerPixel;
if (_measurementPoint1 != null && _measurementPoint2 != null) {
_measuredDistance = _calculateDistance(
_measurementPoint1!,
_measurementPoint2!,
);
}
if (changed) {
notifyListeners();
}
}
/// Set drawing mode
void setDrawingMode(DrawingMode mode) {
if (_drawingMode != mode) {
@@ -98,15 +147,19 @@ class DrawingProvider with ChangeNotifier {
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
_showReceivedDrawings = prefs.getBool(_showReceivedDrawingsKey) ?? true;
_showSarMarkers = prefs.getBool(_showSarMarkersKey) ?? true;
_showReceivedDrawings =
prefs.getBool(_scopedKey(_showReceivedDrawingsKey)) ?? true;
_showSarMarkers = prefs.getBool(_scopedKey(_showSarMarkersKey)) ?? true;
notifyListeners();
}
Future<void> _savePreferences() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_showReceivedDrawingsKey, _showReceivedDrawings);
await prefs.setBool(_showSarMarkersKey, _showSarMarkers);
await prefs.setBool(
_scopedKey(_showReceivedDrawingsKey),
_showReceivedDrawings,
);
await prefs.setBool(_scopedKey(_showSarMarkersKey), _showSarMarkers);
}
/// Start drawing a line
@@ -137,6 +190,10 @@ class DrawingProvider with ChangeNotifier {
color: _selectedColor,
createdAt: DateTime.now(),
points: List.from(_currentLinePoints),
coordinateSpace: _activeCoordinateSpace,
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
? _activeMapId
: null,
);
_drawings.add(drawing);
@@ -180,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
? _rectangleStartPoint!.longitude
: endPoint.longitude,
),
coordinateSpace: _activeCoordinateSpace,
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
? _activeMapId
: null,
);
notifyListeners();
}
@@ -216,6 +277,10 @@ class DrawingProvider with ChangeNotifier {
createdAt: DateTime.now(),
topLeft: topLeft,
bottomRight: bottomRight,
coordinateSpace: _activeCoordinateSpace,
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
? _activeMapId
: null,
);
_drawings.add(drawing);
@@ -237,7 +302,9 @@ class DrawingProvider with ChangeNotifier {
/// Set second measurement point and calculate distance
void setMeasurementPoint2(LatLng point) {
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) {
return;
}
_measurementPoint2 = point;
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
@@ -246,6 +313,13 @@ class DrawingProvider with ChangeNotifier {
/// Calculate distance between two points using Haversine formula
double _calculateDistance(LatLng point1, LatLng point2) {
if (_activeCoordinateSpace == MapCoordinateSpace.customMap &&
_activeMetersPerPixel != null) {
final dy = point2.latitude - point1.latitude;
final dx = point2.longitude - point1.longitude;
final pixelDistance = math.sqrt((dx * dx) + (dy * dy));
return pixelDistance * _activeMetersPerPixel!;
}
const Distance distance = Distance();
return distance.as(LengthUnit.Meter, point1, point2);
}
@@ -302,7 +376,7 @@ class DrawingProvider with ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
final jsonList = _drawings.map((d) => d.toJson()).toList();
final jsonString = jsonEncode(jsonList);
await prefs.setString(_storageKey, jsonString);
await prefs.setString(_scopedKey(_storageKey), jsonString);
} catch (e) {
debugPrint('Error saving drawings: $e');
}
@@ -312,8 +386,11 @@ class DrawingProvider with ChangeNotifier {
Future<void> _loadDrawings() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_storageKey);
if (jsonString == null) return;
final jsonString = prefs.getString(_scopedKey(_storageKey));
if (jsonString == null || jsonString.isEmpty) {
_drawings.clear();
return;
}
final jsonList = jsonDecode(jsonString) as List<dynamic>;
_drawings.clear();
@@ -339,6 +416,10 @@ class DrawingProvider with ChangeNotifier {
color: _selectedColor,
createdAt: DateTime.now(),
points: _currentLinePoints,
coordinateSpace: _activeCoordinateSpace,
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
? _activeMapId
: null,
);
} else if (_drawingMode == DrawingMode.rectangle &&
_currentDrawing != null) {
@@ -376,6 +457,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
} else if (drawing is RectangleDrawing) {
return RectangleDrawing(
@@ -390,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}
return drawing;
@@ -404,9 +489,27 @@ class DrawingProvider with ChangeNotifier {
}
}
List<Map<String, dynamic>> exportDrawingsJson() {
return _drawings.map((drawing) => drawing.toJson()).toList();
}
Future<void> replaceDrawingsFromJson(
List<Map<String, dynamic>> jsonList,
) async {
_drawings
..clear()
..addAll(jsonList.map(MapDrawing.fromJson).whereType<MapDrawing>());
await _saveDrawings();
notifyListeners();
}
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
}
/// Get all unshared drawings (local drawings not yet sent)
List<MapDrawing> getUnsharedDrawings() {
return _drawings.where((d) => !d.isShared && !d.isReceived).toList();
return drawings.where((d) => !d.isShared && !d.isReceived).toList();
}
/// Mark a drawing as shared
@@ -428,6 +531,8 @@ class DrawingProvider with ChangeNotifier {
isShared: true,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
} else if (drawing is RectangleDrawing) {
_drawings[index] = RectangleDrawing(
@@ -442,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
isShared: true,
isSent: drawing.isSent,
isHidden: drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}
@@ -469,6 +576,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: !drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
} else if (drawing is RectangleDrawing) {
_drawings[index] = RectangleDrawing(
@@ -483,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
isShared: drawing.isShared,
isSent: drawing.isSent,
isHidden: !drawing.isHidden,
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}

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

@@ -1,30 +1,48 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:image_picker/image_picker.dart';
import 'package:latlong2/latlong.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/custom_map_config.dart';
import '../models/location_trail.dart';
import '../models/map_coordinate_space.dart';
import '../models/map_drawing.dart';
import '../models/sar_marker.dart';
import '../services/profiles_feature_service.dart';
import '../utils/custom_map_id.dart';
class MapProvider with ChangeNotifier {
static const String _customMapConfigKey = 'custom_map_config_v1';
static const String _customMapModeKey = 'custom_map_mode_v1';
static const String _customMapsDirName = 'custom_maps';
MapProvider() {
unawaited(_loadInitialState());
}
final ImagePicker _imagePicker = ImagePicker();
LatLng? _targetLocation;
LatLngBounds? _targetBounds;
double? _targetZoom;
bool _shouldAnimate = false;
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
String? _targetMapId;
// Track which contact paths are currently visible
final Set<String> _visibleContactPaths = {};
// Location trail tracking
LocationTrail? _currentTrail;
bool _isTrailVisible = true;
final List<LocationTrail> _trailHistory = [];
// WMS overlay toggles
bool _showCadastralOverlay = false;
bool _showForestRoadsOverlay = false;
bool _showHikingTrailsOverlay = false;
@@ -37,29 +55,30 @@ class MapProvider with ChangeNotifier {
bool _showPlaceNamesOverlay = false;
bool _showMunicipalityBordersOverlay = false;
// Contact trail toggles
bool _showAllContactTrails = true; // Default to showing all contact trails
bool _showAllContactTrails = true;
bool _hideRepeatersOnMap = false;
// Imported trail (from GPX)
LocationTrail? _importedTrail;
// Download area selection
bool _isSelectingDownloadArea = false;
LatLngBounds? _downloadAreaBounds;
CustomMapConfig? _customMapConfig;
bool _isUsingCustomMap = false;
LatLng? get targetLocation => _targetLocation;
LatLngBounds? get targetBounds => _targetBounds;
double? get targetZoom => _targetZoom;
bool get shouldAnimate => _shouldAnimate;
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
String? get targetMapId => _targetMapId;
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
// Trail getters
LocationTrail? get currentTrail => _currentTrail;
bool get isTrailVisible => _isTrailVisible;
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
bool get isTrailActive => _currentTrail?.isActive ?? false;
// WMS overlay getters
bool get showCadastralOverlay => _showCadastralOverlay;
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
@@ -72,69 +91,139 @@ class MapProvider with ChangeNotifier {
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
// Contact trail getters
bool get showAllContactTrails => _showAllContactTrails;
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
// Imported trail getters
LocationTrail? get importedTrail => _importedTrail;
// Download area getters
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
LatLngBounds? get downloadAreaBounds => _downloadAreaBounds;
CustomMapConfig? get customMapConfig => _customMapConfig;
bool get hasCustomMap => _customMapConfig != null;
bool get isUsingCustomMap => _isUsingCustomMap && _customMapConfig != null;
bool get shouldHideGpsData => isUsingCustomMap;
LatLngBounds? get customMapBounds => _customMapConfig?.bounds;
Future<void> reloadProfileScopedState() async {
await _loadInitialState();
}
bool matchesActiveCustomMap(String? mapId) {
return hasCustomMap &&
normalizeCustomMapId(_customMapConfig!.mapId) ==
normalizeCustomMapId(mapId);
}
void navigateToLocation({
required LatLng location,
double zoom = 15.0,
bool animate = true,
}) {
if (_isUsingCustomMap) {
_isUsingCustomMap = false;
unawaited(_saveCustomMapState());
}
_targetLocation = location;
_targetBounds = null;
_targetZoom = zoom;
_shouldAnimate = animate;
_targetCoordinateSpace = MapCoordinateSpace.geo;
_targetMapId = null;
notifyListeners();
}
String? navigateToMapPoint({
required LatLng point,
required MapCoordinateSpace coordinateSpace,
String? mapId,
double zoom = 15.0,
bool animate = true,
}) {
if (coordinateSpace == MapCoordinateSpace.customMap) {
if (!matchesActiveCustomMap(mapId)) {
return 'Load the matching custom map to view this item.';
}
if (!_isUsingCustomMap) {
_isUsingCustomMap = true;
unawaited(_saveCustomMapState());
}
} else if (_isUsingCustomMap) {
_isUsingCustomMap = false;
unawaited(_saveCustomMapState());
}
_targetLocation = point;
_targetBounds = null;
_targetZoom = zoom;
_shouldAnimate = animate;
_targetCoordinateSpace = coordinateSpace;
_targetMapId = mapId;
notifyListeners();
return null;
}
String? navigateToBounds({
required LatLngBounds bounds,
required MapCoordinateSpace coordinateSpace,
String? mapId,
bool animate = true,
}) {
if (coordinateSpace == MapCoordinateSpace.customMap) {
if (!matchesActiveCustomMap(mapId)) {
return 'Load the matching custom map to view this item.';
}
if (!_isUsingCustomMap) {
_isUsingCustomMap = true;
unawaited(_saveCustomMapState());
}
} else if (_isUsingCustomMap) {
_isUsingCustomMap = false;
unawaited(_saveCustomMapState());
}
_targetBounds = bounds;
_targetLocation = bounds.center;
_targetZoom = null;
_shouldAnimate = animate;
_targetCoordinateSpace = coordinateSpace;
_targetMapId = mapId;
notifyListeners();
return null;
}
void clearNavigation() {
_targetLocation = null;
_targetBounds = null;
_targetZoom = null;
_shouldAnimate = false;
// Don't notify listeners to avoid rebuilds
_targetCoordinateSpace = MapCoordinateSpace.geo;
_targetMapId = null;
}
/// Navigate to a drawing by its ID
void navigateToDrawing(String drawingId, dynamic drawingProvider) {
debugPrint('🗺️ [MapProvider] navigateToDrawing called with ID: $drawingId');
// Find the drawing in the provider
final drawings = drawingProvider.drawings as List;
debugPrint('🗺️ [MapProvider] Total drawings in provider: ${drawings.length}');
final drawing = drawings.cast<dynamic>().firstWhere(
(d) => d.id == drawingId,
orElse: () => null,
);
String? navigateToDrawing(String drawingId, dynamic drawingProvider) {
final drawing = drawingProvider.getDrawingById(drawingId) as MapDrawing?;
if (drawing == null) {
debugPrint('⚠️ [MapProvider] Drawing $drawingId not found');
debugPrint('⚠️ [MapProvider] Available drawing IDs: ${drawings.map((d) => d.id).toList()}');
return;
return 'Drawing not found.';
}
// Use MapDrawing's built-in getCenter and getBounds methods
final center = drawing.getCenter();
final bounds = drawing.getBounds();
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
if (!matchesActiveCustomMap(drawing.mapId)) {
return 'Load the matching custom map to view this drawing.';
}
return navigateToBounds(
bounds: drawing.getBounds(),
coordinateSpace: drawing.coordinateSpace,
mapId: drawing.mapId,
);
}
// Calculate appropriate zoom level based on bounds
// For larger drawings, use lower zoom to fit the whole drawing
// For smaller drawings, use higher zoom for better detail
final bounds = drawing.getBounds();
final latDiff = (bounds.north - bounds.south).abs();
final lonDiff = (bounds.east - bounds.west).abs();
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
// Zoom scale: smaller drawings get higher zoom
// 0.001 degrees (~100m) -> zoom 17
// 0.005 degrees (~500m) -> zoom 16
// 0.01 degrees (~1km) -> zoom 15
// 0.05 degrees (~5km) -> zoom 13
// 0.1 degrees (~10km) -> zoom 12
double zoom = 15.0;
if (maxDiff < 0.001) {
zoom = 17.0;
@@ -150,9 +239,125 @@ class MapProvider with ChangeNotifier {
zoom = 10.0;
}
final typeStr = drawing is LineDrawing ? 'line' : 'rectangle';
debugPrint('🗺️ [MapProvider] Navigating to drawing: $typeStr, zoom: $zoom');
navigateToLocation(location: center, zoom: zoom, animate: true);
navigateToLocation(
location: drawing.getCenter(),
zoom: zoom,
animate: true,
);
return null;
}
String? navigateToSarMarker(SarMarker marker) {
if (marker.coordinateSpace == MapCoordinateSpace.customMap) {
return navigateToMapPoint(
point: marker.location,
coordinateSpace: MapCoordinateSpace.customMap,
mapId: marker.mapId,
);
}
navigateToLocation(location: marker.location, zoom: 15.0, animate: true);
return null;
}
Future<bool> loadCustomMapFromGallery() async {
final picked = await _imagePicker.pickImage(source: ImageSource.gallery);
if (picked == null) {
return false;
}
final bytes = await picked.readAsBytes();
await setCustomMapImage(bytes: bytes, displayName: picked.name);
return true;
}
Future<void> replaceCustomMap() async {
await loadCustomMapFromGallery();
}
Future<void> setCustomMapImage({
required Uint8List bytes,
required String displayName,
}) async {
final dimensions = await _decodeImageSize(bytes);
final mapId = normalizeCustomMapId(sha256.convert(bytes).toString())!;
final documentsDir = await getApplicationDocumentsDirectory();
final dir = Directory('${documentsDir.path}/$_customMapsDirName');
if (!await dir.exists()) {
await dir.create(recursive: true);
}
final extension = _normalizedExtension(displayName);
final nextPath = '${dir.path}/custom_map_$mapId.$extension';
final nextFile = File(nextPath);
await nextFile.writeAsBytes(bytes, flush: true);
final previousPath = _customMapConfig?.filePath;
_customMapConfig = CustomMapConfig(
filePath: nextPath,
displayName: displayName,
mapId: mapId,
imageWidth: dimensions.$1,
imageHeight: dimensions.$2,
);
_isUsingCustomMap = true;
await _saveCustomMapState();
if (previousPath != null && previousPath != nextPath) {
unawaited(_deleteFileIfExists(previousPath));
}
notifyListeners();
}
Future<void> setCustomMapCalibration({
required LatLng pointA,
required LatLng pointB,
required double metersPerPixel,
}) async {
if (_customMapConfig == null) return;
_customMapConfig = _customMapConfig!.copyWith(
calibrationPointA: pointA,
calibrationPointB: pointB,
metersPerPixel: metersPerPixel,
);
await _saveCustomMapState();
notifyListeners();
}
Future<void> clearCustomMapCalibration() async {
if (_customMapConfig == null) return;
_customMapConfig = _customMapConfig!.copyWith(
clearMetersPerPixel: true,
clearCalibrationPointA: true,
clearCalibrationPointB: true,
);
await _saveCustomMapState();
notifyListeners();
}
Future<void> removeCustomMap() async {
final filePath = _customMapConfig?.filePath;
_customMapConfig = null;
_isUsingCustomMap = false;
clearNavigation();
await _saveCustomMapState();
if (filePath != null) {
await _deleteFileIfExists(filePath);
}
notifyListeners();
}
Future<void> enterCustomMapMode() async {
if (!hasCustomMap || _isUsingCustomMap) return;
_isUsingCustomMap = true;
await _saveCustomMapState();
notifyListeners();
}
Future<void> exitCustomMapMode() async {
if (!_isUsingCustomMap) return;
_isUsingCustomMap = false;
await _saveCustomMapState();
notifyListeners();
}
void updateZoom(double zoom) {
@@ -160,7 +365,6 @@ class MapProvider with ChangeNotifier {
notifyListeners();
}
/// Toggle path visibility for a contact
void toggleContactPath(String publicKeyHex) {
if (_visibleContactPaths.contains(publicKeyHex)) {
_visibleContactPaths.remove(publicKeyHex);
@@ -170,27 +374,22 @@ class MapProvider with ChangeNotifier {
notifyListeners();
}
/// Check if a contact's path is visible
bool isContactPathVisible(String publicKeyHex) {
return _visibleContactPaths.contains(publicKeyHex);
}
/// Hide all contact paths
void hideAllPaths() {
_visibleContactPaths.clear();
notifyListeners();
}
/// Show path for specific contact (hide all others)
void showOnlyPath(String publicKeyHex) {
_visibleContactPaths.clear();
_visibleContactPaths.add(publicKeyHex);
notifyListeners();
}
/// Start a new location trail
void startTrail() {
// End current trail if active
if (_currentTrail != null && _currentTrail!.isActive) {
endTrail();
}
@@ -203,22 +402,22 @@ class MapProvider with ChangeNotifier {
notifyListeners();
}
/// Add a point to the current trail
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
if (_currentTrail == null || !_currentTrail!.isActive) {
startTrail();
}
_currentTrail!.addPoint(TrailPoint(
position: position,
timestamp: DateTime.now(),
accuracy: accuracy,
speed: speed,
));
_currentTrail!.addPoint(
TrailPoint(
position: position,
timestamp: DateTime.now(),
accuracy: accuracy,
speed: speed,
),
);
notifyListeners();
}
/// End the current trail
void endTrail() {
if (_currentTrail != null) {
_currentTrail!.isActive = false;
@@ -231,13 +430,11 @@ class MapProvider with ChangeNotifier {
}
}
/// Toggle trail visibility
void toggleTrailVisibility() {
_isTrailVisible = !_isTrailVisible;
notifyListeners();
}
/// Clear the current trail
void clearCurrentTrail() {
if (_currentTrail != null) {
_currentTrail = null;
@@ -245,158 +442,193 @@ 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(),
loadTrailSettings(),
loadRepeaterVisibilitySettings(),
_loadCustomMapState(),
]);
}
/// 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);
await prefs.setBool(
_scopedKey('map_show_cadastral_overlay'),
_showCadastralOverlay,
);
await prefs.setBool(
_scopedKey('map_show_forest_roads_overlay'),
_showForestRoadsOverlay,
);
await prefs.setBool(
_scopedKey('map_show_hiking_trails_overlay'),
_showHikingTrailsOverlay,
);
await prefs.setBool(
_scopedKey('map_show_main_roads_overlay'),
_showMainRoadsOverlay,
);
await prefs.setBool(
_scopedKey('map_show_house_numbers_overlay'),
_showHouseNumbersOverlay,
);
await prefs.setBool(
_scopedKey('map_show_fire_hazard_zones_overlay'),
_showFireHazardZonesOverlay,
);
await prefs.setBool(
_scopedKey('map_show_historical_fires_overlay'),
_showHistoricalFiresOverlay,
);
await prefs.setBool(
_scopedKey('map_show_firebreaks_overlay'),
_showFirebreaksOverlay,
);
await prefs.setBool(
_scopedKey('map_show_kras_fire_zones_overlay'),
_showKrasFireZonesOverlay,
);
await prefs.setBool(
_scopedKey('map_show_place_names_overlay'),
_showPlaceNamesOverlay,
);
await prefs.setBool(
_scopedKey('map_show_municipality_borders_overlay'),
_showMunicipalityBordersOverlay,
);
}
/// Toggle all contact trails on/off
Future<void> toggleAllContactTrails() async {
_showAllContactTrails = !_showAllContactTrails;
notifyListeners();
await _saveTrailSettings();
}
/// Load trail settings from SharedPreferences
Future<void> loadTrailSettings() async {
final prefs = await SharedPreferences.getInstance();
_showAllContactTrails = prefs.getBool('map_show_all_contact_trails') ?? true; // Default to true (show all)
_showAllContactTrails =
prefs.getBool(_scopedKey('map_show_all_contact_trails')) ?? true;
notifyListeners();
}
/// Save trail settings to SharedPreferences
Future<void> _saveTrailSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
await prefs.setBool(
_scopedKey('map_show_all_contact_trails'),
_showAllContactTrails,
);
}
Future<void> setHideRepeatersOnMap(bool hide) async {
@@ -404,57 +636,168 @@ 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,
'showAllContactTrails': _showAllContactTrails,
'hideRepeatersOnMap': _hideRepeatersOnMap,
};
}
void applyWorkspaceJson(Map<String, dynamic> json) {
_currentTrail = json['currentTrail'] is Map<String, dynamic>
? LocationTrail.fromJson(json['currentTrail'] as Map<String, dynamic>)
: null;
_trailHistory
..clear()
..addAll(
(json['trailHistory'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(LocationTrail.fromJson),
);
_importedTrail = json['importedTrail'] is Map<String, dynamic>
? LocationTrail.fromJson(json['importedTrail'] as Map<String, dynamic>)
: null;
_isTrailVisible = json['isTrailVisible'] as bool? ?? true;
_showCadastralOverlay = json['showCadastralOverlay'] as bool? ?? false;
_showForestRoadsOverlay = json['showForestRoadsOverlay'] as bool? ?? false;
_showHikingTrailsOverlay =
json['showHikingTrailsOverlay'] as bool? ?? false;
_showMainRoadsOverlay = json['showMainRoadsOverlay'] as bool? ?? false;
_showHouseNumbersOverlay =
json['showHouseNumbersOverlay'] as bool? ?? false;
_showFireHazardZonesOverlay =
json['showFireHazardZonesOverlay'] as bool? ?? false;
_showHistoricalFiresOverlay =
json['showHistoricalFiresOverlay'] as bool? ?? false;
_showFirebreaksOverlay = json['showFirebreaksOverlay'] as bool? ?? false;
_showKrasFireZonesOverlay =
json['showKrasFireZonesOverlay'] as bool? ?? false;
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
_showMunicipalityBordersOverlay =
json['showMunicipalityBordersOverlay'] as bool? ?? false;
_showAllContactTrails = json['showAllContactTrails'] as bool? ?? true;
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
notifyListeners();
}
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
}
}

View File

@@ -37,6 +37,7 @@ class MessagesProvider with ChangeNotifier {
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
String? _storageNamespace;
// Track pending sent messages by expected ACK/TAG
final Map<int, Message> _pendingSentMessages = {};
@@ -95,6 +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,
@@ -149,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;
@@ -311,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);
@@ -684,7 +735,10 @@ 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).
return existing.senderKeyShort == message.senderKeyShort &&
existing.senderTimestamp == message.senderTimestamp;
}
if (message.isChannelMessage) {
@@ -692,49 +746,33 @@ 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;
if (!withinChannelRepeatWindow) {
return false;
}
final existingSenderKey = existing.senderKeyShort;
final incomingSenderKey = message.senderKeyShort;
if (existingSenderKey != null &&
incomingSenderKey != null &&
existingSenderKey == incomingSenderKey &&
withinChannelRepeatWindow) {
existingSenderKey == incomingSenderKey) {
return true;
}
final existingSenderName = _normalizedResolvedSenderName(existing);
final incomingSenderName = _normalizedResolvedSenderName(message);
if (existing.isChannelMessage && message.isChannelMessage) {
debugPrint(
'🧪 [MessagesProvider] Channel dedupe check: '
'existing(id=${existing.id}, sent=${existing.isSentMessage}, senderName=${existing.senderName ?? "-"}, resolvedSender=${existingSenderName ?? "-"}, senderKey=${existing.senderKeyShort ?? "-"}, ts=${existing.senderTimestamp}, pathLen=${existing.pathLen}) '
'incoming(id=${message.id}, sent=${message.isSentMessage}, senderName=${message.senderName ?? "-"}, resolvedSender=${incomingSenderName ?? "-"}, senderKey=${message.senderKeyShort ?? "-"}, ts=${message.senderTimestamp}, pathLen=${message.pathLen}) '
'withinWindow=$withinChannelRepeatWindow',
);
}
if (existingSenderName != null &&
incomingSenderName != null &&
existingSenderName == incomingSenderName) {
if (!existing.isSentMessage && !message.isSentMessage) {
return withinChannelRepeatWindow;
}
if (!withinChannelRepeatWindow) {
return false;
}
// Mirror meshcore-open behavior for self-authored channel messages:
// zero-hop self replays are dropped earlier in AppProvider, while
// routed self replays should fold into the local sent bubble when the
// public sender name matches within a short window.
if (existing.isSentMessage &&
!message.isSentMessage &&
existingSenderName == incomingSenderName) {
return true;
}
return true;
}
@@ -1051,6 +1089,7 @@ class MessagesProvider with ChangeNotifier {
messageReceptionDetails: _messageReceptionDetails,
messageTransferDetails: _messageTransferDetails,
messageRouteMetadata: _messageRouteMetadata,
namespace: _storageNamespace,
);
}
} catch (e) {
@@ -1199,7 +1238,10 @@ class MessagesProvider with ChangeNotifier {
Future<void> removeSarMarker(String id) async {
_sarMarkers.remove(id);
_removedSarMarkerIds.add(id);
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
await _storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
);
notifyListeners();
}
@@ -1324,7 +1366,12 @@ class MessagesProvider with ChangeNotifier {
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
}
@@ -1348,23 +1395,15 @@ class MessagesProvider with ChangeNotifier {
/// Clear all messages
void clearMessages() {
_cancelAllTimers();
_messages.clear();
_sarMarkers.clear();
_removedSarMarkerIds.clear();
_messageContactLocations.clear();
_messageReceptionDetails.clear();
_messageTransferDetails.clear();
_messageRouteMetadata.clear();
_pendingSentMessages.clear();
_messageContactMap.clear();
_groupedMessageMapping.clear();
_ackTagToRecipients.clear();
_messageAckHistory.clear();
_ackHistoryLookup.clear();
_completedAckHistory.clear();
_resetInMemoryState();
_retryManager.clearAll();
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
@@ -1372,13 +1411,29 @@ class MessagesProvider with ChangeNotifier {
Future<void> clearSarMarkers() async {
_removedSarMarkerIds.addAll(_sarMarkers.keys);
_sarMarkers.clear();
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
await _storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
);
notifyListeners();
}
/// Clear all data
void clearAll() {
_cancelAllTimers();
_resetInMemoryState();
_retryManager.clearAll();
_persistMessages();
unawaited(
_storageService.saveRemovedSarMarkerIds(
_removedSarMarkerIds,
namespace: _storageNamespace,
),
);
notifyListeners();
}
void _resetInMemoryState() {
_messages.clear();
_sarMarkers.clear();
_removedSarMarkerIds.clear();
@@ -1393,10 +1448,6 @@ class MessagesProvider with ChangeNotifier {
_messageAckHistory.clear();
_ackHistoryLookup.clear();
_completedAckHistory.clear();
_retryManager.clearAll();
_persistMessages();
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
notifyListeners();
}
int transferCountForSession({
@@ -2256,7 +2307,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');
@@ -2296,6 +2347,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,
@@ -2439,6 +2501,8 @@ class MessagesProvider with ChangeNotifier {
text: message.text,
isSarMarker: message.isSarMarker,
sarGpsCoordinates: message.sarGpsCoordinates,
sarCustomMapPoint: message.sarCustomMapPoint,
sarCustomMapId: message.sarCustomMapId,
sarNotes: message.sarNotes,
sarCustomEmoji: message.sarCustomEmoji,
sarColorIndex: message.sarColorIndex,

View File

@@ -5,6 +5,7 @@ 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';
@@ -68,6 +69,8 @@ class SensorsProvider with ChangeNotifier {
unawaited(_loadWatchedSensors());
}
String _key(String baseKey) => ProfileStorageScope.scopedKey(baseKey);
List<String> get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys);
bool get isLoaded => _isLoaded;
bool get isRefreshingAll => _isRefreshingAll;
@@ -78,20 +81,26 @@ class SensorsProvider with ChangeNotifier {
Future<void> _loadWatchedSensors() async {
try {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
final storedSpansJson = prefs.getString(_fieldSpanKey);
final storedLabelsJson = prefs.getString(_metricLabelKey);
final storedOrderJson = prefs.getString(_metricOrderKey);
final storedAutoRefreshJson = prefs.getString(_autoRefreshMinutesKey);
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) {
@@ -159,10 +168,15 @@ class SensorsProvider with ChangeNotifier {
}
}
Future<void> reloadProfileScopedState() async {
_isLoaded = false;
await _loadWatchedSensors();
}
Future<void> _persistWatchedSensors() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys);
await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys);
} catch (e) {
debugPrint('Error saving watched sensors: $e');
}
@@ -175,7 +189,10 @@ class SensorsProvider with ChangeNotifier {
for (final entry in _visibleFieldsBySensor.entries) {
encoded[entry.key] = entry.value.toList();
}
await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded));
await prefs.setString(
_key(_visibleSensorMetricsKey),
jsonEncode(encoded),
);
} catch (e) {
debugPrint('Error saving visible sensor metrics: $e');
}
@@ -184,7 +201,10 @@ 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');
}
@@ -193,7 +213,10 @@ class SensorsProvider with ChangeNotifier {
Future<void> _persistMetricLabels() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_metricLabelKey, jsonEncode(_metricLabelsBySensor));
await prefs.setString(
_key(_metricLabelKey),
jsonEncode(_metricLabelsBySensor),
);
} catch (e) {
debugPrint('Error saving sensor metric labels: $e');
}
@@ -202,7 +225,10 @@ class SensorsProvider with ChangeNotifier {
Future<void> _persistMetricOrder() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_metricOrderKey, jsonEncode(_metricOrderBySensor));
await prefs.setString(
_key(_metricOrderKey),
jsonEncode(_metricOrderBySensor),
);
} catch (e) {
debugPrint('Error saving sensor metric order: $e');
}
@@ -212,7 +238,7 @@ class SensorsProvider with ChangeNotifier {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
_autoRefreshMinutesKey,
_key(_autoRefreshMinutesKey),
jsonEncode(_autoRefreshMinutesBySensor),
);
} catch (e) {

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/connection_provider.dart';
import '../l10n/app_localizations.dart';
class AddContactScreen extends StatefulWidget {
const AddContactScreen({super.key});
@@ -80,7 +81,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Clipboard is empty')));
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)));
return;
}
@@ -141,7 +142,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Contact imported')));
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)));
setState(() {
_importSucceeded = true;
});
@@ -151,107 +152,234 @@ class _AddContactScreenState extends State<AddContactScreen> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final normalized = _normalizeAdvertText(_advertController.text);
final colorScheme = theme.colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Add Contact')),
appBar: AppBar(title: Text(AppLocalizations.of(context)!.addContact)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Import an exported contact advert, like meshcore-open.',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Paste a `meshcore://...` link or raw hex advert from the clipboard.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextField(
controller: _advertController,
minLines: 4,
maxLines: 8,
onChanged: (_) {
if (_validationError != null || _importSucceeded) {
setState(() {
_importSucceeded = false;
_validationError = null;
});
}
},
decoration: InputDecoration(
labelText: 'Contact advert',
hintText: 'meshcore://...',
alignLabelWithHint: true,
border: const OutlineInputBorder(),
errorText: _validationError,
),
),
const SizedBox(height: 12),
if (normalized != null)
Text(
'Advert size: ${normalized.length ~/ 2} bytes',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
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),
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _isImporting ? null : _pasteFromClipboard,
icon: const Icon(Icons.content_paste_go_outlined),
label: const Text('Paste'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton.icon(
onPressed: (_isImporting || _importSucceeded)
? null
: _importContact,
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
? 'Added'
: 'Add Contact',
),
),
),
],
),
if (_importSucceeded) ...[
const SizedBox(height: 12),
Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.check_circle,
size: 18,
color: theme.colorScheme.primary,
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(width: 8),
const SizedBox(height: 14),
Text(
'Contact added. Paste or edit another advert to import again.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
'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),
],
),
);

View File

@@ -493,7 +493,7 @@ class _ContactsTabState extends State<ContactsTab> {
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.message_outlined),
leading: Icon(Icons.message_outlined),
title: Text(l10n.messages),
onTap: () async {
Navigator.pop(sheetContext);
@@ -502,7 +502,7 @@ class _ContactsTabState extends State<ContactsTab> {
),
if (channel.displayLocation != null)
ListTile(
leading: const Icon(Icons.map_outlined),
leading: Icon(Icons.map_outlined),
title: Text(l10n.viewOnMap),
onTap: () {
Navigator.pop(sheetContext);
@@ -511,7 +511,7 @@ class _ContactsTabState extends State<ContactsTab> {
),
if (!channel.isPublicChannel)
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
leading: Icon(Icons.delete, color: Colors.red),
title: Text(
l10n.deleteChannel,
style: const TextStyle(color: Colors.red),
@@ -655,12 +655,12 @@ class _ContactsTabState extends State<ContactsTab> {
size: 64,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 16),
SizedBox(height: 16),
Text(
l10n.noContactsYet,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
SizedBox(height: 8),
Text(
l10n.connectToDeviceToLoadContacts,
style: Theme.of(context).textTheme.bodyMedium,
@@ -674,8 +674,8 @@ class _ContactsTabState extends State<ContactsTab> {
padding: const EdgeInsets.only(top: 16),
child: OutlinedButton.icon(
onPressed: () => _openAddContactScreen(context),
icon: const Icon(Icons.person_add_alt_1_outlined),
label: const Text('Add Contact'),
icon: Icon(Icons.person_add_alt_1_outlined),
label: Text(l10n.addContact),
),
),
],
@@ -688,6 +688,19 @@ class _ContactsTabState extends State<ContactsTab> {
child: ListView(
padding: const EdgeInsets.all(8),
children: [
// Favourites (contacts with firmware favourite flag set)
if (contactsProvider.favouriteContacts.isNotEmpty) ...[
_SectionHeader(
title: l10n.favourites,
count: contactsProvider.favouriteContacts.length,
icon: Icons.star,
),
..._buildContactSectionItems(
contactsProvider.favouriteContacts,
),
const Divider(height: 32),
],
// Team Members (Chat contacts)
if (showTeamMembersSection) ...[
_SectionHeader(
@@ -737,7 +750,31 @@ class _ContactsTabState extends State<ContactsTab> {
title: l10n.repeaters,
count: repeaters.length,
icon: Icons.router,
trailing: _buildSortMenu(context, ContactSection.repeaters),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (context
.watch<ConnectionProvider>()
.deviceInfo
.isConnected)
IconButton(
icon: const Icon(Icons.radar, size: 20),
tooltip: 'Discover repeaters',
visualDensity: VisualDensity.compact,
onPressed: () {
context
.read<ConnectionProvider>()
.discoverNodeType(advertType: 2);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.repeaterDiscoverySent),
),
);
},
),
_buildSortMenu(context, ContactSection.repeaters),
],
),
),
_buildSectionFilterField(
context,
@@ -765,9 +802,8 @@ class _ContactsTabState extends State<ContactsTab> {
_buildNoFilterResults(context)
else if (showRepeatersOthersGroup)
_InferredContactGroupCard(
label: 'Others',
label: l10n.others,
contacts: ungroupedRepeaters,
kindLabel: 'Auto group',
compactContacts: true,
currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters,
@@ -786,10 +822,34 @@ class _ContactsTabState extends State<ContactsTab> {
// Sensors
if (showSensorsSection) ...[
_SectionHeader(
title: 'Sensors',
title: l10n.sensors,
count: sensors.length,
icon: Icons.sensors,
trailing: _buildSortMenu(context, ContactSection.sensors),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (context
.watch<ConnectionProvider>()
.deviceInfo
.isConnected)
IconButton(
icon: const Icon(Icons.radar, size: 20),
tooltip: 'Discover sensors',
visualDensity: VisualDensity.compact,
onPressed: () {
context
.read<ConnectionProvider>()
.discoverNodeType(advertType: 4);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.sensorDiscoverySent),
),
);
},
),
_buildSortMenu(context, ContactSection.sensors),
],
),
),
_buildSectionFilterField(
context,
@@ -886,8 +946,8 @@ class _ContactsTabState extends State<ContactsTab> {
Expanded(
child: OutlinedButton.icon(
onPressed: () => _openAddContactScreen(context),
icon: const Icon(Icons.person_add_alt_1_outlined),
label: const Text('Add Contact'),
icon: Icon(Icons.person_add_alt_1_outlined),
label: Text(l10n.addContact),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
@@ -900,7 +960,7 @@ class _ContactsTabState extends State<ContactsTab> {
Expanded(
child: OutlinedButton.icon(
onPressed: () => _showAddChannelDialog(context),
icon: const Icon(Icons.add_circle_outline),
icon: Icon(Icons.add_circle_outline),
label: Text(l10n.addChannel),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
@@ -984,7 +1044,6 @@ class _ContactsTabState extends State<ContactsTab> {
onDelete: () => context
.read<ContactsProvider>()
.removeSavedGroupById(group.group.id),
kindLabel: group.group.isAutoGroup ? 'Auto group' : 'Saved filter',
),
)
.toList();
@@ -1226,7 +1285,7 @@ class _ContactsTabState extends State<ContactsTab> {
? colorScheme.primary
: null,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Text(l10n.lastSeen),
],
),
@@ -1242,7 +1301,7 @@ class _ContactsTabState extends State<ContactsTab> {
? colorScheme.primary
: null,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Text(l10n.distance),
],
),
@@ -1320,7 +1379,6 @@ class _SectionHeader extends StatelessWidget {
class _InferredContactGroupCard extends StatelessWidget {
final String label;
final List<Contact> contacts;
final String? kindLabel;
final bool compactContacts;
final Position? currentPosition;
final double Function(double, double, double, double) calculateDistance;
@@ -1332,7 +1390,6 @@ class _InferredContactGroupCard extends StatelessWidget {
const _InferredContactGroupCard({
required this.label,
required this.contacts,
this.kindLabel,
this.compactContacts = false,
required this.currentPosition,
required this.calculateDistance,
@@ -1347,10 +1404,10 @@ class _InferredContactGroupCard extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
),
@@ -1358,40 +1415,29 @@ class _InferredContactGroupCard extends StatelessWidget {
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
childrenPadding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
tilePadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
childrenPadding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
minTileHeight: 44,
initiallyExpanded: false,
leading: Icon(
Icons.folder_copy_outlined,
size: 18,
size: 16,
color: colorScheme.primary,
),
title: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
if (kindLabel case final value?)
Text(
value,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
],
child: Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
),
const SizedBox(width: 8),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
@@ -1402,13 +1448,19 @@ class _InferredContactGroupCard extends StatelessWidget {
),
),
if (onDelete != null) ...[
const SizedBox(width: 4),
const SizedBox(width: 2),
IconButton(
tooltip: 'Delete group',
onPressed: onDelete,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 32,
height: 32,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.delete_outline_rounded,
size: 18,
size: 16,
color: colorScheme.error,
),
),
@@ -1419,7 +1471,6 @@ class _InferredContactGroupCard extends StatelessWidget {
...contacts.map(
(contact) => ContactTile(
contact: contact,
groupLabel: label,
compact: compactContacts,
currentPosition: currentPosition,
calculateDistance: calculateDistance,

View File

@@ -182,6 +182,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
bool _telemetryEnabled = false;
bool _repeatEnabled = false;
bool? _gpsEnabled; // null = not supported by hardware
bool _gpsLoading = false;
bool _autoAddDiscoveredContactsEnabled = true;
bool _autoAddUsersEnabled = true;
bool _autoAddRepeatersEnabled = true;
@@ -294,6 +296,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
WidgetsBinding.instance.addPostFrameCallback((_) {
_connectionProvider.getBatteryAndStorage();
unawaited(_connectionProvider.getAutoaddConfig());
unawaited(_loadGpsMode());
});
}
@@ -701,6 +704,37 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}
}
Future<void> _loadGpsMode() async {
try {
final vars = await _connectionProvider.getCustomVars();
if (!mounted) return;
final gpsValue = vars['gps'];
setState(() {
_gpsEnabled = gpsValue != null ? gpsValue == '1' : null;
});
} catch (_) {
// Device may not support custom vars (old firmware / no GPS hardware)
}
}
Future<void> _setGpsMode(bool enabled) async {
setState(() => _gpsLoading = true);
try {
await _connectionProvider.setCustomVar('gps', enabled ? '1' : '0');
if (!mounted) return;
setState(() {
_gpsEnabled = enabled;
_gpsLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _gpsLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to set GPS mode: $e')),
);
}
}
Future<void> _useCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
@@ -793,7 +827,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Wipe device data'),
title: Text(AppLocalizations.of(context)!.wipeDeviceData),
content: const Text(
'This will erase all data on the connected device, including contacts, keys, and saved settings. This cannot be undone.',
),
@@ -808,7 +842,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Wipe device'),
child: Text(AppLocalizations.of(context)!.wipeDevice),
),
],
),
@@ -859,7 +893,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
.toList();
if (contacts.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No device contacts to clear.')),
SnackBar(content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear)),
);
return;
}
@@ -867,7 +901,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Clear all contacts'),
title: Text(AppLocalizations.of(context)!.clearAllContacts),
content: Text(
'This will remove ${contacts.length} contact${contacts.length == 1 ? '' : 's'} from the connected device. Channels and radio settings will not be changed.',
),
@@ -882,7 +916,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Clear contacts'),
child: Text(AppLocalizations.of(context)!.clearContacts),
),
],
),
@@ -954,7 +988,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
.toList();
if (channels.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No custom channels to clear.')),
SnackBar(content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear)),
);
return;
}
@@ -962,7 +996,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Clear all channels'),
title: Text(AppLocalizations.of(context)!.clearAllChannels),
content: Text(
'This will remove ${channels.length} custom channel${channels.length == 1 ? '' : 's'} from the connected device. Contacts and radio settings will not be changed.',
),
@@ -977,7 +1011,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Clear channels'),
child: Text(AppLocalizations.of(context)!.clearChannels),
),
],
),
@@ -1085,23 +1119,23 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
'${_getDeviceTypeString(context, deviceInfo.deviceType)}${deviceInfo.semanticVersion ?? deviceInfo.manufacturerModel ?? AppLocalizations.of(context)!.unknown}',
stats: [
_HeroStatData(
label: 'Location',
label: AppLocalizations.of(context)!.location,
value: locationSet ? 'Shared' : 'Hidden',
icon: Icons.my_location_rounded,
emphasized: locationSet,
),
_HeroStatData(
label: 'Frequency',
label: AppLocalizations.of(context)!.frequency,
value: '${_freqController.text} MHz',
icon: Icons.settings_input_antenna_rounded,
),
_HeroStatData(
label: 'Bandwidth',
label: AppLocalizations.of(context)!.bandwidth,
value: _selectedBandwidth,
icon: Icons.width_normal_rounded,
),
_HeroStatData(
label: 'Model',
label: AppLocalizations.of(context)!.model,
value:
deviceInfo.manufacturerModel ??
AppLocalizations.of(context)!.unknown,
@@ -1109,10 +1143,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
),
],
),
const SizedBox(height: 20),
SizedBox(height: 20),
_ConfigSectionCard(
title: 'Storage',
subtitle: 'Available space on this device.',
title: AppLocalizations.of(context)!.storage,
subtitle: AppLocalizations.of(context)!.availableSpaceOnThisDevice,
icon: Icons.storage_rounded,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1121,16 +1155,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
children: [
Expanded(
child: _StorageStat(
label: 'Used',
label: AppLocalizations.of(context)!.used,
value: _formatStorage(
deviceInfo.storageUsedKb ?? 0,
),
),
),
const SizedBox(width: 12),
SizedBox(width: 12),
Expanded(
child: _StorageStat(
label: 'Total',
label: AppLocalizations.of(context)!.total,
value: deviceInfo.storageTotalKb != null
? _formatStorage(deviceInfo.storageTotalKb!)
: AppLocalizations.of(context)!.unknown,
@@ -1143,9 +1177,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
],
),
),
const SizedBox(height: 20),
SizedBox(height: 20),
_ConfigSectionCard(
title: 'Auto discovery',
title: AppLocalizations.of(context)!.autoDiscovery,
subtitle:
'Control how the radio auto-adds discovered nodes to its contacts table.',
icon: Icons.person_search_rounded,
@@ -1156,7 +1190,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
icon: _autoAddDiscoveredContactsEnabled
? Icons.person_add_alt_1
: Icons.person_add_disabled,
title: 'Enable automatic adding',
title: AppLocalizations.of(context)!.enableAutomaticAdding,
description:
'Turn this off to keep discoveries manual-only on the radio.',
accentColor: _autoAddDiscoveredContactsEnabled
@@ -1172,10 +1206,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
},
),
),
const SizedBox(height: 18),
SizedBox(height: 18),
_SettingHighlightCard(
icon: Icons.person_outline_rounded,
title: 'Auto-add users',
title: AppLocalizations.of(context)!.autoaddUsers,
description:
'Automatically store discovered user/chat nodes.',
accentColor: _autoAddUsersEnabled
@@ -1193,10 +1227,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: null,
),
),
const SizedBox(height: 14),
SizedBox(height: 14),
_SettingHighlightCard(
icon: Icons.router_outlined,
title: 'Auto-add repeaters',
title: AppLocalizations.of(context)!.autoaddRepeaters,
description:
'Automatically store discovered repeater nodes.',
accentColor: _autoAddRepeatersEnabled
@@ -1214,10 +1248,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: null,
),
),
const SizedBox(height: 14),
SizedBox(height: 14),
_SettingHighlightCard(
icon: Icons.meeting_room_outlined,
title: 'Auto-add room servers',
title: AppLocalizations.of(context)!.autoaddRoomServers,
description:
'Automatically store discovered room/server nodes.',
accentColor: _autoAddRoomServersEnabled
@@ -1235,10 +1269,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: null,
),
),
const SizedBox(height: 14),
SizedBox(height: 14),
_SettingHighlightCard(
icon: Icons.sensors_outlined,
title: 'Auto-add sensors',
title: AppLocalizations.of(context)!.autoaddSensors,
description:
'Automatically store discovered sensor nodes.',
accentColor: _autoAddSensorsEnabled
@@ -1256,10 +1290,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: null,
),
),
const SizedBox(height: 14),
SizedBox(height: 14),
_SettingHighlightCard(
icon: Icons.history_toggle_off_rounded,
title: 'Overwrite oldest when full',
title: AppLocalizations.of(context)!.overwriteOldestWhenFull,
description:
'Allow the radio to replace the oldest contact when storage is full.',
accentColor: _overwriteOldestAutoAddEnabled
@@ -1295,16 +1329,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: _saveAutoDiscoverySettings,
isSaving: _isSavingAutoDiscoverySettings,
isSaved: _autoDiscoverySettingsSaved,
label: 'Save discovery settings',
label: AppLocalizations.of(context)!.saveDiscoverySettings,
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(height: 20),
_ConfigSectionCard(
title: AppLocalizations.of(context)!.publicInfo,
subtitle: 'Choose the name and location this device shares.',
subtitle: AppLocalizations.of(context)!.chooseTheNameAndLocationThisDeviceShares,
icon: Icons.public_rounded,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1331,6 +1365,32 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
},
),
),
if (_gpsEnabled != null) ...[
const SizedBox(height: 12),
_SettingHighlightCard(
icon: _gpsEnabled!
? Icons.gps_fixed
: Icons.gps_off,
title: AppLocalizations.of(context)!.gpsModule,
description:
'Enable or disable the onboard GPS hardware.',
accentColor: _gpsEnabled!
? colorScheme.primary
: colorScheme.onSurfaceVariant,
trailing: _gpsLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Switch(
value: _gpsEnabled!,
onChanged: _setGpsMode,
),
),
],
const SizedBox(height: 18),
TextField(
controller: _nameController,
@@ -1423,16 +1483,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
onPressed: _isSavingPublicInfo ? null : _savePublicInfo,
isSaving: _isSavingPublicInfo,
isSaved: _publicInfoSaved,
label: 'Save public info',
label: AppLocalizations.of(context)!.savePublicInfo,
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(height: 20),
_ConfigSectionCard(
title: AppLocalizations.of(context)!.radioSettings,
subtitle: 'Choose a preset or fine-tune custom radio settings.',
subtitle: AppLocalizations.of(context)!.chooseAPresetOrFinetuneCustomRadioSettings,
icon: Icons.settings_input_antenna_rounded,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1452,9 +1512,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
),
isExpanded: true,
items: [
const DropdownMenuItem<_RadioPreset?>(
DropdownMenuItem<_RadioPreset?>(
value: null,
child: Text('Custom'),
child: Text(AppLocalizations.of(context)!.custom),
),
..._radioPresets.map(
(preset) => DropdownMenuItem<_RadioPreset?>(
@@ -1653,10 +1713,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
),
),
if (deviceInfo.clientRepeat != null) ...[
const SizedBox(height: 16),
SizedBox(height: 16),
_SettingHighlightCard(
icon: Icons.repeat_rounded,
title: 'Repeat nearby traffic',
title: AppLocalizations.of(context)!.repeatNearbyTraffic,
description:
deviceInfo.allowedRepeatFreqRanges != null &&
deviceInfo.allowedRepeatFreqRanges!.isNotEmpty
@@ -1693,16 +1753,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: _saveRadioSettings,
isSaving: _isSavingRadioSettings,
isSaved: _radioSettingsSaved,
label: 'Save radio settings',
label: AppLocalizations.of(context)!.saveRadioSettings,
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(height: 20),
_ConfigSectionCard(
title: 'Danger zone',
subtitle: 'Destructive device actions.',
title: AppLocalizations.of(context)!.dangerZone,
subtitle: AppLocalizations.of(context)!.destructiveDeviceActions,
icon: Icons.warning_amber_rounded,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1770,8 +1830,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
strokeWidth: 2,
),
)
: const Icon(Icons.people_alt_outlined),
label: const Text('Clear all contacts'),
: Icon(Icons.people_alt_outlined),
label: Text(AppLocalizations.of(context)!.clearAllContacts),
),
),
const SizedBox(height: 12),
@@ -1794,8 +1854,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
strokeWidth: 2,
),
)
: const Icon(Icons.forum_outlined),
label: const Text('Clear all channels'),
: Icon(Icons.forum_outlined),
label: Text(AppLocalizations.of(context)!.clearAllChannels),
),
),
const SizedBox(height: 12),
@@ -1810,8 +1870,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
foregroundColor: colorScheme.onError,
minimumSize: const Size.fromHeight(52),
),
icon: const Icon(Icons.delete_forever_rounded),
label: const Text('Wipe device data'),
icon: Icon(Icons.delete_forever_rounded),
label: Text(AppLocalizations.of(context)!.wipeDeviceData),
),
),
],

View File

@@ -54,7 +54,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Clear discoveries'),
title: Text(AppLocalizations.of(context)!.clearDiscoveries),
content: Text(
'Remove all $pendingCount pending discover${pendingCount == 1 ? 'y' : 'ies'} from this device?',
),
@@ -65,7 +65,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Clear all'),
child: Text(AppLocalizations.of(context)!.clearAllLabel),
),
],
),
@@ -80,7 +80,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cleared pending discoveries.')),
SnackBar(content: Text(AppLocalizations.of(context)!.clearedPendingDiscoveries)),
);
}
@@ -169,8 +169,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
});
try {
final contactsProvider = context.read<ContactsProvider>();
for (final advert in adverts) {
if (!mounted) break;
// Skip already-resolved contacts
if (contactsProvider.findContactByKey(advert.publicKey) != null) {
continue;
}
await _resolveAdvert(advert);
}
} finally {
@@ -358,7 +363,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('Discovery'),
title: Text(l10n.discovery),
actions: [
Consumer<ConnectionProvider>(
builder: (context, connectionProvider, child) {
@@ -388,8 +393,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
)
: const Icon(Icons.router_outlined),
const SizedBox(width: 12),
const Text('Discover repeaters'),
SizedBox(width: 12),
Text(l10n.discoverRepeaters),
],
),
),
@@ -407,8 +412,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
)
: const Icon(Icons.sensors_outlined),
const SizedBox(width: 12),
const Text('Discover sensors'),
SizedBox(width: 12),
Text(l10n.discoverSensors),
],
),
),
@@ -426,6 +431,16 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final isConnected = connectionProvider.deviceInfo.isConnected;
final cachedNodes = nodesSnapshot.data ?? const <MeshMapNode>[];
// Track which pending adverts are already in the contacts list
final resolvedKeySet = <String>{};
for (final advert in pendingAdverts) {
if (contactsProvider.findContactByKey(advert.publicKey) != null) {
resolvedKeySet.add(advert.publicKeyHex);
}
}
final totalCount = pendingAdverts.length;
return ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -442,7 +457,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
const SizedBox(width: 12),
Expanded(
child: Text(
'Pending discoveries (${pendingAdverts.length})',
'Discovered nodes ($totalCount)',
style: Theme.of(context).textTheme.titleMedium,
),
),
@@ -475,7 +490,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
: const Icon(
Icons.download_for_offline_outlined,
),
label: const Text('Resolve all'),
label: Text(l10n.resolveAll),
),
),
const SizedBox(width: 10),
@@ -484,8 +499,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
onPressed: pendingAdverts.isNotEmpty
? _clearAllDiscoveries
: null,
icon: const Icon(Icons.clear_all_rounded),
label: const Text('Clear all'),
icon: Icon(Icons.clear_all_rounded),
label: Text(l10n.clearAllLabel),
),
),
],
@@ -495,7 +510,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
),
const SizedBox(height: 16),
if (pendingAdverts.isEmpty)
if (totalCount == 0)
Padding(
padding: const EdgeInsets.symmetric(vertical: 48),
child: Column(
@@ -507,112 +522,29 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
const SizedBox(height: 16),
Text(
'No pending discoveries',
'No discovered nodes',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Unknown adverts will appear here until you choose to resolve them.',
'Use the menu to discover repeaters and sensors on the mesh.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
...pendingAdverts.map((advert) {
final isResolving = _resolvingAdvertKeys.contains(
advert.publicKeyHex,
);
final displayName = _displayNameForAdvert(
advert,
contactsProvider,
cachedNodes,
);
final typeLabel = _resolvedTypeLabelForAdvert(
advert,
cachedNodes,
);
final downMetric = SignalMetric.fromValues(
rssiDbm: advert.rxRssiDbm,
snrDb: advert.rxSnr,
);
final upMetric = SignalMetric.fromValues(
rssiDbm: advert.repeaterLastRssi,
snrDb: advert.repeaterLastSnr,
);
final detailLines = <String>[
'${l10n.publicKey}: ${advert.shortDisplayKey}',
];
final summaryParts = <String>[];
final battery = advert.repeaterBatteryPercent;
if (battery != null) {
summaryParts.add('Battery ${battery.round()}%');
}
if (advert.repeaterQueueLen != null) {
summaryParts.add('Queue ${advert.repeaterQueueLen}');
}
if (summaryParts.isNotEmpty) {
detailLines.add(summaryParts.join(''));
}
detailLines.add(
'${l10n.lastSeen}: ${_formatRelativeTime(context, advert.receivedAt)}',
);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(child: Icon(_iconForAdvert(advert))),
const SizedBox(width: 12),
Expanded(
child: _buildAdvertTitle(
context,
displayName: displayName,
subtitle: typeLabel,
),
),
if (downMetric != null || upMetric != null) ...[
const SizedBox(width: 12),
_buildSignalSummary(
context,
downMetric: downMetric,
upMetric: upMetric,
),
],
const SizedBox(width: 8),
isResolving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: IconButton(
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.person_add_alt_1),
tooltip: 'Resolve contact',
onPressed: isConnected
? () => _resolveAdvert(advert)
: null,
),
],
),
const SizedBox(height: 10),
Text(
detailLines.join('\n'),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}),
...pendingAdverts.map((advert) =>
_buildPendingAdvertCard(
context,
advert: advert,
contactsProvider: contactsProvider,
isConnected: isConnected,
isResolved: resolvedKeySet.contains(advert.publicKeyHex),
cachedNodes: cachedNodes,
l10n: l10n,
),
),
],
);
},
@@ -621,6 +553,105 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
);
}
Widget _buildPendingAdvertCard(
BuildContext context, {
required PendingAdvert advert,
required ContactsProvider contactsProvider,
required bool isConnected,
required bool isResolved,
required List<MeshMapNode> cachedNodes,
required AppLocalizations l10n,
}) {
final isResolving = _resolvingAdvertKeys.contains(advert.publicKeyHex);
final displayName = _displayNameForAdvert(advert, contactsProvider, cachedNodes);
final typeLabel = _resolvedTypeLabelForAdvert(advert, cachedNodes);
final downMetric = SignalMetric.fromValues(
rssiDbm: advert.rxRssiDbm,
snrDb: advert.rxSnr,
);
final upMetric = SignalMetric.fromValues(
rssiDbm: advert.repeaterLastRssi,
snrDb: advert.repeaterLastSnr,
);
final detailLines = <String>[
'${l10n.publicKey}: ${advert.shortDisplayKey}',
];
final summaryParts = <String>[];
final battery = advert.repeaterBatteryPercent;
if (battery != null) {
summaryParts.add('Battery ${battery.round()}%');
}
if (advert.repeaterQueueLen != null) {
summaryParts.add('Queue ${advert.repeaterQueueLen}');
}
if (summaryParts.isNotEmpty) {
detailLines.add(summaryParts.join(''));
}
detailLines.add(
'${l10n.lastSeen}: ${_formatRelativeTime(context, advert.receivedAt)}',
);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(child: Icon(_iconForAdvert(advert))),
const SizedBox(width: 12),
Expanded(
child: _buildAdvertTitle(
context,
displayName: displayName,
subtitle: typeLabel,
),
),
if (downMetric != null || upMetric != null) ...[
const SizedBox(width: 12),
_buildSignalSummary(
context,
downMetric: downMetric,
upMetric: upMetric,
),
],
const SizedBox(width: 8),
if (isResolved)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.primary,
size: 24,
)
else if (isResolving)
const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
IconButton(
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.person_add_alt_1),
tooltip: 'Resolve contact',
onPressed: isConnected
? () => _resolveAdvert(advert)
: null,
),
],
),
const SizedBox(height: 10),
Text(
detailLines.join('\n'),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
Widget _buildSignalSummary(
BuildContext context, {
SignalMetric? downMetric,

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@@ -20,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';
@@ -28,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 }
@@ -57,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;
@@ -65,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 [
@@ -88,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;
@@ -105,6 +116,10 @@ class _HomeScreenState extends State<HomeScreen>
_showPermissionDialog();
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleConnectionProviderChanged();
});
}
void _initTabController() {
@@ -220,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);
}
@@ -237,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;
});
}
}
@@ -250,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;
@@ -355,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),
@@ -381,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),
@@ -395,42 +434,24 @@ 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,
),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
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: 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,
width: 2.5,
height: 4.0 + (i * 2),
decoration: BoxDecoration(
color: i < activeBars ? color : inactive,
borderRadius: BorderRadius.circular(1.5),
),
),
],
),
],
);
}
@@ -590,6 +611,9 @@ class _HomeScreenState extends State<HomeScreen>
}
context.watch<AppProvider>();
final activeProfileId = context.select<ProfileManager, String>(
(manager) => manager.activeProfileId,
);
final enabledTabs = _enabledTabs;
final isMapTabActive = _currentTab == _HomeTab.map;
@@ -614,7 +638,7 @@ class _HomeScreenState extends State<HomeScreen>
onPressed: () async {
await provider.disconnect();
},
icon: const Icon(Icons.power_settings_new),
icon: Icon(Icons.power_settings_new),
tooltip: AppLocalizations.of(context)!.disconnect,
color: Colors.red.shade700,
);
@@ -626,6 +650,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(
@@ -633,8 +660,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: () {
@@ -655,11 +682,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: () {
@@ -690,11 +717,11 @@ class _HomeScreenState extends State<HomeScreen>
items.add(
PopupMenuItem(
child: const Row(
child: Row(
children: [
Icon(Icons.router_outlined),
SizedBox(width: 8),
Text('Repeaters Map'),
const Icon(Icons.router_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.repeatersMap),
],
),
onTap: () {
@@ -751,7 +778,7 @@ class _HomeScreenState extends State<HomeScreen>
child: Row(
children: [
const Icon(Icons.settings),
const SizedBox(width: 8),
SizedBox(width: 8),
Text(AppLocalizations.of(context)!.settings),
],
),
@@ -776,6 +803,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;
},
),
@@ -809,6 +861,7 @@ class _HomeScreenState extends State<HomeScreen>
);
case _HomeTab.map:
return MapTab(
key: ValueKey<String>('map:$activeProfileId'),
onFullscreenChanged: (isFullscreen) {
setState(() {
_isMapFullscreen = isFullscreen;
@@ -865,7 +918,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:
@@ -890,19 +943,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),
@@ -913,22 +982,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),
),
),
),
@@ -936,7 +1002,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,
@@ -1008,17 +1074,12 @@ class _HomeScreenState extends State<HomeScreen>
if (!isTcpConnected &&
deviceInfo.signalRssi != null) ...[
const SizedBox(width: 4),
SizedBox(
width: 28,
child: Text(
'${deviceInfo.signalRssi}',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: signalColor,
),
maxLines: 1,
),
_buildMiniSignalBars(
activeBars:
BatteryDisplayHelper.getSignalBars(
deviceInfo.signalRssi!,
),
color: signalColor,
),
],
if (deviceInfo.batteryPercent != null) ...[
@@ -1033,20 +1094,16 @@ class _HomeScreenState extends State<HomeScreen>
deviceInfo.batteryPercent!,
),
),
const SizedBox(width: 4),
SizedBox(
width: 30,
child: Text(
'${deviceInfo.batteryPercent!.round()}%',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color:
BatteryDisplayHelper.getBatteryColor(
deviceInfo.batteryPercent!,
),
),
const SizedBox(width: 2),
Text(
'${deviceInfo.batteryPercent!.round()}%',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color:
BatteryDisplayHelper.getBatteryColor(
deviceInfo.batteryPercent!,
),
),
),
],
@@ -1147,48 +1204,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

@@ -15,6 +15,7 @@ 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 {
@@ -147,7 +148,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('Live Traffic'),
title: Text(AppLocalizations.of(context)!.liveTraffic),
actions: [
if (widget.openPacketLogs != null)
IconButton(
@@ -327,17 +328,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,
@@ -350,14 +351,14 @@ class _SummaryPanel extends StatelessWidget {
runSpacing: 10,
children: [
_MetricTile(
label: 'RX packets',
label: AppLocalizations.of(context)!.rxPackets,
value: '${snapshot.rxCount}',
subtitle: totalRxCount == null
? _windowSummaryLabel(snapshot.windowDuration)
: 'Device total $totalRxCount',
),
_MetricTile(
label: 'RSSI',
label: AppLocalizations.of(context)!.rssi,
value: snapshot.latestRssiDbm == null
? 'No RX data'
: '${snapshot.latestRssiDbm} dBm',
@@ -366,7 +367,7 @@ class _SummaryPanel extends StatelessWidget {
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
),
_MetricTile(
label: 'SNR',
label: AppLocalizations.of(context)!.snr,
value: snapshot.latestSnrDb == null
? 'No RX data'
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
@@ -375,7 +376,7 @@ class _SummaryPanel extends StatelessWidget {
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
),
_MetricTile(
label: 'Multi-hop',
label: AppLocalizations.of(context)!.multihop,
value: '${snapshot.multiHopCount}',
subtitle: routeHashCounts.summaryLabel,
footer: snapshot.avgHopCount == null
@@ -455,7 +456,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
child: Row(
children: [
_FilterChip(
label: 'All',
label: AppLocalizations.of(context)!.all,
selected: selectedType == null,
onTap: () => onSelected(null),
),

File diff suppressed because it is too large Load Diff

View File

@@ -516,7 +516,6 @@ class _MessagesTabState extends State<MessagesTab> {
}
Future<void> _replyToMessage(Message message) async {
final l10n = AppLocalizations.of(context)!;
final contactsProvider = context.read<ContactsProvider>();
Contact? senderContact;
String? senderDisplayName;
@@ -535,7 +534,7 @@ class _MessagesTabState extends State<MessagesTab> {
}).firstOrNull;
if (recipient == null) {
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
return;
}
}
@@ -564,13 +563,13 @@ class _MessagesTabState extends State<MessagesTab> {
} else {
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix == null || senderPrefix.length < 6) {
ToastLogger.error(context, l10n.cannotReplySenderMissing);
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplySenderMissing);
return;
}
recipient = contactsProvider.findContactByPrefix(senderPrefix);
if (recipient == null) {
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
return;
}
@@ -1376,7 +1375,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. '
@@ -1389,7 +1388,7 @@ class _MessagesTabState extends State<MessagesTab> {
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Send anyway'),
child: Text(AppLocalizations.of(context)!.sendAnyway),
),
],
),
@@ -1449,8 +1448,8 @@ class _MessagesTabState extends State<MessagesTab> {
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.search),
title: const Text('Search messages'),
leading: Icon(Icons.search),
title: Text(AppLocalizations.of(context)!.searchMessages),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
_showFilteredMessageSearch();
@@ -1458,7 +1457,7 @@ class _MessagesTabState extends State<MessagesTab> {
},
),
ListTile(
leading: const Icon(Icons.add_location_alt),
leading: Icon(Icons.add_location_alt),
title: Text(AppLocalizations.of(context)!.sendSarMarker),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
@@ -1485,8 +1484,8 @@ class _MessagesTabState extends State<MessagesTab> {
),
ListTile(
enabled: !_isSendingImage,
leading: const Icon(Icons.photo_library),
title: const Text('Send image from gallery'),
leading: Icon(Icons.photo_library),
title: Text(AppLocalizations.of(context)!.sendImageFromGallery),
onTap: _isSendingImage
? null
: () async {
@@ -1497,8 +1496,8 @@ class _MessagesTabState extends State<MessagesTab> {
),
ListTile(
enabled: !_isSendingImage,
leading: const Icon(Icons.camera_alt),
title: const Text('Take photo'),
leading: Icon(Icons.camera_alt),
title: Text(AppLocalizations.of(context)!.takePhoto),
onTap: _isSendingImage
? null
: () async {
@@ -1508,9 +1507,9 @@ class _MessagesTabState extends State<MessagesTab> {
},
),
ListTile(
leading: const Icon(Icons.grid_3x3),
title: const Text('Start Tic-Tac-Toe'),
subtitle: const Text('DM only'),
leading: Icon(Icons.grid_3x3),
title: Text(AppLocalizations.of(context)!.startTictactoe),
subtitle: Text(AppLocalizations.of(context)!.dmOnly),
onTap: () async {
await _runAfterSheetDismissal(sheetContext, () async {
await _startTicTacToeGame();
@@ -2020,12 +2019,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;
}
@@ -2034,7 +2040,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();
}
}

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!),
@@ -854,17 +854,17 @@ class _RouteSection extends StatelessWidget {
children: [
_FactCard(
icon: Icons.route,
label: 'Payload',
label: AppLocalizations.of(context)!.payload,
value: _payloadTypeLabel(route.payloadType),
),
_FactCard(
icon: Icons.hub,
label: 'Hops',
label: AppLocalizations.of(context)!.hops,
value: '${route.hopCount}',
),
_FactCard(
icon: Icons.tag,
label: 'Hash size',
label: AppLocalizations.of(context)!.hashSize,
value:
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
),

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

View File

@@ -8,6 +8,7 @@ import '../providers/connection_provider.dart';
import '../providers/contacts_provider.dart';
import '../providers/sensors_provider.dart';
import '../widgets/sensors/sensor_telemetry_card.dart';
import '../l10n/app_localizations.dart';
class SensorsTab extends StatefulWidget {
final bool isActive;
@@ -121,12 +122,12 @@ class _SensorsTabState extends State<SensorsTab> {
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 20),
children: [
const ListTile(
title: Text(
ListTile(
title: const Text(
'Add sensor node',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text('Pick a relay or node to watch in Sensors.'),
subtitle: Text(AppLocalizations.of(context)!.pickARelayOrNodeToWatchInSensors),
),
...candidates.map(
(contact) => ListTile(
@@ -209,7 +210,7 @@ class _SensorsTabState extends State<SensorsTab> {
final didSave = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Rename value'),
title: Text(AppLocalizations.of(context)!.renameValue),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -388,10 +389,10 @@ class _SensorCustomizeView extends StatelessWidget {
title: Text('Customize ${contact?.displayName ?? 'Sensor'}'),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
padding: EdgeInsets.fromLTRB(16, 16, 16, 24),
children: [
_SensorCustomizeSectionCard(
title: 'Live preview',
title: AppLocalizations.of(context)!.livePreview,
subtitle:
'Changes apply immediately. This card matches the current dashboard layout for this sensor.',
child: SensorTelemetryCard(
@@ -414,7 +415,7 @@ class _SensorCustomizeView extends StatelessWidget {
),
),
_SensorCustomizeSectionCard(
title: 'Refresh schedule',
title: AppLocalizations.of(context)!.refreshSchedule,
subtitle:
'Choose how often this sensor should refresh while your device stays connected.',
child: SensorAutoRefreshOptions(
@@ -702,8 +703,8 @@ class SensorMetricSelectorItem extends StatelessWidget {
fontWeight: FontWeight.w700,
),
),
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Rename'),
icon: Icon(Icons.edit_outlined, size: 18),
label: Text(AppLocalizations.of(context)!.rename),
),
if (showChannelChip)
Container(
@@ -867,11 +868,63 @@ class _SensorCandidatePreview extends StatelessWidget {
}
}
class _EmptySensorsState extends StatelessWidget {
class _EmptySensorsState extends StatefulWidget {
const _EmptySensorsState();
@override
State<_EmptySensorsState> createState() => _EmptySensorsStateState();
}
class _EmptySensorsStateState extends State<_EmptySensorsState> {
bool _discoveryTriggered = false;
bool _discoveryInProgress = false;
@override
void initState() {
super.initState();
// Auto-trigger sensor discovery when the empty state is shown
WidgetsBinding.instance.addPostFrameCallback((_) {
_autoDiscover();
});
}
Future<void> _autoDiscover() async {
if (_discoveryTriggered || !mounted) return;
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) return;
_discoveryTriggered = true;
setState(() => _discoveryInProgress = true);
try {
await connectionProvider.discoverNodeType(advertType: 4);
} finally {
if (mounted) setState(() => _discoveryInProgress = false);
}
}
Future<void> _discoverSensors() async {
if (_discoveryInProgress || !mounted) return;
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) return;
setState(() => _discoveryInProgress = true);
try {
await connectionProvider.discoverNodeType(advertType: 4);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.sensorDiscoverySent)),
);
}
} finally {
if (mounted) setState(() => _discoveryInProgress = false);
}
}
@override
Widget build(BuildContext context) {
final isConnected =
context.watch<ConnectionProvider>().deviceInfo.isConnected;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 64),
child: Column(
@@ -902,6 +955,25 @@ class _EmptySensorsState extends StatelessWidget {
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: isConnected && !_discoveryInProgress
? _discoverSensors
: null,
icon: _discoveryInProgress
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.sensors_outlined),
label: Text(_discoveryInProgress
? 'Discovering...'
: 'Discover Sensors'),
),
],
),
);

View File

@@ -16,6 +16,7 @@ import '../providers/app_provider.dart';
import '../providers/connection_provider.dart';
import '../providers/drawing_provider.dart';
import '../providers/map_provider.dart';
import '../models/config_profile.dart';
import '../services/location_tracking_service.dart';
import '../services/locale_preferences.dart';
import '../services/mesh_map_nodes_service.dart';
@@ -26,6 +27,9 @@ import '../services/route_hash_preferences.dart';
import '../services/image_codec_service.dart';
import '../services/developer_mode_service.dart';
import '../services/notification_service.dart';
import '../services/profile_manager.dart';
import '../services/profile_workspace_coordinator.dart';
import '../services/profiles_feature_service.dart';
import '../utils/sample_data_generator.dart';
import '../utils/image_message_parser.dart';
import '../utils/voice_message_parser.dart';
@@ -33,6 +37,7 @@ import '../theme/app_theme.dart';
import '../l10n/app_localizations.dart';
import '../widgets/update_dialog.dart';
import 'sar_template_management_screen.dart';
import 'profiles_screen.dart';
import 'welcome_wizard_screen.dart';
class SettingsScreen extends StatefulWidget {
@@ -83,6 +88,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _updateNotificationsEnabled = true;
bool _muteForegroundNotifications = true;
bool _isDeveloperModeEnabled = false;
bool _profilesEnabled = false;
DateTime? _onlineTraceCacheUpdatedAt;
bool _isClearingOnlineTraceCache = false;
int _versionTapCount = 0;
@@ -102,6 +108,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_loadImagePreferences();
_loadFastLocationSettings();
_loadDeveloperMode();
_loadProfilesEnabled();
_loadOnlineTraceCacheStatus();
_loadMapPreferences();
_loadNotificationPreferences();
@@ -129,7 +136,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
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;
});
}
}
@@ -142,6 +153,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
}
Future<void> _loadProfilesEnabled() async {
final isEnabled = await ProfilesFeatureService.isEnabled();
if (!mounted) return;
setState(() {
_profilesEnabled = isEnabled;
});
}
Future<void> _loadNotificationPreferences() async {
final service = NotificationService();
await service.initialize();
@@ -173,7 +192,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Developer mode disabled')));
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeDisabled)));
return;
}
@@ -187,7 +206,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Developer mode enabled')));
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeEnabled)));
return;
}
@@ -199,22 +218,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _saveRxTxPreference(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('show_rx_tx_indicators', value);
await prefs.setBool(
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
value,
);
}
Future<void> _loadMapPreferences() async {
final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
setState(() {
_rotateMapWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
_openMapInFullscreen = prefs.getBool('map_fullscreen') ?? false;
_rotateMapWithHeading =
prefs.getBool(
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
) ??
false;
_showMapDebugInfo =
prefs.getBool(ProfileStorageScope.scopedKey('map_show_debug_info')) ??
false;
_openMapInFullscreen =
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
false;
});
}
Future<void> _saveMapPreference(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, value);
await prefs.setBool(ProfileStorageScope.scopedKey(key), value);
}
Future<void> _loadVoicePreferences() async {
@@ -298,7 +328,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final value = await showDialog<double>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Fast GPS movement threshold'),
title: Text(AppLocalizations.of(context)!.fastGpsMovementThreshold),
content: TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -339,7 +369,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final value = await showDialog<int>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Fast GPS active-use interval'),
title: Text(AppLocalizations.of(context)!.fastGpsActiveuseInterval),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
@@ -487,7 +517,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Load settings and restore tracking state
final prefs = await SharedPreferences.getInstance();
final wasTracking =
prefs.getBool('background_tracking_enabled') ?? false;
prefs.getBool(
ProfileStorageScope.scopedKey('background_tracking_enabled'),
) ??
false;
if (wasTracking) {
await _startBackgroundTracking();
@@ -525,8 +558,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Update check is only available on Android'),
SnackBar(
content: Text(AppLocalizations.of(context)!.updateCheckIsOnlyAvailableOnAndroid),
backgroundColor: Colors.orange,
),
);
@@ -550,8 +583,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!updateInfo.isAvailable) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('You are running the latest version'),
SnackBar(
content: Text(AppLocalizations.of(context)!.youAreRunningTheLatestVersion),
backgroundColor: Colors.green,
),
);
@@ -560,8 +593,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (updateInfo.downloadUrl == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Update available but download URL not found'),
SnackBar(
content: Text(AppLocalizations.of(context)!.updateAvailableButDownloadUrlNotFound),
backgroundColor: Colors.orange,
),
);
@@ -703,7 +736,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Row(
children: [
const Icon(Icons.settings, size: 24),
const SizedBox(width: 12),
SizedBox(width: 12),
Text(AppLocalizations.of(context)!.locationPermission),
],
),
@@ -845,7 +878,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear Messages'),
title: Text(AppLocalizations.of(context)!.clearMessages),
content: const Text(
'This will permanently delete all stored messages. Are you sure?',
),
@@ -874,8 +907,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('All messages cleared'),
SnackBar(
content: Text(AppLocalizations.of(context)!.allMessagesCleared),
backgroundColor: Colors.orange,
),
);
@@ -885,7 +918,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear online trace database'),
title: Text(AppLocalizations.of(context)!.clearOnlineTraceDatabase),
content: const Text(
'This removes the cached online node database used as a trace fallback.',
),
@@ -918,8 +951,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Online trace database cleared'),
SnackBar(
content: Text(AppLocalizations.of(context)!.onlineTraceDatabaseCleared),
backgroundColor: Colors.orange,
),
);
@@ -945,9 +978,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final selected = await showDialog<int>(
context: context,
builder: (context) => SimpleDialog(
title: const Text('Route path byte size'),
title: Text(AppLocalizations.of(context)!.routePathByteSize),
children: [
for (final value in [1, 2, 3])
for (final value in RouteHashPreferences.supportedSizes)
SimpleDialogOption(
onPressed: () => Navigator.of(context).pop(value),
child: Row(
@@ -957,7 +990,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('$value byte${value == 1 ? '' : 's'}'),
Text('$value-byte (max ${64 ~/ value} hops)'),
Text(
'Use ${value * 2} hex characters per hop',
style: Theme.of(context).textTheme.bodySmall,
@@ -991,21 +1024,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Appearance'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.palette),
leading: Icon(Icons.palette),
title: Text(AppLocalizations.of(context)!.theme),
subtitle: Text(AppTheme.getThemeDisplayName(_selectedTheme)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeDialog(),
),
ListTile(
leading: const Icon(Icons.language),
leading: Icon(Icons.language),
title: Text(AppLocalizations.of(context)!.language),
subtitle: Text(LocalePreferences.getDisplayName(_selectedLocale)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(),
),
SwitchListTile(
secondary: const Icon(Icons.radar),
secondary: Icon(Icons.radar),
title: Text(AppLocalizations.of(context)!.showRxTxIndicators),
subtitle: Text(
AppLocalizations.of(context)!.displayPacketActivity,
@@ -1023,8 +1056,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Notifications'),
_buildSettingsCard([
SwitchListTile(
secondary: const Icon(Icons.chat_bubble_outline),
title: const Text('Message notifications'),
secondary: Icon(Icons.chat_bubble_outline),
title: Text(AppLocalizations.of(context)!.messageNotifications),
subtitle: const Text(
'Notify for incoming direct and channel messages',
),
@@ -1039,8 +1072,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.warning_amber_outlined),
title: const Text('SAR alerts'),
secondary: Icon(Icons.warning_amber_outlined),
title: Text(AppLocalizations.of(context)!.sarAlerts),
subtitle: const Text(
'Notify for incoming SAR markers such as found person or fire',
),
@@ -1053,8 +1086,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.contact_page_outlined),
title: const Text('Discovery notifications'),
secondary: Icon(Icons.contact_page_outlined),
title: Text(AppLocalizations.of(context)!.discoveryNotifications),
subtitle: const Text(
'Notify when new contacts appear in Discovery',
),
@@ -1069,8 +1102,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.system_update),
title: const Text('Update notifications'),
secondary: Icon(Icons.system_update),
title: Text(AppLocalizations.of(context)!.updateNotifications),
subtitle: const Text(
'Notify when a newer app version is available',
),
@@ -1085,8 +1118,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.visibility_off_outlined),
title: const Text('Mute while app is open'),
secondary: Icon(Icons.visibility_off_outlined),
title: Text(AppLocalizations.of(context)!.muteWhileAppIsOpen),
subtitle: const Text(
'Do not show local notifications while the app is in the foreground',
),
@@ -1106,7 +1139,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSettingsCard([
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.map_outlined),
secondary: Icon(Icons.map_outlined),
title: Text(AppLocalizations.of(context)!.disableMap),
subtitle: Text(
AppLocalizations.of(context)!.disableMapDescription,
@@ -1119,8 +1152,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.contacts_outlined),
title: const Text('Disable Contacts'),
secondary: Icon(Icons.contacts_outlined),
title: Text(AppLocalizations.of(context)!.disableContacts),
subtitle: const Text(
'Hide the contacts tab to simplify navigation',
),
@@ -1132,8 +1165,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.sensors),
title: const Text('Enable Sensors tab'),
secondary: Icon(Icons.sensors),
title: Text(AppLocalizations.of(context)!.enableSensorsTab),
subtitle: const Text(
'Show a dedicated tab for watched relay and node telemetry',
),
@@ -1148,8 +1181,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Messaging'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.alt_route),
title: const Text('Route path byte size'),
leading: Icon(Icons.alt_route),
title: Text(AppLocalizations.of(context)!.routePathByteSize),
subtitle: Text(
'$_routeHashSize byte${_routeHashSize == 1 ? '' : 's'} for manual contact routes',
),
@@ -1158,8 +1191,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.swap_horiz),
title: const Text('Auto route rotation'),
secondary: Icon(Icons.swap_horiz),
title: Text(AppLocalizations.of(context)!.autoRouteRotation),
subtitle: const Text(
'Rotate between best known direct paths and flood mode for room/contact sends',
),
@@ -1171,8 +1204,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.route),
title: const Text('Nearest repeater fallback'),
secondary: Icon(Icons.route),
title: Text(AppLocalizations.of(context)!.nearestRepeaterFallback),
subtitle: const Text(
'After normal retries fail, try one final resend through the nearest repeater',
),
@@ -1184,8 +1217,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.route),
title: const Text('Clear path on max retry'),
secondary: Icon(Icons.route),
title: Text(AppLocalizations.of(context)!.clearPathOnMaxRetry),
subtitle: const Text(
'Clear the route only after all retries and final router fallback fail',
),
@@ -1201,13 +1234,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Clear Messages',
style: TextStyle(color: Colors.red),
),
subtitle: const Text('Delete all stored message history'),
subtitle: Text(AppLocalizations.of(context)!.deleteAllStoredMessageHistory),
onTap: _clearMessages,
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => ListTile(
leading: const Icon(Icons.format_size),
title: const Text('Message font size'),
leading: Icon(Icons.format_size),
title: Text(AppLocalizations.of(context)!.messageFontSize),
subtitle: Text(
'${(appProvider.messageFontScale * 100).round()}% of default',
),
@@ -1231,8 +1264,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Tracing'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.cloud_sync),
title: const Text('Online trace database'),
leading: Icon(Icons.cloud_sync),
title: Text(AppLocalizations.of(context)!.onlineTraceDatabase),
subtitle: Text(_onlineTraceCacheSubtitle()),
),
ListTile(
@@ -1266,8 +1299,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Map'),
_buildSettingsCard([
SwitchListTile(
secondary: const Icon(Icons.explore),
title: const Text('Rotate map with heading'),
secondary: Icon(Icons.explore),
title: Text(AppLocalizations.of(context)!.rotateMapWithHeading),
subtitle: const Text(
'Rotate the map based on your compass or movement heading',
),
@@ -1280,8 +1313,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.bug_report_outlined),
title: const Text('Show map debug info'),
secondary: Icon(Icons.bug_report_outlined),
title: Text(AppLocalizations.of(context)!.showMapDebugInfo),
subtitle: const Text(
'Display extra map diagnostics and internal state overlays',
),
@@ -1294,8 +1327,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.fullscreen),
title: const Text('Open map in fullscreen'),
secondary: Icon(Icons.fullscreen),
title: Text(AppLocalizations.of(context)!.openMapInFullscreen),
subtitle: const Text(
'Start the map tab in fullscreen mode by default',
),
@@ -1309,9 +1342,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<DrawingProvider>(
builder: (context, drawingProvider, child) => SwitchListTile(
secondary: const Icon(Icons.fmd_good_outlined),
title: const Text('Show SAR markers'),
subtitle: const Text('Display SAR markers on the main map'),
secondary: Icon(Icons.fmd_good_outlined),
title: Text(AppLocalizations.of(context)!.showSarMarkersLabel),
subtitle: Text(AppLocalizations.of(context)!.displaySarMarkersOnTheMainMap),
value: drawingProvider.showSarMarkers,
onChanged: (value) {
drawingProvider.toggleSarMarkers();
@@ -1320,8 +1353,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<MapProvider>(
builder: (context, mapProvider, child) => SwitchListTile(
secondary: const Icon(Icons.timeline),
title: const Text('Show all contact trails'),
secondary: Icon(Icons.timeline),
title: Text(AppLocalizations.of(context)!.showAllContactTrailsLabel),
subtitle: const Text(
'Display location trails for all contacts that have history',
),
@@ -1333,8 +1366,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<MapProvider>(
builder: (context, mapProvider, child) => SwitchListTile(
secondary: const Icon(Icons.router_outlined),
title: const Text('Hide repeaters on map'),
secondary: Icon(Icons.router_outlined),
title: Text(AppLocalizations.of(context)!.hideRepeatersOnMap),
subtitle: const Text(
'Hide repeater contacts from the main map view',
),
@@ -1364,16 +1397,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.graphic_eq),
title: const Text('Voice bitrate'),
leading: Icon(Icons.graphic_eq),
title: Text(AppLocalizations.of(context)!.voiceBitrate),
subtitle: Text(_voiceBitrateSubtitle(_voiceBitrate)),
trailing: const Icon(Icons.chevron_right),
onTap: _showVoiceBitrateDialog,
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.tune),
title: const Text('Band-pass filter voice'),
secondary: Icon(Icons.tune),
title: Text(AppLocalizations.of(context)!.bandpassFilterVoice),
subtitle: const Text(
'Keeps speech frequencies and cuts low/high noise',
),
@@ -1385,9 +1418,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.compress),
title: const Text('Voice compressor'),
subtitle: const Text('Balances quiet and loud speech levels'),
secondary: Icon(Icons.compress),
title: Text(AppLocalizations.of(context)!.voiceCompressor),
subtitle: Text(AppLocalizations.of(context)!.balancesQuietAndLoudSpeechLevels),
value: appProvider.isVoiceCompressorEnabled,
onChanged: (value) async {
await appProvider.toggleVoiceCompressorEnabled(value);
@@ -1396,9 +1429,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.speed),
title: const Text('Voice limiter'),
subtitle: const Text('Prevents clipping peaks before encoding'),
secondary: Icon(Icons.speed),
title: Text(AppLocalizations.of(context)!.voiceLimiter),
subtitle: Text(AppLocalizations.of(context)!.preventsClippingPeaksBeforeEncoding),
value: appProvider.isVoiceLimiterEnabled,
onChanged: (value) async {
await appProvider.toggleVoiceLimiterEnabled(value);
@@ -1407,9 +1440,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.auto_fix_high),
title: const Text('Mic auto gain'),
subtitle: const Text('Lets the recorder adjust input level'),
secondary: Icon(Icons.auto_fix_high),
title: Text(AppLocalizations.of(context)!.micAutoGain),
subtitle: Text(AppLocalizations.of(context)!.letsTheRecorderAdjustInputLevel),
value: appProvider.isVoiceAutoGainEnabled,
onChanged: (value) async {
await appProvider.toggleVoiceAutoGainEnabled(value);
@@ -1418,8 +1451,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.hearing_disabled),
title: const Text('Echo cancellation'),
secondary: Icon(Icons.hearing_disabled),
title: Text(AppLocalizations.of(context)!.echoCancellation),
subtitle: const Text(
'Uses recorder echo cancellation if available',
),
@@ -1431,8 +1464,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.noise_control_off),
title: const Text('Noise suppression'),
secondary: Icon(Icons.noise_control_off),
title: Text(AppLocalizations.of(context)!.noiseSuppression),
subtitle: const Text(
'Uses recorder noise suppression if available',
),
@@ -1444,8 +1477,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.content_cut),
title: const Text('Trim silence in voice messages'),
secondary: Icon(Icons.content_cut),
title: Text(AppLocalizations.of(context)!.trimSilenceInVoiceMessages),
subtitle: const Text(
'Removes long silent parts before sending voice',
),
@@ -1460,15 +1493,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Images'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.photo_size_select_large),
title: const Text('Max image size'),
leading: Icon(Icons.photo_size_select_large),
title: Text(AppLocalizations.of(context)!.maxImageSize),
subtitle: Text('$_imageMaxSize×$_imageMaxSize px'),
trailing: const Icon(Icons.chevron_right),
onTap: _showImageMaxSizeDialog,
),
ListTile(
leading: const Icon(Icons.tune),
title: const Text('Image compression'),
leading: Icon(Icons.tune),
title: Text(AppLocalizations.of(context)!.imageCompression),
subtitle: Text(
'$_imageCompression / 90 (higher = smaller file)',
),
@@ -1486,8 +1519,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
SwitchListTile(
secondary: const Icon(Icons.invert_colors),
title: const Text('Grayscale'),
secondary: Icon(Icons.invert_colors),
title: Text(AppLocalizations.of(context)!.grayscale),
subtitle: const Text(
'Converts image to grayscale for smaller file size',
),
@@ -1499,8 +1532,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
SwitchListTile(
secondary: const Icon(Icons.compress),
title: const Text('Ultra mode'),
secondary: Icon(Icons.compress),
title: Text(AppLocalizations.of(context)!.ultraMode),
subtitle: const Text(
'Extra-aggressive compression with stronger AVIF settings',
),
@@ -1518,10 +1551,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: (context, connectionProvider, child) =>
_buildImageModePreviewCard(connectionProvider),
),
_buildSectionHeader('Profiles'),
_buildSettingsCard([
SwitchListTile(
secondary: Icon(Icons.layers_outlined),
title: Text(AppLocalizations.of(context)!.enableProfiles),
subtitle: const Text(
'Show profile management UI while keeping the hidden Default profile as the current workspace.',
),
value: _profilesEnabled,
onChanged: (value) async {
await context
.read<ProfileWorkspaceCoordinator>()
.setProfilesEnabled(value);
if (!mounted) return;
setState(() {
_profilesEnabled = value;
});
},
),
if (_profilesEnabled)
ListTile(
leading: Icon(Icons.folder_copy_outlined),
title: Text(AppLocalizations.of(context)!.manageProfiles),
subtitle: Text(
context.watch<ProfileManager>().activeProfileId ==
ConfigProfile.defaultProfileId
? 'Default is active'
: 'Custom profile active',
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfilesScreen(),
),
);
},
),
]),
_buildSectionHeader('Templates & Help'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.location_searching),
leading: Icon(Icons.location_searching),
title: Text(AppLocalizations.of(context)!.sarTemplates),
subtitle: Text(AppLocalizations.of(context)!.manageSarTemplates),
trailing: const Icon(Icons.chevron_right),
@@ -1535,7 +1608,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
ListTile(
leading: const Icon(Icons.school),
leading: Icon(Icons.school),
title: Text(AppLocalizations.of(context)!.viewWelcomeTutorial),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
@@ -1556,8 +1629,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
_buildSettingsCard([
SwitchListTile(
secondary: const Icon(Icons.gps_fixed),
title: const Text('Fast private GPS updates'),
secondary: Icon(Icons.gps_fixed),
title: Text(AppLocalizations.of(context)!.fastPrivateGpsUpdates),
subtitle: const Text(
'Use private zero-hop updates while moving significantly or while actively using map/messages.',
),
@@ -1565,8 +1638,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
onChanged: _setFastLocationUpdatesEnabled,
),
ListTile(
leading: const Icon(Icons.straighten),
title: const Text('Movement threshold'),
leading: Icon(Icons.straighten),
title: Text(AppLocalizations.of(context)!.movementThreshold),
subtitle: Text(
'${_fastLocationMovementThresholdMeters.toStringAsFixed(0)} m',
),
@@ -1574,14 +1647,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: _editFastLocationMovementThreshold,
),
ListTile(
leading: const Icon(Icons.timer),
title: const Text('Active-use update interval'),
leading: Icon(Icons.timer),
title: Text(AppLocalizations.of(context)!.activeuseUpdateInterval),
subtitle: Text('$_fastLocationActiveCadenceSeconds s'),
trailing: const Icon(Icons.chevron_right),
onTap: _editFastLocationActiveCadence,
),
ListTile(
leading: const Icon(Icons.location_on),
leading: Icon(Icons.location_on),
title: Text(AppLocalizations.of(context)!.locationPermission),
subtitle: FutureBuilder<LocationPermission>(
future: Geolocator.checkPermission(),
@@ -1634,7 +1707,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader(AppLocalizations.of(context)!.about),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.info),
leading: Icon(Icons.info),
title: Text(AppLocalizations.of(context)!.appVersion),
subtitle: Text(
_packageInfo != null
@@ -1644,12 +1717,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: _handleVersionTap,
),
ListTile(
leading: const Icon(Icons.badge),
leading: Icon(Icons.badge),
title: Text(AppLocalizations.of(context)!.appName),
subtitle: Text(_packageInfo?.appName ?? 'MeshCore SAR'),
),
ListTile(
leading: const Icon(Icons.description),
leading: Icon(Icons.description),
title: Text(AppLocalizations.of(context)!.aboutMeshCoreSar),
subtitle: Text(
AppLocalizations.of(context)!.aboutDescription.split('\n\n')[0],
@@ -1683,19 +1756,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Developer & Data'),
_buildSettingsCard([
ListTile(
leading: const Icon(Icons.bug_report),
leading: Icon(Icons.bug_report),
title: Text(AppLocalizations.of(context)!.packageName),
subtitle: Text(_packageInfo?.packageName ?? 'com.meshcore.sar'),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(
AppLocalizations.of(context)!.sampleData,
style: Theme.of(context).textTheme.titleSmall,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(
AppLocalizations.of(context)!.sampleDataDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
@@ -1718,7 +1791,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.add_circle_outline),
: Icon(Icons.add_circle_outline),
label: Text(AppLocalizations.of(context)!.loadSampleData),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
@@ -1729,7 +1802,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Expanded(
child: OutlinedButton.icon(
onPressed: _isLoadingSampleData ? null : _clearSampleData,
icon: const Icon(Icons.delete_outline),
icon: Icon(Icons.delete_outline),
label: Text(AppLocalizations.of(context)!.clearAllData),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
@@ -1835,8 +1908,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
onPressed: _isPreviewLoading
? null
: _selectPreviewImageFromGallery,
icon: const Icon(Icons.photo_library_outlined),
label: const Text('Select from gallery'),
icon: Icon(Icons.photo_library_outlined),
label: Text(AppLocalizations.of(context)!.selectFromGallery),
),
],
),
@@ -2042,21 +2115,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Expanded(
child: _voiceStatChip(
label: 'Band-pass',
label: AppLocalizations.of(context)!.bandpass,
enabled: bandPassEnabled,
),
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: _voiceStatChip(
label: 'Compressor',
label: AppLocalizations.of(context)!.compressor,
enabled: compressorEnabled,
),
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: _voiceStatChip(
label: 'Limiter',
label: AppLocalizations.of(context)!.limiter,
enabled: limiterEnabled,
),
),
@@ -2067,21 +2140,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Expanded(
child: _voiceStatChip(
label: 'Auto gain',
label: AppLocalizations.of(context)!.autoGain,
enabled: autoGainEnabled,
),
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: _voiceStatChip(
label: 'Echo cancel',
label: AppLocalizations.of(context)!.echoCancel,
enabled: echoCancellationEnabled,
),
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: _voiceStatChip(
label: 'Noise suppress',
label: AppLocalizations.of(context)!.noiseSuppress,
enabled: noiseSuppressionEnabled,
),
),
@@ -2092,7 +2165,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Expanded(
child: _voiceStatChip(
label: 'Silence trim',
label: AppLocalizations.of(context)!.silenceTrim,
enabled: silenceTrimEnabled,
),
),
@@ -2164,7 +2237,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text(AppLocalizations.of(context)!.blueDarkTheme),
value: AppThemeMode.dark,
),
const Divider(),
Divider(),
RadioListTile<AppThemeMode>(
title: Row(
children: [
@@ -2228,7 +2301,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
value: AppThemeMode.sarNavyBlue,
),
const Divider(),
Divider(),
RadioListTile<AppThemeMode>(
title: Text(AppLocalizations.of(context)!.autoSystem),
subtitle: Text(
@@ -2295,7 +2368,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Max image size'),
title: Text(AppLocalizations.of(context)!.maxImageSize),
content: SingleChildScrollView(
child: RadioGroup<int>(
groupValue: _imageMaxSize,
@@ -2311,7 +2384,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
value: size,
title: Text('$size×$size px'),
subtitle: size == ImagePreferences.defaultMaxSize
? const Text('Default')
? Text(AppLocalizations.of(context)!.defaultValue)
: null,
),
)
@@ -2333,7 +2406,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Voice bitrate'),
title: Text(AppLocalizations.of(context)!.voiceBitrate),
content: SingleChildScrollView(
child: RadioGroup<int>(
groupValue: _voiceBitrate,
@@ -2352,7 +2425,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Text('$bitrate bps'),
subtitle:
bitrate == VoiceBitratePreferences.defaultBitrate
? const Text('Default')
? Text(AppLocalizations.of(context)!.defaultValue)
: null,
),
)
@@ -2391,16 +2464,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Version ${_packageInfo?.version ?? '1.0.0'}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
SizedBox(height: 16),
Text(AppLocalizations.of(context)!.aboutDescription),
const SizedBox(height: 16),
SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.technologiesUsed,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SizedBox(height: 8),
Text(AppLocalizations.of(context)!.technologiesList),
],
),
@@ -2413,7 +2486,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
},
icon: const Icon(Icons.open_in_new),
icon: Icon(Icons.open_in_new),
label: Text(AppLocalizations.of(context)!.moreInfo),
),
TextButton(

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

@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/contact.dart';
import '../models/contact_group.dart';
import 'profiles_feature_service.dart';
import '../utils/key_comparison.dart';
import 'package:latlong2/latlong.dart';
@@ -14,8 +15,12 @@ class ContactStorageService {
static const int _maxStoredContacts = 500; // Store up to 500 contacts
static const int _maxStoredPendingAdverts = 500;
String _key(String baseKey, {String? namespace}) {
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
}
/// Save contacts to persistent storage
Future<void> saveContacts(List<Contact> contacts) async {
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -30,7 +35,10 @@ class ContactStorageService {
: jsonList;
final jsonString = jsonEncode(limitedList);
await prefs.setString(_contactsKey, jsonString);
await prefs.setString(
_key(_contactsKey, namespace: namespace),
jsonString,
);
debugPrint(
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
@@ -42,10 +50,15 @@ class ContactStorageService {
/// Load contacts from persistent storage
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
Future<List<Contact>> loadContacts({Uint8List? excludePublicKey}) async {
Future<List<Contact>> loadContacts({
Uint8List? excludePublicKey,
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_contactsKey);
final jsonString = prefs.getString(
_key(_contactsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
debugPrint(' [ContactStorage] No stored contacts found');
@@ -84,23 +97,29 @@ class ContactStorageService {
}
/// Clear all stored contacts
Future<void> clearContacts() async {
Future<void> clearContacts({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_contactsKey);
await prefs.remove(_key(_contactsKey, namespace: namespace));
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
} catch (e) {
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
}
}
Future<void> saveContactGroups(List<SavedContactGroup> groups) async {
Future<void> saveContactGroups(
List<SavedContactGroup> groups, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = jsonEncode(
groups.map((group) => _contactGroupToJson(group)).toList(),
);
await prefs.setString(_contactGroupsKey, jsonString);
await prefs.setString(
_key(_contactGroupsKey, namespace: namespace),
jsonString,
);
debugPrint(
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
);
@@ -109,10 +128,12 @@ class ContactStorageService {
}
}
Future<List<SavedContactGroup>> loadContactGroups() async {
Future<List<SavedContactGroup>> loadContactGroups({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_contactGroupsKey);
final jsonString = prefs.getString(
_key(_contactGroupsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
@@ -128,13 +149,29 @@ class ContactStorageService {
}
}
Future<void> savePendingAdverts(List<Map<String, dynamic>> adverts) async {
Future<void> clearContactGroups({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key(_contactGroupsKey, namespace: namespace));
debugPrint('✅ [ContactStorage] Cleared all contact groups');
} catch (e) {
debugPrint('❌ [ContactStorage] Error clearing contact groups: $e');
}
}
Future<void> savePendingAdverts(
List<Map<String, dynamic>> adverts, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final limitedList = adverts.length > _maxStoredPendingAdverts
? adverts.sublist(adverts.length - _maxStoredPendingAdverts)
: adverts;
await prefs.setString(_pendingAdvertsKey, jsonEncode(limitedList));
await prefs.setString(
_key(_pendingAdvertsKey, namespace: namespace),
jsonEncode(limitedList),
);
debugPrint(
'✅ [ContactStorage] Saved ${limitedList.length} pending adverts to storage',
);
@@ -143,10 +180,14 @@ class ContactStorageService {
}
}
Future<List<Map<String, dynamic>>> loadPendingAdverts() async {
Future<List<Map<String, dynamic>>> loadPendingAdverts({
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_pendingAdvertsKey);
final jsonString = prefs.getString(
_key(_pendingAdvertsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return const [];
}
@@ -159,21 +200,55 @@ class ContactStorageService {
}
}
Future<void> clearPendingAdverts() async {
Future<void> clearPendingAdverts({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_pendingAdvertsKey);
await prefs.remove(_key(_pendingAdvertsKey, namespace: namespace));
debugPrint('✅ [ContactStorage] Cleared all stored pending adverts');
} catch (e) {
debugPrint('❌ [ContactStorage] Error clearing pending adverts: $e');
}
}
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats() async {
// ── Favorites ───────────────────────────────────────────────────────────────
static const String _favoritesKey = 'contact_favorites';
Future<void> saveFavorites(
Set<String> publicKeyHexes, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_contactsKey);
await prefs.setStringList(
_key(_favoritesKey, namespace: namespace),
publicKeyHexes.toList(),
);
} catch (e) {
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
}
}
Future<Set<String>> loadFavorites({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(
_key(_favoritesKey, namespace: namespace),
);
return list?.toSet() ?? {};
} catch (e) {
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
return {};
}
}
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(
_key(_contactsKey, namespace: namespace),
);
if (jsonString == null || jsonString.isEmpty) {
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};

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

@@ -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,10 +41,12 @@ class MapMarkerService {
Function(Contact)? onTap,
double mapRotation = 0,
Position? userPosition,
Map<String, LatLng>? estimatedLocations,
}) {
return contacts
.map((contact) {
final location = contact.displayLocation;
final location = contact.displayLocation ??
estimatedLocations?[contact.publicKeyHex];
if (location == null) return null;
return Marker(
@@ -152,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

View File

@@ -0,0 +1,100 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../models/config_profile.dart';
import '../providers/drawing_provider.dart';
import '../providers/map_provider.dart';
import 'profiles_feature_service.dart';
class MapWorkspaceSnapshotService {
Future<MapWorkspaceProfileSection> capture({
required MapProvider mapProvider,
required DrawingProvider drawingProvider,
}) async {
final prefs = await SharedPreferences.getInstance();
return MapWorkspaceProfileSection(
mapPrefs: {
'map_rotate_with_heading':
prefs.getBool(
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
) ??
false,
'map_show_debug_info':
prefs.getBool(
ProfileStorageScope.scopedKey('map_show_debug_info'),
) ??
false,
'map_fullscreen':
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
false,
'map_last_latitude': prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_latitude'),
),
'map_last_longitude': prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_longitude'),
),
'map_last_zoom': prefs.getDouble(
ProfileStorageScope.scopedKey('map_last_zoom'),
),
'map_last_layer_type': prefs.getInt(
ProfileStorageScope.scopedKey('map_last_layer_type'),
),
'map_gps_update_distance': prefs.getDouble(
ProfileStorageScope.scopedKey('map_gps_update_distance'),
),
'background_tracking_enabled': prefs.getBool(
ProfileStorageScope.scopedKey('background_tracking_enabled'),
),
},
drawings: drawingProvider.exportDrawingsJson(),
currentTrail: mapProvider.currentTrail?.toJson(),
trailHistory: mapProvider.trailHistory
.map((trail) => trail.toJson())
.toList(),
importedTrail: mapProvider.importedTrail?.toJson(),
isTrailVisible: mapProvider.isTrailVisible,
showCadastralOverlay: mapProvider.showCadastralOverlay,
showForestRoadsOverlay: mapProvider.showForestRoadsOverlay,
showHikingTrailsOverlay: mapProvider.showHikingTrailsOverlay,
showMainRoadsOverlay: mapProvider.showMainRoadsOverlay,
showHouseNumbersOverlay: mapProvider.showHouseNumbersOverlay,
showFireHazardZonesOverlay: mapProvider.showFireHazardZonesOverlay,
showHistoricalFiresOverlay: mapProvider.showHistoricalFiresOverlay,
showFirebreaksOverlay: mapProvider.showFirebreaksOverlay,
showKrasFireZonesOverlay: mapProvider.showKrasFireZonesOverlay,
showPlaceNamesOverlay: mapProvider.showPlaceNamesOverlay,
showMunicipalityBordersOverlay:
mapProvider.showMunicipalityBordersOverlay,
showAllContactTrails: mapProvider.showAllContactTrails,
hideRepeatersOnMap: mapProvider.hideRepeatersOnMap,
);
}
Future<void> apply(
MapWorkspaceProfileSection? section, {
required MapProvider mapProvider,
required DrawingProvider drawingProvider,
}) async {
if (section == null || section.isEmpty) {
return;
}
final prefs = await SharedPreferences.getInstance();
final mapPrefs = section.mapPrefs ?? const <String, dynamic>{};
for (final entry in mapPrefs.entries) {
final key = ProfileStorageScope.scopedKey(entry.key);
final value = entry.value;
if (value == null) {
await prefs.remove(key);
} else if (value is bool) {
await prefs.setBool(key, value);
} else if (value is int) {
await prefs.setInt(key, value);
} else if (value is double) {
await prefs.setDouble(key, value);
}
}
mapProvider.applyWorkspaceJson(section.toJson());
await drawingProvider.replaceDrawingsFromJson(section.drawings);
}
}

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};
@@ -346,6 +392,9 @@ class MessageStorageService {
'isSarMarker': message.isSarMarker,
'sarGpsLat': message.sarGpsCoordinates?.latitude,
'sarGpsLon': message.sarGpsCoordinates?.longitude,
'sarCustomMapLat': message.sarCustomMapPoint?.latitude,
'sarCustomMapLon': message.sarCustomMapPoint?.longitude,
'sarCustomMapId': message.sarCustomMapId,
'sarNotes': message.sarNotes,
'sarCustomEmoji': message.sarCustomEmoji,
'sarColorIndex': message.sarColorIndex,
@@ -398,10 +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 {};
}
@@ -424,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 {};
}
@@ -482,8 +538,19 @@ class MessageStorageService {
isSarMarker: json['isSarMarker'] as bool? ?? false,
sarGpsCoordinates:
json['sarGpsLat'] != null && json['sarGpsLon'] != null
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
? LatLng(
(json['sarGpsLat'] as num).toDouble(),
(json['sarGpsLon'] as num).toDouble(),
)
: null,
sarCustomMapPoint:
json['sarCustomMapLat'] != null && json['sarCustomMapLon'] != null
? LatLng(
(json['sarCustomMapLat'] as num).toDouble(),
(json['sarCustomMapLon'] as num).toDouble(),
)
: null,
sarCustomMapId: json['sarCustomMapId'] as String?,
sarNotes: json['sarNotes'] as String?,
sarCustomEmoji: json['sarCustomEmoji'] as String?,
sarColorIndex: json['sarColorIndex'] as int?,

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

@@ -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,478 @@
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 '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.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 AppProvider appProvider;
final AppConfigSnapshotService _appConfigSnapshotService;
final MapWorkspaceSnapshotService _mapWorkspaceSnapshotService;
final DeviceConfigApplicator _deviceConfigApplicator;
final MessageStorageService _messageStorageService;
final ContactStorageService _contactStorageService;
bool _isSyncingDeviceProfile = false;
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 || _isSyncingDeviceProfile) {
return;
}
final deviceKey = _currentDeviceProfileKey;
if (deviceKey == null) {
return;
}
_isSyncingDeviceProfile = true;
try {
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,
);
} finally {
_isSyncingDeviceProfile = false;
}
}
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 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,100 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:usb_serial/usb_serial.dart';
import 'package:meshcore_client/meshcore_client.dart';
/// Android USB OTG transport for MeshCore serial connection.
///
/// Wraps the `usb_serial` package to provide raw byte I/O to
/// [MeshCoreSerialService].
class UsbSerialTransport {
static const int _baudRate = 115200;
UsbPort? _port;
StreamSubscription? _inputSubscription;
final MeshCoreSerialService _service;
UsbSerialTransport(this._service);
MeshCoreSerialService get service => _service;
bool get isConnected => _port != null;
/// List available USB serial devices.
static Future<List<UsbDevice>> listDevices() async {
return UsbSerial.listDevices();
}
/// Connect to a USB serial device.
Future<bool> connect(UsbDevice device) async {
try {
_port = await device.create();
if (_port == null) {
debugPrint('❌ [USB] Failed to create port for ${device.productName}');
return false;
}
final opened = await _port!.open();
if (!opened) {
debugPrint('❌ [USB] Failed to open port');
_port = null;
return false;
}
await _port!.setDTR(true);
await _port!.setRTS(true);
await _port!.setPortParameters(
_baudRate,
UsbPort.DATABITS_8,
UsbPort.STOPBITS_1,
UsbPort.PARITY_NONE,
);
// Wire write callback
_service.writeRaw = _write;
// Listen for incoming data
_inputSubscription = _port!.inputStream?.listen(
(data) => _service.feedRawBytes(data),
onError: (error) {
debugPrint('❌ [USB] Read error: $error');
disconnect();
},
onDone: () {
debugPrint('⚠️ [USB] Port closed');
disconnect();
},
);
debugPrint(
'✅ [USB] Connected to ${device.productName} at $_baudRate baud',
);
// Initialize MeshCore session
return _service.markConnected();
} catch (e) {
debugPrint('❌ [USB] Connection failed: $e');
await disconnect();
return false;
}
}
/// Disconnect from the USB device.
Future<void> disconnect() async {
_service.markDisconnected();
_service.writeRaw = null;
await _inputSubscription?.cancel();
_inputSubscription = null;
await _port?.close();
_port = null;
}
Future<void> _write(Uint8List data) async {
if (_port == null) throw Exception('USB not connected');
await _port!.write(data);
}
void dispose() {
disconnect();
}
}

View File

@@ -0,0 +1,20 @@
import 'dart:async';
import 'package:meshcore_client/meshcore_client.dart';
// Web Serial API is only available on web platform.
// This file provides the interface; the actual JS interop is conditional.
/// Web Serial transport stub for non-web platforms.
/// On web, use [WebSerialTransportImpl] from web_serial_transport_web.dart.
class WebSerialTransport {
final MeshCoreSerialService service;
WebSerialTransport(this.service);
bool get isConnected => false;
bool get isSupported => false;
Future<bool> requestAndConnect() async => false;
Future<void> disconnect() async {}
void dispose() {}
}

View File

@@ -1,21 +1,32 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'profiles_feature_service.dart';
import '../utils/voice_message_parser.dart';
/// Stores user-selected voice bitrate and maps it to supported codec modes.
class VoiceBitratePreferences {
static const String _bitrateKey = 'voice_bitrate';
static const int defaultBitrate = 1300;
static const List<int> supportedBitrates = [700, 1200, 1300, 1400, 1600, 2400, 3200];
static const List<int> supportedBitrates = [
700,
1200,
1300,
1400,
1600,
2400,
3200,
];
static Future<int> getBitrate() async {
final prefs = await SharedPreferences.getInstance();
final value = prefs.getInt(_bitrateKey) ?? defaultBitrate;
final value =
prefs.getInt(ProfileStorageScope.scopedKey(_bitrateKey)) ??
defaultBitrate;
return supportedBitrates.contains(value) ? value : defaultBitrate;
}
static Future<void> setBitrate(int bitrate) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_bitrateKey, bitrate);
await prefs.setInt(ProfileStorageScope.scopedKey(_bitrateKey), bitrate);
}
static VoicePacketMode toVoiceMode(int bitrate) {

View File

@@ -24,4 +24,14 @@ class BatteryDisplayHelper {
if (rssi > -70) return Colors.orange;
return Colors.red;
}
/// Number of active signal bars (0-5) for a BLE RSSI value.
static int getSignalBars(int rssi) {
if (rssi > -50) return 5;
if (rssi > -60) return 4;
if (rssi > -70) return 3;
if (rssi > -80) return 2;
if (rssi > -90) return 1;
return 0;
}
}

View File

@@ -0,0 +1,18 @@
const int customMapKey6HexLength = 12;
String? normalizeCustomMapId(String? mapId) {
if (mapId == null) {
return null;
}
final normalized = mapId.trim().toLowerCase();
if (normalized.isEmpty) {
return null;
}
if (normalized.length <= customMapKey6HexLength) {
return normalized;
}
return normalized.substring(0, customMapKey6HexLength);
}

View File

@@ -1,19 +1,17 @@
import 'dart:convert';
import '../models/map_coordinate_space.dart';
import '../models/map_drawing.dart';
import 'custom_map_id.dart';
/// Parser for drawing messages transmitted over mesh network
class DrawingMessageParser {
/// Drawing message prefix
static const String prefix = 'D:';
static const String legacyPrefix = 'D:';
static const String customMapPrefix = 'D2:';
/// Check if message is a drawing message
static bool isDrawingMessage(String text) {
return text.startsWith(prefix);
return text.startsWith(legacyPrefix) || text.startsWith(customMapPrefix);
}
/// Parse drawing message text into MapDrawing object
/// senderName and messageId should be extracted from packet metadata
/// Returns null if parsing fails
static MapDrawing? parseDrawingMessage(
String text, {
String? senderName,
@@ -24,126 +22,96 @@ class DrawingMessageParser {
}
try {
// Remove prefix
final jsonStr = text.substring(prefix.length);
if (text.startsWith(customMapPrefix)) {
final json =
jsonDecode(text.substring(customMapPrefix.length))
as Map<String, dynamic>;
final mapId = normalizeCustomMapId(json['m'] as String?);
if (mapId == null || mapId.isEmpty) {
return null;
}
return MapDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
coordinateSpace: MapCoordinateSpace.customMap,
mapId: mapId,
);
}
// Parse JSON
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
// Use ultra-compact network format parser
// Sender name and message ID come from packet metadata, not JSON
final json =
jsonDecode(text.substring(legacyPrefix.length))
as Map<String, dynamic>;
return MapDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
coordinateSpace: MapCoordinateSpace.geo,
);
} catch (e) {
} catch (_) {
return null;
}
}
/// Create drawing message text from MapDrawing object
/// Sender will be determined from packet metadata on receiving end
static String createDrawingMessage(MapDrawing drawing) {
final json = drawing.toNetworkJson();
final jsonStr = jsonEncode(json).toString();
return '$prefix$jsonStr';
final prefix = drawing.coordinateSpace == MapCoordinateSpace.customMap
? customMapPrefix
: legacyPrefix;
return '$prefix${jsonEncode(json)}';
}
/// Get drawing type display name from drawing message text
/// Returns "Line" or "Rectangle", or null if parsing fails
static String? getDrawingTypeDisplay(String text) {
if (!isDrawingMessage(text)) return null;
try {
final jsonStr = text.substring(prefix.length);
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
final typeNum = json['t'] as int?;
if (typeNum == null) return null;
switch (typeNum) {
case 0:
return 'Line';
case 1:
return 'Rectangle';
default:
return null;
}
} catch (e) {
return null;
}
final metadata = getDrawingMetadata(text);
return metadata?['type'] as String?;
}
/// Get color name from drawing message text
/// Returns color name like "Red", "Blue", etc., or null if parsing fails
static String? getColorName(String text) {
if (!isDrawingMessage(text)) return null;
try {
final jsonStr = text.substring(prefix.length);
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
final colorIndex = json['c'] as int?;
if (colorIndex == null) return null;
// Color mapping from DrawingColor enum
const colorNames = [
'Red', // 0
'Blue', // 1
'Green', // 2
'Yellow', // 3
'Orange', // 4
'Purple', // 5
'Pink', // 6
'Cyan', // 7
];
if (colorIndex >= 0 && colorIndex < colorNames.length) {
return colorNames[colorIndex];
}
return null;
} catch (e) {
return null;
}
final metadata = getDrawingMetadata(text);
return metadata?['color'] as String?;
}
/// Get drawing metadata for display in message bubbles
/// Returns map with type, color, and pointCount, or null if parsing fails
static Map<String, dynamic>? getDrawingMetadata(String text) {
if (!isDrawingMessage(text)) return null;
try {
final jsonStr = text.substring(prefix.length);
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
final json =
jsonDecode(
text.startsWith(customMapPrefix)
? text.substring(customMapPrefix.length)
: text.substring(legacyPrefix.length),
)
as Map<String, dynamic>;
final typeNum = json['t'] as int?;
final colorIndex = json['c'] as int?;
if (typeNum == null || colorIndex == null) return null;
// Get type display name
String type;
int? pointCount;
switch (typeNum) {
case 0: // Line
case 0:
type = 'Line';
final points = json['p'] as List?;
pointCount = points != null ? points.length ~/ 2 : null;
break;
case 1: // Rectangle
case 1:
type = 'Rectangle';
pointCount = 4; // Rectangles always have 4 corners
pointCount = 4;
break;
default:
return null;
}
// Get color name
const colorNames = [
'Red', 'Blue', 'Green', 'Yellow', 'Orange', 'Purple', 'Pink', 'Cyan',
'Red',
'Blue',
'Green',
'Yellow',
'Orange',
'Purple',
'Pink',
'Cyan',
];
final color = colorIndex >= 0 && colorIndex < colorNames.length
? colorNames[colorIndex]
@@ -153,8 +121,12 @@ class DrawingMessageParser {
'type': type,
'color': color,
'pointCount': pointCount,
'coordinateSpace': text.startsWith(customMapPrefix)
? MapCoordinateSpace.customMap.name
: MapCoordinateSpace.geo.name,
'mapId': normalizeCustomMapId(json['m'] as String?),
};
} catch (e) {
} catch (_) {
return null;
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
int rssiScore(int rssiDbm) {
if (rssiDbm >= -60) return 5;
if (rssiDbm >= -70) return 4;
if (rssiDbm >= -80) return 3;
if (rssiDbm >= -90) return 2;
if (rssiDbm >= -100) return 1;
return 0;
}
int snrScore(double snrDb) {
if (snrDb >= 10) return 5;
if (snrDb >= 5) return 4;
if (snrDb >= 0) return 3;
if (snrDb >= -5) return 2;
if (snrDb >= -10) return 1;
return 0;
}
String linkQualityLabel(int? rssiDbm, double? snrDb) {
var totalScore = 0;
var metricCount = 0;
if (rssiDbm != null) {
totalScore += rssiScore(rssiDbm);
metricCount += 1;
}
if (snrDb != null) {
totalScore += snrScore(snrDb);
metricCount += 1;
}
if (metricCount == 0) return 'Weak';
final averageScore = totalScore / metricCount;
if (averageScore >= 4.5) return 'Excellent';
if (averageScore >= 3.5) return 'Good';
if (averageScore >= 2.5) return 'Fair';
return 'Weak';
}
Color linkQualityColor(String quality) {
switch (quality) {
case 'Excellent':
return Colors.green;
case 'Good':
return Colors.lightGreen;
case 'Fair':
return Colors.orange;
default:
return Colors.redAccent;
}
}

View File

@@ -0,0 +1,160 @@
import 'dart:math' as math;
import 'package:latlong2/latlong.dart';
/// An RSSI observation from a single repeater.
class RssiObservation {
final LatLng repeaterLocation;
final int rssiDbm;
final DateTime observedAt;
const RssiObservation({
required this.repeaterLocation,
required this.rssiDbm,
required this.observedAt,
});
double? get estimatedDistanceMeters =>
RssiLocationEstimator.estimateDistanceMeters(rssiDbm);
}
/// Estimate distance from RSSI using the log-distance path loss model.
///
/// For LoRa at ~900 MHz:
/// - Path loss exponent (n) ≈ 2.7-3.5 for outdoor environments
/// - Reference distance: 1m, reference RSSI: -30 dBm (typical LoRa at 1m)
///
/// Formula: distance = 10 ^ ((txPower - rssi) / (10 * n))
class RssiLocationEstimator {
/// Estimate distance in meters from RSSI value.
static double? estimateDistanceMeters(int rssiDbm) {
if (rssiDbm > -20 || rssiDbm < -140) return null;
const double referenceRssi = -30.0;
const double pathLossExponent = 3.0;
final distance = math.pow(
10.0,
(referenceRssi - rssiDbm) / (10.0 * pathLossExponent),
).toDouble();
return distance.clamp(10.0, 50000.0);
}
/// Offset a point by distance and bearing (Haversine inverse).
static LatLng offsetPoint(
LatLng origin,
double distanceMeters,
double bearingDegrees,
) {
const double earthRadius = 6371000.0;
final lat1 = origin.latitude * math.pi / 180.0;
final lon1 = origin.longitude * math.pi / 180.0;
final bearing = bearingDegrees * math.pi / 180.0;
final angDist = distanceMeters / earthRadius;
final lat2 = math.asin(
math.sin(lat1) * math.cos(angDist) +
math.cos(lat1) * math.sin(angDist) * math.cos(bearing),
);
final lon2 = lon1 +
math.atan2(
math.sin(bearing) * math.sin(angDist) * math.cos(lat1),
math.cos(angDist) - math.sin(lat1) * math.sin(lat2),
);
return LatLng(lat2 * 180.0 / math.pi, lon2 * 180.0 / math.pi);
}
/// Single-repeater estimate: offset from repeater by RSSI distance.
static LatLng? estimateFromRepeater({
required LatLng repeaterLocation,
required int rssiDbm,
required List<int> contactPublicKey,
}) {
final distance = estimateDistanceMeters(rssiDbm);
if (distance == null) return null;
final keyHash = contactPublicKey.fold<int>(0, (a, b) => a ^ b);
final bearing = (keyHash % 360).toDouble();
return offsetPoint(repeaterLocation, distance, bearing);
}
/// Trilateration from multiple RSSI observations.
///
/// With 1 observation: offset from repeater (bearing from key hash).
/// With 2 observations: weighted midpoint on the line between circles.
/// With 3+ observations: weighted centroid of circle intersection region.
///
/// Each observation is weighted by 1/distance² (closer = more accurate).
static LatLng? trilaterate({
required List<RssiObservation> observations,
required List<int> contactPublicKey,
Duration maxAge = const Duration(minutes: 30),
}) {
final now = DateTime.now();
final recent = observations
.where((o) =>
now.difference(o.observedAt) <= maxAge &&
o.estimatedDistanceMeters != null)
.toList();
if (recent.isEmpty) return null;
if (recent.length == 1) {
return estimateFromRepeater(
repeaterLocation: recent.first.repeaterLocation,
rssiDbm: recent.first.rssiDbm,
contactPublicKey: contactPublicKey,
);
}
// Weighted centroid: each repeater contributes a candidate point
// on the circle towards the centroid of all repeaters.
// Weight = 1/distance² (inverse square — closer observations dominate).
// Step 1: compute raw centroid of all repeater locations
double centroidLat = 0, centroidLon = 0;
for (final obs in recent) {
centroidLat += obs.repeaterLocation.latitude;
centroidLon += obs.repeaterLocation.longitude;
}
centroidLat /= recent.length;
centroidLon /= recent.length;
final centroid = LatLng(centroidLat, centroidLon);
// Step 2: for each observation, compute a candidate point on the
// circle (at RSSI distance) in the direction of the centroid.
double weightedLat = 0, weightedLon = 0, totalWeight = 0;
for (final obs in recent) {
final dist = obs.estimatedDistanceMeters!;
final weight = 1.0 / (dist * dist);
// Bearing from this repeater towards the centroid
final bearing = _bearingDegrees(obs.repeaterLocation, centroid);
final candidate = offsetPoint(obs.repeaterLocation, dist, bearing);
weightedLat += candidate.latitude * weight;
weightedLon += candidate.longitude * weight;
totalWeight += weight;
}
if (totalWeight <= 0) return null;
return LatLng(weightedLat / totalWeight, weightedLon / totalWeight);
}
/// Bearing in degrees from point A to point B.
static double _bearingDegrees(LatLng a, LatLng b) {
final lat1 = a.latitude * math.pi / 180.0;
final lat2 = b.latitude * math.pi / 180.0;
final dLon = (b.longitude - a.longitude) * math.pi / 180.0;
final y = math.sin(dLon) * math.cos(lat2);
final x = math.cos(lat1) * math.sin(lat2) -
math.sin(lat1) * math.cos(lat2) * math.cos(dLon);
return (math.atan2(y, x) * 180.0 / math.pi + 360.0) % 360.0;
}
}

View File

@@ -1,216 +1,206 @@
import 'package:latlong2/latlong.dart';
import '../models/sar_marker.dart';
import '../models/message.dart';
import 'dart:convert';
import 'package:latlong2/latlong.dart';
import '../models/map_coordinate_space.dart';
import '../models/message.dart';
import '../models/sar_marker.dart';
import 'custom_map_id.dart';
/// Parser for SAR (Search & Rescue) special messages
/// Old format: `S:<emoji>:<latitude>,<longitude>:<optional_message>`
/// New format: `S:<emoji>:<colorIndex>:<latitude>,<longitude>:<optional_message>`
/// Examples:
/// S:🧑:37.7749,-122.4194 (old format)
/// S:🧑:2:37.7749,-122.4194 (new format with green color)
/// S:🔥:0:40.7128,-74.0060:Large wildfire spreading (new format with red color)
class SarMessageParser {
// Regex for new format with color index: S:emoji:colorIndex:lat,lon:notes
// Captures: emoji, colorIndex (single digit), latitude, longitude, optional message
static const String legacyPrefix = 'S:';
static const String customMapPrefix = 'S2:';
static final RegExp _sarPatternNew = RegExp(
r'^S:([^:]+):(\d):(-?\d+\.?\d*),(-?\d+\.?\d*):?(.*)',
multiLine: false,
);
// Regex for old format (backward compatibility): S:emoji:lat,lon:notes
// Captures: emoji, latitude, longitude, optional message
static final RegExp _sarPatternOld = RegExp(
r'^S:([^:]+):(-?\d+\.?\d*),(-?\d+\.?\d*):?(.*)',
multiLine: false,
);
/// Check if a message is a SAR marker message
static bool isSarMessage(String text) {
// Extract just the first line for matching
final firstLine = text.trim().split('\n').first;
return firstLine.startsWith('S:') &&
(_sarPatternNew.hasMatch(firstLine) ||
_sarPatternOld.hasMatch(firstLine));
final lines = text.trim().split('\n');
final firstLine = lines.isEmpty ? text.trim() : lines.first;
return firstLine.startsWith(legacyPrefix) ||
firstLine.startsWith(customMapPrefix);
}
/// Parse a SAR message and extract marker information
/// Returns null if the message is not a valid SAR message
/// Supports both old format (S:emoji:lat,lon:notes) and new format (S:emoji:colorIndex:lat,lon:notes)
static SarMarkerInfo? parse(String text) {
final trimmed = text.trim();
if (!trimmed.startsWith('S:')) return null;
// Extract first line (actual SAR marker)
final firstLine = trimmed.split('\n').first;
// Try new format first (with color index)
var match = _sarPatternNew.firstMatch(firstLine);
bool isNewFormat = match != null;
// If new format didn't match, try old format
if (match == null) {
match = _sarPatternOld.firstMatch(firstLine);
if (match == null) return null;
if (trimmed.startsWith(customMapPrefix)) {
return _parseCustomMap(trimmed);
}
if (!trimmed.startsWith(legacyPrefix)) {
return null;
}
final firstLine = trimmed.split('\n').first;
var match = _sarPatternNew.firstMatch(firstLine);
final isNewFormat = match != null;
match ??= _sarPatternOld.firstMatch(firstLine);
if (match == null) return null;
try {
String emoji;
double latitude;
double longitude;
String? inlineMessage;
int? colorIndex;
if (isNewFormat) {
// New format: S:emoji:colorIndex:lat,lon:notes
emoji = match.group(1)!;
colorIndex = int.parse(match.group(2)!);
latitude = double.parse(match.group(3)!);
longitude = double.parse(match.group(4)!);
inlineMessage = match.group(5)?.trim();
} else {
// Old format: S:emoji:lat,lon:notes
emoji = match.group(1)!;
colorIndex = null; // No color index in old format
latitude = double.parse(match.group(2)!);
longitude = double.parse(match.group(3)!);
inlineMessage = match.group(4)?.trim();
}
// Validate coordinates
final emoji = match.group(1)!;
final colorIndex = isNewFormat ? int.parse(match.group(2)!) : null;
final latitude = double.parse(match.group(isNewFormat ? 3 : 2)!);
final longitude = double.parse(match.group(isNewFormat ? 4 : 3)!);
final inlineMessage = match.group(isNewFormat ? 5 : 4)?.trim();
if (latitude < -90 || latitude > 90) return null;
if (longitude < -180 || longitude > 180) return null;
// Validate color index if present
if (colorIndex != null && (colorIndex < 0 || colorIndex > 7)) {
colorIndex = null; // Invalid index, ignore it
}
final markerType = SarMarkerType.fromEmoji(emoji);
final location = LatLng(latitude, longitude);
// Combine inline message with multi-line notes
final additionalNotes = extractNotes(text);
String? notes;
if (inlineMessage != null && inlineMessage.isNotEmpty) {
notes = inlineMessage;
}
// Check for multi-line notes (lines after the first line)
final additionalNotes = extractNotes(text);
if (additionalNotes != null) {
if (additionalNotes != null && additionalNotes.isNotEmpty) {
notes = notes != null ? '$notes\n$additionalNotes' : additionalNotes;
}
return SarMarkerInfo(
type: markerType,
location: location,
type: SarMarkerType.fromEmoji(emoji),
location: LatLng(latitude, longitude),
emoji: emoji,
notes: notes,
colorIndex: colorIndex,
colorIndex: colorIndex != null && colorIndex >= 0 && colorIndex <= 7
? colorIndex
: null,
coordinateSpace: MapCoordinateSpace.geo,
);
} catch (e) {
} catch (_) {
return null;
}
}
static SarMarkerInfo? _parseCustomMap(String text) {
try {
final json =
jsonDecode(text.substring(customMapPrefix.length))
as Map<String, dynamic>;
final emoji = json['e'];
final mapId = normalizeCustomMapId(json['m'] as String?);
final rawPoint = json['p'];
if (emoji is! String || mapId == null || rawPoint is! List) {
return null;
}
final point = rawPoint.cast<num>();
if (point.length != 2) return null;
final colorIndex = json['c'];
final notes = json['n'];
return SarMarkerInfo(
type: SarMarkerType.fromEmoji(emoji),
location: LatLng(point[0].toDouble(), point[1].toDouble()),
emoji: emoji,
notes: notes is String && notes.isNotEmpty ? notes : null,
colorIndex: colorIndex is int ? colorIndex : null,
coordinateSpace: MapCoordinateSpace.customMap,
mapId: mapId,
);
} catch (_) {
return null;
}
}
/// Enhance a Message with SAR marker information
static Message enhanceMessage(Message message) {
final sarInfo = parse(message.text);
if (sarInfo == null) return message;
return message.copyWith(
isSarMarker: true,
sarGpsCoordinates: sarInfo.location,
sarNotes: sarInfo.notes, // Extract and store notes
sarCustomEmoji: sarInfo.emoji, // Always store emoji for type inference
sarColorIndex: sarInfo.colorIndex, // Store color index
sarGpsCoordinates: sarInfo.coordinateSpace == MapCoordinateSpace.geo
? sarInfo.location
: null,
sarCustomMapPoint: sarInfo.coordinateSpace == MapCoordinateSpace.customMap
? sarInfo.location
: null,
sarCustomMapId: sarInfo.mapId,
sarNotes: sarInfo.notes,
sarCustomEmoji: sarInfo.emoji,
sarColorIndex: sarInfo.colorIndex,
);
}
/// Create a SAR marker message text (new format with color index)
static String createSarMessage({
required SarMarkerType type,
required LatLng location,
String? notes,
int? colorIndex,
}) {
// New format: S:emoji:colorIndex:lat,lon:notes
final colorIdx = colorIndex ?? 0; // Default to red if not specified
final colorIdx = colorIndex ?? 0;
final text =
'S:${type.emoji}:$colorIdx:${location.latitude.toString()},${location.longitude.toString()}';
'S:${type.emoji}:$colorIdx:${location.latitude},${location.longitude}';
if (notes != null && notes.isNotEmpty) {
// Use colon-separated format for inline message
return '$text:$notes';
}
return text;
}
/// Extract additional notes from SAR message (text after the marker)
static String createCustomMapSarMessage({
required String emoji,
required String mapId,
required LatLng point,
String? notes,
int? colorIndex,
}) {
final payload = <String, dynamic>{
'e': emoji,
'c': colorIndex ?? 0,
'm': normalizeCustomMapId(mapId),
'p': [point.latitude.round(), point.longitude.round()],
};
if (notes != null && notes.isNotEmpty) {
payload['n'] = notes;
}
return '$customMapPrefix${jsonEncode(payload)}';
}
static String? extractNotes(String text) {
final trimmed = text.trim();
final lines = trimmed.split('\n');
if (lines.length <= 1) return null;
// Everything after the first line is considered notes
return lines.sublist(1).join('\n').trim();
}
/// Validate SAR message format
static bool isValidFormat(String text) {
return isSarMessage(text) && parse(text) != null;
}
/// Get a user-friendly error message for invalid SAR format
static String? getFormatError(String text) {
if (!text.trim().startsWith('S:')) {
final trimmed = text.trim();
if (trimmed.startsWith(customMapPrefix)) {
return parse(text) == null
? 'Invalid format for custom map SAR marker'
: null;
}
if (!trimmed.startsWith(legacyPrefix)) {
return 'SAR message must start with "S:"';
}
final parts = text.trim().split(':');
if (parts.length < 3) {
return 'Invalid format. Use: S:<emoji>:<latitude>,<longitude>';
final firstLine = trimmed.split('\n').first;
if (firstLine == legacyPrefix) {
return 'Invalid format for SAR marker';
}
final parts = firstLine.split(':');
if (parts.length < 2 || parts[1].isEmpty) {
return 'Missing emoji';
}
final emoji = parts[1];
if (emoji.isEmpty) {
return 'Missing emoji marker (🧑, 🔥, or 🏕️)';
}
final coords = parts[2];
if (!coords.contains(',')) {
return 'Coordinates must be separated by comma';
}
final coordParts = coords.split(',');
if (coordParts.length != 2) {
return 'Invalid coordinates format';
}
try {
final lat = double.parse(coordParts[0]);
final lon = double.parse(coordParts[1]);
if (lat < -90 || lat > 90) {
return 'Latitude must be between -90 and 90';
}
if (lon < -180 || lon > 180) {
return 'Longitude must be between -180 and 180';
}
} catch (e) {
return 'Invalid coordinate values';
}
return null;
return parse(text) == null ? 'Invalid format for SAR marker' : null;
}
}
/// Parsed SAR marker information
class SarMarkerInfo {
final SarMarkerType type;
final LatLng location;
final String emoji;
final String? notes;
final int?
colorIndex; // Color index from standard palette (0-7), null for backward compatibility
final int? colorIndex;
final MapCoordinateSpace coordinateSpace;
final String? mapId;
SarMarkerInfo({
required this.type,
@@ -218,10 +208,12 @@ class SarMarkerInfo {
required this.emoji,
this.notes,
this.colorIndex,
required this.coordinateSpace,
this.mapId,
});
@override
String toString() {
return 'SarMarkerInfo(type: ${type.displayName}, location: $location, colorIndex: $colorIndex, notes: $notes)';
return 'SarMarkerInfo(type: ${type.displayName}, location: $location, colorIndex: $colorIndex, space: ${coordinateSpace.name}, mapId: $mapId)';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -133,7 +133,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
onPressed: _isCreating
? null
: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
icon: Icon(Icons.close),
tooltip: l10n.close,
),
],
@@ -158,7 +158,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
size: 20,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: Text(
l10n.channelTypesInfo,
@@ -170,7 +170,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
],
),
),
const SizedBox(height: 16),
SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
@@ -196,7 +196,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
},
),
if (!isHashChannel) ...[
const SizedBox(height: 16),
SizedBox(height: 16),
TextFormField(
controller: _secretController,
decoration: InputDecoration(
@@ -211,7 +211,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleCreate(),
),
const SizedBox(height: 8),
SizedBox(height: 8),
Text(
l10n.channelSecretHelp,
style: theme.textTheme.bodySmall?.copyWith(
@@ -236,7 +236,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
size: 20,
color: Colors.blue,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: Text(
l10n.hashChannelInfo,

View File

@@ -7,10 +7,12 @@ import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../models/path_history.dart';
import '../../l10n/app_localizations.dart';
import '../../providers/app_provider.dart';
import '../../providers/connection_provider.dart';
import '../../services/contact_route_resolver.dart';
import '../../services/path_history_service.dart';
import '../../services/relay_candidate_sorter.dart';
import '../../services/route_hash_preferences.dart';
class ContactRouteDialogResult {
@@ -72,6 +74,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
late final TextEditingController _controller;
late final TextEditingController _relaySearchController;
final PathHistoryService _pathHistoryService = PathHistoryService();
final RelayCandidateSorter _relayCandidateSorter =
const RelayCandidateSorter();
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
ParsedContactRoute? _parsedRoute;
String? _errorText;
@@ -133,13 +137,16 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
}
}
List<Contact> get _routeCandidates =>
widget.availableContacts
.where(
(contact) => contact.isRepeater && contact.displayLocation != null,
)
.toList()
..sort((a, b) => a.displayName.compareTo(b.displayName));
List<Contact> _routeCandidates({required LatLng? selfPoint}) =>
_relayCandidateSorter.sortByDistanceFromSelf(
widget.availableContacts
.where(
(contact) =>
contact.isRepeater && contact.displayLocation != null,
)
.toList(),
selfPoint: selfPoint,
);
List<Contact> _mapSelectionForText(String text) {
final tokens = text
@@ -151,7 +158,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
final selected = <Contact>[];
final seen = <String>{};
for (final token in tokens) {
final match = _routeCandidates
final match = _routeCandidates(selfPoint: null)
.where(
(contact) => contact.publicKeyHex.toUpperCase().startsWith(token),
)
@@ -164,7 +171,13 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
}
Future<void> _loadHashSizePreference() async {
final hashSize = await RouteHashPreferences.getHashSize();
// Use the contact's current path hash size if it has a path,
// otherwise fall back to the global preference.
final contactHashSize = widget.contact.hasPath
? widget.contact.pathHashSize
: null;
final hashSize =
contactHashSize ?? await RouteHashPreferences.getHashSize();
if (!mounted) return;
setState(() {
_selectedHashSize = hashSize;
@@ -201,6 +214,16 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
);
}
ParsedContactRoute? get _effectiveRoute {
if (_parsedRoute != null) {
return _parsedRoute;
}
if (_errorText != null || _controller.text.trim().isNotEmpty) {
return null;
}
return ContactRouteCodec.direct(hashSize: _selectedHashSize);
}
void _toggleHop(Contact contact) {
setState(() {
if (_selectedMapHops.any(
@@ -264,7 +287,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
.toList();
if (tokens.isEmpty) return null;
final lastToken = tokens.last;
final match = _routeCandidates
final match = _routeCandidates(selfPoint: null)
.where(
(contact) => contact.publicKeyHex.toUpperCase().startsWith(lastToken),
)
@@ -403,13 +426,14 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
),
trailing: FilledButton.tonal(
onPressed: () => _applyHistoryRecord(record),
child: const Text('Use'),
child: Text(AppLocalizations.of(context)!.use),
),
),
);
}
Widget _buildPreviewSection() {
final previewRoute = _effectiveRoute;
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
@@ -423,20 +447,21 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_parsedRoute == null ? 'Route preview' : _parsedRoute!.summary,
previewRoute == null ? 'Route preview' : previewRoute.summary,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
_parsedRoute == null
previewRoute == null
? 'Pick relays from the list below or open manual edit if you need exact hop tokens.'
: '${_parsedRoute!.byteLength} bytes • descriptor 0x${_parsedRoute!.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
: '${previewRoute.byteLength} bytes • descriptor 0x${previewRoute.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
style: Theme.of(context).textTheme.bodySmall,
),
if (_parsedRoute != null) ...[
if (previewRoute != null &&
previewRoute.canonicalText.isNotEmpty) ...[
const SizedBox(height: 10),
SelectableText(
_parsedRoute!.canonicalText,
previewRoute.canonicalText,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
@@ -457,7 +482,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
border: Border.all(color: Theme.of(context).dividerColor),
),
child: Text(
'No relays selected yet. Add repeaters below or let auto resolve build a route.',
'No relays selected. Save now to use a direct path, or add repeaters below.',
style: Theme.of(context).textTheme.bodyMedium,
),
);
@@ -578,7 +603,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_showManualEditor = expanded;
});
},
title: const Text('Manual route edit'),
title: Text(AppLocalizations.of(context)!.manualRouteEdit),
subtitle: const Text(
'Use this when you need to paste or tweak hop tokens directly.',
),
@@ -594,7 +619,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
? 'AABB,CCDD'
: 'AABBCC,DDEEFF',
helperText:
'Comma-separated hops. Hash size follows global Settings. Colon form like AA:BB is also accepted.',
'Comma-separated hops using the selected path size. Colon form like AA:BB is also accepted.',
errorText: _errorText,
border: const OutlineInputBorder(),
),
@@ -690,14 +715,34 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Path Size', style: Theme.of(context).textTheme.labelLarge),
const Spacer(),
SegmentedButton<int>(
segments: [
for (final size in RouteHashPreferences.supportedSizes)
ButtonSegment<int>(value: size, label: Text('${size}B')),
],
selected: {_selectedHashSize},
onSelectionChanged: (selection) {
setState(() {
_selectedHashSize = selection.first;
_syncControllerFromSelectedHops();
});
},
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: _resolvePathAutomatically,
icon: const Icon(Icons.auto_fix_high),
label: const Text('Auto resolve'),
icon: Icon(Icons.auto_fix_high),
label: Text(AppLocalizations.of(context)!.autoResolve),
),
OutlinedButton.icon(
onPressed: _selectedMapHops.isEmpty
@@ -708,8 +753,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_syncControllerFromSelectedHops();
});
},
icon: const Icon(Icons.clear_all),
label: const Text('Clear relays'),
icon: Icon(Icons.clear_all),
label: Text(AppLocalizations.of(context)!.clearRelays),
),
],
),
@@ -764,7 +809,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (observedRecord != null) ...[
_buildHistoryRecordTile(observedRecord, title: 'Observed mesh route'),
_buildHistoryRecordTile(observedRecord, title: AppLocalizations.of(context)!.observedMeshRoute),
const SizedBox(height: 16),
],
if (remainingRecords.isEmpty)
@@ -790,8 +835,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
@override
Widget build(BuildContext context) {
final effectiveRoute = _effectiveRoute;
final appProvider = context.watch<AppProvider>();
final routeCandidates = _routeCandidates;
final connectionProvider = context.watch<ConnectionProvider>();
final selfPoint =
connectionProvider.deviceInfo.advLat != null &&
@@ -803,6 +848,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
connectionProvider.deviceInfo.advLon! / 1e6,
)
: null;
final routeCandidates = _routeCandidates(selfPoint: selfPoint);
final recipientLocation = widget.contact.displayLocation;
final recipientPoint = recipientLocation == null
? null
@@ -907,22 +953,24 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
TextButton(
onPressed: () => Navigator.of(
context,
).pop(const ContactRouteDialogResult.clear()),
child: const Text('Clear Route'),
).pop(ContactRouteDialogResult.clear()),
child: Text(
'${AppLocalizations.of(context)!.clearRoute} (${AppLocalizations.of(context)!.flood})',
),
)
else
const SizedBox.shrink(),
FilledButton(
onPressed: _parsedRoute == null
onPressed: effectiveRoute == null
? null
: () => Navigator.of(context).pop(
ContactRouteDialogResult.setWithFallback(
_parsedRoute!,
effectiveRoute,
inferredFallbackLocation:
_buildSyntheticFallbackLocation(),
),
),
child: const Text('Save Path'),
child: Text(AppLocalizations.of(context)!.savePath),
),
],
),

View File

@@ -1,6 +1,6 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' as flutter_map;
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
@@ -17,6 +17,8 @@ import 'contact_trace_sheet.dart';
import 'room_login_sheet.dart';
import '../common/contact_avatar.dart';
import '../sensors/sensor_telemetry_card.dart';
import '../../utils/link_quality.dart';
import '../../utils/time_ago_extensions.dart';
import '../../utils/toast_logger.dart';
import '../../l10n/app_localizations.dart';
@@ -160,7 +162,7 @@ class ContactTile extends StatelessWidget {
Row(children: [_buildRoutePill(context, contact)]),
] else
Padding(
padding: const EdgeInsets.only(top: 4),
padding: EdgeInsets.only(top: 4),
child: Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(
@@ -348,8 +350,58 @@ class ContactTile extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!contact.isChannel)
ListTile(
leading: Icon(
contact.isFavourite ? Icons.star : Icons.star_outline,
color: contact.isFavourite ? Colors.amber : null,
),
title: Text(
contact.isFavourite
? 'Remove from Favourites'
: 'Add to Favourites',
),
onTap: () async {
Navigator.pop(sheetContext);
final toggled = contact.toggleFavourite();
final connectionProvider = context
.read<ConnectionProvider>();
await connectionProvider.addOrUpdateContact(toggled);
if (context.mounted) {
// Refresh contact from device so the local cache is updated
await connectionProvider.getContact(contact.publicKey);
}
},
),
if (!contact.isChannel)
ListTile(
leading: Icon(Icons.share_outlined),
title: Text(l10n.shareContact),
onTap: () async {
Navigator.pop(sheetContext);
final connectionProvider = context
.read<ConnectionProvider>();
final url = await connectionProvider.exportContactUrl(
contact.publicKey,
);
if (url != null && context.mounted) {
await Clipboard.setData(ClipboardData(text: url));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.contactLinkCopiedToClipboard),
),
);
}
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.failedToExportContact)),
);
}
},
),
ListTile(
leading: const Icon(Icons.message_outlined),
leading: Icon(Icons.message_outlined),
title: Text(l10n.messages),
enabled: canMessage,
onTap: !canMessage
@@ -361,7 +413,7 @@ class ContactTile extends StatelessWidget {
),
if (contact.displayLocation != null)
ListTile(
leading: const Icon(Icons.map_outlined),
leading: Icon(Icons.map_outlined),
title: Text(l10n.viewOnMap),
onTap: () {
Navigator.pop(sheetContext);
@@ -387,8 +439,8 @@ class ContactTile extends StatelessWidget {
),
if (canPreviewSensor)
ListTile(
leading: const Icon(Icons.visibility_outlined),
title: const Text('Preview'),
leading: Icon(Icons.visibility_outlined),
title: Text(l10n.preview),
onTap: () async {
Navigator.pop(sheetContext);
await Future<void>.delayed(Duration.zero);
@@ -416,7 +468,7 @@ class ContactTile extends StatelessWidget {
),
if (canSetPath)
ListTile(
leading: const Icon(Icons.alt_route),
leading: Icon(Icons.alt_route),
title: Text(l10n.contactSetPath),
onTap: () {
Navigator.pop(sheetContext);
@@ -425,17 +477,26 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isChannel)
ListTile(
leading: const Icon(Icons.route),
title: const Text('Trace'),
leading: Icon(Icons.route),
title: Text(l10n.trace),
onTap: () {
Navigator.pop(sheetContext);
_showTraceSheet(context, contact);
},
),
if (contact.type == ContactType.repeater)
ListTile(
leading: const Icon(Icons.hub_outlined),
title: const Text('View Neighbours'),
onTap: () {
Navigator.pop(sheetContext);
_showNeighbours(context, contact);
},
),
if (!contact.isPublicChannel)
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text('Edit name'),
leading: Icon(Icons.edit_outlined),
title: Text(l10n.editName),
onTap: () {
Navigator.pop(sheetContext);
_showNameOverrideDialog(context, contact);
@@ -443,7 +504,7 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isPublicChannel)
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
leading: Icon(Icons.delete, color: Colors.red),
title: Text(
contact.isChannel ? l10n.deleteChannel : l10n.deleteContact,
style: const TextStyle(color: Colors.red),
@@ -544,6 +605,18 @@ class ContactTile extends StatelessWidget {
);
}
void _showNeighbours(BuildContext context, Contact contact) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => _NeighboursSheet(contact: contact),
);
}
void _showTraceSheet(BuildContext context, Contact contact) {
showModalBottomSheet(
context: context,
@@ -716,12 +789,13 @@ class ContactTile extends StatelessWidget {
paddedPathBytes: parsedRoute.paddedPathBytes,
);
if (context.mounted) {
final routeLabel = parsedRoute.hopCount == 0
? AppLocalizations.of(context)!.direct
: parsedRoute.canonicalText;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.contactRouteSet(parsedRoute.canonicalText),
AppLocalizations.of(context)!.contactRouteSet(routeLabel),
),
),
);
@@ -1164,3 +1238,567 @@ class _ContactNameOverrideSheetState extends State<_ContactNameOverrideSheet> {
);
}
}
/// Bottom sheet that sends "neighbors" text command to a repeater and shows results.
class _NeighboursSheet extends StatefulWidget {
final Contact contact;
const _NeighboursSheet({required this.contact});
@override
State<_NeighboursSheet> createState() => _NeighboursSheetState();
}
class _NeighboursSheetState extends State<_NeighboursSheet> {
bool _loading = true;
String? _error;
List<_Neighbour> _neighbours = const [];
@override
void initState() {
super.initState();
_fetchNeighbours();
}
Future<void> _fetchNeighbours() async {
final connectionProvider = context.read<ConnectionProvider>();
final previousOnMessageReceived = connectionProvider.onMessageReceived;
String? responseText;
void onMessage(message) {
if (message.senderPublicKeyPrefix != null &&
widget.contact.publicKey
.sublist(0, 6)
.every((b) => message.senderPublicKeyPrefix!.contains(b))) {
responseText = message.text;
}
}
void sheetListener(message) {
previousOnMessageReceived?.call(message);
onMessage(message);
}
connectionProvider.onMessageReceived = sheetListener;
try {
await connectionProvider.sendTextMessage(
contactPublicKey: widget.contact.publicKey,
text: 'neighbors',
);
for (int i = 0; i < 30; i++) {
await Future.delayed(const Duration(milliseconds: 500));
if (responseText != null) break;
}
if (!mounted) return;
if (responseText == null) {
setState(() {
_loading = false;
_error = 'No response from repeater (timeout)';
});
return;
}
final text = responseText!;
if (text.toLowerCase().contains('unknown')) {
setState(() {
_loading = false;
_error = 'Neighbours feature not supported by this firmware';
});
return;
}
if (text.toLowerCase().contains('-none-') || text.trim().isEmpty) {
setState(() {
_loading = false;
});
return;
}
final parsed = <_Neighbour>[];
for (final line in text.split('\n')) {
final trimmed = line.trim();
if (trimmed.isEmpty) continue;
final parts = trimmed.split(':');
if (parts.length >= 3) {
final keyHex = parts[0];
final timestampOrMs = int.tryParse(parts[1]);
final snrRaw = int.tryParse(parts[2]);
final isMs = timestampOrMs != null && timestampOrMs < 1e9;
parsed.add(
_Neighbour(
publicKeyHex: keyHex,
lastSeenAt: !isMs && timestampOrMs != null
? DateTime.fromMillisecondsSinceEpoch(timestampOrMs * 1000)
: null,
lastSeenMs: isMs ? timestampOrMs : null,
snrDb: snrRaw != null ? snrRaw / 4.0 : null,
),
);
}
}
setState(() {
_loading = false;
_neighbours = parsed;
});
} catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_error = 'Failed: $e';
});
} finally {
if (identical(connectionProvider.onMessageReceived, sheetListener)) {
connectionProvider.onMessageReceived = previousOnMessageReceived;
}
}
}
Contact? _resolveNeighbourContact(String keyHex) {
final contactsProvider = context.read<ContactsProvider>();
for (final contact in contactsProvider.contacts) {
if (contact.publicKeyHex.toLowerCase().startsWith(keyHex.toLowerCase())) {
return contact;
}
}
return null;
}
String _resolveNeighbourName(String keyHex) {
final contact = _resolveNeighbourContact(keyHex);
if (contact != null) {
return contact.displayName;
}
return keyHex.length > 12 ? '${keyHex.substring(0, 12)}...' : keyHex;
}
String _formatAge(BuildContext context, _Neighbour neighbour) {
if (neighbour.lastSeenAt != null) {
return DateTime.now()
.difference(neighbour.lastSeenAt!)
.toLocalizedTimeAgoWithSeconds(context);
}
if (neighbour.lastSeenMs != null) {
if (neighbour.lastSeenMs! < 1000) {
return AppLocalizations.of(context)!.justNow;
}
return Duration(
milliseconds: neighbour.lastSeenMs!,
).toLocalizedTimeAgoWithSeconds(context);
}
return AppLocalizations.of(context)!.justNow;
}
List<_MappedNeighbour> _mappedNeighbours() {
return _neighbours
.map((neighbour) {
final contact = _resolveNeighbourContact(neighbour.publicKeyHex);
final location = contact?.displayLocation;
if (contact == null || location == null) {
return null;
}
return _MappedNeighbour(
neighbour: neighbour,
contact: contact,
location: LatLng(location.latitude, location.longitude),
);
})
.whereType<_MappedNeighbour>()
.toList();
}
Widget _buildSummaryChip(
BuildContext context, {
required IconData icon,
required String label,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 6),
Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w700,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildRepeaterMarker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: const BoxConstraints(maxWidth: 132),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.78),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.contact.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 11,
),
),
),
const SizedBox(height: 4),
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: const Icon(Icons.hub_outlined, color: Colors.white, size: 18),
),
],
);
}
Widget _buildNeighbourMarker(BuildContext context, _MappedNeighbour mapped) {
final quality = linkQualityLabel(null, mapped.neighbour.snrDb);
final qualityColor = linkQualityColor(quality);
final ageLabel = _formatAge(context, mapped.neighbour);
final signalLabel = mapped.neighbour.snrDb == null
? quality
: '$quality${mapped.neighbour.snrDb!.toStringAsFixed(1)} dB';
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: const BoxConstraints(maxWidth: 146),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: qualityColor.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(999),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Text(
signalLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 10,
),
),
),
const SizedBox(height: 4),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(color: qualityColor, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Icon(
mapped.contact.isRepeater
? Icons.router_outlined
: Icons.location_on_outlined,
color: qualityColor,
size: 16,
),
),
const SizedBox(height: 4),
Container(
constraints: const BoxConstraints(maxWidth: 148),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.78),
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
mapped.contact.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 11,
),
),
const SizedBox(height: 2),
Text(
ageLabel,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white70,
fontWeight: FontWeight.w600,
fontSize: 10,
),
),
],
),
),
],
);
}
Widget _buildRouteMap(
BuildContext context, {
required LatLng repeaterLocation,
required List<_MappedNeighbour> mappedNeighbours,
}) {
final points = <LatLng>[
repeaterLocation,
...mappedNeighbours.map((mapped) => mapped.location),
];
final colorScheme = Theme.of(context).colorScheme;
return ClipRRect(
borderRadius: BorderRadius.circular(18),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: colorScheme.outlineVariant),
),
child: flutter_map.FlutterMap(
options: flutter_map.MapOptions(
initialCameraFit: flutter_map.CameraFit.bounds(
bounds: flutter_map.LatLngBounds.fromPoints(points),
padding: const EdgeInsets.all(42),
),
),
children: [
flutter_map.TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.meshcore.sar',
),
flutter_map.PolylineLayer(
polylines: mappedNeighbours.map((mapped) {
final quality = linkQualityLabel(null, mapped.neighbour.snrDb);
final qualityColor = linkQualityColor(quality);
return flutter_map.Polyline(
points: [repeaterLocation, mapped.location],
color: qualityColor.withValues(alpha: 0.9),
strokeWidth: 4,
borderColor: Colors.white.withValues(alpha: 0.7),
borderStrokeWidth: 1.5,
);
}).toList(),
),
flutter_map.MarkerLayer(
markers: [
flutter_map.Marker(
point: repeaterLocation,
width: 150,
height: 74,
child: _buildRepeaterMarker(context),
),
...mappedNeighbours.map(
(mapped) => flutter_map.Marker(
point: mapped.location,
width: 164,
height: 112,
child: _buildNeighbourMarker(context, mapped),
),
),
],
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final repeaterDisplayLocation = widget.contact.displayLocation;
final repeaterLocation = repeaterDisplayLocation == null
? null
: LatLng(
repeaterDisplayLocation.latitude,
repeaterDisplayLocation.longitude,
);
final mappedNeighbours = _mappedNeighbours();
final missingLocations = _neighbours.length - mappedNeighbours.length;
return SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.72,
child: Column(
children: [
const SizedBox(height: 12),
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).dividerColor,
borderRadius: BorderRadius.circular(2),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
child: Row(
children: [
const Icon(Icons.hub_outlined),
const SizedBox(width: 10),
Expanded(
child: Text(
'Neighbours of ${widget.contact.displayName}',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_error!, textAlign: TextAlign.center),
),
)
: _neighbours.isEmpty
? const Center(child: Text('No neighbours found'))
: Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildSummaryChip(
context,
icon: Icons.route_outlined,
label:
'${_neighbours.length} neighbour${_neighbours.length == 1 ? '' : 's'}',
),
_buildSummaryChip(
context,
icon: Icons.map_outlined,
label: '${mappedNeighbours.length} on map',
),
if (missingLocations > 0)
_buildSummaryChip(
context,
icon: Icons.location_off_outlined,
label: '$missingLocations without GPS',
),
],
),
const SizedBox(height: 12),
Expanded(
child: repeaterLocation == null
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: Text(
'${widget.contact.displayName} has no saved location, so neighbour routes cannot be drawn on the map yet.',
textAlign: TextAlign.center,
),
),
)
: mappedNeighbours.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: Text(
'Neighbours responded, but no geolocated contacts matched saved nodes to plot. Recent neighbour: ${_resolveNeighbourName(_neighbours.first.publicKeyHex)}${_formatAge(context, _neighbours.first)}',
textAlign: TextAlign.center,
),
),
)
: _buildRouteMap(
context,
repeaterLocation: repeaterLocation,
mappedNeighbours: mappedNeighbours,
),
),
],
),
),
),
],
),
),
);
}
}
class _Neighbour {
final String publicKeyHex;
final DateTime? lastSeenAt;
final int? lastSeenMs;
final double? snrDb;
const _Neighbour({
required this.publicKeyHex,
this.lastSeenAt,
this.lastSeenMs,
this.snrDb,
});
}
class _MappedNeighbour {
final _Neighbour neighbour;
final Contact contact;
final LatLng location;
const _MappedNeighbour({
required this.neighbour,
required this.contact,
required this.location,
});
}

View File

@@ -95,14 +95,12 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
final concreteNodes = routeEntries
.where((entry) => entry.resolved.node != null)
.map((entry) => entry.resolved.node!)
.where((node) => node.hasValidCoordinates)
.toList();
final mapPoints = concreteNodes
.map((node) => LatLng(node.latitude, node.longitude))
.toList();
final hasMapPath = mapPoints.length >= 2;
final relayNodes = trace.matchedRelayNodes
.map((entry) => entry.node)
.whereType<MeshMapNode>();
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
@@ -268,41 +266,14 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
),
title: Text(entry.value.label),
subtitle: Text(
'Relay${entry.value.keyLabel == null ? '' : '${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : '${entry.value.matchSummary}'}${entry.value.resolved.cycleSummary == null ? '' : '${entry.value.resolved.cycleSummary}'}',
'Path node${entry.value.keyLabel == null ? '' : '${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : '${entry.value.matchSummary}'}${entry.value.resolved.cycleSummary == null ? '' : '${entry.value.resolved.cycleSummary}'}',
),
trailing: entry.value.resolved.canCycle
? const Icon(Icons.sync_alt)
: null,
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Relays (${relayNodes.length})',
style: Theme.of(context).textTheme.titleMedium,
),
),
if (relayNodes.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
'No relay nodes could be matched for this contact.',
),
),
...relayNodes.map(
(node) => ListTile(
leading: const Icon(Icons.router),
title: Text(node.name),
subtitle: Text(
'${node.publicKey.substring(0, math.min(12, node.publicKey.length))}'
'${node.latitude.toStringAsFixed(5)}, ${node.longitude.toStringAsFixed(5)}',
),
),
),
const SizedBox(height: 16),
],
),
),
@@ -425,6 +396,7 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
);
})
.whereType<MeshMapNode>()
.where((node) => node.hasValidCoordinates)
.toList();
final selfNode = _selfNode(connectionProvider);
@@ -445,7 +417,7 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
return null;
}
return MeshMapNode(
final node = MeshMapNode(
type: -1,
name: connectionProvider.deviceInfo.selfName?.trim().isNotEmpty == true
? connectionProvider.deviceInfo.selfName!.trim()
@@ -458,6 +430,7 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
longitude: advLon / 1e6,
updatedAtMs: DateTime.now().millisecondsSinceEpoch,
);
return node.hasValidCoordinates ? node : null;
}
List<MeshMapNode> _mergeNodes(

View File

@@ -401,7 +401,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
color: colorScheme.onPrimaryContainer,
size: 20,
),
const SizedBox(width: 12),
SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.enterPasswordInfo,
@@ -488,7 +488,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.login),
: Icon(Icons.login),
label: Text(
_isLoggingIn
? AppLocalizations.of(context)!.loggingInDots

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