mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Compare commits
67 Commits
feat/senso
...
v2026.0322
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a24e699e22 | ||
|
|
7486837ad0 | ||
|
|
059125c714 | ||
|
|
9279ff2158 | ||
|
|
dc297b0b9f | ||
|
|
2d24481aba | ||
|
|
20a1df94eb | ||
|
|
5cc81bef65 | ||
|
|
6145462555 | ||
|
|
721fb7e7a2 | ||
|
|
5a58e45e13 | ||
|
|
de4258c0a4 | ||
|
|
d8cb35c595 | ||
|
|
8da69705ee | ||
|
|
e474168ba8 | ||
|
|
03dba8b3ce | ||
|
|
bc9e37bd22 | ||
|
|
a32d5d2c85 | ||
|
|
e683ab933f | ||
|
|
f286b0afd1 | ||
|
|
68ec72534d | ||
|
|
6af8b62d9b | ||
|
|
01fbb7de6c | ||
|
|
9303d24b69 | ||
|
|
140088fe2f | ||
|
|
67d8e1c906 | ||
|
|
29b75a3155 | ||
|
|
87cb890877 | ||
|
|
095458ece4 | ||
|
|
5659242672 | ||
|
|
3da4ba8fb2 | ||
|
|
89dc1f2862 | ||
|
|
251932bb47 | ||
|
|
b48139964a | ||
|
|
d29b85feb8 | ||
|
|
c01ef7a059 | ||
|
|
fa2b586ab7 | ||
|
|
bbd5d108cb | ||
|
|
e17f24a6bc | ||
|
|
372224a62c | ||
|
|
e4c8709ee2 | ||
|
|
3c469b62d0 | ||
|
|
1e97ba9831 | ||
|
|
6187067017 | ||
|
|
cd165d2372 | ||
|
|
59f6df4260 | ||
|
|
464fc5e715 | ||
|
|
a76ee4f8f6 | ||
|
|
c884f81ca5 | ||
|
|
993563f059 | ||
|
|
cfd233d38e | ||
|
|
9b14ff4e1a | ||
|
|
84cd8eec00 | ||
|
|
434d3df53e | ||
|
|
1fe260b95e | ||
|
|
d3d53995a6 | ||
|
|
efbddc8831 | ||
|
|
0e3acfa4ca | ||
|
|
450b8382e8 | ||
|
|
420690d683 | ||
|
|
fedd7b9a2b | ||
|
|
c82e0c3452 | ||
|
|
a55cfffa59 | ||
|
|
2bb5b353bd | ||
|
|
991421d097 | ||
|
|
1dcd85f178 | ||
|
|
7a9be3eb76 |
21
.github/workflows/build-artifacts.yml
vendored
21
.github/workflows/build-artifacts.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Cache Android NDK
|
||||
uses: actions/cache@v4
|
||||
@@ -95,6 +98,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Linux desktop
|
||||
run: flutter config --enable-linux-desktop
|
||||
@@ -132,6 +138,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Windows desktop
|
||||
shell: pwsh
|
||||
@@ -174,6 +183,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable macOS desktop
|
||||
run: flutter config --enable-macos-desktop
|
||||
@@ -181,6 +193,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Install macOS native build tools
|
||||
run: brew install automake libtool
|
||||
|
||||
- name: Build macOS release
|
||||
run: flutter build macos --release
|
||||
|
||||
@@ -216,6 +231,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
@@ -272,6 +290,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable web
|
||||
run: flutter config --enable-web
|
||||
|
||||
3
.github/workflows/build-multiplatform.yml
vendored
3
.github/workflows/build-multiplatform.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,6 +28,7 @@ migrate_working_dir/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
pubspec_overrides.yaml
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
@@ -84,6 +84,8 @@ PODS:
|
||||
- Flutter
|
||||
- vibration (3.0.0):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
@@ -108,6 +110,7 @@ DEPENDENCIES:
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -161,6 +164,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||
@@ -189,6 +194,7 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 117;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>117</string>
|
||||
<string>124</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.554978">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.144858">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="100.17557">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="98.142592">
|
||||
|
||||
</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="157.12775">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1638,5 +1638,252 @@
|
||||
"contactFailedToClearRoute": "Pfad konnte nicht gelöscht werden: {error}",
|
||||
"contactRouteCleared": "Pfad gelöscht",
|
||||
"contactRouteSet": "Pfad gesetzt: {route}",
|
||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}"
|
||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK-Zeitüberschreitung",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Nutzlast",
|
||||
"hops": "Hops",
|
||||
"hashSize": "Hash-Größe",
|
||||
"pathBytes": "Pfad-Bytes",
|
||||
"selectedPath": "Ausgewählter Pfad",
|
||||
"estimatedTx": "Geschätzte Sendung",
|
||||
"senderToReceipt": "Sender bis Empfang",
|
||||
"receivedCopies": "Empfangene Kopien",
|
||||
"retryCause": "Wiederholungsursache",
|
||||
"retryMode": "Wiederholungsmodus",
|
||||
"retryResult": "Wiederholungsergebnis",
|
||||
"lastRetry": "Letzter Versuch",
|
||||
"rxPackets": "RX-Pakete",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Rate",
|
||||
"window": "Fenster",
|
||||
"posttxDelay": "Sendeverzögerung",
|
||||
"bandpass": "Bandpass",
|
||||
"bandpassFilterVoice": "Bandpassfilter Sprache",
|
||||
"frequency": "Frequenz",
|
||||
"australia": "Australien",
|
||||
"australiaNarrow": "Australien (Schmal)",
|
||||
"australiaQld": "Australien: QLD",
|
||||
"australiaSaWa": "Australien: SA, WA",
|
||||
"newZealand": "Neuseeland",
|
||||
"newZealandNarrow": "Neuseeland (Schmal)",
|
||||
"switzerland": "Schweiz",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Tschechien (Schmal)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Langstrecke)",
|
||||
"euukDeprecated": "EU/UK (Veraltet)",
|
||||
"euukNarrow": "EU/UK (Schmal)",
|
||||
"usacanadaRecommended": "USA/Kanada (Empfohlen)",
|
||||
"vietnamDeprecated": "Vietnam (Veraltet)",
|
||||
"vietnamNarrow": "Vietnam (Schmal)",
|
||||
"active": "Aktiv",
|
||||
"addContact": "Kontakt hinzufügen",
|
||||
"all": "Alle",
|
||||
"autoResolve": "Automatisch auflösen",
|
||||
"clearAllLabel": "Alle löschen",
|
||||
"clearRelays": "Relais löschen",
|
||||
"clearFilters": "Filter löschen",
|
||||
"clearRoute": "Route löschen",
|
||||
"clearMessages": "Nachrichten löschen",
|
||||
"clearScale": "Maßstab löschen",
|
||||
"clearDiscoveries": "Entdeckungen löschen",
|
||||
"clearOnlineTraceDatabase": "Online-Trace-Datenbank löschen",
|
||||
"clearAllChannels": "Alle Kanäle löschen",
|
||||
"clearAllContacts": "Alle Kontakte löschen",
|
||||
"clearChannels": "Kanäle löschen",
|
||||
"clearContacts": "Kontakte löschen",
|
||||
"clearPathOnMaxRetry": "Pfad bei max. Wiederholung löschen",
|
||||
"create": "Erstellen",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"defaultValue": "Standard",
|
||||
"duplicate": "Duplizieren",
|
||||
"editName": "Name bearbeiten",
|
||||
"open": "Öffnen",
|
||||
"paste": "Einfügen",
|
||||
"preview": "Vorschau",
|
||||
"remove": "Entfernen",
|
||||
"rename": "Umbenennen",
|
||||
"resolveAll": "Alle auflösen",
|
||||
"send": "Senden",
|
||||
"sendAnyway": "Trotzdem senden",
|
||||
"share": "Teilen",
|
||||
"shareContact": "Kontakt teilen",
|
||||
"trace": "Trace",
|
||||
"use": "Verwenden",
|
||||
"useSelectedFrequency": "Ausgewählte Frequenz verwenden",
|
||||
"discovery": "Erkennung",
|
||||
"discoverRepeaters": "Repeater entdecken",
|
||||
"discoverSensors": "Sensoren entdecken",
|
||||
"repeaterDiscoverySent": "Repeater-Erkennung gesendet",
|
||||
"sensorDiscoverySent": "Sensor-Erkennung gesendet",
|
||||
"clearedPendingDiscoveries": "Ausstehende Entdeckungen gelöscht.",
|
||||
"autoDiscovery": "Automatische Erkennung",
|
||||
"enableAutomaticAdding": "Automatisches Hinzufügen aktivieren",
|
||||
"autoaddRepeaters": "Repeater automatisch hinzufügen",
|
||||
"autoaddRoomServers": "Raumserver automatisch hinzufügen",
|
||||
"autoaddSensors": "Sensoren automatisch hinzufügen",
|
||||
"autoaddUsers": "Benutzer automatisch hinzufügen",
|
||||
"overwriteOldestWhenFull": "Älteste bei vollem Speicher überschreiben",
|
||||
"storage": "Speicher",
|
||||
"dangerZone": "Gefahrenbereich",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Favoriten",
|
||||
"sensors": "Sensoren",
|
||||
"others": "Andere",
|
||||
"gpsModule": "GPS-Modul",
|
||||
"liveTraffic": "Live-Verkehr",
|
||||
"repeatersMap": "Repeater-Karte",
|
||||
"spectrumScan": "Spektrum-Scan",
|
||||
"blePacketLogs": "BLE-Paketprotokolle",
|
||||
"onlineTraceDatabase": "Online-Trace-Datenbank",
|
||||
"routePathByteSize": "Pfad-Bytegröße",
|
||||
"messageNotifications": "Nachrichtenbenachrichtigungen",
|
||||
"sarAlerts": "SAR-Alarme",
|
||||
"discoveryNotifications": "Erkennungsbenachrichtigungen",
|
||||
"updateNotifications": "Updatebenachrichtigungen",
|
||||
"muteWhileAppIsOpen": "Stumm bei geöffneter App",
|
||||
"disableContacts": "Kontakte deaktivieren",
|
||||
"enableSensorsTab": "Sensoren-Tab aktivieren",
|
||||
"enableProfiles": "Profile aktivieren",
|
||||
"autoRouteRotation": "Automatische Routenrotation",
|
||||
"nearestRepeaterFallback": "Nächster Repeater als Fallback",
|
||||
"deleteAllStoredMessageHistory": "Gesamten Nachrichtenverlauf löschen",
|
||||
"messageFontSize": "Nachrichtenschriftgröße",
|
||||
"rotateMapWithHeading": "Karte mit Richtung drehen",
|
||||
"showMapDebugInfo": "Karten-Debug-Info anzeigen",
|
||||
"openMapInFullscreen": "Karte im Vollbild öffnen",
|
||||
"showSarMarkersLabel": "SAR-Markierungen anzeigen",
|
||||
"displaySarMarkersOnTheMainMap": "SAR-Markierungen auf der Hauptkarte anzeigen",
|
||||
"showAllContactTrailsLabel": "Alle Kontaktpfade anzeigen",
|
||||
"hideRepeatersOnMap": "Repeater auf Karte ausblenden",
|
||||
"setMapScale": "Kartenmaßstab einstellen",
|
||||
"customMapScaleSaved": "Benutzerdefinierter Kartenmaßstab gespeichert",
|
||||
"voiceBitrate": "Sprach-Bitrate",
|
||||
"voiceCompressor": "Sprachkompressor",
|
||||
"balancesQuietAndLoudSpeechLevels": "Gleicht leise und laute Sprache aus",
|
||||
"voiceLimiter": "Sprach-Limiter",
|
||||
"preventsClippingPeaksBeforeEncoding": "Verhindert Clipping-Spitzen vor der Kodierung",
|
||||
"micAutoGain": "Mikrofon-Automatik",
|
||||
"letsTheRecorderAdjustInputLevel": "Lässt den Rekorder den Eingangspegel anpassen",
|
||||
"echoCancellation": "Echounterdrückung",
|
||||
"noiseSuppression": "Rauschunterdrückung",
|
||||
"trimSilenceInVoiceMessages": "Stille in Sprachnachrichten trimmen",
|
||||
"compressor": "Kompressor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatische Verstärkung",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Rauschen",
|
||||
"silenceTrim": "Stille",
|
||||
"maxImageSize": "Maximale Bildgröße",
|
||||
"imageCompression": "Bildkomprimierung",
|
||||
"grayscale": "Graustufen",
|
||||
"ultraMode": "Ultra-Modus",
|
||||
"fastPrivateGpsUpdates": "Schnelle private GPS-Updates",
|
||||
"movementThreshold": "Bewegungsschwelle",
|
||||
"fastGpsMovementThreshold": "Schneller GPS-Bewegungsschwellenwert",
|
||||
"fastGpsActiveuseInterval": "Schnelles GPS aktive Nutzung Intervall",
|
||||
"activeuseUpdateInterval": "Aktive Nutzung Updateintervall",
|
||||
"repeatNearbyTraffic": "Nahverkehr wiederholen",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Über Repeater im Mesh weiterleiten",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Nur in der Nähe, ohne Repeater-Flooding",
|
||||
"multihop": "Multi-Hop",
|
||||
"createProfile": "Profil erstellen",
|
||||
"renameProfile": "Profil umbenennen",
|
||||
"newProfile": "Neues Profil",
|
||||
"manageProfiles": "Profile verwalten",
|
||||
"enableProfilesToStartManagingThem": "Aktivieren Sie Profile, um sie zu verwalten.",
|
||||
"openMessage": "Nachricht öffnen",
|
||||
"jumpToTheRelatedSarMessage": "Zur zugehörigen SAR-Nachricht springen",
|
||||
"removeSarMarker": "SAR-Markierung entfernen",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Bitte wählen Sie ein Ziel zum Senden der SAR-Markierung",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-Markierung an öffentlichen Kanal gesendet",
|
||||
"sarMarkerSentToRoom": "SAR-Markierung an Raum gesendet",
|
||||
"loadFromGallery": "Aus Galerie laden",
|
||||
"replaceImage": "Bild ersetzen",
|
||||
"selectFromGallery": "Aus Galerie auswählen",
|
||||
"team": "Team",
|
||||
"found": "Gefunden",
|
||||
"staging": "Sammelpunkt",
|
||||
"object": "Objekt",
|
||||
"quiet": "Ruhig",
|
||||
"moderate": "Mäßig",
|
||||
"busy": "Beschäftigt",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum-Scan hat keine Kandidatenfrequenzen gefunden",
|
||||
"searchMessages": "Nachrichten suchen",
|
||||
"sendImageFromGallery": "Bild aus Galerie senden",
|
||||
"takePhoto": "Foto aufnehmen",
|
||||
"dmOnly": "Nur Direktnachricht",
|
||||
"allMessages": "Alle Nachrichten",
|
||||
"sendToPublicChannel": "An öffentlichen Kanal senden?",
|
||||
"selectMarkerTypeAndDestination": "Markierungstyp und Ziel auswählen",
|
||||
"noDestinationsAvailableLabel": "Keine Ziele verfügbar",
|
||||
"image": "Bild",
|
||||
"format": "Format",
|
||||
"dimensions": "Abmessungen",
|
||||
"segments": "Segmente",
|
||||
"transfers": "Übertragungen",
|
||||
"downloadedBy": "Heruntergeladen von",
|
||||
"saveDiscoverySettings": "Erkennungseinstellungen speichern",
|
||||
"savePublicInfo": "Öffentliche Info speichern",
|
||||
"saveRadioSettings": "Funkeinstellungen speichern",
|
||||
"savePath": "Pfad speichern",
|
||||
"wipeDeviceData": "Gerätedaten löschen",
|
||||
"wipeDevice": "Gerät löschen",
|
||||
"destructiveDeviceActions": "Destruktive Geräteaktionen.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wählen Sie eine Voreinstellung oder passen Sie die Funkeinstellungen an.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wählen Sie den Namen und Standort, den dieses Gerät teilt.",
|
||||
"availableSpaceOnThisDevice": "Verfügbarer Speicherplatz auf diesem Gerät.",
|
||||
"used": "Belegt",
|
||||
"total": "Gesamt",
|
||||
"renameValue": "Wert umbenennen",
|
||||
"customizeFields": "Felder anpassen",
|
||||
"livePreview": "Live-Vorschau",
|
||||
"refreshSchedule": "Aktualisierungsplan",
|
||||
"noResponse": "Keine Antwort",
|
||||
"refreshing": "Wird aktualisiert",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wählen Sie einen Relay oder Knoten zur Beobachtung.",
|
||||
"publicKeyLabel": "Öffentlicher Schlüssel",
|
||||
"alreadyInContacts": "Bereits in Kontakten",
|
||||
"connectToADeviceBeforeAddingContacts": "Verbinden Sie sich mit einem Gerät, bevor Sie Kontakte hinzufügen",
|
||||
"fromContacts": "Aus Kontakten",
|
||||
"onlineOnly": "Nur online",
|
||||
"inBoth": "In beiden",
|
||||
"source": "Quelle",
|
||||
"manualRouteEdit": "Manuelle Routenbearbeitung",
|
||||
"observedMeshRoute": "Beobachtete Mesh-Route",
|
||||
"allMessagesCleared": "Alle Nachrichten gelöscht",
|
||||
"onlineTraceDatabaseCleared": "Online-Trace-Datenbank gelöscht",
|
||||
"packetLogsCleared": "Paketprotokolle gelöscht",
|
||||
"hexDataCopiedToClipboard": "Hex-Daten in die Zwischenablage kopiert",
|
||||
"developerModeEnabled": "Entwicklermodus aktiviert",
|
||||
"developerModeDisabled": "Entwicklermodus deaktiviert",
|
||||
"clipboardIsEmpty": "Zwischenablage ist leer",
|
||||
"contactImported": "Kontakt importiert",
|
||||
"contactLinkCopiedToClipboard": "Kontaktlink in Zwischenablage kopiert",
|
||||
"failedToExportContact": "Kontaktexport fehlgeschlagen",
|
||||
"noLogsToExport": "Keine Protokolle zum Exportieren",
|
||||
"exportAsCsv": "Als CSV exportieren",
|
||||
"exportAsText": "Als Text exportieren",
|
||||
"receivedRfc3339": "Empfangen (RFC3339)",
|
||||
"buildTime": "Build-Zeit",
|
||||
"downloadUrlNotAvailable": "Download-URL nicht verfügbar",
|
||||
"cannotOpenDownloadUrl": "Download-URL kann nicht geöffnet werden",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Update-Prüfung nur auf Android verfügbar",
|
||||
"youAreRunningTheLatestVersion": "Sie verwenden die neueste Version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Update verfügbar, aber Download-URL nicht gefunden",
|
||||
"startTictactoe": "Tic-Tac-Toe starten",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nicht verfügbar",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: Gegner unbekannt",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: Warten auf Start",
|
||||
"acceptsShareLinks": "Akzeptiert freigegebene Links",
|
||||
"supportsRawHex": "Unterstützt rohes Hex",
|
||||
"clipboardfriendly": "Zwischenablage-freundlich",
|
||||
"captured": "Erfasst",
|
||||
"size": "Größe",
|
||||
"noCustomChannelsToClear": "Keine benutzerdefinierten Kanäle zum Löschen.",
|
||||
"noDeviceContactsToClear": "Keine Gerätekontakte zum Löschen."
|
||||
}
|
||||
|
||||
@@ -2303,5 +2303,252 @@
|
||||
"contactFailedToClearRoute": "Αποτυχία εκκαθάρισης διαδρομής: {error}",
|
||||
"contactRouteCleared": "Η διαδρομή εκκαθαρίστηκε",
|
||||
"contactRouteSet": "Ορίστηκε διαδρομή: {route}",
|
||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}"
|
||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Λήξη χρόνου ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ωφέλιμο φορτίο",
|
||||
"hops": "Αναπηδήσεις",
|
||||
"hashSize": "Μέγεθος hash",
|
||||
"pathBytes": "Bytes διαδρομής",
|
||||
"selectedPath": "Επιλεγμένη διαδρομή",
|
||||
"estimatedTx": "Εκτιμώμενη εκπομπή",
|
||||
"senderToReceipt": "Αποστολέας σε απόδειξη",
|
||||
"receivedCopies": "Ληφθέντα αντίγραφα",
|
||||
"retryCause": "Αιτία επανάληψης",
|
||||
"retryMode": "Λειτουργία επανάληψης",
|
||||
"retryResult": "Αποτέλεσμα επανάληψης",
|
||||
"lastRetry": "Τελευταία επανάληψη",
|
||||
"rxPackets": "Πακέτα RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Ρυθμός",
|
||||
"window": "Παράθυρο",
|
||||
"posttxDelay": "Καθυστέρηση μετά-tx",
|
||||
"bandpass": "Ζωνοπερατό",
|
||||
"bandpassFilterVoice": "Ζωνοπερατό φίλτρο φωνής",
|
||||
"frequency": "Συχνότητα",
|
||||
"australia": "Αυστραλία",
|
||||
"australiaNarrow": "Αυστραλία (Στενό)",
|
||||
"australiaQld": "Αυστραλία: QLD",
|
||||
"australiaSaWa": "Αυστραλία: SA, WA",
|
||||
"newZealand": "Νέα Ζηλανδία",
|
||||
"newZealandNarrow": "Νέα Ζηλανδία (Στενό)",
|
||||
"switzerland": "Ελβετία",
|
||||
"portugal433": "Πορτογαλία 433",
|
||||
"portugal868": "Πορτογαλία 868",
|
||||
"czechRepublicNarrow": "Τσεχία (Στενό)",
|
||||
"eu433mhzLongRange": "ΕΕ 433MHz (Μεγάλη Εμβέλεια)",
|
||||
"euukDeprecated": "ΕΕ/ΗΒ (Παρωχημένο)",
|
||||
"euukNarrow": "ΕΕ/ΗΒ (Στενό)",
|
||||
"usacanadaRecommended": "ΗΠΑ/Καναδάς (Προτεινόμενο)",
|
||||
"vietnamDeprecated": "Βιετνάμ (Παρωχημένο)",
|
||||
"vietnamNarrow": "Βιετνάμ (Στενό)",
|
||||
"active": "Ενεργό",
|
||||
"addContact": "Προσθήκη επαφής",
|
||||
"all": "Όλα",
|
||||
"autoResolve": "Αυτόματη επίλυση",
|
||||
"clearAllLabel": "Εκκαθάριση όλων",
|
||||
"clearRelays": "Εκκαθάριση αναμεταδοτών",
|
||||
"clearFilters": "Εκκαθάριση φίλτρων",
|
||||
"clearRoute": "Εκκαθάριση διαδρομής",
|
||||
"clearMessages": "Εκκαθάριση μηνυμάτων",
|
||||
"clearScale": "Εκκαθάριση κλίμακας",
|
||||
"clearDiscoveries": "Εκκαθάριση ανακαλύψεων",
|
||||
"clearOnlineTraceDatabase": "Εκκαθάριση βάσης ιχνών",
|
||||
"clearAllChannels": "Εκκαθάριση όλων των καναλιών",
|
||||
"clearAllContacts": "Εκκαθάριση όλων των επαφών",
|
||||
"clearChannels": "Εκκαθάριση καναλιών",
|
||||
"clearContacts": "Εκκαθάριση επαφών",
|
||||
"clearPathOnMaxRetry": "Εκκαθάριση μονοπατιού στο μέγιστο",
|
||||
"create": "Δημιουργία",
|
||||
"custom": "Προσαρμοσμένο",
|
||||
"defaultValue": "Προεπιλογή",
|
||||
"duplicate": "Αντιγραφή",
|
||||
"editName": "Επεξεργασία ονόματος",
|
||||
"open": "Άνοιγμα",
|
||||
"paste": "Επικόλληση",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"remove": "Αφαίρεση",
|
||||
"rename": "Μετονομασία",
|
||||
"resolveAll": "Επίλυση όλων",
|
||||
"send": "Αποστολή",
|
||||
"sendAnyway": "Αποστολή ούτως ή άλλως",
|
||||
"share": "Κοινοποίηση",
|
||||
"shareContact": "Κοινοποίηση επαφής",
|
||||
"trace": "Ίχνος",
|
||||
"use": "Χρήση",
|
||||
"useSelectedFrequency": "Χρήση επιλεγμένης συχνότητας",
|
||||
"discovery": "Ανακάλυψη",
|
||||
"discoverRepeaters": "Ανακάλυψη αναμεταδοτών",
|
||||
"discoverSensors": "Ανακάλυψη αισθητήρων",
|
||||
"repeaterDiscoverySent": "Αποστολή ανακάλυψης αναμεταδοτών",
|
||||
"sensorDiscoverySent": "Αποστολή ανακάλυψης αισθητήρων",
|
||||
"clearedPendingDiscoveries": "Εκκαθάριση εκκρεμών ανακαλύψεων.",
|
||||
"autoDiscovery": "Αυτόματη ανακάλυψη",
|
||||
"enableAutomaticAdding": "Ενεργοποίηση αυτόματης προσθήκης",
|
||||
"autoaddRepeaters": "Αυτόματη προσθήκη αναμεταδοτών",
|
||||
"autoaddRoomServers": "Αυτόματη προσθήκη διακομιστών δωματίου",
|
||||
"autoaddSensors": "Αυτόματη προσθήκη αισθητήρων",
|
||||
"autoaddUsers": "Αυτόματη προσθήκη χρηστών",
|
||||
"overwriteOldestWhenFull": "Αντικατάσταση παλαιότερων όταν γεμίσει",
|
||||
"storage": "Αποθήκευση",
|
||||
"dangerZone": "Ζώνη κινδύνου",
|
||||
"profiles": "Προφίλ",
|
||||
"favourites": "Αγαπημένα",
|
||||
"sensors": "Αισθητήρες",
|
||||
"others": "Άλλοι",
|
||||
"gpsModule": "Μονάδα GPS",
|
||||
"liveTraffic": "Ζωντανή κίνηση",
|
||||
"repeatersMap": "Χάρτης αναμεταδοτών",
|
||||
"spectrumScan": "Σάρωση φάσματος",
|
||||
"blePacketLogs": "Αρχεία πακέτων BLE",
|
||||
"onlineTraceDatabase": "Βάση δεδομένων ιχνών",
|
||||
"routePathByteSize": "Μέγεθος διαδρομής σε bytes",
|
||||
"messageNotifications": "Ειδοποιήσεις μηνυμάτων",
|
||||
"sarAlerts": "Ειδοποιήσεις SAR",
|
||||
"discoveryNotifications": "Ειδοποιήσεις ανακάλυψης",
|
||||
"updateNotifications": "Ειδοποιήσεις ενημερώσεων",
|
||||
"muteWhileAppIsOpen": "Σίγαση με ανοιχτή εφαρμογή",
|
||||
"disableContacts": "Απενεργοποίηση επαφών",
|
||||
"enableSensorsTab": "Ενεργοποίηση καρτέλας Αισθητήρων",
|
||||
"enableProfiles": "Ενεργοποίηση προφίλ",
|
||||
"autoRouteRotation": "Αυτόματη εναλλαγή διαδρομής",
|
||||
"nearestRepeaterFallback": "Πλησιέστερος αναμεταδότης ως εφεδρεία",
|
||||
"deleteAllStoredMessageHistory": "Διαγραφή όλου του ιστορικού",
|
||||
"messageFontSize": "Μέγεθος γραμματοσειράς μηνυμάτων",
|
||||
"rotateMapWithHeading": "Περιστροφή χάρτη με κατεύθυνση",
|
||||
"showMapDebugInfo": "Εμφάνιση πληροφοριών αποσφαλμάτωσης",
|
||||
"openMapInFullscreen": "Άνοιγμα χάρτη σε πλήρη οθόνη",
|
||||
"showSarMarkersLabel": "Εμφάνιση δεικτών SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Εμφάνιση δεικτών SAR στον κύριο χάρτη",
|
||||
"showAllContactTrailsLabel": "Εμφάνιση όλων των ιχνών επαφών",
|
||||
"hideRepeatersOnMap": "Απόκρυψη αναμεταδοτών στον χάρτη",
|
||||
"setMapScale": "Ρύθμιση κλίμακας χάρτη",
|
||||
"customMapScaleSaved": "Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε",
|
||||
"voiceBitrate": "Ρυθμός bit φωνής",
|
||||
"voiceCompressor": "Συμπιεστής φωνής",
|
||||
"balancesQuietAndLoudSpeechLevels": "Εξισορρόπηση χαμηλών και δυνατών επιπέδων",
|
||||
"voiceLimiter": "Περιοριστής φωνής",
|
||||
"preventsClippingPeaksBeforeEncoding": "Αποτρέπει κοπή κορυφών πριν την κωδικοποίηση",
|
||||
"micAutoGain": "Αυτόματη ενίσχυση μικροφώνου",
|
||||
"letsTheRecorderAdjustInputLevel": "Αφήνει τη συσκευή να ρυθμίσει την ένταση",
|
||||
"echoCancellation": "Ακύρωση ηχούς",
|
||||
"noiseSuppression": "Καταστολή θορύβου",
|
||||
"trimSilenceInVoiceMessages": "Περικοπή σιωπής σε φωνητικά μηνύματα",
|
||||
"compressor": "Συμπιεστής",
|
||||
"limiter": "Περιοριστής",
|
||||
"autoGain": "Αυτόματη ενίσχυση",
|
||||
"echoCancel": "Ηχώ",
|
||||
"noiseSuppress": "Θόρυβος",
|
||||
"silenceTrim": "Σιωπή",
|
||||
"maxImageSize": "Μέγιστο μέγεθος εικόνας",
|
||||
"imageCompression": "Συμπίεση εικόνας",
|
||||
"grayscale": "Κλίμακα του γκρι",
|
||||
"ultraMode": "Λειτουργία ultra",
|
||||
"fastPrivateGpsUpdates": "Γρήγορες ιδιωτικές ενημερώσεις GPS",
|
||||
"movementThreshold": "Κατώφλι κίνησης",
|
||||
"fastGpsMovementThreshold": "Κατώφλι κίνησης γρήγορου GPS",
|
||||
"fastGpsActiveuseInterval": "Διάστημα ενεργής χρήσης γρήγορου GPS",
|
||||
"activeuseUpdateInterval": "Διάστημα ενημέρωσης ενεργής χρήσης",
|
||||
"repeatNearbyTraffic": "Επανάληψη κοντινής κίνησης",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Αναμετάδοση μέσω αναμεταδοτών στο δίκτυο",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Μόνο κοντά, χωρίς πλημμυρισμό",
|
||||
"multihop": "Πολλαπλών αναπηδήσεων",
|
||||
"createProfile": "Δημιουργία προφίλ",
|
||||
"renameProfile": "Μετονομασία προφίλ",
|
||||
"newProfile": "Νέο προφίλ",
|
||||
"manageProfiles": "Διαχείριση προφίλ",
|
||||
"enableProfilesToStartManagingThem": "Ενεργοποιήστε τα προφίλ για να τα διαχειριστείτε.",
|
||||
"openMessage": "Άνοιγμα μηνύματος",
|
||||
"jumpToTheRelatedSarMessage": "Μετάβαση στο σχετικό μήνυμα SAR",
|
||||
"removeSarMarker": "Αφαίρεση δείκτη SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Επιλέξτε προορισμό για αποστολή δείκτη SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Δείκτης SAR εκπεμπόμενος στο δημόσιο κανάλι",
|
||||
"sarMarkerSentToRoom": "Δείκτης SAR εστάλη στο δωμάτιο",
|
||||
"loadFromGallery": "Φόρτωση από τη γκαλερί",
|
||||
"replaceImage": "Αντικατάσταση εικόνας",
|
||||
"selectFromGallery": "Επιλογή από τη γκαλερί",
|
||||
"team": "Ομάδα",
|
||||
"found": "Βρέθηκε",
|
||||
"staging": "Χώρος συγκέντρωσης",
|
||||
"object": "Αντικείμενο",
|
||||
"quiet": "Ήσυχο",
|
||||
"moderate": "Μέτριο",
|
||||
"busy": "Απασχολημένο",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Η σάρωση φάσματος δεν βρήκε υποψήφιες συχνότητες",
|
||||
"searchMessages": "Αναζήτηση μηνυμάτων",
|
||||
"sendImageFromGallery": "Αποστολή εικόνας από τη γκαλερί",
|
||||
"takePhoto": "Λήψη φωτογραφίας",
|
||||
"dmOnly": "Μόνο απευθείας",
|
||||
"allMessages": "Όλα τα μηνύματα",
|
||||
"sendToPublicChannel": "Αποστολή στο δημόσιο κανάλι;",
|
||||
"selectMarkerTypeAndDestination": "Επιλέξτε τύπο δείκτη και προορισμό",
|
||||
"noDestinationsAvailableLabel": "Δεν υπάρχουν διαθέσιμοι προορισμοί",
|
||||
"image": "Εικόνα",
|
||||
"format": "Μορφή",
|
||||
"dimensions": "Διαστάσεις",
|
||||
"segments": "Τμήματα",
|
||||
"transfers": "Μεταφορές",
|
||||
"downloadedBy": "Λήφθηκε από",
|
||||
"saveDiscoverySettings": "Αποθήκευση ρυθμίσεων ανακάλυψης",
|
||||
"savePublicInfo": "Αποθήκευση δημόσιων πληροφοριών",
|
||||
"saveRadioSettings": "Αποθήκευση ρυθμίσεων ραδιοφώνου",
|
||||
"savePath": "Αποθήκευση διαδρομής",
|
||||
"wipeDeviceData": "Διαγραφή δεδομένων συσκευής",
|
||||
"wipeDevice": "Διαγραφή συσκευής",
|
||||
"destructiveDeviceActions": "Καταστροφικές ενέργειες συσκευής.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Επιλέξτε το όνομα και την τοποθεσία που μοιράζεται αυτή η συσκευή.",
|
||||
"availableSpaceOnThisDevice": "Διαθέσιμος χώρος σε αυτή τη συσκευή.",
|
||||
"used": "Χρησιμοποιήθηκε",
|
||||
"total": "Σύνολο",
|
||||
"renameValue": "Μετονομασία τιμής",
|
||||
"customizeFields": "Προσαρμογή πεδίων",
|
||||
"livePreview": "Ζωντανή προεπισκόπηση",
|
||||
"refreshSchedule": "Χρονοδιάγραμμα ανανέωσης",
|
||||
"noResponse": "Χωρίς απάντηση",
|
||||
"refreshing": "Ανανέωση",
|
||||
"unavailable": "Μη διαθέσιμο",
|
||||
"pickARelayOrNodeToWatchInSensors": "Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.",
|
||||
"publicKeyLabel": "Δημόσιο κλειδί",
|
||||
"alreadyInContacts": "Ήδη στις επαφές",
|
||||
"connectToADeviceBeforeAddingContacts": "Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές",
|
||||
"fromContacts": "Από τις επαφές",
|
||||
"onlineOnly": "Μόνο συνδεδεμένοι",
|
||||
"inBoth": "Σε αμφότερα",
|
||||
"source": "Πηγή",
|
||||
"manualRouteEdit": "Χειροκίνητη επεξεργασία διαδρομής",
|
||||
"observedMeshRoute": "Παρατηρούμενη διαδρομή δικτύου",
|
||||
"allMessagesCleared": "Όλα τα μηνύματα διαγράφηκαν",
|
||||
"onlineTraceDatabaseCleared": "Βάση δεδομένων ιχνών εκκαθαρίστηκε",
|
||||
"packetLogsCleared": "Τα αρχεία πακέτων εκκαθαρίστηκαν",
|
||||
"hexDataCopiedToClipboard": "Τα δεδομένα hex αντιγράφηκαν",
|
||||
"developerModeEnabled": "Λειτουργία προγραμματιστή ενεργοποιήθηκε",
|
||||
"developerModeDisabled": "Λειτουργία προγραμματιστή απενεργοποιήθηκε",
|
||||
"clipboardIsEmpty": "Το πρόχειρο είναι κενό",
|
||||
"contactImported": "Επαφή εισήχθη",
|
||||
"contactLinkCopiedToClipboard": "Ο σύνδεσμος επαφής αντιγράφηκε",
|
||||
"failedToExportContact": "Αποτυχία εξαγωγής επαφής",
|
||||
"noLogsToExport": "Δεν υπάρχουν αρχεία για εξαγωγή",
|
||||
"exportAsCsv": "Εξαγωγή ως CSV",
|
||||
"exportAsText": "Εξαγωγή ως κείμενο",
|
||||
"receivedRfc3339": "Ελήφθη (RFC3339)",
|
||||
"buildTime": "Ώρα κατασκευής",
|
||||
"downloadUrlNotAvailable": "Η URL λήψης δεν είναι διαθέσιμη",
|
||||
"cannotOpenDownloadUrl": "Αδυναμία ανοίγματος URL λήψης",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Ο έλεγχος ενημερώσεων είναι διαθέσιμος μόνο σε Android",
|
||||
"youAreRunningTheLatestVersion": "Χρησιμοποιείτε την πιο πρόσφατη έκδοση",
|
||||
"updateAvailableButDownloadUrlNotFound": "Διαθέσιμη ενημέρωση αλλά η URL λήψης δεν βρέθηκε",
|
||||
"startTictactoe": "Εκκίνηση Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe μη διαθέσιμο",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: άγνωστος αντίπαλος",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: αναμονή για εκκίνηση",
|
||||
"acceptsShareLinks": "Αποδέχεται κοινόχρηστους συνδέσμους",
|
||||
"supportsRawHex": "Υποστηρίζει ακατέργαστο hex",
|
||||
"clipboardfriendly": "Φιλικό προς πρόχειρο",
|
||||
"captured": "Καταγράφηκε",
|
||||
"size": "Μέγεθος",
|
||||
"noCustomChannelsToClear": "Δεν υπάρχουν προσαρμοσμένα κανάλια.",
|
||||
"noDeviceContactsToClear": "Δεν υπάρχουν επαφές συσκευής."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -1638,5 +1638,252 @@
|
||||
"contactFailedToClearRoute": "No se pudo borrar la ruta: {error}",
|
||||
"contactRouteCleared": "Ruta borrada",
|
||||
"contactRouteSet": "Ruta establecida: {route}",
|
||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}"
|
||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tiempo de espera ACK",
|
||||
"opcode": "Código de operación",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamaño del hash",
|
||||
"pathBytes": "Bytes de ruta",
|
||||
"selectedPath": "Ruta seleccionada",
|
||||
"estimatedTx": "Transmisión estimada",
|
||||
"senderToReceipt": "Del remitente al recibo",
|
||||
"receivedCopies": "Copias recibidas",
|
||||
"retryCause": "Causa del reintento",
|
||||
"retryMode": "Modo de reintento",
|
||||
"retryResult": "Resultado del reintento",
|
||||
"lastRetry": "Último reintento",
|
||||
"rxPackets": "Paquetes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocidad",
|
||||
"window": "Ventana",
|
||||
"posttxDelay": "Retardo post-tx",
|
||||
"bandpass": "Paso de banda",
|
||||
"bandpassFilterVoice": "Filtro de paso de banda de voz",
|
||||
"frequency": "Frecuencia",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Estrecho)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nueva Zelanda",
|
||||
"newZealandNarrow": "Nueva Zelanda (Estrecho)",
|
||||
"switzerland": "Suiza",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estrecho)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Largo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estrecho)",
|
||||
"usacanadaRecommended": "EE.UU./Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietnam (Obsoleto)",
|
||||
"vietnamNarrow": "Vietnam (Estrecho)",
|
||||
"active": "Activo",
|
||||
"addContact": "Añadir contacto",
|
||||
"all": "Todo",
|
||||
"autoResolve": "Resolver automáticamente",
|
||||
"clearAllLabel": "Borrar todo",
|
||||
"clearRelays": "Borrar repetidores",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"clearRoute": "Borrar ruta",
|
||||
"clearMessages": "Borrar mensajes",
|
||||
"clearScale": "Borrar escala",
|
||||
"clearDiscoveries": "Borrar descubrimientos",
|
||||
"clearOnlineTraceDatabase": "Borrar base de datos de trazas",
|
||||
"clearAllChannels": "Borrar todos los canales",
|
||||
"clearAllContacts": "Borrar todos los contactos",
|
||||
"clearChannels": "Borrar canales",
|
||||
"clearContacts": "Borrar contactos",
|
||||
"clearPathOnMaxRetry": "Borrar ruta en máximo reintento",
|
||||
"create": "Crear",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Predeterminado",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nombre",
|
||||
"open": "Abrir",
|
||||
"paste": "Pegar",
|
||||
"preview": "Vista previa",
|
||||
"remove": "Eliminar",
|
||||
"rename": "Renombrar",
|
||||
"resolveAll": "Resolver todo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar de todos modos",
|
||||
"share": "Compartir",
|
||||
"shareContact": "Compartir contacto",
|
||||
"trace": "Traza",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frecuencia seleccionada",
|
||||
"discovery": "Descubrimiento",
|
||||
"discoverRepeaters": "Descubrir repetidores",
|
||||
"discoverSensors": "Descubrir sensores",
|
||||
"repeaterDiscoverySent": "Descubrimiento de repetidores enviado",
|
||||
"sensorDiscoverySent": "Descubrimiento de sensores enviado",
|
||||
"clearedPendingDiscoveries": "Descubrimientos pendientes borrados.",
|
||||
"autoDiscovery": "Descubrimiento automático",
|
||||
"enableAutomaticAdding": "Habilitar adición automática",
|
||||
"autoaddRepeaters": "Añadir repetidores automáticamente",
|
||||
"autoaddRoomServers": "Añadir servidores de sala automáticamente",
|
||||
"autoaddSensors": "Añadir sensores automáticamente",
|
||||
"autoaddUsers": "Añadir usuarios automáticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescribir más antiguos cuando esté lleno",
|
||||
"storage": "Almacenamiento",
|
||||
"dangerZone": "Zona de peligro",
|
||||
"profiles": "Perfiles",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Otros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfico en vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Escaneo de espectro",
|
||||
"blePacketLogs": "Registros de paquetes BLE",
|
||||
"onlineTraceDatabase": "Base de datos de trazas",
|
||||
"routePathByteSize": "Tamaño en bytes de la ruta",
|
||||
"messageNotifications": "Notificaciones de mensajes",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificaciones de descubrimiento",
|
||||
"updateNotifications": "Notificaciones de actualización",
|
||||
"muteWhileAppIsOpen": "Silenciar con la app abierta",
|
||||
"disableContacts": "Desactivar contactos",
|
||||
"enableSensorsTab": "Habilitar pestaña Sensores",
|
||||
"enableProfiles": "Habilitar perfiles",
|
||||
"autoRouteRotation": "Rotación automática de ruta",
|
||||
"nearestRepeaterFallback": "Repetidor más cercano como reserva",
|
||||
"deleteAllStoredMessageHistory": "Eliminar todo el historial de mensajes",
|
||||
"messageFontSize": "Tamaño de fuente de mensajes",
|
||||
"rotateMapWithHeading": "Rotar mapa con rumbo",
|
||||
"showMapDebugInfo": "Mostrar info de depuración del mapa",
|
||||
"openMapInFullscreen": "Abrir mapa en pantalla completa",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR en el mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos los rastros de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores en el mapa",
|
||||
"setMapScale": "Establecer escala del mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Tasa de bits de voz",
|
||||
"voiceCompressor": "Compresor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra niveles de habla suaves y altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene recorte de picos antes de codificar",
|
||||
"micAutoGain": "Ganancia automática del micrófono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite al grabador ajustar el nivel de entrada",
|
||||
"echoCancellation": "Cancelación de eco",
|
||||
"noiseSuppression": "Supresión de ruido",
|
||||
"trimSilenceInVoiceMessages": "Recortar silencio en mensajes de voz",
|
||||
"compressor": "Compresor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganancia automática",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruido",
|
||||
"silenceTrim": "Silencio",
|
||||
"maxImageSize": "Tamaño máximo de imagen",
|
||||
"imageCompression": "Compresión de imagen",
|
||||
"grayscale": "Escala de grises",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Actualizaciones GPS privadas rápidas",
|
||||
"movementThreshold": "Umbral de movimiento",
|
||||
"fastGpsMovementThreshold": "Umbral de movimiento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso activo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de actualización de uso activo",
|
||||
"repeatNearbyTraffic": "Repetir tráfico cercano",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir a través de repetidores en la red",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo cercanos, sin inundación de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Crear perfil",
|
||||
"renameProfile": "Renombrar perfil",
|
||||
"newProfile": "Nuevo perfil",
|
||||
"manageProfiles": "Gestionar perfiles",
|
||||
"enableProfilesToStartManagingThem": "Habilite los perfiles para comenzar a gestionarlos.",
|
||||
"openMessage": "Abrir mensaje",
|
||||
"jumpToTheRelatedSarMessage": "Ir al mensaje SAR relacionado",
|
||||
"removeSarMarker": "Eliminar marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleccione un destino para enviar el marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido al canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado a la sala",
|
||||
"loadFromGallery": "Cargar desde galería",
|
||||
"replaceImage": "Reemplazar imagen",
|
||||
"selectFromGallery": "Seleccionar de galería",
|
||||
"team": "Equipo",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunión",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "El escaneo de espectro no encontró frecuencias candidatas",
|
||||
"searchMessages": "Buscar mensajes",
|
||||
"sendImageFromGallery": "Enviar imagen desde galería",
|
||||
"takePhoto": "Tomar foto",
|
||||
"dmOnly": "Solo mensaje directo",
|
||||
"allMessages": "Todos los mensajes",
|
||||
"sendToPublicChannel": "¿Enviar al canal público?",
|
||||
"selectMarkerTypeAndDestination": "Seleccione tipo de marcador y destino",
|
||||
"noDestinationsAvailableLabel": "No hay destinos disponibles",
|
||||
"image": "Imagen",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensiones",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferencias",
|
||||
"downloadedBy": "Descargado por",
|
||||
"saveDiscoverySettings": "Guardar ajustes de descubrimiento",
|
||||
"savePublicInfo": "Guardar información pública",
|
||||
"saveRadioSettings": "Guardar ajustes de radio",
|
||||
"savePath": "Guardar ruta",
|
||||
"wipeDeviceData": "Borrar datos del dispositivo",
|
||||
"wipeDevice": "Borrar dispositivo",
|
||||
"destructiveDeviceActions": "Acciones destructivas del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Elija un preajuste o ajuste la configuración de radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Elija el nombre y la ubicación que comparte este dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Espacio disponible en este dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renombrar valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Vista previa en vivo",
|
||||
"refreshSchedule": "Programación de actualización",
|
||||
"noResponse": "Sin respuesta",
|
||||
"refreshing": "Actualizando",
|
||||
"unavailable": "No disponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Elija un relay o nodo para observar.",
|
||||
"publicKeyLabel": "Clave pública",
|
||||
"alreadyInContacts": "Ya en contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conéctese a un dispositivo antes de añadir contactos",
|
||||
"fromContacts": "De contactos",
|
||||
"onlineOnly": "Solo en línea",
|
||||
"inBoth": "En ambos",
|
||||
"source": "Fuente",
|
||||
"manualRouteEdit": "Edición manual de ruta",
|
||||
"observedMeshRoute": "Ruta mesh observada",
|
||||
"allMessagesCleared": "Todos los mensajes borrados",
|
||||
"onlineTraceDatabaseCleared": "Base de datos de trazas borrada",
|
||||
"packetLogsCleared": "Registros de paquetes borrados",
|
||||
"hexDataCopiedToClipboard": "Datos hex copiados al portapapeles",
|
||||
"developerModeEnabled": "Modo desarrollador activado",
|
||||
"developerModeDisabled": "Modo desarrollador desactivado",
|
||||
"clipboardIsEmpty": "El portapapeles está vacío",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Enlace de contacto copiado al portapapeles",
|
||||
"failedToExportContact": "Error al exportar el contacto",
|
||||
"noLogsToExport": "No hay registros para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recibido (RFC3339)",
|
||||
"buildTime": "Hora de compilación",
|
||||
"downloadUrlNotAvailable": "URL de descarga no disponible",
|
||||
"cannotOpenDownloadUrl": "No se puede abrir la URL de descarga",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La comprobación de actualizaciones solo está disponible en Android",
|
||||
"youAreRunningTheLatestVersion": "Está ejecutando la última versión",
|
||||
"updateAvailableButDownloadUrlNotFound": "Actualización disponible pero URL de descarga no encontrada",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe no disponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconocido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: esperando inicio",
|
||||
"acceptsShareLinks": "Acepta enlaces compartidos",
|
||||
"supportsRawHex": "Soporta hex sin procesar",
|
||||
"clipboardfriendly": "Compatible con portapapeles",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamaño",
|
||||
"noCustomChannelsToClear": "No hay canales personalizados que borrar.",
|
||||
"noDeviceContactsToClear": "No hay contactos del dispositivo que borrar."
|
||||
}
|
||||
|
||||
@@ -1641,5 +1641,252 @@
|
||||
"contactFailedToClearRoute": "Échec de l'effacement de la route : {error}",
|
||||
"contactRouteCleared": "Route effacée",
|
||||
"contactRouteSet": "Route définie : {route}",
|
||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}"
|
||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Délai ACK expiré",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Charge utile",
|
||||
"hops": "Sauts",
|
||||
"hashSize": "Taille du hash",
|
||||
"pathBytes": "Octets du chemin",
|
||||
"selectedPath": "Chemin sélectionné",
|
||||
"estimatedTx": "Émission estimée",
|
||||
"senderToReceipt": "Émetteur au reçu",
|
||||
"receivedCopies": "Copies reçues",
|
||||
"retryCause": "Cause de la relance",
|
||||
"retryMode": "Mode de relance",
|
||||
"retryResult": "Résultat de la relance",
|
||||
"lastRetry": "Dernière relance",
|
||||
"rxPackets": "Paquets RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Débit",
|
||||
"window": "Fenêtre",
|
||||
"posttxDelay": "Délai post-émission",
|
||||
"bandpass": "Passe-bande",
|
||||
"bandpassFilterVoice": "Filtre passe-bande voix",
|
||||
"frequency": "Fréquence",
|
||||
"australia": "Australie",
|
||||
"australiaNarrow": "Australie (Étroit)",
|
||||
"australiaQld": "Australie: QLD",
|
||||
"australiaSaWa": "Australie: SA, WA",
|
||||
"newZealand": "Nouvelle-Zélande",
|
||||
"newZealandNarrow": "Nouvelle-Zélande (Étroit)",
|
||||
"switzerland": "Suisse",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "République tchèque (Étroit)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longue Portée)",
|
||||
"euukDeprecated": "UE/RU (Obsolète)",
|
||||
"euukNarrow": "UE/RU (Étroit)",
|
||||
"usacanadaRecommended": "USA/Canada (Recommandé)",
|
||||
"vietnamDeprecated": "Vietnam (Obsolète)",
|
||||
"vietnamNarrow": "Vietnam (Étroit)",
|
||||
"active": "Actif",
|
||||
"addContact": "Ajouter un contact",
|
||||
"all": "Tout",
|
||||
"autoResolve": "Résolution automatique",
|
||||
"clearAllLabel": "Tout effacer",
|
||||
"clearRelays": "Effacer les relais",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"clearRoute": "Effacer la route",
|
||||
"clearMessages": "Effacer les messages",
|
||||
"clearScale": "Effacer l'échelle",
|
||||
"clearDiscoveries": "Effacer les découvertes",
|
||||
"clearOnlineTraceDatabase": "Effacer la base de traces",
|
||||
"clearAllChannels": "Effacer tous les canaux",
|
||||
"clearAllContacts": "Effacer tous les contacts",
|
||||
"clearChannels": "Effacer les canaux",
|
||||
"clearContacts": "Effacer les contacts",
|
||||
"clearPathOnMaxRetry": "Effacer le chemin au max de relances",
|
||||
"create": "Créer",
|
||||
"custom": "Personnalisé",
|
||||
"defaultValue": "Par défaut",
|
||||
"duplicate": "Dupliquer",
|
||||
"editName": "Modifier le nom",
|
||||
"open": "Ouvrir",
|
||||
"paste": "Coller",
|
||||
"preview": "Aperçu",
|
||||
"remove": "Supprimer",
|
||||
"rename": "Renommer",
|
||||
"resolveAll": "Résoudre tout",
|
||||
"send": "Envoyer",
|
||||
"sendAnyway": "Envoyer quand même",
|
||||
"share": "Partager",
|
||||
"shareContact": "Partager le contact",
|
||||
"trace": "Trace",
|
||||
"use": "Utiliser",
|
||||
"useSelectedFrequency": "Utiliser la fréquence sélectionnée",
|
||||
"discovery": "Découverte",
|
||||
"discoverRepeaters": "Découvrir les répéteurs",
|
||||
"discoverSensors": "Découvrir les capteurs",
|
||||
"repeaterDiscoverySent": "Découverte de répéteurs envoyée",
|
||||
"sensorDiscoverySent": "Découverte de capteurs envoyée",
|
||||
"clearedPendingDiscoveries": "Découvertes en attente effacées.",
|
||||
"autoDiscovery": "Découverte automatique",
|
||||
"enableAutomaticAdding": "Activer l'ajout automatique",
|
||||
"autoaddRepeaters": "Ajout auto des répéteurs",
|
||||
"autoaddRoomServers": "Ajout auto des serveurs de salle",
|
||||
"autoaddSensors": "Ajout auto des capteurs",
|
||||
"autoaddUsers": "Ajout auto des utilisateurs",
|
||||
"overwriteOldestWhenFull": "Écraser les plus anciens si plein",
|
||||
"storage": "Stockage",
|
||||
"dangerZone": "Zone de danger",
|
||||
"profiles": "Profils",
|
||||
"favourites": "Favoris",
|
||||
"sensors": "Capteurs",
|
||||
"others": "Autres",
|
||||
"gpsModule": "Module GPS",
|
||||
"liveTraffic": "Trafic en direct",
|
||||
"repeatersMap": "Carte des répéteurs",
|
||||
"spectrumScan": "Scan spectral",
|
||||
"blePacketLogs": "Journaux de paquets BLE",
|
||||
"onlineTraceDatabase": "Base de données de traces",
|
||||
"routePathByteSize": "Taille en octets du chemin",
|
||||
"messageNotifications": "Notifications de messages",
|
||||
"sarAlerts": "Alertes SAR",
|
||||
"discoveryNotifications": "Notifications de découverte",
|
||||
"updateNotifications": "Notifications de mise à jour",
|
||||
"muteWhileAppIsOpen": "Muet quand l'appli est ouverte",
|
||||
"disableContacts": "Désactiver les contacts",
|
||||
"enableSensorsTab": "Activer l'onglet Capteurs",
|
||||
"enableProfiles": "Activer les profils",
|
||||
"autoRouteRotation": "Rotation automatique de route",
|
||||
"nearestRepeaterFallback": "Répéteur le plus proche en repli",
|
||||
"deleteAllStoredMessageHistory": "Supprimer tout l'historique",
|
||||
"messageFontSize": "Taille de police des messages",
|
||||
"rotateMapWithHeading": "Tourner la carte avec le cap",
|
||||
"showMapDebugInfo": "Afficher les infos de débogage",
|
||||
"openMapInFullscreen": "Ouvrir la carte en plein écran",
|
||||
"showSarMarkersLabel": "Afficher les marqueurs SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Afficher les marqueurs SAR sur la carte",
|
||||
"showAllContactTrailsLabel": "Afficher toutes les traces de contacts",
|
||||
"hideRepeatersOnMap": "Masquer les répéteurs sur la carte",
|
||||
"setMapScale": "Définir l'échelle de la carte",
|
||||
"customMapScaleSaved": "Échelle de carte personnalisée enregistrée",
|
||||
"voiceBitrate": "Débit vocal",
|
||||
"voiceCompressor": "Compresseur vocal",
|
||||
"balancesQuietAndLoudSpeechLevels": "Équilibre les niveaux faibles et forts",
|
||||
"voiceLimiter": "Limiteur vocal",
|
||||
"preventsClippingPeaksBeforeEncoding": "Empêche l'écrêtage avant l'encodage",
|
||||
"micAutoGain": "Gain auto du micro",
|
||||
"letsTheRecorderAdjustInputLevel": "Laisse l'enregistreur ajuster le niveau d'entrée",
|
||||
"echoCancellation": "Annulation d'écho",
|
||||
"noiseSuppression": "Suppression du bruit",
|
||||
"trimSilenceInVoiceMessages": "Couper le silence dans les messages vocaux",
|
||||
"compressor": "Compresseur",
|
||||
"limiter": "Limiteur",
|
||||
"autoGain": "Gain automatique",
|
||||
"echoCancel": "Écho",
|
||||
"noiseSuppress": "Bruit",
|
||||
"silenceTrim": "Silence",
|
||||
"maxImageSize": "Taille d'image maximale",
|
||||
"imageCompression": "Compression d'image",
|
||||
"grayscale": "Niveaux de gris",
|
||||
"ultraMode": "Mode ultra",
|
||||
"fastPrivateGpsUpdates": "Mises à jour GPS privées rapides",
|
||||
"movementThreshold": "Seuil de mouvement",
|
||||
"fastGpsMovementThreshold": "Seuil de mouvement GPS rapide",
|
||||
"fastGpsActiveuseInterval": "Intervalle d'utilisation active GPS rapide",
|
||||
"activeuseUpdateInterval": "Intervalle de mise à jour en utilisation active",
|
||||
"repeatNearbyTraffic": "Répéter le trafic proche",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Relayer via les répéteurs du réseau",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Proximité seulement, sans inondation",
|
||||
"multihop": "Multi-saut",
|
||||
"createProfile": "Créer un profil",
|
||||
"renameProfile": "Renommer le profil",
|
||||
"newProfile": "Nouveau profil",
|
||||
"manageProfiles": "Gérer les profils",
|
||||
"enableProfilesToStartManagingThem": "Activez les profils pour commencer à les gérer.",
|
||||
"openMessage": "Ouvrir le message",
|
||||
"jumpToTheRelatedSarMessage": "Aller au message SAR associé",
|
||||
"removeSarMarker": "Supprimer le marqueur SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Sélectionnez une destination pour envoyer le marqueur",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marqueur SAR diffusé sur le canal public",
|
||||
"sarMarkerSentToRoom": "Marqueur SAR envoyé à la salle",
|
||||
"loadFromGallery": "Charger depuis la galerie",
|
||||
"replaceImage": "Remplacer l'image",
|
||||
"selectFromGallery": "Sélectionner depuis la galerie",
|
||||
"team": "Équipe",
|
||||
"found": "Trouvé",
|
||||
"staging": "Zone de regroupement",
|
||||
"object": "Objet",
|
||||
"quiet": "Calme",
|
||||
"moderate": "Modéré",
|
||||
"busy": "Occupé",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Le scan spectral n'a trouvé aucune fréquence candidate",
|
||||
"searchMessages": "Rechercher des messages",
|
||||
"sendImageFromGallery": "Envoyer une image de la galerie",
|
||||
"takePhoto": "Prendre une photo",
|
||||
"dmOnly": "Message direct uniquement",
|
||||
"allMessages": "Tous les messages",
|
||||
"sendToPublicChannel": "Envoyer au canal public ?",
|
||||
"selectMarkerTypeAndDestination": "Sélectionnez le type de marqueur et la destination",
|
||||
"noDestinationsAvailableLabel": "Aucune destination disponible",
|
||||
"image": "Image",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimensions",
|
||||
"segments": "Segments",
|
||||
"transfers": "Transferts",
|
||||
"downloadedBy": "Téléchargé par",
|
||||
"saveDiscoverySettings": "Enregistrer les paramètres de découverte",
|
||||
"savePublicInfo": "Enregistrer les infos publiques",
|
||||
"saveRadioSettings": "Enregistrer les paramètres radio",
|
||||
"savePath": "Enregistrer le chemin",
|
||||
"wipeDeviceData": "Effacer les données de l'appareil",
|
||||
"wipeDevice": "Effacer l'appareil",
|
||||
"destructiveDeviceActions": "Actions destructives de l'appareil.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Choisissez un préréglage ou ajustez les paramètres radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Choisissez le nom et l'emplacement partagés par cet appareil.",
|
||||
"availableSpaceOnThisDevice": "Espace disponible sur cet appareil.",
|
||||
"used": "Utilisé",
|
||||
"total": "Total",
|
||||
"renameValue": "Renommer la valeur",
|
||||
"customizeFields": "Personnaliser les champs",
|
||||
"livePreview": "Aperçu en direct",
|
||||
"refreshSchedule": "Planification d'actualisation",
|
||||
"noResponse": "Pas de réponse",
|
||||
"refreshing": "Actualisation",
|
||||
"unavailable": "Indisponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Choisissez un relais ou nœud à surveiller.",
|
||||
"publicKeyLabel": "Clé publique",
|
||||
"alreadyInContacts": "Déjà dans les contacts",
|
||||
"connectToADeviceBeforeAddingContacts": "Connectez-vous à un appareil avant d'ajouter des contacts",
|
||||
"fromContacts": "Des contacts",
|
||||
"onlineOnly": "En ligne uniquement",
|
||||
"inBoth": "Dans les deux",
|
||||
"source": "Source",
|
||||
"manualRouteEdit": "Édition manuelle de route",
|
||||
"observedMeshRoute": "Route mesh observée",
|
||||
"allMessagesCleared": "Tous les messages effacés",
|
||||
"onlineTraceDatabaseCleared": "Base de données de traces effacée",
|
||||
"packetLogsCleared": "Journaux de paquets effacés",
|
||||
"hexDataCopiedToClipboard": "Données hex copiées dans le presse-papiers",
|
||||
"developerModeEnabled": "Mode développeur activé",
|
||||
"developerModeDisabled": "Mode développeur désactivé",
|
||||
"clipboardIsEmpty": "Le presse-papiers est vide",
|
||||
"contactImported": "Contact importé",
|
||||
"contactLinkCopiedToClipboard": "Lien du contact copié dans le presse-papiers",
|
||||
"failedToExportContact": "Échec de l'exportation du contact",
|
||||
"noLogsToExport": "Aucun journal à exporter",
|
||||
"exportAsCsv": "Exporter en CSV",
|
||||
"exportAsText": "Exporter en texte",
|
||||
"receivedRfc3339": "Reçu (RFC3339)",
|
||||
"buildTime": "Heure de build",
|
||||
"downloadUrlNotAvailable": "URL de téléchargement non disponible",
|
||||
"cannotOpenDownloadUrl": "Impossible d'ouvrir l'URL de téléchargement",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La vérification des mises à jour n'est disponible que sur Android",
|
||||
"youAreRunningTheLatestVersion": "Vous utilisez la dernière version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Mise à jour disponible mais URL de téléchargement introuvable",
|
||||
"startTictactoe": "Démarrer Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: adversaire inconnu",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: en attente du début",
|
||||
"acceptsShareLinks": "Accepte les liens partagés",
|
||||
"supportsRawHex": "Supporte le hex brut",
|
||||
"clipboardfriendly": "Compatible avec le presse-papiers",
|
||||
"captured": "Capturé",
|
||||
"size": "Taille",
|
||||
"noCustomChannelsToClear": "Aucun canal personnalisé à effacer.",
|
||||
"noDeviceContactsToClear": "Aucun contact de l'appareil à effacer."
|
||||
}
|
||||
|
||||
@@ -563,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Brisanje rute nije uspjelo: {error}",
|
||||
"contactRouteCleared": "Ruta obrisana",
|
||||
"contactRouteSet": "Ruta postavljena: {route}",
|
||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}"
|
||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK istek vremena",
|
||||
"opcode": "Opkod",
|
||||
"payload": "Sadržaj",
|
||||
"hops": "Skokovi",
|
||||
"hashSize": "Veličina hash-a",
|
||||
"pathBytes": "Bajtovi putanje",
|
||||
"selectedPath": "Odabrana putanja",
|
||||
"estimatedTx": "Procijenjeno slanje",
|
||||
"senderToReceipt": "Od pošiljatelja do primitka",
|
||||
"receivedCopies": "Primljene kopije",
|
||||
"retryCause": "Razlog ponovnog pokušaja",
|
||||
"retryMode": "Način ponovnog pokušaja",
|
||||
"retryResult": "Rezultat ponovnog pokušaja",
|
||||
"lastRetry": "Zadnji pokušaj",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Brzina",
|
||||
"window": "Prozor",
|
||||
"posttxDelay": "Kašnjenje nakon slanja",
|
||||
"bandpass": "Pojasni filtar",
|
||||
"bandpassFilterVoice": "Pojasni filtar glasa",
|
||||
"frequency": "Frekvencija",
|
||||
"australia": "Australija",
|
||||
"australiaNarrow": "Australija (usko)",
|
||||
"australiaQld": "Australija: QLD",
|
||||
"australiaSaWa": "Australija: SA, WA",
|
||||
"newZealand": "Novi Zeland",
|
||||
"newZealandNarrow": "Novi Zeland (usko)",
|
||||
"switzerland": "Švicarska",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Češka (usko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (veliki domet)",
|
||||
"euukDeprecated": "EU/UK (zastarjelo)",
|
||||
"euukNarrow": "EU/UK (usko)",
|
||||
"usacanadaRecommended": "SAD/Kanada (preporučeno)",
|
||||
"vietnamDeprecated": "Vijetnam (zastarjelo)",
|
||||
"vietnamNarrow": "Vijetnam (usko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Sve",
|
||||
"autoResolve": "Automatski razriješi",
|
||||
"clearAllLabel": "Očisti sve",
|
||||
"clearRelays": "Očisti posrednike",
|
||||
"clearFilters": "Očisti filtre",
|
||||
"clearRoute": "Očisti putanju",
|
||||
"clearMessages": "Očisti poruke",
|
||||
"clearScale": "Očisti mjerilo",
|
||||
"clearDiscoveries": "Očisti otkrića",
|
||||
"clearOnlineTraceDatabase": "Očisti bazu mrežnih tragova",
|
||||
"clearAllChannels": "Očisti sve kanale",
|
||||
"clearAllContacts": "Očisti sve kontakte",
|
||||
"clearChannels": "Očisti kanale",
|
||||
"clearContacts": "Očisti kontakte",
|
||||
"clearPathOnMaxRetry": "Očisti putanju kod maks. pokušaja",
|
||||
"create": "Stvori",
|
||||
"custom": "Prilagođeno",
|
||||
"defaultValue": "Zadano",
|
||||
"duplicate": "Dupliciraj",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Otvori",
|
||||
"paste": "Zalijepi",
|
||||
"preview": "Pregled",
|
||||
"remove": "Ukloni",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razriješi sve",
|
||||
"send": "Pošalji",
|
||||
"sendAnyway": "Svejedno pošalji",
|
||||
"share": "Dijeli",
|
||||
"shareContact": "Dijeli kontakt",
|
||||
"trace": "Praćenje",
|
||||
"use": "Koristi",
|
||||
"useSelectedFrequency": "Koristi odabranu frekvenciju",
|
||||
"discovery": "Otkrivanje",
|
||||
"discoverRepeaters": "Otkrij posrednike",
|
||||
"discoverSensors": "Otkrij senzore",
|
||||
"repeaterDiscoverySent": "Otkrivanje posrednika poslano",
|
||||
"sensorDiscoverySent": "Otkrivanje senzora poslano",
|
||||
"clearedPendingDiscoveries": "Očišćena čekajuća otkrića.",
|
||||
"autoDiscovery": "Automatsko otkrivanje",
|
||||
"enableAutomaticAdding": "Omogući automatsko dodavanje",
|
||||
"autoaddRepeaters": "Automatski dodaj posrednike",
|
||||
"autoaddRoomServers": "Automatski dodaj poslužitelje soba",
|
||||
"autoaddSensors": "Automatski dodaj senzore",
|
||||
"autoaddUsers": "Automatski dodaj korisnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarije kad je puno",
|
||||
"storage": "Pohrana",
|
||||
"dangerZone": "Opasna zona",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Favoriti",
|
||||
"sensors": "Senzori",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet uživo",
|
||||
"repeatersMap": "Karta posrednika",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "BLE zapisi paketa",
|
||||
"onlineTraceDatabase": "Baza mrežnih tragova",
|
||||
"routePathByteSize": "Veličina putanje u bajtima",
|
||||
"messageNotifications": "Obavijesti o porukama",
|
||||
"sarAlerts": "SAR upozorenja",
|
||||
"discoveryNotifications": "Obavijesti o otkrivanju",
|
||||
"updateNotifications": "Obavijesti o ažuriranju",
|
||||
"muteWhileAppIsOpen": "Utišaj dok je aplikacija otvorena",
|
||||
"disableContacts": "Onemogući kontakte",
|
||||
"enableSensorsTab": "Omogući karticu Senzori",
|
||||
"enableProfiles": "Omogući profile",
|
||||
"autoRouteRotation": "Automatsko rotiranje putanje",
|
||||
"nearestRepeaterFallback": "Najbliži posrednik kao rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši svu pohranjenu povijest poruka",
|
||||
"messageFontSize": "Veličina fonta poruka",
|
||||
"rotateMapWithHeading": "Rotiraj kartu prema smjeru",
|
||||
"showMapDebugInfo": "Prikaži debug info karte",
|
||||
"openMapInFullscreen": "Otvori kartu na cijelom zaslonu",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnoj karti",
|
||||
"showAllContactTrailsLabel": "Prikaži sve tragove kontakata",
|
||||
"hideRepeatersOnMap": "Sakrij posrednike na karti",
|
||||
"setMapScale": "Postavi mjerilo karte",
|
||||
"customMapScaleSaved": "Prilagođeno mjerilo karte spremljeno",
|
||||
"voiceBitrate": "Bitna brzina glasa",
|
||||
"voiceCompressor": "Kompresor glasa",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe i glasne razine govora",
|
||||
"voiceLimiter": "Ograničivač glasa",
|
||||
"preventsClippingPeaksBeforeEncoding": "Sprječava rezanje vrhova prije kodiranja",
|
||||
"micAutoGain": "Automatsko pojačanje mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Dopušta snimaču prilagodbu razine ulaza",
|
||||
"echoCancellation": "Poništavanje odjeka",
|
||||
"noiseSuppression": "Potiskivanje šuma",
|
||||
"trimSilenceInVoiceMessages": "Izreži tišinu u glasovnim porukama",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Ograničivač",
|
||||
"autoGain": "Automatsko pojačanje",
|
||||
"echoCancel": "Odjek",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Maksimalna veličina slike",
|
||||
"imageCompression": "Kompresija slike",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Brza privatna GPS ažuriranja",
|
||||
"movementThreshold": "Prag kretanja",
|
||||
"fastGpsMovementThreshold": "Prag kretanja za brzi GPS",
|
||||
"fastGpsActiveuseInterval": "Interval aktivnog korištenja brzog GPS-a",
|
||||
"activeuseUpdateInterval": "Interval ažuriranja pri aktivnom korištenju",
|
||||
"repeatNearbyTraffic": "Ponovi obližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Proslijedi kroz posrednike kroz mrežu",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo obližnji, bez preplavljivanja posrednika",
|
||||
"multihop": "Višeskočno",
|
||||
"createProfile": "Stvori profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Novi profil",
|
||||
"manageProfiles": "Upravljaj profilima",
|
||||
"enableProfilesToStartManagingThem": "Omogućite profile da biste ih počeli upravljati.",
|
||||
"openMessage": "Otvori poruku",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezanu SAR poruku",
|
||||
"removeSarMarker": "Ukloni SAR oznaku",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Odaberite odredište za slanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka emitirana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana u sobu",
|
||||
"loadFromGallery": "Učitaj iz galerije",
|
||||
"replaceImage": "Zamijeni sliku",
|
||||
"selectFromGallery": "Odaberi iz galerije",
|
||||
"team": "Tim",
|
||||
"found": "Pronađeno",
|
||||
"staging": "Sabirno mjesto",
|
||||
"object": "Objekt",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Umjereno",
|
||||
"busy": "Zauzeto",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje nije pronašlo prikladne frekvencije",
|
||||
"searchMessages": "Pretraži poruke",
|
||||
"sendImageFromGallery": "Pošalji sliku iz galerije",
|
||||
"takePhoto": "Snimi fotografiju",
|
||||
"dmOnly": "Samo izravno",
|
||||
"allMessages": "Sve poruke",
|
||||
"sendToPublicChannel": "Pošalji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Odaberite vrstu oznake i odredište",
|
||||
"noDestinationsAvailableLabel": "Nema dostupnih odredišta",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prijenosi",
|
||||
"downloadedBy": "Preuzeo",
|
||||
"saveDiscoverySettings": "Spremi postavke otkrivanja",
|
||||
"savePublicInfo": "Spremi javne podatke",
|
||||
"saveRadioSettings": "Spremi radijske postavke",
|
||||
"savePath": "Spremi putanju",
|
||||
"wipeDeviceData": "Obriši podatke uređaja",
|
||||
"wipeDevice": "Obriši uređaj",
|
||||
"destructiveDeviceActions": "Destruktivne radnje na uređaju.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Odaberite predefiniranu postavku ili fino podesite radijske postavke.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Odaberite ime i lokaciju koju ovaj uređaj dijeli.",
|
||||
"availableSpaceOnThisDevice": "Raspoloživi prostor na ovom uređaju.",
|
||||
"used": "Korišteno",
|
||||
"total": "Ukupno",
|
||||
"renameValue": "Preimenuj vrijednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Pregled uživo",
|
||||
"refreshSchedule": "Raspored osvježavanja",
|
||||
"noResponse": "Nema odgovora",
|
||||
"refreshing": "Osvježavam",
|
||||
"unavailable": "Nedostupno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Odaberite posrednika ili čvor za praćenje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Već u kontaktima",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se s uređajem prije dodavanja kontakata",
|
||||
"fromContacts": "Iz kontakata",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "U oba",
|
||||
"source": "Izvor",
|
||||
"manualRouteEdit": "Ručno uređivanje putanje",
|
||||
"observedMeshRoute": "Opažena mrežna putanja",
|
||||
"allMessagesCleared": "Sve poruke očišćene",
|
||||
"onlineTraceDatabaseCleared": "Baza mrežnih tragova očišćena",
|
||||
"packetLogsCleared": "Zapisi paketa očišćeni",
|
||||
"hexDataCopiedToClipboard": "Hex podaci kopirani u međuspremnik",
|
||||
"developerModeEnabled": "Razvojni način omogućen",
|
||||
"developerModeDisabled": "Razvojni način onemogućen",
|
||||
"clipboardIsEmpty": "Međuspremnik je prazan",
|
||||
"contactImported": "Kontakt uvezen",
|
||||
"contactLinkCopiedToClipboard": "Poveznica na kontakt kopirana u međuspremnik",
|
||||
"failedToExportContact": "Izvoz kontakta nije uspio",
|
||||
"noLogsToExport": "Nema zapisa za izvoz",
|
||||
"exportAsCsv": "Izvezi kao CSV",
|
||||
"exportAsText": "Izvezi kao tekst",
|
||||
"receivedRfc3339": "Primljeno (RFC3339)",
|
||||
"buildTime": "Vrijeme gradnje",
|
||||
"downloadUrlNotAvailable": "URL za preuzimanje nije dostupan",
|
||||
"cannotOpenDownloadUrl": "Ne mogu otvoriti URL za preuzimanje",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Provjera ažuriranja dostupna je samo na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Koristite najnoviju verziju",
|
||||
"updateAvailableButDownloadUrlNotFound": "Ažuriranje dostupno, ali URL za preuzimanje nije pronađen",
|
||||
"startTictactoe": "Pokreni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nije dostupan",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: protivnik nepoznat",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čekanje na početak",
|
||||
"acceptsShareLinks": "Prihvaća dijeljene veze",
|
||||
"supportsRawHex": "Podržava sirovi hex",
|
||||
"clipboardfriendly": "Pogodno za međuspremnik",
|
||||
"captured": "Zabilježeno",
|
||||
"size": "Veličina",
|
||||
"noCustomChannelsToClear": "Nema prilagođenih kanala za brisanje.",
|
||||
"noDeviceContactsToClear": "Nema kontakata uređaja za brisanje."
|
||||
}
|
||||
|
||||
@@ -1641,5 +1641,252 @@
|
||||
"contactFailedToClearRoute": "Impossibile cancellare il percorso: {error}",
|
||||
"contactRouteCleared": "Percorso cancellato",
|
||||
"contactRouteSet": "Percorso impostato: {route}",
|
||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}"
|
||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Timeout ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Payload",
|
||||
"hops": "Hop",
|
||||
"hashSize": "Dimensione hash",
|
||||
"pathBytes": "Byte percorso",
|
||||
"selectedPath": "Percorso selezionato",
|
||||
"estimatedTx": "Trasmissione stimata",
|
||||
"senderToReceipt": "Mittente a ricevuta",
|
||||
"receivedCopies": "Copie ricevute",
|
||||
"retryCause": "Causa del ritentativo",
|
||||
"retryMode": "Modalità ritentativo",
|
||||
"retryResult": "Risultato ritentativo",
|
||||
"lastRetry": "Ultimo ritentativo",
|
||||
"rxPackets": "Pacchetti RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocità",
|
||||
"window": "Finestra",
|
||||
"posttxDelay": "Ritardo post-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda voce",
|
||||
"frequency": "Frequenza",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Stretto)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nuova Zelanda",
|
||||
"newZealandNarrow": "Nuova Zelanda (Stretto)",
|
||||
"switzerland": "Svizzera",
|
||||
"portugal433": "Portogallo 433",
|
||||
"portugal868": "Portogallo 868",
|
||||
"czechRepublicNarrow": "Repubblica Ceca (Stretto)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Lungo Raggio)",
|
||||
"euukDeprecated": "UE/UK (Deprecato)",
|
||||
"euukNarrow": "UE/UK (Stretto)",
|
||||
"usacanadaRecommended": "USA/Canada (Consigliato)",
|
||||
"vietnamDeprecated": "Vietnam (Deprecato)",
|
||||
"vietnamNarrow": "Vietnam (Stretto)",
|
||||
"active": "Attivo",
|
||||
"addContact": "Aggiungi contatto",
|
||||
"all": "Tutto",
|
||||
"autoResolve": "Risolvi automaticamente",
|
||||
"clearAllLabel": "Cancella tutto",
|
||||
"clearRelays": "Cancella relay",
|
||||
"clearFilters": "Cancella filtri",
|
||||
"clearRoute": "Cancella percorso",
|
||||
"clearMessages": "Cancella messaggi",
|
||||
"clearScale": "Cancella scala",
|
||||
"clearDiscoveries": "Cancella scoperte",
|
||||
"clearOnlineTraceDatabase": "Cancella database tracce",
|
||||
"clearAllChannels": "Cancella tutti i canali",
|
||||
"clearAllContacts": "Cancella tutti i contatti",
|
||||
"clearChannels": "Cancella canali",
|
||||
"clearContacts": "Cancella contatti",
|
||||
"clearPathOnMaxRetry": "Cancella percorso al max ritentativo",
|
||||
"create": "Crea",
|
||||
"custom": "Personalizzato",
|
||||
"defaultValue": "Predefinito",
|
||||
"duplicate": "Duplica",
|
||||
"editName": "Modifica nome",
|
||||
"open": "Apri",
|
||||
"paste": "Incolla",
|
||||
"preview": "Anteprima",
|
||||
"remove": "Rimuovi",
|
||||
"rename": "Rinomina",
|
||||
"resolveAll": "Risolvi tutto",
|
||||
"send": "Invia",
|
||||
"sendAnyway": "Invia comunque",
|
||||
"share": "Condividi",
|
||||
"shareContact": "Condividi contatto",
|
||||
"trace": "Traccia",
|
||||
"use": "Usa",
|
||||
"useSelectedFrequency": "Usa frequenza selezionata",
|
||||
"discovery": "Scoperta",
|
||||
"discoverRepeaters": "Scopri ripetitori",
|
||||
"discoverSensors": "Scopri sensori",
|
||||
"repeaterDiscoverySent": "Scoperta ripetitori inviata",
|
||||
"sensorDiscoverySent": "Scoperta sensori inviata",
|
||||
"clearedPendingDiscoveries": "Scoperte in attesa cancellate.",
|
||||
"autoDiscovery": "Scoperta automatica",
|
||||
"enableAutomaticAdding": "Abilita aggiunta automatica",
|
||||
"autoaddRepeaters": "Aggiungi ripetitori automaticamente",
|
||||
"autoaddRoomServers": "Aggiungi server stanze automaticamente",
|
||||
"autoaddSensors": "Aggiungi sensori automaticamente",
|
||||
"autoaddUsers": "Aggiungi utenti automaticamente",
|
||||
"overwriteOldestWhenFull": "Sovrascrivi i più vecchi quando pieno",
|
||||
"storage": "Archiviazione",
|
||||
"dangerZone": "Zona pericolosa",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Preferiti",
|
||||
"sensors": "Sensori",
|
||||
"others": "Altri",
|
||||
"gpsModule": "Modulo GPS",
|
||||
"liveTraffic": "Traffico in tempo reale",
|
||||
"repeatersMap": "Mappa ripetitori",
|
||||
"spectrumScan": "Scansione spettro",
|
||||
"blePacketLogs": "Log pacchetti BLE",
|
||||
"onlineTraceDatabase": "Database tracce",
|
||||
"routePathByteSize": "Dimensione percorso in byte",
|
||||
"messageNotifications": "Notifiche messaggi",
|
||||
"sarAlerts": "Avvisi SAR",
|
||||
"discoveryNotifications": "Notifiche scoperte",
|
||||
"updateNotifications": "Notifiche aggiornamenti",
|
||||
"muteWhileAppIsOpen": "Silenzia con l'app aperta",
|
||||
"disableContacts": "Disabilita contatti",
|
||||
"enableSensorsTab": "Abilita scheda Sensori",
|
||||
"enableProfiles": "Abilita profili",
|
||||
"autoRouteRotation": "Rotazione automatica percorso",
|
||||
"nearestRepeaterFallback": "Ripetitore più vicino come fallback",
|
||||
"deleteAllStoredMessageHistory": "Elimina tutta la cronologia messaggi",
|
||||
"messageFontSize": "Dimensione carattere messaggi",
|
||||
"rotateMapWithHeading": "Ruota mappa con direzione",
|
||||
"showMapDebugInfo": "Mostra info debug mappa",
|
||||
"openMapInFullscreen": "Apri mappa a schermo intero",
|
||||
"showSarMarkersLabel": "Mostra marcatori SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostra marcatori SAR sulla mappa principale",
|
||||
"showAllContactTrailsLabel": "Mostra tutti i percorsi contatti",
|
||||
"hideRepeatersOnMap": "Nascondi ripetitori sulla mappa",
|
||||
"setMapScale": "Imposta scala mappa",
|
||||
"customMapScaleSaved": "Scala mappa personalizzata salvata",
|
||||
"voiceBitrate": "Bitrate voce",
|
||||
"voiceCompressor": "Compressore voce",
|
||||
"balancesQuietAndLoudSpeechLevels": "Bilancia i livelli di voce bassi e alti",
|
||||
"voiceLimiter": "Limitatore voce",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene il clipping prima della codifica",
|
||||
"micAutoGain": "Guadagno automatico microfono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permette al registratore di regolare il livello",
|
||||
"echoCancellation": "Cancellazione eco",
|
||||
"noiseSuppression": "Soppressione rumore",
|
||||
"trimSilenceInVoiceMessages": "Taglia silenzio nei messaggi vocali",
|
||||
"compressor": "Compressore",
|
||||
"limiter": "Limitatore",
|
||||
"autoGain": "Guadagno automatico",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Rumore",
|
||||
"silenceTrim": "Silenzio",
|
||||
"maxImageSize": "Dimensione massima immagine",
|
||||
"imageCompression": "Compressione immagine",
|
||||
"grayscale": "Scala di grigi",
|
||||
"ultraMode": "Modalità ultra",
|
||||
"fastPrivateGpsUpdates": "Aggiornamenti GPS privati veloci",
|
||||
"movementThreshold": "Soglia di movimento",
|
||||
"fastGpsMovementThreshold": "Soglia movimento GPS veloce",
|
||||
"fastGpsActiveuseInterval": "Intervallo uso attivo GPS veloce",
|
||||
"activeuseUpdateInterval": "Intervallo aggiornamento uso attivo",
|
||||
"repeatNearbyTraffic": "Ripeti traffico vicino",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Inoltra tramite ripetitori nella rete",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo vicini, senza flood dei ripetitori",
|
||||
"multihop": "Multi-hop",
|
||||
"createProfile": "Crea profilo",
|
||||
"renameProfile": "Rinomina profilo",
|
||||
"newProfile": "Nuovo profilo",
|
||||
"manageProfiles": "Gestisci profili",
|
||||
"enableProfilesToStartManagingThem": "Abilita i profili per iniziare a gestirli.",
|
||||
"openMessage": "Apri messaggio",
|
||||
"jumpToTheRelatedSarMessage": "Vai al messaggio SAR correlato",
|
||||
"removeSarMarker": "Rimuovi marcatore SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleziona una destinazione per inviare il marcatore SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcatore SAR trasmesso al canale pubblico",
|
||||
"sarMarkerSentToRoom": "Marcatore SAR inviato alla stanza",
|
||||
"loadFromGallery": "Carica dalla galleria",
|
||||
"replaceImage": "Sostituisci immagine",
|
||||
"selectFromGallery": "Seleziona dalla galleria",
|
||||
"team": "Squadra",
|
||||
"found": "Trovato",
|
||||
"staging": "Area di raccolta",
|
||||
"object": "Oggetto",
|
||||
"quiet": "Silenzioso",
|
||||
"moderate": "Moderato",
|
||||
"busy": "Occupato",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "La scansione spettro non ha trovato frequenze candidate",
|
||||
"searchMessages": "Cerca messaggi",
|
||||
"sendImageFromGallery": "Invia immagine dalla galleria",
|
||||
"takePhoto": "Scatta foto",
|
||||
"dmOnly": "Solo messaggio diretto",
|
||||
"allMessages": "Tutti i messaggi",
|
||||
"sendToPublicChannel": "Inviare al canale pubblico?",
|
||||
"selectMarkerTypeAndDestination": "Seleziona tipo marcatore e destinazione",
|
||||
"noDestinationsAvailableLabel": "Nessuna destinazione disponibile",
|
||||
"image": "Immagine",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensioni",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Trasferimenti",
|
||||
"downloadedBy": "Scaricato da",
|
||||
"saveDiscoverySettings": "Salva impostazioni scoperta",
|
||||
"savePublicInfo": "Salva info pubbliche",
|
||||
"saveRadioSettings": "Salva impostazioni radio",
|
||||
"savePath": "Salva percorso",
|
||||
"wipeDeviceData": "Cancella dati dispositivo",
|
||||
"wipeDevice": "Cancella dispositivo",
|
||||
"destructiveDeviceActions": "Azioni distruttive del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Scegli un preset o personalizza le impostazioni radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Scegli il nome e la posizione condivisi da questo dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Spazio disponibile su questo dispositivo.",
|
||||
"used": "Usato",
|
||||
"total": "Totale",
|
||||
"renameValue": "Rinomina valore",
|
||||
"customizeFields": "Personalizza campi",
|
||||
"livePreview": "Anteprima dal vivo",
|
||||
"refreshSchedule": "Programma aggiornamento",
|
||||
"noResponse": "Nessuna risposta",
|
||||
"refreshing": "Aggiornamento",
|
||||
"unavailable": "Non disponibile",
|
||||
"pickARelayOrNodeToWatchInSensors": "Scegli un relay o nodo da monitorare.",
|
||||
"publicKeyLabel": "Chiave pubblica",
|
||||
"alreadyInContacts": "Già nei contatti",
|
||||
"connectToADeviceBeforeAddingContacts": "Connettiti a un dispositivo prima di aggiungere contatti",
|
||||
"fromContacts": "Dai contatti",
|
||||
"onlineOnly": "Solo online",
|
||||
"inBoth": "In entrambi",
|
||||
"source": "Origine",
|
||||
"manualRouteEdit": "Modifica manuale percorso",
|
||||
"observedMeshRoute": "Percorso mesh osservato",
|
||||
"allMessagesCleared": "Tutti i messaggi cancellati",
|
||||
"onlineTraceDatabaseCleared": "Database tracce cancellata",
|
||||
"packetLogsCleared": "Log pacchetti cancellati",
|
||||
"hexDataCopiedToClipboard": "Dati hex copiati negli appunti",
|
||||
"developerModeEnabled": "Modalità sviluppatore attivata",
|
||||
"developerModeDisabled": "Modalità sviluppatore disattivata",
|
||||
"clipboardIsEmpty": "Gli appunti sono vuoti",
|
||||
"contactImported": "Contatto importato",
|
||||
"contactLinkCopiedToClipboard": "Link contatto copiato negli appunti",
|
||||
"failedToExportContact": "Esportazione contatto fallita",
|
||||
"noLogsToExport": "Nessun log da esportare",
|
||||
"exportAsCsv": "Esporta come CSV",
|
||||
"exportAsText": "Esporta come testo",
|
||||
"receivedRfc3339": "Ricevuto (RFC3339)",
|
||||
"buildTime": "Ora build",
|
||||
"downloadUrlNotAvailable": "URL di download non disponibile",
|
||||
"cannotOpenDownloadUrl": "Impossibile aprire URL di download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Il controllo aggiornamenti è disponibile solo su Android",
|
||||
"youAreRunningTheLatestVersion": "Stai utilizzando l'ultima versione",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aggiornamento disponibile ma URL di download non trovato",
|
||||
"startTictactoe": "Avvia Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe non disponibile",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: avversario sconosciuto",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: in attesa dell'inizio",
|
||||
"acceptsShareLinks": "Accetta link condivisi",
|
||||
"supportsRawHex": "Supporta hex grezzo",
|
||||
"clipboardfriendly": "Compatibile con appunti",
|
||||
"captured": "Catturato",
|
||||
"size": "Dimensione",
|
||||
"noCustomChannelsToClear": "Nessun canale personalizzato da cancellare.",
|
||||
"noDeviceContactsToClear": "Nessun contatto dispositivo da cancellare."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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 => 'Δεν υπάρχουν επαφές συσκευής.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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 => 'Нет контактов устройства для очистки.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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 => 'Aç';
|
||||
|
||||
@override
|
||||
String get paste => 'Yapıştır';
|
||||
|
||||
@override
|
||||
String get preview => 'Önizleme';
|
||||
|
||||
@override
|
||||
String get remove => 'Kaldır';
|
||||
|
||||
@override
|
||||
String get rename => 'Yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Tümünü çöz';
|
||||
|
||||
@override
|
||||
String get send => 'Gönder';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Yine de gönder';
|
||||
|
||||
@override
|
||||
String get share => 'Paylaş';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Kişiyi paylaş';
|
||||
|
||||
@override
|
||||
String get trace => 'İz';
|
||||
|
||||
@override
|
||||
String get use => 'Kullan';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Seçilen frekansı kullan';
|
||||
|
||||
@override
|
||||
String get discovery => 'Keşif';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Tekrarlayıcıları keşfet';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Sensörleri keşfet';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Tekrarlayıcı keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensör keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Bekleyen keşifler temizlendi.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Otomatik keşif';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Otomatik eklemeyi etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Tekrarlayıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Oda sunucularını otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Sensörleri otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Kullanıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Dolduğunda en eskiyi üzerine yaz';
|
||||
|
||||
@override
|
||||
String get storage => 'Depolama';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Tehlikeli bölge';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profiller';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriler';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensörler';
|
||||
|
||||
@override
|
||||
String get others => 'Diğerleri';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS Modülü';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Canlı trafik';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Tekrarlayıcılar haritası';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektrum taraması';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE paket günlükleri';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Çevrimiçi iz veritabanı';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Yol bayt boyutu';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Mesaj bildirimleri';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR uyarıları';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Keşif bildirimleri';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Güncelleme bildirimleri';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Uygulama açıkken sessize al';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Kişileri devre dışı bırak';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Sensörler sekmesini etkinleştir';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Profilleri etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Otomatik rota rotasyonu';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'En yakın tekrarlayıcıya geri dönüş';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Tüm mesaj geçmişini sil';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Mesaj yazı tipi boyutu';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Haritayı yön ile döndür';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Harita hata ayıklama bilgisini göster';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Haritayı tam ekranda aç';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'SAR işaretçilerini göster';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'SAR işaretçilerini ana haritada göster';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Tüm kişi izlerini göster';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Haritada tekrarlayıcıları gizle';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Harita ölçeğini ayarla';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Özel harita ölçeği kaydedildi';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Ses bit hızı';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Ses sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Sessiz ve yüksek konuşma seviyelerini dengeler';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Ses sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Kodlama öncesi tepe kırpmasını önler';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mikrofon otomatik kazancı';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Kaydedicinin giriş seviyesini ayarlamasına izin verir';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Yankı iptali';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Gürültü bastırma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Sesli mesajlarda sessizliği kırp';
|
||||
|
||||
@override
|
||||
String get compressor => 'Sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get limiter => 'Sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Otomatik kazanç';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Yankı';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Gürültü';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Sessizlik';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksimum resim boyutu';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Resim sıkıştırma';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Gri tonlama';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra mod';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Hızlı özel GPS güncellemeleri';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Hızlı GPS hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Hızlı GPS aktif kullanım aralığı';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Aktif kullanım güncelleme aralığı';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Yakın trafiği tekrarla';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Yalnızca yakın, tekrarlayıcı taşkını olmadan';
|
||||
|
||||
@override
|
||||
String get multihop => 'Çok atlamalı';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Profil oluştur';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Profili yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Yeni profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Profilleri yönet';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Profilleri yönetmeye başlamak için etkinleştirin.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Mesajı aç';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'İlgili SAR mesajına git';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'SAR işaretçisini kaldır';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'SAR işaretçisi göndermek için bir hedef seçin';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR işaretçisi genel kanala yayınlandı';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR işaretçisi odaya gönderildi';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Galeriden yükle';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Resmi değiştir';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Galeriden seç';
|
||||
|
||||
@override
|
||||
String get team => 'Takım';
|
||||
|
||||
@override
|
||||
String get found => 'Bulundu';
|
||||
|
||||
@override
|
||||
String get staging => 'Toplanma alanı';
|
||||
|
||||
@override
|
||||
String get object => 'Nesne';
|
||||
|
||||
@override
|
||||
String get quiet => 'Sessiz';
|
||||
|
||||
@override
|
||||
String get moderate => 'Orta';
|
||||
|
||||
@override
|
||||
String get busy => 'Meşgul';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektrum taraması aday frekans bulamadı';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Mesajlarda ara';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Galeriden resim gönder';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Fotoğraf çek';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Yalnızca DM';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tüm mesajlar';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Genel kanala gönderilsin mi?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'İşaretçi türünü ve hedefi seçin';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Kullanılabilir hedef yok';
|
||||
|
||||
@override
|
||||
String get image => 'Resim';
|
||||
|
||||
@override
|
||||
String get format => 'Biçim';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Boyutlar';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentler';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferler';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'İndiren';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Keşif ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Genel bilgileri kaydet';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Radyo ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePath => 'Yolu kaydet';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Cihaz verilerini sil';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Cihazı sil';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Yıkıcı cihaz işlemleri.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Bu cihazın paylaştığı ad ve konumu seçin.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => 'Bu cihazda kullanılabilir alan.';
|
||||
|
||||
@override
|
||||
String get used => 'Kullanılan';
|
||||
|
||||
@override
|
||||
String get total => 'Toplam';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Değeri yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Alanları özelleştir';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Canlı önizleme';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Yenileme programı';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Yanıt yok';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Yenileniyor';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Kullanılamaz';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'İzlemek için bir röle veya düğüm seçin.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Genel anahtar';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Zaten kişilerde';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Kişi eklemeden önce bir cihaza bağlanın';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Kişilerden';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Yalnızca çevrimiçi';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Her ikisinde';
|
||||
|
||||
@override
|
||||
String get source => 'Kaynak';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manuel rota düzenle';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Gözlemlenen mesh rotası';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tüm mesajlar temizlendi';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Çevrimiçi iz veritabanı temizlendi';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Paket günlükleri temizlendi';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex verisi panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Geliştirici modu etkinleştirildi';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Geliştirici modu devre dışı bırakıldı';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Pano boş';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kişi içe aktarıldı';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Kişi bağlantısı panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Kişi dışa aktarılamadı';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Dışa aktarılacak günlük yok';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'CSV olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Metin olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Alındı (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Derleme zamanı';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'İndirme URL\'si mevcut değil';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'İndirme URL\'si açılamıyor';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Güncelleme kontrolü yalnızca Android\'de kullanılabilir';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'En son sürümü kullanıyorsunuz';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Güncelleme mevcut ancak indirme URL\'si bulunamadı';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Tic-Tac-Toe başlat';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe kullanılamıyor';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: rakip bilinmiyor';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: başlangıç bekleniyor';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Paylaşım bağlantılarını kabul eder';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Ham hex destekler';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Pano dostu';
|
||||
|
||||
@override
|
||||
String get captured => 'Yakalandı';
|
||||
|
||||
@override
|
||||
String get size => 'Boyut';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Temizlenecek özel kanal yok.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Temizlenecek cihaz kişisi yok.';
|
||||
}
|
||||
|
||||
@@ -1927,4 +1927,773 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Не вдалося задати маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Тайм-аут ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Опкод';
|
||||
|
||||
@override
|
||||
String get payload => 'Корисне навантаження';
|
||||
|
||||
@override
|
||||
String get hops => 'Хопи';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Розмір хешу';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Байти шляху';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Обраний шлях';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Очікувана передача';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Від відправника до отримання';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Отримані копії';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Причина повтору';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Режим повтору';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Результат повтору';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Остання спроба';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-пакети';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Швидкість';
|
||||
|
||||
@override
|
||||
String get window => 'Вікно';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Затримка після передачі';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Смуговий';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Смуговий фільтр голосу';
|
||||
|
||||
@override
|
||||
String get frequency => 'Частота';
|
||||
|
||||
@override
|
||||
String get australia => 'Австралія';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Австралія (Вузький)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Австралія: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Австралія: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Нова Зеландія';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Нова Зеландія (Вузький)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Швейцарія';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Португалія 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Португалія 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Чехія (Вузький)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ЄС 433МГц (Далекий)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ЄС/Великобританія (Застаріле)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ЄС/Великобританія (Вузький)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'США/Канада (Рекомендовано)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'В\'єтнам (Застаріле)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'В\'єтнам (Вузький)';
|
||||
|
||||
@override
|
||||
String get active => 'Активно';
|
||||
|
||||
@override
|
||||
String get addContact => 'Додати контакт';
|
||||
|
||||
@override
|
||||
String get all => 'Усі';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Автоматичне вирішення';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Очистити все';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Очистити реле';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Очистити фільтри';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Очистити маршрут';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Очистити повідомлення';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Очистити масштаб';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Очистити виявлення';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Очистити базу трасувань';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Очистити всі канали';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Очистити всі контакти';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Очистити канали';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Очистити контакти';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Очистити шлях при макс. спробі';
|
||||
|
||||
@override
|
||||
String get create => 'Створити';
|
||||
|
||||
@override
|
||||
String get custom => 'Користувацький';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'За замовчуванням';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Дублювати';
|
||||
|
||||
@override
|
||||
String get editName => 'Редагувати ім\'я';
|
||||
|
||||
@override
|
||||
String get open => 'Відкрити';
|
||||
|
||||
@override
|
||||
String get paste => 'Вставити';
|
||||
|
||||
@override
|
||||
String get preview => 'Попередній перегляд';
|
||||
|
||||
@override
|
||||
String get remove => 'Видалити';
|
||||
|
||||
@override
|
||||
String get rename => 'Перейменувати';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Вирішити все';
|
||||
|
||||
@override
|
||||
String get send => 'Надіслати';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Надіслати все одно';
|
||||
|
||||
@override
|
||||
String get share => 'Поділитися';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Поділитися контактом';
|
||||
|
||||
@override
|
||||
String get trace => 'Трасування';
|
||||
|
||||
@override
|
||||
String get use => 'Використати';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Використати обрану частоту';
|
||||
|
||||
@override
|
||||
String get discovery => 'Виявлення';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Виявити повторювачі';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Виявити датчики';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Виявлення повторювачів надіслано';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Виявлення датчиків надіслано';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Очікувані виявлення очищено.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Автоматичне виявлення';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Увімкнути автоматичне додавання';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Автододавання повторювачів';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Автододавання серверів кімнат';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Автододавання датчиків';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Автододавання користувачів';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Перезаписати найстаріші при заповненні';
|
||||
|
||||
@override
|
||||
String get storage => 'Сховище';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Небезпечна зона';
|
||||
|
||||
@override
|
||||
String get profiles => 'Профілі';
|
||||
|
||||
@override
|
||||
String get favourites => 'Обране';
|
||||
|
||||
@override
|
||||
String get sensors => 'Датчики';
|
||||
|
||||
@override
|
||||
String get others => 'Інші';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Модуль GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Живий трафік';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Карта повторювачів';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Сканування спектру';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Журнали BLE-пакетів';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'База трасувань';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Розмір шляху в байтах';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Сповіщення про повідомлення';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-сповіщення';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Сповіщення про виявлення';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Сповіщення про оновлення';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Без звуку при відкритому додатку';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Вимкнути контакти';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Увімкнути вкладку Датчики';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Увімкнути профілі';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Автоматична ротація маршруту';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Найближчий повторювач як резерв';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Видалити всю історію повідомлень';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Розмір шрифту повідомлень';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Обертати карту за напрямком';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показати налагоджувальну інфо карти';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Відкрити карту на повний екран';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Показати SAR-маркери';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Показувати SAR-маркери на головній карті';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Показати всі сліди контактів';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Сховати повторювачі на карті';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Встановити масштаб карти';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Користувацький масштаб карти збережено';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Бітрейт голосу';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Компресор голосу';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Вирівнює тихий і гучний рівні мови';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Лімітер голосу';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Запобігає обрізанню піків перед кодуванням';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Автопідсилення мікрофона';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Дозволяє рекордеру налаштувати рівень входу';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Придушення ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Придушення шуму';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Обрізати тишу в голосових повідомленнях';
|
||||
|
||||
@override
|
||||
String get compressor => 'Компресор';
|
||||
|
||||
@override
|
||||
String get limiter => 'Лімітер';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Автопідсилення';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Шум';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Тиша';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Максимальний розмір зображення';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Стиснення зображення';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Відтінки сірого';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ультра режим';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Швидкі приватні GPS-оновлення';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Поріг руху';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Поріг руху швидкого GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Інтервал активного використання швидкого GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Інтервал оновлення при активному використанні';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Повторювати близький трафік';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ретранслювати через повторювачі по мережі';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Лише поблизу, без flood повторювачів';
|
||||
|
||||
@override
|
||||
String get multihop => 'Багатохоповий';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Створити профіль';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Перейменувати профіль';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Новий профіль';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Керувати профілями';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Увімкніть профілі, щоб почати керувати ними.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Відкрити повідомлення';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Перейти до пов\'язаного SAR-повідомлення';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Видалити SAR-маркер';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Виберіть призначення для надсилання SAR-маркера';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-маркер надіслано на публічний канал';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-маркер надіслано в кімнату';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Завантажити з галереї';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Замінити зображення';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Вибрати з галереї';
|
||||
|
||||
@override
|
||||
String get team => 'Команда';
|
||||
|
||||
@override
|
||||
String get found => 'Знайдено';
|
||||
|
||||
@override
|
||||
String get staging => 'Місце збору';
|
||||
|
||||
@override
|
||||
String get object => 'Об\'єкт';
|
||||
|
||||
@override
|
||||
String get quiet => 'Тихо';
|
||||
|
||||
@override
|
||||
String get moderate => 'Помірно';
|
||||
|
||||
@override
|
||||
String get busy => 'Зайнято';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Сканування спектру не знайшло частот-кандидатів';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Пошук повідомлень';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Надіслати зображення з галереї';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Зробити фото';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Лише особисті повідомлення';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Усі повідомлення';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Надіслати в публічний канал?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Виберіть тип маркера та призначення';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Немає доступних призначень';
|
||||
|
||||
@override
|
||||
String get image => 'Зображення';
|
||||
|
||||
@override
|
||||
String get format => 'Формат';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Розміри';
|
||||
|
||||
@override
|
||||
String get segments => 'Сегменти';
|
||||
|
||||
@override
|
||||
String get transfers => 'Передачі';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Завантажено';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Зберегти налаштування виявлення';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Зберегти публічну інформацію';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Зберегти налаштування радіо';
|
||||
|
||||
@override
|
||||
String get savePath => 'Зберегти шлях';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Стерти дані пристрою';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Стерти пристрій';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Деструктивні дії пристрою.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Оберіть пресет або налаштуйте радіо вручну.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Оберіть ім\'я та місцеположення, якими ділиться пристрій.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Доступний простір на цьому пристрої.';
|
||||
|
||||
@override
|
||||
String get used => 'Використано';
|
||||
|
||||
@override
|
||||
String get total => 'Загалом';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Перейменувати значення';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Налаштувати поля';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Попередній перегляд наживо';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Розклад оновлення';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Немає відповіді';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Оновлення';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Недоступно';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Оберіть реле або вузол для спостереження.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Публічний ключ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Вже в контактах';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Підключіться до пристрою перед додаванням контактів';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'З контактів';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Лише онлайн';
|
||||
|
||||
@override
|
||||
String get inBoth => 'В обох';
|
||||
|
||||
@override
|
||||
String get source => 'Джерело';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ручне редагування маршруту';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Спостережуваний mesh-маршрут';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Усі повідомлення очищено';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Базу трасувань очищено';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Журнали пакетів очищено';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex-дані скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Режим розробника увімкнено';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Режим розробника вимкнено';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Буфер обміну порожній';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Контакт імпортовано';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Посилання на контакт скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Не вдалося експортувати контакт';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Немає журналів для експорту';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Експортувати як CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Експортувати як текст';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Отримано (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Час збірки';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL завантаження недоступний';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Не вдається відкрити URL завантаження';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Перевірка оновлень доступна лише на Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Ви використовуєте останню версію';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Оновлення доступне, але URL завантаження не знайдено';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Почати Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe недоступно';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: суперник невідомий';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: очікування початку';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Приймає посилання для обміну';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Підтримує raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Зручно для буфера';
|
||||
|
||||
@override
|
||||
String get captured => 'Захоплено';
|
||||
|
||||
@override
|
||||
String get size => 'Розмір';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Немає користувацьких каналів для очищення.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Немає контактів пристрою для очищення.';
|
||||
}
|
||||
|
||||
@@ -1827,4 +1827,745 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return '设置路由失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK 超时';
|
||||
|
||||
@override
|
||||
String get opcode => '操作码';
|
||||
|
||||
@override
|
||||
String get payload => '有效载荷';
|
||||
|
||||
@override
|
||||
String get hops => '跳数';
|
||||
|
||||
@override
|
||||
String get hashSize => '哈希大小';
|
||||
|
||||
@override
|
||||
String get pathBytes => '路径字节';
|
||||
|
||||
@override
|
||||
String get selectedPath => '选定路径';
|
||||
|
||||
@override
|
||||
String get estimatedTx => '预计传输';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => '发送到接收';
|
||||
|
||||
@override
|
||||
String get receivedCopies => '收到的副本';
|
||||
|
||||
@override
|
||||
String get retryCause => '重试原因';
|
||||
|
||||
@override
|
||||
String get retryMode => '重试模式';
|
||||
|
||||
@override
|
||||
String get retryResult => '重试结果';
|
||||
|
||||
@override
|
||||
String get lastRetry => '最后重试';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX 数据包';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => '速率';
|
||||
|
||||
@override
|
||||
String get window => '窗口';
|
||||
|
||||
@override
|
||||
String get posttxDelay => '发送后延迟';
|
||||
|
||||
@override
|
||||
String get bandpass => '带通';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => '带通语音滤波';
|
||||
|
||||
@override
|
||||
String get frequency => '频率';
|
||||
|
||||
@override
|
||||
String get australia => '澳大利亚';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => '澳大利亚(窄带)';
|
||||
|
||||
@override
|
||||
String get australiaQld => '澳大利亚:QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => '澳大利亚:SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => '新西兰';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => '新西兰(窄带)';
|
||||
|
||||
@override
|
||||
String get switzerland => '瑞士';
|
||||
|
||||
@override
|
||||
String get portugal433 => '葡萄牙 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => '葡萄牙 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => '捷克(窄带)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => '欧盟 433MHz(远距离)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => '欧盟/英国(已弃用)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => '欧盟/英国(窄带)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => '美国/加拿大(推荐)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => '越南(已弃用)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => '越南(窄带)';
|
||||
|
||||
@override
|
||||
String get active => '活动';
|
||||
|
||||
@override
|
||||
String get addContact => '添加联系人';
|
||||
|
||||
@override
|
||||
String get all => '全部';
|
||||
|
||||
@override
|
||||
String get autoResolve => '自动解析';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => '清除全部';
|
||||
|
||||
@override
|
||||
String get clearRelays => '清除中继';
|
||||
|
||||
@override
|
||||
String get clearFilters => '清除过滤器';
|
||||
|
||||
@override
|
||||
String get clearRoute => '清除路由';
|
||||
|
||||
@override
|
||||
String get clearMessages => '清除消息';
|
||||
|
||||
@override
|
||||
String get clearScale => '清除比例';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => '清除发现';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => '清除在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => '清除所有频道';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => '清除所有联系人';
|
||||
|
||||
@override
|
||||
String get clearChannels => '清除频道';
|
||||
|
||||
@override
|
||||
String get clearContacts => '清除联系人';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => '最大重试时清除路径';
|
||||
|
||||
@override
|
||||
String get create => '创建';
|
||||
|
||||
@override
|
||||
String get custom => '自定义';
|
||||
|
||||
@override
|
||||
String get defaultValue => '默认';
|
||||
|
||||
@override
|
||||
String get duplicate => '复制';
|
||||
|
||||
@override
|
||||
String get editName => '编辑名称';
|
||||
|
||||
@override
|
||||
String get open => '打开';
|
||||
|
||||
@override
|
||||
String get paste => '粘贴';
|
||||
|
||||
@override
|
||||
String get preview => '预览';
|
||||
|
||||
@override
|
||||
String get remove => '移除';
|
||||
|
||||
@override
|
||||
String get rename => '重命名';
|
||||
|
||||
@override
|
||||
String get resolveAll => '解析全部';
|
||||
|
||||
@override
|
||||
String get send => '发送';
|
||||
|
||||
@override
|
||||
String get sendAnyway => '仍然发送';
|
||||
|
||||
@override
|
||||
String get share => '分享';
|
||||
|
||||
@override
|
||||
String get shareContact => '分享联系人';
|
||||
|
||||
@override
|
||||
String get trace => '追踪';
|
||||
|
||||
@override
|
||||
String get use => '使用';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => '使用选定频率';
|
||||
|
||||
@override
|
||||
String get discovery => '发现';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => '发现中继器';
|
||||
|
||||
@override
|
||||
String get discoverSensors => '发现传感器';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => '中继器发现已发送';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => '传感器发现已发送';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => '已清除待处理的发现。';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => '自动发现';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => '启用自动添加';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => '自动添加中继器';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => '自动添加房间服务器';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => '自动添加传感器';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => '自动添加用户';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => '满时覆盖最旧的';
|
||||
|
||||
@override
|
||||
String get storage => '存储';
|
||||
|
||||
@override
|
||||
String get dangerZone => '危险区域';
|
||||
|
||||
@override
|
||||
String get profiles => '配置文件';
|
||||
|
||||
@override
|
||||
String get favourites => '收藏夹';
|
||||
|
||||
@override
|
||||
String get sensors => '传感器';
|
||||
|
||||
@override
|
||||
String get others => '其他';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS 模块';
|
||||
|
||||
@override
|
||||
String get liveTraffic => '实时流量';
|
||||
|
||||
@override
|
||||
String get repeatersMap => '中继器地图';
|
||||
|
||||
@override
|
||||
String get spectrumScan => '频谱扫描';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE 数据包日志';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => '在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => '路由路径字节大小';
|
||||
|
||||
@override
|
||||
String get messageNotifications => '消息通知';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR 警报';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => '发现通知';
|
||||
|
||||
@override
|
||||
String get updateNotifications => '更新通知';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => '应用打开时静音';
|
||||
|
||||
@override
|
||||
String get disableContacts => '禁用联系人';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => '启用传感器标签';
|
||||
|
||||
@override
|
||||
String get enableProfiles => '启用配置文件';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => '自动路由轮换';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => '最近中继器回退';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => '删除所有存储的消息历史';
|
||||
|
||||
@override
|
||||
String get messageFontSize => '消息字体大小';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => '随航向旋转地图';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => '显示地图调试信息';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => '全屏打开地图';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => '显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap => '在主地图上显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => '显示所有联系人轨迹';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => '在地图上隐藏中继器';
|
||||
|
||||
@override
|
||||
String get setMapScale => '设置地图比例';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => '自定义地图比例已保存';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => '语音比特率';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => '语音压缩器';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels => '平衡安静和响亮的语音级别';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => '语音限制器';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding => '防止编码前的削波';
|
||||
|
||||
@override
|
||||
String get micAutoGain => '麦克风自动增益';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel => '让录音器调整输入级别';
|
||||
|
||||
@override
|
||||
String get echoCancellation => '回声消除';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => '噪音抑制';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => '修剪语音消息中的静音';
|
||||
|
||||
@override
|
||||
String get compressor => '压缩器';
|
||||
|
||||
@override
|
||||
String get limiter => '限制器';
|
||||
|
||||
@override
|
||||
String get autoGain => '自动增益';
|
||||
|
||||
@override
|
||||
String get echoCancel => '回声';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => '噪声';
|
||||
|
||||
@override
|
||||
String get silenceTrim => '静音';
|
||||
|
||||
@override
|
||||
String get maxImageSize => '最大图片大小';
|
||||
|
||||
@override
|
||||
String get imageCompression => '图片压缩';
|
||||
|
||||
@override
|
||||
String get grayscale => '灰度';
|
||||
|
||||
@override
|
||||
String get ultraMode => '超级模式';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => '快速私有 GPS 更新';
|
||||
|
||||
@override
|
||||
String get movementThreshold => '移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => '快速 GPS 移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => '快速 GPS 活动使用间隔';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => '活动使用更新间隔';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => '转发附近流量';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh => '通过网格中的中继器转发';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding => '仅附近,无中继器泛洪';
|
||||
|
||||
@override
|
||||
String get multihop => '多跳';
|
||||
|
||||
@override
|
||||
String get createProfile => '创建配置文件';
|
||||
|
||||
@override
|
||||
String get renameProfile => '重命名配置文件';
|
||||
|
||||
@override
|
||||
String get newProfile => '新配置文件';
|
||||
|
||||
@override
|
||||
String get manageProfiles => '管理配置文件';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem => '启用配置文件以开始管理它们。';
|
||||
|
||||
@override
|
||||
String get openMessage => '打开消息';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => '跳转到相关的 SAR 消息';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => '移除 SAR 标记';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker => '请选择目标以发送 SAR 标记';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel => 'SAR 标记已广播到公共频道';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR 标记已发送到房间';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => '从图库加载';
|
||||
|
||||
@override
|
||||
String get replaceImage => '替换图片';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => '从图库选择';
|
||||
|
||||
@override
|
||||
String get team => '团队';
|
||||
|
||||
@override
|
||||
String get found => '已找到';
|
||||
|
||||
@override
|
||||
String get staging => '集结区';
|
||||
|
||||
@override
|
||||
String get object => '物体';
|
||||
|
||||
@override
|
||||
String get quiet => '安静';
|
||||
|
||||
@override
|
||||
String get moderate => '中等';
|
||||
|
||||
@override
|
||||
String get busy => '繁忙';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies => '频谱扫描未找到候选频率';
|
||||
|
||||
@override
|
||||
String get searchMessages => '搜索消息';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => '从图库发送图片';
|
||||
|
||||
@override
|
||||
String get takePhoto => '拍照';
|
||||
|
||||
@override
|
||||
String get dmOnly => '仅私信';
|
||||
|
||||
@override
|
||||
String get allMessages => '所有消息';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => '发送到公共频道?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => '选择标记类型和目标';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => '没有可用的目标';
|
||||
|
||||
@override
|
||||
String get image => '图片';
|
||||
|
||||
@override
|
||||
String get format => '格式';
|
||||
|
||||
@override
|
||||
String get dimensions => '尺寸';
|
||||
|
||||
@override
|
||||
String get segments => '分段';
|
||||
|
||||
@override
|
||||
String get transfers => '传输';
|
||||
|
||||
@override
|
||||
String get downloadedBy => '已下载';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => '保存发现设置';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => '保存公共信息';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => '保存无线电设置';
|
||||
|
||||
@override
|
||||
String get savePath => '保存路径';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => '擦除设备数据';
|
||||
|
||||
@override
|
||||
String get wipeDevice => '擦除设备';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => '破坏性设备操作。';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings => '选择预设或微调自定义无线电设置。';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares => '选择此设备共享的名称和位置。';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => '此设备上的可用空间。';
|
||||
|
||||
@override
|
||||
String get used => '已使用';
|
||||
|
||||
@override
|
||||
String get total => '总计';
|
||||
|
||||
@override
|
||||
String get renameValue => '重命名值';
|
||||
|
||||
@override
|
||||
String get customizeFields => '自定义字段';
|
||||
|
||||
@override
|
||||
String get livePreview => '实时预览';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => '刷新计划';
|
||||
|
||||
@override
|
||||
String get noResponse => '无响应';
|
||||
|
||||
@override
|
||||
String get refreshing => '刷新中';
|
||||
|
||||
@override
|
||||
String get unavailable => '不可用';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors => '选择要在传感器中监视的中继或节点。';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => '公钥';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => '已在联系人中';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts => '添加联系人前请连接到设备';
|
||||
|
||||
@override
|
||||
String get fromContacts => '来自联系人';
|
||||
|
||||
@override
|
||||
String get onlineOnly => '仅在线';
|
||||
|
||||
@override
|
||||
String get inBoth => '两者中';
|
||||
|
||||
@override
|
||||
String get source => '来源';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => '手动路由编辑';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => '观察到的网格路由';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => '所有消息已清除';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => '在线追踪数据库已清除';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => '数据包日志已清除';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => '十六进制数据已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => '开发者模式已启用';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => '开发者模式已禁用';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => '剪贴板为空';
|
||||
|
||||
@override
|
||||
String get contactImported => '联系人已导入';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => '联系人链接已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => '导出联系人失败';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => '没有日志可导出';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => '导出为 CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => '导出为文本';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => '收到 (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => '构建时间';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => '下载链接不可用';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => '无法打开下载链接';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid => '更新检查仅在 Android 上可用';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => '您正在使用最新版本';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound => '有更新可用但未找到下载链接';
|
||||
|
||||
@override
|
||||
String get startTictactoe => '开始井字棋';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => '井字棋不可用';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => '井字棋:对手未知';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => '井字棋:等待开始';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => '接受分享链接';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => '支持原始十六进制';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => '剪贴板友好';
|
||||
|
||||
@override
|
||||
String get captured => '已捕获';
|
||||
|
||||
@override
|
||||
String get size => '大小';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => '没有自定义频道可清除。';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => '没有设备联系人可清除。';
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Nie udało się wyczyścić trasy: {error}",
|
||||
"contactRouteCleared": "Trasa wyczyszczona",
|
||||
"contactRouteSet": "Ustawiono trasę: {route}",
|
||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}"
|
||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Limit czasu ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ładunek",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Rozmiar hash",
|
||||
"pathBytes": "Bajty ścieżki",
|
||||
"selectedPath": "Wybrana ścieżka",
|
||||
"estimatedTx": "Szacowane nadawanie",
|
||||
"senderToReceipt": "Od nadawcy do odbioru",
|
||||
"receivedCopies": "Otrzymane kopie",
|
||||
"retryCause": "Przyczyna ponowienia",
|
||||
"retryMode": "Tryb ponowienia",
|
||||
"retryResult": "Wynik ponowienia",
|
||||
"lastRetry": "Ostatnia próba",
|
||||
"rxPackets": "Pakiety RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Szybkość",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Opóźnienie po nadawaniu",
|
||||
"bandpass": "Pasmowy",
|
||||
"bandpassFilterVoice": "Filtr pasmowy głosu",
|
||||
"frequency": "Częstotliwość",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (wąski)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nowa Zelandia",
|
||||
"newZealandNarrow": "Nowa Zelandia (wąski)",
|
||||
"switzerland": "Szwajcaria",
|
||||
"portugal433": "Portugalia 433",
|
||||
"portugal868": "Portugalia 868",
|
||||
"czechRepublicNarrow": "Czechy (wąski)",
|
||||
"eu433mhzLongRange": "EU 433MHz (daleki zasięg)",
|
||||
"euukDeprecated": "EU/UK (przestarzały)",
|
||||
"euukNarrow": "EU/UK (wąski)",
|
||||
"usacanadaRecommended": "USA/Kanada (zalecany)",
|
||||
"vietnamDeprecated": "Wietnam (przestarzały)",
|
||||
"vietnamNarrow": "Wietnam (wąski)",
|
||||
"active": "Aktywny",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Wszystko",
|
||||
"autoResolve": "Automatyczne rozwiązanie",
|
||||
"clearAllLabel": "Wyczyść wszystko",
|
||||
"clearRelays": "Wyczyść przekaźniki",
|
||||
"clearFilters": "Wyczyść filtry",
|
||||
"clearRoute": "Wyczyść trasę",
|
||||
"clearMessages": "Wyczyść wiadomości",
|
||||
"clearScale": "Wyczyść skalę",
|
||||
"clearDiscoveries": "Wyczyść odkrycia",
|
||||
"clearOnlineTraceDatabase": "Wyczyść bazę śladów",
|
||||
"clearAllChannels": "Wyczyść wszystkie kanały",
|
||||
"clearAllContacts": "Wyczyść wszystkie kontakty",
|
||||
"clearChannels": "Wyczyść kanały",
|
||||
"clearContacts": "Wyczyść kontakty",
|
||||
"clearPathOnMaxRetry": "Wyczyść ścieżkę przy maks. próbie",
|
||||
"create": "Utwórz",
|
||||
"custom": "Niestandardowy",
|
||||
"defaultValue": "Domyślny",
|
||||
"duplicate": "Duplikuj",
|
||||
"editName": "Edytuj nazwę",
|
||||
"open": "Otwórz",
|
||||
"paste": "Wklej",
|
||||
"preview": "Podgląd",
|
||||
"remove": "Usuń",
|
||||
"rename": "Zmień nazwę",
|
||||
"resolveAll": "Rozwiąż wszystko",
|
||||
"send": "Wyślij",
|
||||
"sendAnyway": "Wyślij mimo to",
|
||||
"share": "Udostępnij",
|
||||
"shareContact": "Udostępnij kontakt",
|
||||
"trace": "Śledzenie",
|
||||
"use": "Użyj",
|
||||
"useSelectedFrequency": "Użyj wybranej częstotliwości",
|
||||
"discovery": "Wykrywanie",
|
||||
"discoverRepeaters": "Wykryj przekaźniki",
|
||||
"discoverSensors": "Wykryj czujniki",
|
||||
"repeaterDiscoverySent": "Wykrywanie przekaźników wysłane",
|
||||
"sensorDiscoverySent": "Wykrywanie czujników wysłane",
|
||||
"clearedPendingDiscoveries": "Wyczyszczono oczekujące odkrycia.",
|
||||
"autoDiscovery": "Automatyczne wykrywanie",
|
||||
"enableAutomaticAdding": "Włącz automatyczne dodawanie",
|
||||
"autoaddRepeaters": "Automatycznie dodaj przekaźniki",
|
||||
"autoaddRoomServers": "Automatycznie dodaj serwery pokoi",
|
||||
"autoaddSensors": "Automatycznie dodaj czujniki",
|
||||
"autoaddUsers": "Automatycznie dodaj użytkowników",
|
||||
"overwriteOldestWhenFull": "Nadpisz najstarsze gdy pełne",
|
||||
"storage": "Pamięć",
|
||||
"dangerZone": "Strefa zagrożenia",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Ulubione",
|
||||
"sensors": "Czujniki",
|
||||
"others": "Inne",
|
||||
"gpsModule": "Moduł GPS",
|
||||
"liveTraffic": "Ruch na żywo",
|
||||
"repeatersMap": "Mapa przekaźników",
|
||||
"spectrumScan": "Skanowanie spektrum",
|
||||
"blePacketLogs": "Logi pakietów BLE",
|
||||
"onlineTraceDatabase": "Baza śladów",
|
||||
"routePathByteSize": "Rozmiar ścieżki w bajtach",
|
||||
"messageNotifications": "Powiadomienia o wiadomościach",
|
||||
"sarAlerts": "Alerty SAR",
|
||||
"discoveryNotifications": "Powiadomienia o wykrywaniu",
|
||||
"updateNotifications": "Powiadomienia o aktualizacjach",
|
||||
"muteWhileAppIsOpen": "Wycisz gdy aplikacja jest otwarta",
|
||||
"disableContacts": "Wyłącz kontakty",
|
||||
"enableSensorsTab": "Włącz kartę Czujniki",
|
||||
"enableProfiles": "Włącz profile",
|
||||
"autoRouteRotation": "Automatyczna rotacja trasy",
|
||||
"nearestRepeaterFallback": "Najbliższy przekaźnik awaryjny",
|
||||
"deleteAllStoredMessageHistory": "Usuń całą historię wiadomości",
|
||||
"messageFontSize": "Rozmiar czcionki wiadomości",
|
||||
"rotateMapWithHeading": "Obracaj mapę z kierunkiem",
|
||||
"showMapDebugInfo": "Pokaż info debugowania mapy",
|
||||
"openMapInFullscreen": "Otwórz mapę na pełnym ekranie",
|
||||
"showSarMarkersLabel": "Pokaż znaczniki SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Wyświetlaj znaczniki SAR na głównej mapie",
|
||||
"showAllContactTrailsLabel": "Pokaż wszystkie ślady kontaktów",
|
||||
"hideRepeatersOnMap": "Ukryj przekaźniki na mapie",
|
||||
"setMapScale": "Ustaw skalę mapy",
|
||||
"customMapScaleSaved": "Niestandardowa skala mapy zapisana",
|
||||
"voiceBitrate": "Bitrate głosu",
|
||||
"voiceCompressor": "Kompresor głosu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Wyrównuje ciche i głośne poziomy mowy",
|
||||
"voiceLimiter": "Limiter głosu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Zapobiega przycinaniu szczytów przed kodowaniem",
|
||||
"micAutoGain": "Automatyczne wzmocnienie mikrofonu",
|
||||
"letsTheRecorderAdjustInputLevel": "Pozwala nagrywarkowi dostosować poziom wejścia",
|
||||
"echoCancellation": "Usuwanie echa",
|
||||
"noiseSuppression": "Tłumienie szumu",
|
||||
"trimSilenceInVoiceMessages": "Przytnij ciszę w wiadomościach głosowych",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatyczne wzmocnienie",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Szum",
|
||||
"silenceTrim": "Cisza",
|
||||
"maxImageSize": "Maksymalny rozmiar obrazu",
|
||||
"imageCompression": "Kompresja obrazu",
|
||||
"grayscale": "Skala szarości",
|
||||
"ultraMode": "Tryb ultra",
|
||||
"fastPrivateGpsUpdates": "Szybkie prywatne aktualizacje GPS",
|
||||
"movementThreshold": "Próg ruchu",
|
||||
"fastGpsMovementThreshold": "Próg ruchu szybkiego GPS",
|
||||
"fastGpsActiveuseInterval": "Interwał aktywnego użycia szybkiego GPS",
|
||||
"activeuseUpdateInterval": "Interwał aktualizacji przy aktywnym użyciu",
|
||||
"repeatNearbyTraffic": "Powtarzaj pobliski ruch",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Przekazuj przez przekaźniki w sieci",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Tylko w pobliżu, bez zalewania przekaźników",
|
||||
"multihop": "Wieloskokowy",
|
||||
"createProfile": "Utwórz profil",
|
||||
"renameProfile": "Zmień nazwę profilu",
|
||||
"newProfile": "Nowy profil",
|
||||
"manageProfiles": "Zarządzaj profilami",
|
||||
"enableProfilesToStartManagingThem": "Włącz profile, aby zacząć nimi zarządzać.",
|
||||
"openMessage": "Otwórz wiadomość",
|
||||
"jumpToTheRelatedSarMessage": "Przejdź do powiązanej wiadomości SAR",
|
||||
"removeSarMarker": "Usuń znacznik SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Wybierz cel wysłania znacznika SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Znacznik SAR wysłany na kanał publiczny",
|
||||
"sarMarkerSentToRoom": "Znacznik SAR wysłany do pokoju",
|
||||
"loadFromGallery": "Załaduj z galerii",
|
||||
"replaceImage": "Zamień obraz",
|
||||
"selectFromGallery": "Wybierz z galerii",
|
||||
"team": "Zespół",
|
||||
"found": "Znaleziono",
|
||||
"staging": "Punkt zbiórki",
|
||||
"object": "Obiekt",
|
||||
"quiet": "Cicho",
|
||||
"moderate": "Umiarkowane",
|
||||
"busy": "Zajęty",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Skanowanie spektrum nie znalazło częstotliwości kandydatów",
|
||||
"searchMessages": "Szukaj wiadomości",
|
||||
"sendImageFromGallery": "Wyślij obraz z galerii",
|
||||
"takePhoto": "Zrób zdjęcie",
|
||||
"dmOnly": "Tylko wiadomość bezpośrednia",
|
||||
"allMessages": "Wszystkie wiadomości",
|
||||
"sendToPublicChannel": "Wysłać na kanał publiczny?",
|
||||
"selectMarkerTypeAndDestination": "Wybierz typ znacznika i cel",
|
||||
"noDestinationsAvailableLabel": "Brak dostępnych celów",
|
||||
"image": "Obraz",
|
||||
"format": "Format",
|
||||
"dimensions": "Wymiary",
|
||||
"segments": "Segmenty",
|
||||
"transfers": "Transfery",
|
||||
"downloadedBy": "Pobrane przez",
|
||||
"saveDiscoverySettings": "Zapisz ustawienia wykrywania",
|
||||
"savePublicInfo": "Zapisz informacje publiczne",
|
||||
"saveRadioSettings": "Zapisz ustawienia radia",
|
||||
"savePath": "Zapisz ścieżkę",
|
||||
"wipeDeviceData": "Wyczyść dane urządzenia",
|
||||
"wipeDevice": "Wyczyść urządzenie",
|
||||
"destructiveDeviceActions": "Destrukcyjne akcje na urządzeniu.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wybierz ustawienie wstępne lub dostosuj ustawienia radia.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wybierz nazwę i lokalizację udostępnianą przez to urządzenie.",
|
||||
"availableSpaceOnThisDevice": "Dostępne miejsce na tym urządzeniu.",
|
||||
"used": "Użyto",
|
||||
"total": "Razem",
|
||||
"renameValue": "Zmień nazwę wartości",
|
||||
"customizeFields": "Dostosuj pola",
|
||||
"livePreview": "Podgląd na żywo",
|
||||
"refreshSchedule": "Harmonogram odświeżania",
|
||||
"noResponse": "Brak odpowiedzi",
|
||||
"refreshing": "Odświeżanie",
|
||||
"unavailable": "Niedostępny",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wybierz przekaźnik lub węzeł do obserwacji.",
|
||||
"publicKeyLabel": "Klucz publiczny",
|
||||
"alreadyInContacts": "Już w kontaktach",
|
||||
"connectToADeviceBeforeAddingContacts": "Połącz się z urządzeniem przed dodaniem kontaktów",
|
||||
"fromContacts": "Z kontaktów",
|
||||
"onlineOnly": "Tylko online",
|
||||
"inBoth": "W obu",
|
||||
"source": "Źródło",
|
||||
"manualRouteEdit": "Ręczna edycja trasy",
|
||||
"observedMeshRoute": "Obserwowana trasa sieci",
|
||||
"allMessagesCleared": "Wszystkie wiadomości wyczyszczone",
|
||||
"onlineTraceDatabaseCleared": "Baza śladów wyczyszczona",
|
||||
"packetLogsCleared": "Logi pakietów wyczyszczone",
|
||||
"hexDataCopiedToClipboard": "Dane hex skopiowane do schowka",
|
||||
"developerModeEnabled": "Tryb programisty włączony",
|
||||
"developerModeDisabled": "Tryb programisty wyłączony",
|
||||
"clipboardIsEmpty": "Schowek jest pusty",
|
||||
"contactImported": "Kontakt zaimportowany",
|
||||
"contactLinkCopiedToClipboard": "Link kontaktu skopiowany do schowka",
|
||||
"failedToExportContact": "Nie udało się wyeksportować kontaktu",
|
||||
"noLogsToExport": "Brak logów do eksportu",
|
||||
"exportAsCsv": "Eksportuj jako CSV",
|
||||
"exportAsText": "Eksportuj jako tekst",
|
||||
"receivedRfc3339": "Odebrano (RFC3339)",
|
||||
"buildTime": "Czas budowania",
|
||||
"downloadUrlNotAvailable": "URL pobierania niedostępny",
|
||||
"cannotOpenDownloadUrl": "Nie można otworzyć URL pobierania",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Sprawdzanie aktualizacji dostępne tylko na Androidzie",
|
||||
"youAreRunningTheLatestVersion": "Używasz najnowszej wersji",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aktualizacja dostępna, ale URL pobierania nie znaleziony",
|
||||
"startTictactoe": "Rozpocznij Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe niedostępny",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: przeciwnik nieznany",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: oczekiwanie na start",
|
||||
"acceptsShareLinks": "Akceptuje udostępnione linki",
|
||||
"supportsRawHex": "Obsługuje surowy hex",
|
||||
"clipboardfriendly": "Przyjazne dla schowka",
|
||||
"captured": "Przechwycone",
|
||||
"size": "Rozmiar",
|
||||
"noCustomChannelsToClear": "Brak niestandardowych kanałów do wyczyszczenia.",
|
||||
"noDeviceContactsToClear": "Brak kontaktów urządzenia do wyczyszczenia."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Falha ao limpar a rota: {error}",
|
||||
"contactRouteCleared": "Rota limpa",
|
||||
"contactRouteSet": "Rota definida: {route}",
|
||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}"
|
||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tempo limite ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamanho do hash",
|
||||
"pathBytes": "Bytes do caminho",
|
||||
"selectedPath": "Caminho selecionado",
|
||||
"estimatedTx": "Transmissão estimada",
|
||||
"senderToReceipt": "Do remetente ao recibo",
|
||||
"receivedCopies": "Cópias recebidas",
|
||||
"retryCause": "Causa da nova tentativa",
|
||||
"retryMode": "Modo de nova tentativa",
|
||||
"retryResult": "Resultado da nova tentativa",
|
||||
"lastRetry": "Última tentativa",
|
||||
"rxPackets": "Pacotes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Taxa",
|
||||
"window": "Janela",
|
||||
"posttxDelay": "Atraso pós-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda de voz",
|
||||
"frequency": "Frequência",
|
||||
"australia": "Austrália",
|
||||
"australiaNarrow": "Austrália (Estreito)",
|
||||
"australiaQld": "Austrália: QLD",
|
||||
"australiaSaWa": "Austrália: SA, WA",
|
||||
"newZealand": "Nova Zelândia",
|
||||
"newZealandNarrow": "Nova Zelândia (Estreito)",
|
||||
"switzerland": "Suíça",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estreito)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estreito)",
|
||||
"usacanadaRecommended": "EUA/Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietname (Obsoleto)",
|
||||
"vietnamNarrow": "Vietname (Estreito)",
|
||||
"active": "Ativo",
|
||||
"addContact": "Adicionar contacto",
|
||||
"all": "Tudo",
|
||||
"autoResolve": "Resolver automaticamente",
|
||||
"clearAllLabel": "Limpar tudo",
|
||||
"clearRelays": "Limpar relays",
|
||||
"clearFilters": "Limpar filtros",
|
||||
"clearRoute": "Limpar rota",
|
||||
"clearMessages": "Limpar mensagens",
|
||||
"clearScale": "Limpar escala",
|
||||
"clearDiscoveries": "Limpar descobertas",
|
||||
"clearOnlineTraceDatabase": "Limpar base de dados de rastreio",
|
||||
"clearAllChannels": "Limpar todos os canais",
|
||||
"clearAllContacts": "Limpar todos os contactos",
|
||||
"clearChannels": "Limpar canais",
|
||||
"clearContacts": "Limpar contactos",
|
||||
"clearPathOnMaxRetry": "Limpar caminho no máx. de tentativas",
|
||||
"create": "Criar",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Padrão",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nome",
|
||||
"open": "Abrir",
|
||||
"paste": "Colar",
|
||||
"preview": "Pré-visualização",
|
||||
"remove": "Remover",
|
||||
"rename": "Renomear",
|
||||
"resolveAll": "Resolver tudo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar mesmo assim",
|
||||
"share": "Partilhar",
|
||||
"shareContact": "Partilhar contacto",
|
||||
"trace": "Rastreio",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frequência selecionada",
|
||||
"discovery": "Descoberta",
|
||||
"discoverRepeaters": "Descobrir repetidores",
|
||||
"discoverSensors": "Descobrir sensores",
|
||||
"repeaterDiscoverySent": "Descoberta de repetidores enviada",
|
||||
"sensorDiscoverySent": "Descoberta de sensores enviada",
|
||||
"clearedPendingDiscoveries": "Descobertas pendentes limpas.",
|
||||
"autoDiscovery": "Descoberta automática",
|
||||
"enableAutomaticAdding": "Ativar adição automática",
|
||||
"autoaddRepeaters": "Adicionar repetidores automaticamente",
|
||||
"autoaddRoomServers": "Adicionar servidores de sala automaticamente",
|
||||
"autoaddSensors": "Adicionar sensores automaticamente",
|
||||
"autoaddUsers": "Adicionar utilizadores automaticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescrever mais antigos quando cheio",
|
||||
"storage": "Armazenamento",
|
||||
"dangerZone": "Zona de perigo",
|
||||
"profiles": "Perfis",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Outros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfego ao vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Varredura de espectro",
|
||||
"blePacketLogs": "Registos de pacotes BLE",
|
||||
"onlineTraceDatabase": "Base de dados de rastreio",
|
||||
"routePathByteSize": "Tamanho do caminho em bytes",
|
||||
"messageNotifications": "Notificações de mensagens",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificações de descoberta",
|
||||
"updateNotifications": "Notificações de atualização",
|
||||
"muteWhileAppIsOpen": "Silenciar com a app aberta",
|
||||
"disableContacts": "Desativar contactos",
|
||||
"enableSensorsTab": "Ativar separador Sensores",
|
||||
"enableProfiles": "Ativar perfis",
|
||||
"autoRouteRotation": "Rotação automática de rota",
|
||||
"nearestRepeaterFallback": "Repetidor mais próximo como reserva",
|
||||
"deleteAllStoredMessageHistory": "Apagar todo o histórico de mensagens",
|
||||
"messageFontSize": "Tamanho da fonte de mensagens",
|
||||
"rotateMapWithHeading": "Rodar mapa com direção",
|
||||
"showMapDebugInfo": "Mostrar info de depuração do mapa",
|
||||
"openMapInFullscreen": "Abrir mapa em ecrã inteiro",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR no mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos os rastos de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores no mapa",
|
||||
"setMapScale": "Definir escala do mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Taxa de bits de voz",
|
||||
"voiceCompressor": "Compressor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra níveis de fala suaves e altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previne corte de picos antes da codificação",
|
||||
"micAutoGain": "Ganho automático do microfone",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite ao gravador ajustar o nível de entrada",
|
||||
"echoCancellation": "Cancelamento de eco",
|
||||
"noiseSuppression": "Supressão de ruído",
|
||||
"trimSilenceInVoiceMessages": "Cortar silêncio em mensagens de voz",
|
||||
"compressor": "Compressor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganho automático",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruído",
|
||||
"silenceTrim": "Silêncio",
|
||||
"maxImageSize": "Tamanho máximo de imagem",
|
||||
"imageCompression": "Compressão de imagem",
|
||||
"grayscale": "Escala de cinzentos",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Atualizações GPS privadas rápidas",
|
||||
"movementThreshold": "Limiar de movimento",
|
||||
"fastGpsMovementThreshold": "Limiar de movimento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso ativo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de atualização de uso ativo",
|
||||
"repeatNearbyTraffic": "Repetir tráfego próximo",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir através de repetidores na rede",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Apenas próximos, sem flood de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Criar perfil",
|
||||
"renameProfile": "Renomear perfil",
|
||||
"newProfile": "Novo perfil",
|
||||
"manageProfiles": "Gerir perfis",
|
||||
"enableProfilesToStartManagingThem": "Ative os perfis para começar a geri-los.",
|
||||
"openMessage": "Abrir mensagem",
|
||||
"jumpToTheRelatedSarMessage": "Ir para a mensagem SAR relacionada",
|
||||
"removeSarMarker": "Remover marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Selecione um destino para enviar o marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido no canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado para a sala",
|
||||
"loadFromGallery": "Carregar da galeria",
|
||||
"replaceImage": "Substituir imagem",
|
||||
"selectFromGallery": "Selecionar da galeria",
|
||||
"team": "Equipa",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunião",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "A varredura de espectro não encontrou frequências candidatas",
|
||||
"searchMessages": "Pesquisar mensagens",
|
||||
"sendImageFromGallery": "Enviar imagem da galeria",
|
||||
"takePhoto": "Tirar foto",
|
||||
"dmOnly": "Apenas mensagem direta",
|
||||
"allMessages": "Todas as mensagens",
|
||||
"sendToPublicChannel": "Enviar para o canal público?",
|
||||
"selectMarkerTypeAndDestination": "Selecione tipo de marcador e destino",
|
||||
"noDestinationsAvailableLabel": "Nenhum destino disponível",
|
||||
"image": "Imagem",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensões",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferências",
|
||||
"downloadedBy": "Transferido por",
|
||||
"saveDiscoverySettings": "Guardar definições de descoberta",
|
||||
"savePublicInfo": "Guardar informação pública",
|
||||
"saveRadioSettings": "Guardar definições de rádio",
|
||||
"savePath": "Guardar caminho",
|
||||
"wipeDeviceData": "Apagar dados do dispositivo",
|
||||
"wipeDevice": "Apagar dispositivo",
|
||||
"destructiveDeviceActions": "Ações destrutivas do dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Escolha uma predefinição ou ajuste as definições de rádio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Escolha o nome e a localização que este dispositivo partilha.",
|
||||
"availableSpaceOnThisDevice": "Espaço disponível neste dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renomear valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Pré-visualização ao vivo",
|
||||
"refreshSchedule": "Agenda de atualização",
|
||||
"noResponse": "Sem resposta",
|
||||
"refreshing": "A atualizar",
|
||||
"unavailable": "Indisponível",
|
||||
"pickARelayOrNodeToWatchInSensors": "Escolha um relay ou nó para observar.",
|
||||
"publicKeyLabel": "Chave pública",
|
||||
"alreadyInContacts": "Já nos contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conecte-se a um dispositivo antes de adicionar contactos",
|
||||
"fromContacts": "Dos contactos",
|
||||
"onlineOnly": "Apenas online",
|
||||
"inBoth": "Em ambos",
|
||||
"source": "Fonte",
|
||||
"manualRouteEdit": "Edição manual de rota",
|
||||
"observedMeshRoute": "Rota mesh observada",
|
||||
"allMessagesCleared": "Todas as mensagens limpas",
|
||||
"onlineTraceDatabaseCleared": "Base de dados de rastreio limpa",
|
||||
"packetLogsCleared": "Registos de pacotes limpos",
|
||||
"hexDataCopiedToClipboard": "Dados hex copiados para a área de transferência",
|
||||
"developerModeEnabled": "Modo de programador ativado",
|
||||
"developerModeDisabled": "Modo de programador desativado",
|
||||
"clipboardIsEmpty": "A área de transferência está vazia",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Link do contacto copiado para a área de transferência",
|
||||
"failedToExportContact": "Falha ao exportar contacto",
|
||||
"noLogsToExport": "Sem registos para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recebido (RFC3339)",
|
||||
"buildTime": "Hora de compilação",
|
||||
"downloadUrlNotAvailable": "URL de download não disponível",
|
||||
"cannotOpenDownloadUrl": "Não é possível abrir o URL de download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "A verificação de atualizações só está disponível no Android",
|
||||
"youAreRunningTheLatestVersion": "Está a usar a versão mais recente",
|
||||
"updateAvailableButDownloadUrlNotFound": "Atualização disponível mas URL de download não encontrado",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponível",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconhecido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: a aguardar início",
|
||||
"acceptsShareLinks": "Aceita links partilhados",
|
||||
"supportsRawHex": "Suporta hex bruto",
|
||||
"clipboardfriendly": "Compatível com a área de transferência",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamanho",
|
||||
"noCustomChannelsToClear": "Sem canais personalizados para limpar.",
|
||||
"noDeviceContactsToClear": "Sem contactos do dispositivo para limpar."
|
||||
}
|
||||
|
||||
@@ -563,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Не удалось очистить маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищен",
|
||||
"contactRouteSet": "Маршрут задан: {route}",
|
||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}"
|
||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Таймаут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Полезная нагрузка",
|
||||
"hops": "Хопы",
|
||||
"hashSize": "Размер хеша",
|
||||
"pathBytes": "Байты пути",
|
||||
"selectedPath": "Выбранный путь",
|
||||
"estimatedTx": "Ожидаемая передача",
|
||||
"senderToReceipt": "От отправителя до получения",
|
||||
"receivedCopies": "Полученные копии",
|
||||
"retryCause": "Причина повтора",
|
||||
"retryMode": "Режим повтора",
|
||||
"retryResult": "Результат повтора",
|
||||
"lastRetry": "Последний повтор",
|
||||
"rxPackets": "RX-пакеты",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Скорость",
|
||||
"window": "Окно",
|
||||
"posttxDelay": "Задержка после передачи",
|
||||
"bandpass": "Полосовой",
|
||||
"bandpassFilterVoice": "Полосовой фильтр голоса",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралия",
|
||||
"australiaNarrow": "Австралия (Узкий)",
|
||||
"australiaQld": "Австралия: QLD",
|
||||
"australiaSaWa": "Австралия: SA, WA",
|
||||
"newZealand": "Новая Зеландия",
|
||||
"newZealandNarrow": "Новая Зеландия (Узкий)",
|
||||
"switzerland": "Швейцария",
|
||||
"portugal433": "Португалия 433",
|
||||
"portugal868": "Португалия 868",
|
||||
"czechRepublicNarrow": "Чехия (Узкий)",
|
||||
"eu433mhzLongRange": "ЕС 433МГц (Дальний)",
|
||||
"euukDeprecated": "ЕС/Великобритания (Устарело)",
|
||||
"euukNarrow": "ЕС/Великобритания (Узкий)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендуется)",
|
||||
"vietnamDeprecated": "Вьетнам (Устарело)",
|
||||
"vietnamNarrow": "Вьетнам (Узкий)",
|
||||
"active": "Активно",
|
||||
"addContact": "Добавить контакт",
|
||||
"all": "Все",
|
||||
"autoResolve": "Автоматическое разрешение",
|
||||
"clearAllLabel": "Очистить все",
|
||||
"clearRelays": "Очистить реле",
|
||||
"clearFilters": "Очистить фильтры",
|
||||
"clearRoute": "Очистить маршрут",
|
||||
"clearMessages": "Очистить сообщения",
|
||||
"clearScale": "Очистить масштаб",
|
||||
"clearDiscoveries": "Очистить обнаружения",
|
||||
"clearOnlineTraceDatabase": "Очистить базу трассировок",
|
||||
"clearAllChannels": "Очистить все каналы",
|
||||
"clearAllContacts": "Очистить все контакты",
|
||||
"clearChannels": "Очистить каналы",
|
||||
"clearContacts": "Очистить контакты",
|
||||
"clearPathOnMaxRetry": "Очистить путь при макс. повторе",
|
||||
"create": "Создать",
|
||||
"custom": "Пользовательский",
|
||||
"defaultValue": "По умолчанию",
|
||||
"duplicate": "Дублировать",
|
||||
"editName": "Редактировать имя",
|
||||
"open": "Открыть",
|
||||
"paste": "Вставить",
|
||||
"preview": "Предпросмотр",
|
||||
"remove": "Удалить",
|
||||
"rename": "Переименовать",
|
||||
"resolveAll": "Разрешить все",
|
||||
"send": "Отправить",
|
||||
"sendAnyway": "Отправить все равно",
|
||||
"share": "Поделиться",
|
||||
"shareContact": "Поделиться контактом",
|
||||
"trace": "Трассировка",
|
||||
"use": "Использовать",
|
||||
"useSelectedFrequency": "Использовать выбранную частоту",
|
||||
"discovery": "Обнаружение",
|
||||
"discoverRepeaters": "Обнаружить повторители",
|
||||
"discoverSensors": "Обнаружить датчики",
|
||||
"repeaterDiscoverySent": "Обнаружение повторителей отправлено",
|
||||
"sensorDiscoverySent": "Обнаружение датчиков отправлено",
|
||||
"clearedPendingDiscoveries": "Ожидающие обнаружения очищены.",
|
||||
"autoDiscovery": "Автоматическое обнаружение",
|
||||
"enableAutomaticAdding": "Включить автоматическое добавление",
|
||||
"autoaddRepeaters": "Автодобавление повторителей",
|
||||
"autoaddRoomServers": "Автодобавление серверов комнат",
|
||||
"autoaddSensors": "Автодобавление датчиков",
|
||||
"autoaddUsers": "Автодобавление пользователей",
|
||||
"overwriteOldestWhenFull": "Перезаписать старейшие при заполнении",
|
||||
"storage": "Хранилище",
|
||||
"dangerZone": "Опасная зона",
|
||||
"profiles": "Профили",
|
||||
"favourites": "Избранное",
|
||||
"sensors": "Датчики",
|
||||
"others": "Другие",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живой трафик",
|
||||
"repeatersMap": "Карта повторителей",
|
||||
"spectrumScan": "Сканирование спектра",
|
||||
"blePacketLogs": "Журналы BLE-пакетов",
|
||||
"onlineTraceDatabase": "База трассировок",
|
||||
"routePathByteSize": "Размер пути в байтах",
|
||||
"messageNotifications": "Уведомления о сообщениях",
|
||||
"sarAlerts": "SAR-оповещения",
|
||||
"discoveryNotifications": "Уведомления об обнаружении",
|
||||
"updateNotifications": "Уведомления об обновлениях",
|
||||
"muteWhileAppIsOpen": "Без звука при открытом приложении",
|
||||
"disableContacts": "Отключить контакты",
|
||||
"enableSensorsTab": "Включить вкладку Датчики",
|
||||
"enableProfiles": "Включить профили",
|
||||
"autoRouteRotation": "Автоматическая ротация маршрута",
|
||||
"nearestRepeaterFallback": "Ближайший повторитель как резерв",
|
||||
"deleteAllStoredMessageHistory": "Удалить всю историю сообщений",
|
||||
"messageFontSize": "Размер шрифта сообщений",
|
||||
"rotateMapWithHeading": "Вращать карту по направлению",
|
||||
"showMapDebugInfo": "Показать отладочную информацию карты",
|
||||
"openMapInFullscreen": "Открыть карту на весь экран",
|
||||
"showSarMarkersLabel": "Показать SAR-маркеры",
|
||||
"displaySarMarkersOnTheMainMap": "Отображать SAR-маркеры на основной карте",
|
||||
"showAllContactTrailsLabel": "Показать все следы контактов",
|
||||
"hideRepeatersOnMap": "Скрыть повторители на карте",
|
||||
"setMapScale": "Установить масштаб карты",
|
||||
"customMapScaleSaved": "Пользовательский масштаб карты сохранён",
|
||||
"voiceBitrate": "Битрейт голоса",
|
||||
"voiceCompressor": "Компрессор голоса",
|
||||
"balancesQuietAndLoudSpeechLevels": "Выравнивает тихую и громкую речь",
|
||||
"voiceLimiter": "Лимитер голоса",
|
||||
"preventsClippingPeaksBeforeEncoding": "Предотвращает обрезку пиков перед кодированием",
|
||||
"micAutoGain": "Автоусиление микрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Позволяет рекордеру регулировать уровень входа",
|
||||
"echoCancellation": "Подавление эха",
|
||||
"noiseSuppression": "Подавление шума",
|
||||
"trimSilenceInVoiceMessages": "Обрезать тишину в голосовых сообщениях",
|
||||
"compressor": "Компрессор",
|
||||
"limiter": "Лимитер",
|
||||
"autoGain": "Автоусиление",
|
||||
"echoCancel": "Эхо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тишина",
|
||||
"maxImageSize": "Максимальный размер изображения",
|
||||
"imageCompression": "Сжатие изображения",
|
||||
"grayscale": "Оттенки серого",
|
||||
"ultraMode": "Режим ультра",
|
||||
"fastPrivateGpsUpdates": "Быстрые приватные GPS-обновления",
|
||||
"movementThreshold": "Порог движения",
|
||||
"fastGpsMovementThreshold": "Порог движения быстрого GPS",
|
||||
"fastGpsActiveuseInterval": "Интервал активного использования быстрого GPS",
|
||||
"activeuseUpdateInterval": "Интервал обновления при активном использовании",
|
||||
"repeatNearbyTraffic": "Повторять ближний трафик",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслировать через повторители по сети",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Только рядом, без flood повторителей",
|
||||
"multihop": "Многохоповый",
|
||||
"createProfile": "Создать профиль",
|
||||
"renameProfile": "Переименовать профиль",
|
||||
"newProfile": "Новый профиль",
|
||||
"manageProfiles": "Управление профилями",
|
||||
"enableProfilesToStartManagingThem": "Включите профили, чтобы начать управлять ими.",
|
||||
"openMessage": "Открыть сообщение",
|
||||
"jumpToTheRelatedSarMessage": "Перейти к связанному SAR-сообщению",
|
||||
"removeSarMarker": "Удалить SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Выберите назначение для отправки SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер отправлен в публичный канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер отправлен в комнату",
|
||||
"loadFromGallery": "Загрузить из галереи",
|
||||
"replaceImage": "Заменить изображение",
|
||||
"selectFromGallery": "Выбрать из галереи",
|
||||
"team": "Команда",
|
||||
"found": "Найдено",
|
||||
"staging": "Место сбора",
|
||||
"object": "Объект",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Умеренно",
|
||||
"busy": "Занято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканирование спектра не нашло частот-кандидатов",
|
||||
"searchMessages": "Поиск сообщений",
|
||||
"sendImageFromGallery": "Отправить изображение из галереи",
|
||||
"takePhoto": "Сделать фото",
|
||||
"dmOnly": "Только личные сообщения",
|
||||
"allMessages": "Все сообщения",
|
||||
"sendToPublicChannel": "Отправить в публичный канал?",
|
||||
"selectMarkerTypeAndDestination": "Выберите тип маркера и назначение",
|
||||
"noDestinationsAvailableLabel": "Нет доступных назначений",
|
||||
"image": "Изображение",
|
||||
"format": "Формат",
|
||||
"dimensions": "Размеры",
|
||||
"segments": "Сегменты",
|
||||
"transfers": "Передачи",
|
||||
"downloadedBy": "Загружено",
|
||||
"saveDiscoverySettings": "Сохранить настройки обнаружения",
|
||||
"savePublicInfo": "Сохранить публичную информацию",
|
||||
"saveRadioSettings": "Сохранить настройки радио",
|
||||
"savePath": "Сохранить путь",
|
||||
"wipeDeviceData": "Стереть данные устройства",
|
||||
"wipeDevice": "Стереть устройство",
|
||||
"destructiveDeviceActions": "Деструктивные действия устройства.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Выберите пресет или настройте радио вручную.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Выберите имя и местоположение, которыми делится устройство.",
|
||||
"availableSpaceOnThisDevice": "Доступное место на этом устройстве.",
|
||||
"used": "Использовано",
|
||||
"total": "Всего",
|
||||
"renameValue": "Переименовать значение",
|
||||
"customizeFields": "Настроить поля",
|
||||
"livePreview": "Предпросмотр в реальном времени",
|
||||
"refreshSchedule": "Расписание обновления",
|
||||
"noResponse": "Нет ответа",
|
||||
"refreshing": "Обновление",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Выберите реле или узел для наблюдения.",
|
||||
"publicKeyLabel": "Публичный ключ",
|
||||
"alreadyInContacts": "Уже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Подключитесь к устройству перед добавлением контактов",
|
||||
"fromContacts": "Из контактов",
|
||||
"onlineOnly": "Только онлайн",
|
||||
"inBoth": "В обоих",
|
||||
"source": "Источник",
|
||||
"manualRouteEdit": "Ручное редактирование маршрута",
|
||||
"observedMeshRoute": "Наблюдаемый mesh-маршрут",
|
||||
"allMessagesCleared": "Все сообщения очищены",
|
||||
"onlineTraceDatabaseCleared": "База трассировок очищена",
|
||||
"packetLogsCleared": "Журналы пакетов очищены",
|
||||
"hexDataCopiedToClipboard": "Hex-данные скопированы в буфер",
|
||||
"developerModeEnabled": "Режим разработчика включён",
|
||||
"developerModeDisabled": "Режим разработчика отключён",
|
||||
"clipboardIsEmpty": "Буфер обмена пуст",
|
||||
"contactImported": "Контакт импортирован",
|
||||
"contactLinkCopiedToClipboard": "Ссылка на контакт скопирована в буфер",
|
||||
"failedToExportContact": "Не удалось экспортировать контакт",
|
||||
"noLogsToExport": "Нет журналов для экспорта",
|
||||
"exportAsCsv": "Экспортировать как CSV",
|
||||
"exportAsText": "Экспортировать как текст",
|
||||
"receivedRfc3339": "Получено (RFC3339)",
|
||||
"buildTime": "Время сборки",
|
||||
"downloadUrlNotAvailable": "URL загрузки недоступен",
|
||||
"cannotOpenDownloadUrl": "Не удаётся открыть URL загрузки",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Проверка обновлений доступна только на Android",
|
||||
"youAreRunningTheLatestVersion": "Вы используете последнюю версию",
|
||||
"updateAvailableButDownloadUrlNotFound": "Обновление доступно, но URL загрузки не найден",
|
||||
"startTictactoe": "Начать Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: противник неизвестен",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: ожидание начала",
|
||||
"acceptsShareLinks": "Принимает общие ссылки",
|
||||
"supportsRawHex": "Поддерживает raw hex",
|
||||
"clipboardfriendly": "Удобно для буфера",
|
||||
"captured": "Захвачено",
|
||||
"size": "Размер",
|
||||
"noCustomChannelsToClear": "Нет пользовательских каналов для очистки.",
|
||||
"noDeviceContactsToClear": "Нет контактов устройства для очистки."
|
||||
}
|
||||
|
||||
@@ -511,8 +511,7 @@
|
||||
"rawDump": "Surovi izpis",
|
||||
"cannotRetryMissingRecipient": "Ponovnega pošiljanja ni mogoče: informacije o prejemniku manjkajo",
|
||||
"voiceUnavailable": "Glas trenutno ni na voljo",
|
||||
"requestingVoice": "Zahteva za glasom"
|
||||
,
|
||||
"requestingVoice": "Zahteva za glasom",
|
||||
"device": "naprava",
|
||||
"change": "Spremeni",
|
||||
"wizardOverviewDescription": "Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.",
|
||||
@@ -564,5 +563,252 @@
|
||||
"contactFailedToClearRoute": "Brisanje poti ni uspelo: {error}",
|
||||
"contactRouteCleared": "Pot izbrisana",
|
||||
"contactRouteSet": "Pot nastavljena: {route}",
|
||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}"
|
||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Časovna omejitev ACK",
|
||||
"opcode": "Opkoda",
|
||||
"payload": "Vsebina",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Velikost zgoščevalne vrednosti",
|
||||
"pathBytes": "Bajti poti",
|
||||
"selectedPath": "Izbrana pot",
|
||||
"estimatedTx": "Ocenjeni čas oddaje",
|
||||
"senderToReceipt": "Od pošiljatelja do prejema",
|
||||
"receivedCopies": "Prejete kopije",
|
||||
"retryCause": "Razlog ponovitve",
|
||||
"retryMode": "Način ponovitve",
|
||||
"retryResult": "Rezultat ponovitve",
|
||||
"lastRetry": "Zadnja ponovitev",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hitrost",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Zakasnitev po oddaji",
|
||||
"bandpass": "Pasovno sito",
|
||||
"bandpassFilterVoice": "Pasovno sito za glas",
|
||||
"frequency": "Frekvenca",
|
||||
"australia": "Avstralija",
|
||||
"australiaNarrow": "Avstralija (ozko)",
|
||||
"australiaQld": "Avstralija: QLD",
|
||||
"australiaSaWa": "Avstralija: SA, WA",
|
||||
"newZealand": "Nova Zelandija",
|
||||
"newZealandNarrow": "Nova Zelandija (ozko)",
|
||||
"switzerland": "Švica",
|
||||
"portugal433": "Portugalska 433",
|
||||
"portugal868": "Portugalska 868",
|
||||
"czechRepublicNarrow": "Češka (ozko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (dolg doseg)",
|
||||
"euukDeprecated": "EU/UK (zastarelo)",
|
||||
"euukNarrow": "EU/UK (ozko)",
|
||||
"usacanadaRecommended": "ZDA/Kanada (priporočeno)",
|
||||
"vietnamDeprecated": "Vietnam (zastarelo)",
|
||||
"vietnamNarrow": "Vietnam (ozko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj stik",
|
||||
"all": "Vse",
|
||||
"autoResolve": "Samodejno razreši",
|
||||
"clearAllLabel": "Počisti vse",
|
||||
"clearRelays": "Počisti posrednike",
|
||||
"clearFilters": "Počisti filtre",
|
||||
"clearRoute": "Počisti pot",
|
||||
"clearMessages": "Počisti sporočila",
|
||||
"clearScale": "Počisti merilo",
|
||||
"clearDiscoveries": "Počisti odkritja",
|
||||
"clearOnlineTraceDatabase": "Počisti bazo sledi",
|
||||
"clearAllChannels": "Počisti vse kanale",
|
||||
"clearAllContacts": "Počisti vse stike",
|
||||
"clearChannels": "Počisti kanale",
|
||||
"clearContacts": "Počisti stike",
|
||||
"clearPathOnMaxRetry": "Počisti pot ob maks. ponovitvi",
|
||||
"create": "Ustvari",
|
||||
"custom": "Po meri",
|
||||
"defaultValue": "Privzeto",
|
||||
"duplicate": "Podvoji",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Odpri",
|
||||
"paste": "Prilepi",
|
||||
"preview": "Predogled",
|
||||
"remove": "Odstrani",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razreši vse",
|
||||
"send": "Pošlji",
|
||||
"sendAnyway": "Vseeno pošlji",
|
||||
"share": "Deli",
|
||||
"shareContact": "Deli stik",
|
||||
"trace": "Sledenje",
|
||||
"use": "Uporabi",
|
||||
"useSelectedFrequency": "Uporabi izbrano frekvenco",
|
||||
"discovery": "Odkrivanje",
|
||||
"discoverRepeaters": "Odkrij posrednike",
|
||||
"discoverSensors": "Odkrij senzorje",
|
||||
"repeaterDiscoverySent": "Odkrivanje posrednikov poslano",
|
||||
"sensorDiscoverySent": "Odkrivanje senzorjev poslano",
|
||||
"clearedPendingDiscoveries": "Čakalna odkritja počiščena.",
|
||||
"autoDiscovery": "Samodejno odkrivanje",
|
||||
"enableAutomaticAdding": "Omogoči samodejno dodajanje",
|
||||
"autoaddRepeaters": "Samodejno dodaj posrednike",
|
||||
"autoaddRoomServers": "Samodejno dodaj strežnike sob",
|
||||
"autoaddSensors": "Samodejno dodaj senzorje",
|
||||
"autoaddUsers": "Samodejno dodaj uporabnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarejše ob polnem",
|
||||
"storage": "Shramba",
|
||||
"dangerZone": "Nevarno območje",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Priljubljeni",
|
||||
"sensors": "Senzorji",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet v živo",
|
||||
"repeatersMap": "Zemljevid posrednikov",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "Dnevniki BLE paketov",
|
||||
"onlineTraceDatabase": "Baza sledi",
|
||||
"routePathByteSize": "Velikost poti v bajtih",
|
||||
"messageNotifications": "Obvestila o sporočilih",
|
||||
"sarAlerts": "SAR opozorila",
|
||||
"discoveryNotifications": "Obvestila o odkritjih",
|
||||
"updateNotifications": "Obvestila o posodobitvah",
|
||||
"muteWhileAppIsOpen": "Utišaj ko je aplikacija odprta",
|
||||
"disableContacts": "Onemogoči stike",
|
||||
"enableSensorsTab": "Omogoči zavihek Senzorji",
|
||||
"enableProfiles": "Omogoči profile",
|
||||
"autoRouteRotation": "Samodejno kroženje poti",
|
||||
"nearestRepeaterFallback": "Najbližji posrednik kot rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši vso shranjeno zgodovino sporočil",
|
||||
"messageFontSize": "Velikost pisave sporočil",
|
||||
"rotateMapWithHeading": "Vrti zemljevid s smerjo",
|
||||
"showMapDebugInfo": "Prikaži razhroščevalne info zemljevida",
|
||||
"openMapInFullscreen": "Odpri zemljevid na cel zaslon",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnem zemljevidu",
|
||||
"showAllContactTrailsLabel": "Prikaži vse sledi stikov",
|
||||
"hideRepeatersOnMap": "Skrij posrednike na zemljevidu",
|
||||
"setMapScale": "Nastavi merilo zemljevida",
|
||||
"customMapScaleSaved": "Merilo zemljevida shranjeno",
|
||||
"voiceBitrate": "Bitna hitrost glasu",
|
||||
"voiceCompressor": "Kompresor glasu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe in glasne govorne ravni",
|
||||
"voiceLimiter": "Omejevalnik glasu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Preprečuje rezanje vrhov pred kodiranjem",
|
||||
"micAutoGain": "Samodejna ojačitev mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Omogoči snemalniku prilagoditev vhodne ravni",
|
||||
"echoCancellation": "Odstranjevanje odmeva",
|
||||
"noiseSuppression": "Dušenje šuma",
|
||||
"trimSilenceInVoiceMessages": "Obreži tišino v glasovnih sporočilih",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Omejevalnik",
|
||||
"autoGain": "Samodejna ojačitev",
|
||||
"echoCancel": "Odmev",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Največja velikost slike",
|
||||
"imageCompression": "Stiskanje slik",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Hitri zasebni GPS podatki",
|
||||
"movementThreshold": "Prag gibanja",
|
||||
"fastGpsMovementThreshold": "Prag gibanja za hitri GPS",
|
||||
"fastGpsActiveuseInterval": "Interval uporabe hitrega GPS-a",
|
||||
"activeuseUpdateInterval": "Interval posodobitve pri aktivni uporabi",
|
||||
"repeatNearbyTraffic": "Ponavljaj bližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Posreduj skozi posrednike po mreži",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo bližnji, brez poplavljanja posrednikov",
|
||||
"multihop": "Večskočno",
|
||||
"createProfile": "Ustvari profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Nov profil",
|
||||
"manageProfiles": "Upravljaj profile",
|
||||
"enableProfilesToStartManagingThem": "Omogočite profile, da jih začnete upravljati.",
|
||||
"openMessage": "Odpri sporočilo",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezano SAR sporočilo",
|
||||
"removeSarMarker": "Odstrani SAR oznako",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Izberite cilj za pošiljanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka poslana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana v sobo",
|
||||
"loadFromGallery": "Naloži iz galerije",
|
||||
"replaceImage": "Zamenjaj sliko",
|
||||
"selectFromGallery": "Izberi iz galerije",
|
||||
"team": "Ekipa",
|
||||
"found": "Najdeno",
|
||||
"staging": "Zbirno mesto",
|
||||
"object": "Predmet",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Zmerno",
|
||||
"busy": "Zasedeno",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje ni našlo primernih frekvenc",
|
||||
"searchMessages": "Iskanje sporočil",
|
||||
"sendImageFromGallery": "Pošlji sliko iz galerije",
|
||||
"takePhoto": "Posnemi fotografijo",
|
||||
"dmOnly": "Samo neposredno",
|
||||
"allMessages": "Vsa sporočila",
|
||||
"sendToPublicChannel": "Pošlji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Izberite vrsto oznake in cilj",
|
||||
"noDestinationsAvailableLabel": "Ni razpoložljivih ciljev",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prenosi",
|
||||
"downloadedBy": "Preneseno od",
|
||||
"saveDiscoverySettings": "Shrani nastavitve odkrivanja",
|
||||
"savePublicInfo": "Shrani javne podatke",
|
||||
"saveRadioSettings": "Shrani radijske nastavitve",
|
||||
"savePath": "Shrani pot",
|
||||
"wipeDeviceData": "Izbriši podatke naprave",
|
||||
"wipeDevice": "Izbriši napravo",
|
||||
"destructiveDeviceActions": "Destruktivna dejanja na napravi.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Izberite prednastavitev ali natančno prilagodite radijske nastavitve.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Izberite ime in lokacijo, ki ju naprava deli.",
|
||||
"availableSpaceOnThisDevice": "Razpoložljiv prostor na tej napravi.",
|
||||
"used": "Uporabljeno",
|
||||
"total": "Skupaj",
|
||||
"renameValue": "Preimenuj vrednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Predogled v živo",
|
||||
"refreshSchedule": "Interval osvežitve",
|
||||
"noResponse": "Ni odgovora",
|
||||
"refreshing": "Osvežujem",
|
||||
"unavailable": "Nedostopno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Izberite posrednika ali vozlišče za opazovanje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Že med stiki",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se z napravo pred dodajanjem stikov",
|
||||
"fromContacts": "Iz stikov",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "V obeh",
|
||||
"source": "Vir",
|
||||
"manualRouteEdit": "Ročno urejanje poti",
|
||||
"observedMeshRoute": "Opazovana mrežna pot",
|
||||
"allMessagesCleared": "Vsa sporočila počiščena",
|
||||
"onlineTraceDatabaseCleared": "Baza sledi počiščena",
|
||||
"packetLogsCleared": "Dnevniki paketov počiščeni",
|
||||
"hexDataCopiedToClipboard": "Hex podatki kopirani v odložišče",
|
||||
"developerModeEnabled": "Razvojni način omogočen",
|
||||
"developerModeDisabled": "Razvojni način onemogočen",
|
||||
"clipboardIsEmpty": "Odložišče je prazno",
|
||||
"contactImported": "Stik uvožen",
|
||||
"contactLinkCopiedToClipboard": "Povezava na stik kopirana v odložišče",
|
||||
"failedToExportContact": "Izvoz stika ni uspel",
|
||||
"noLogsToExport": "Ni dnevnikov za izvoz",
|
||||
"exportAsCsv": "Izvozi kot CSV",
|
||||
"exportAsText": "Izvozi kot besedilo",
|
||||
"receivedRfc3339": "Prejeto (RFC3339)",
|
||||
"buildTime": "Čas gradnje",
|
||||
"downloadUrlNotAvailable": "URL za prenos ni na voljo",
|
||||
"cannotOpenDownloadUrl": "URL za prenos ni mogoče odpreti",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Preverjanje posodobitev je na voljo le na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Uporabljate najnovejšo različico",
|
||||
"updateAvailableButDownloadUrlNotFound": "Posodobitev na voljo, a URL za prenos ni najden",
|
||||
"startTictactoe": "Začni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe ni na voljo",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: nasprotnik neznan",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čakanje na začetek",
|
||||
"acceptsShareLinks": "Sprejema deljene povezave",
|
||||
"supportsRawHex": "Podpira surove hex",
|
||||
"clipboardfriendly": "Primerno za odložišče",
|
||||
"captured": "Zajeto",
|
||||
"size": "Velikost",
|
||||
"noCustomChannelsToClear": "Ni prilagojenih kanalov za brisanje.",
|
||||
"noDeviceContactsToClear": "Ni stikov naprave za brisanje."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Rota temizlenemedi: {error}",
|
||||
"contactRouteCleared": "Rota temizlendi",
|
||||
"contactRouteSet": "Rota ayarlandı: {route}",
|
||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}"
|
||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK zaman aşımı",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Yük",
|
||||
"hops": "Atlama",
|
||||
"hashSize": "Hash boyutu",
|
||||
"pathBytes": "Yol baytları",
|
||||
"selectedPath": "Seçilen yol",
|
||||
"estimatedTx": "Tahmini gönderim",
|
||||
"senderToReceipt": "Gönderenden alındıya",
|
||||
"receivedCopies": "Alınan kopyalar",
|
||||
"retryCause": "Yeniden deneme nedeni",
|
||||
"retryMode": "Yeniden deneme modu",
|
||||
"retryResult": "Yeniden deneme sonucu",
|
||||
"lastRetry": "Son deneme",
|
||||
"rxPackets": "RX paketleri",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hız",
|
||||
"window": "Pencere",
|
||||
"posttxDelay": "Gönderim sonrası gecikme",
|
||||
"bandpass": "Bant geçiren",
|
||||
"bandpassFilterVoice": "Bant geçiren filtre ses",
|
||||
"frequency": "Frekans",
|
||||
"australia": "Avustralya",
|
||||
"australiaNarrow": "Avustralya (Dar)",
|
||||
"australiaQld": "Avustralya: QLD",
|
||||
"australiaSaWa": "Avustralya: SA, WA",
|
||||
"newZealand": "Yeni Zelanda",
|
||||
"newZealandNarrow": "Yeni Zelanda (Dar)",
|
||||
"switzerland": "İsviçre",
|
||||
"portugal433": "Portekiz 433",
|
||||
"portugal868": "Portekiz 868",
|
||||
"czechRepublicNarrow": "Çek Cumhuriyeti (Dar)",
|
||||
"eu433mhzLongRange": "AB 433MHz (Uzun Menzil)",
|
||||
"euukDeprecated": "AB/BK (Kullanımdan kalktı)",
|
||||
"euukNarrow": "AB/BK (Dar)",
|
||||
"usacanadaRecommended": "ABD/Kanada (Önerilen)",
|
||||
"vietnamDeprecated": "Vietnam (Kullanımdan kalktı)",
|
||||
"vietnamNarrow": "Vietnam (Dar)",
|
||||
"active": "Aktif",
|
||||
"addContact": "Kişi ekle",
|
||||
"all": "Tümü",
|
||||
"autoResolve": "Otomatik çöz",
|
||||
"clearAllLabel": "Tümünü temizle",
|
||||
"clearRelays": "Röleleri temizle",
|
||||
"clearFilters": "Filtreleri temizle",
|
||||
"clearRoute": "Rotayı temizle",
|
||||
"clearMessages": "Mesajları temizle",
|
||||
"clearScale": "Ölçeği temizle",
|
||||
"clearDiscoveries": "Keşifleri temizle",
|
||||
"clearOnlineTraceDatabase": "Çevrimiçi iz veritabanını temizle",
|
||||
"clearAllChannels": "Tüm kanalları temizle",
|
||||
"clearAllContacts": "Tüm kişileri temizle",
|
||||
"clearChannels": "Kanalları temizle",
|
||||
"clearContacts": "Kişileri temizle",
|
||||
"clearPathOnMaxRetry": "Maks denemede yolu temizle",
|
||||
"create": "Oluştur",
|
||||
"custom": "Özel",
|
||||
"defaultValue": "Varsayılan",
|
||||
"duplicate": "Çoğalt",
|
||||
"editName": "Adı düzenle",
|
||||
"open": "Aç",
|
||||
"paste": "Yapıştır",
|
||||
"preview": "Önizleme",
|
||||
"remove": "Kaldır",
|
||||
"rename": "Yeniden adlandır",
|
||||
"resolveAll": "Tümünü çöz",
|
||||
"send": "Gönder",
|
||||
"sendAnyway": "Yine de gönder",
|
||||
"share": "Paylaş",
|
||||
"shareContact": "Kişiyi paylaş",
|
||||
"trace": "İz",
|
||||
"use": "Kullan",
|
||||
"useSelectedFrequency": "Seçilen frekansı kullan",
|
||||
"discovery": "Keşif",
|
||||
"discoverRepeaters": "Tekrarlayıcıları keşfet",
|
||||
"discoverSensors": "Sensörleri keşfet",
|
||||
"repeaterDiscoverySent": "Tekrarlayıcı keşfi gönderildi",
|
||||
"sensorDiscoverySent": "Sensör keşfi gönderildi",
|
||||
"clearedPendingDiscoveries": "Bekleyen keşifler temizlendi.",
|
||||
"autoDiscovery": "Otomatik keşif",
|
||||
"enableAutomaticAdding": "Otomatik eklemeyi etkinleştir",
|
||||
"autoaddRepeaters": "Tekrarlayıcıları otomatik ekle",
|
||||
"autoaddRoomServers": "Oda sunucularını otomatik ekle",
|
||||
"autoaddSensors": "Sensörleri otomatik ekle",
|
||||
"autoaddUsers": "Kullanıcıları otomatik ekle",
|
||||
"overwriteOldestWhenFull": "Dolduğunda en eskiyi üzerine yaz",
|
||||
"storage": "Depolama",
|
||||
"dangerZone": "Tehlikeli bölge",
|
||||
"profiles": "Profiller",
|
||||
"favourites": "Favoriler",
|
||||
"sensors": "Sensörler",
|
||||
"others": "Diğerleri",
|
||||
"gpsModule": "GPS Modülü",
|
||||
"liveTraffic": "Canlı trafik",
|
||||
"repeatersMap": "Tekrarlayıcılar haritası",
|
||||
"spectrumScan": "Spektrum taraması",
|
||||
"blePacketLogs": "BLE paket günlükleri",
|
||||
"onlineTraceDatabase": "Çevrimiçi iz veritabanı",
|
||||
"routePathByteSize": "Yol bayt boyutu",
|
||||
"messageNotifications": "Mesaj bildirimleri",
|
||||
"sarAlerts": "SAR uyarıları",
|
||||
"discoveryNotifications": "Keşif bildirimleri",
|
||||
"updateNotifications": "Güncelleme bildirimleri",
|
||||
"muteWhileAppIsOpen": "Uygulama açıkken sessize al",
|
||||
"disableContacts": "Kişileri devre dışı bırak",
|
||||
"enableSensorsTab": "Sensörler sekmesini etkinleştir",
|
||||
"enableProfiles": "Profilleri etkinleştir",
|
||||
"autoRouteRotation": "Otomatik rota rotasyonu",
|
||||
"nearestRepeaterFallback": "En yakın tekrarlayıcıya geri dönüş",
|
||||
"deleteAllStoredMessageHistory": "Tüm mesaj geçmişini sil",
|
||||
"messageFontSize": "Mesaj yazı tipi boyutu",
|
||||
"rotateMapWithHeading": "Haritayı yön ile döndür",
|
||||
"showMapDebugInfo": "Harita hata ayıklama bilgisini göster",
|
||||
"openMapInFullscreen": "Haritayı tam ekranda aç",
|
||||
"showSarMarkersLabel": "SAR işaretçilerini göster",
|
||||
"displaySarMarkersOnTheMainMap": "SAR işaretçilerini ana haritada göster",
|
||||
"showAllContactTrailsLabel": "Tüm kişi izlerini göster",
|
||||
"hideRepeatersOnMap": "Haritada tekrarlayıcıları gizle",
|
||||
"setMapScale": "Harita ölçeğini ayarla",
|
||||
"customMapScaleSaved": "Özel harita ölçeği kaydedildi",
|
||||
"voiceBitrate": "Ses bit hızı",
|
||||
"voiceCompressor": "Ses sıkıştırıcı",
|
||||
"balancesQuietAndLoudSpeechLevels": "Sessiz ve yüksek konuşma seviyelerini dengeler",
|
||||
"voiceLimiter": "Ses sınırlayıcı",
|
||||
"preventsClippingPeaksBeforeEncoding": "Kodlama öncesi tepe kırpmasını önler",
|
||||
"micAutoGain": "Mikrofon otomatik kazancı",
|
||||
"letsTheRecorderAdjustInputLevel": "Kaydedicinin giriş seviyesini ayarlamasına izin verir",
|
||||
"echoCancellation": "Yankı iptali",
|
||||
"noiseSuppression": "Gürültü bastırma",
|
||||
"trimSilenceInVoiceMessages": "Sesli mesajlarda sessizliği kırp",
|
||||
"compressor": "Sıkıştırıcı",
|
||||
"limiter": "Sınırlayıcı",
|
||||
"autoGain": "Otomatik kazanç",
|
||||
"echoCancel": "Yankı",
|
||||
"noiseSuppress": "Gürültü",
|
||||
"silenceTrim": "Sessizlik",
|
||||
"maxImageSize": "Maksimum resim boyutu",
|
||||
"imageCompression": "Resim sıkıştırma",
|
||||
"grayscale": "Gri tonlama",
|
||||
"ultraMode": "Ultra mod",
|
||||
"fastPrivateGpsUpdates": "Hızlı özel GPS güncellemeleri",
|
||||
"movementThreshold": "Hareket eşiği",
|
||||
"fastGpsMovementThreshold": "Hızlı GPS hareket eşiği",
|
||||
"fastGpsActiveuseInterval": "Hızlı GPS aktif kullanım aralığı",
|
||||
"activeuseUpdateInterval": "Aktif kullanım güncelleme aralığı",
|
||||
"repeatNearbyTraffic": "Yakın trafiği tekrarla",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Yalnızca yakın, tekrarlayıcı taşkını olmadan",
|
||||
"multihop": "Çok atlamalı",
|
||||
"createProfile": "Profil oluştur",
|
||||
"renameProfile": "Profili yeniden adlandır",
|
||||
"newProfile": "Yeni profil",
|
||||
"manageProfiles": "Profilleri yönet",
|
||||
"enableProfilesToStartManagingThem": "Profilleri yönetmeye başlamak için etkinleştirin.",
|
||||
"openMessage": "Mesajı aç",
|
||||
"jumpToTheRelatedSarMessage": "İlgili SAR mesajına git",
|
||||
"removeSarMarker": "SAR işaretçisini kaldır",
|
||||
"pleaseSelectADestinationToSendSarMarker": "SAR işaretçisi göndermek için bir hedef seçin",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR işaretçisi genel kanala yayınlandı",
|
||||
"sarMarkerSentToRoom": "SAR işaretçisi odaya gönderildi",
|
||||
"loadFromGallery": "Galeriden yükle",
|
||||
"replaceImage": "Resmi değiştir",
|
||||
"selectFromGallery": "Galeriden seç",
|
||||
"team": "Takım",
|
||||
"found": "Bulundu",
|
||||
"staging": "Toplanma alanı",
|
||||
"object": "Nesne",
|
||||
"quiet": "Sessiz",
|
||||
"moderate": "Orta",
|
||||
"busy": "Meşgul",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum taraması aday frekans bulamadı",
|
||||
"searchMessages": "Mesajlarda ara",
|
||||
"sendImageFromGallery": "Galeriden resim gönder",
|
||||
"takePhoto": "Fotoğraf çek",
|
||||
"dmOnly": "Yalnızca DM",
|
||||
"allMessages": "Tüm mesajlar",
|
||||
"sendToPublicChannel": "Genel kanala gönderilsin mi?",
|
||||
"selectMarkerTypeAndDestination": "İşaretçi türünü ve hedefi seçin",
|
||||
"noDestinationsAvailableLabel": "Kullanılabilir hedef yok",
|
||||
"image": "Resim",
|
||||
"format": "Biçim",
|
||||
"dimensions": "Boyutlar",
|
||||
"segments": "Segmentler",
|
||||
"transfers": "Transferler",
|
||||
"downloadedBy": "İndiren",
|
||||
"saveDiscoverySettings": "Keşif ayarlarını kaydet",
|
||||
"savePublicInfo": "Genel bilgileri kaydet",
|
||||
"saveRadioSettings": "Radyo ayarlarını kaydet",
|
||||
"savePath": "Yolu kaydet",
|
||||
"wipeDeviceData": "Cihaz verilerini sil",
|
||||
"wipeDevice": "Cihazı sil",
|
||||
"destructiveDeviceActions": "Yıkıcı cihaz işlemleri.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Bu cihazın paylaştığı ad ve konumu seçin.",
|
||||
"availableSpaceOnThisDevice": "Bu cihazda kullanılabilir alan.",
|
||||
"used": "Kullanılan",
|
||||
"total": "Toplam",
|
||||
"renameValue": "Değeri yeniden adlandır",
|
||||
"customizeFields": "Alanları özelleştir",
|
||||
"livePreview": "Canlı önizleme",
|
||||
"refreshSchedule": "Yenileme programı",
|
||||
"noResponse": "Yanıt yok",
|
||||
"refreshing": "Yenileniyor",
|
||||
"unavailable": "Kullanılamaz",
|
||||
"pickARelayOrNodeToWatchInSensors": "İzlemek için bir röle veya düğüm seçin.",
|
||||
"publicKeyLabel": "Genel anahtar",
|
||||
"alreadyInContacts": "Zaten kişilerde",
|
||||
"connectToADeviceBeforeAddingContacts": "Kişi eklemeden önce bir cihaza bağlanın",
|
||||
"fromContacts": "Kişilerden",
|
||||
"onlineOnly": "Yalnızca çevrimiçi",
|
||||
"inBoth": "Her ikisinde",
|
||||
"source": "Kaynak",
|
||||
"manualRouteEdit": "Manuel rota düzenle",
|
||||
"observedMeshRoute": "Gözlemlenen mesh rotası",
|
||||
"allMessagesCleared": "Tüm mesajlar temizlendi",
|
||||
"onlineTraceDatabaseCleared": "Çevrimiçi iz veritabanı temizlendi",
|
||||
"packetLogsCleared": "Paket günlükleri temizlendi",
|
||||
"hexDataCopiedToClipboard": "Hex verisi panoya kopyalandı",
|
||||
"developerModeEnabled": "Geliştirici modu etkinleştirildi",
|
||||
"developerModeDisabled": "Geliştirici modu devre dışı bırakıldı",
|
||||
"clipboardIsEmpty": "Pano boş",
|
||||
"contactImported": "Kişi içe aktarıldı",
|
||||
"contactLinkCopiedToClipboard": "Kişi bağlantısı panoya kopyalandı",
|
||||
"failedToExportContact": "Kişi dışa aktarılamadı",
|
||||
"noLogsToExport": "Dışa aktarılacak günlük yok",
|
||||
"exportAsCsv": "CSV olarak dışa aktar",
|
||||
"exportAsText": "Metin olarak dışa aktar",
|
||||
"receivedRfc3339": "Alındı (RFC3339)",
|
||||
"buildTime": "Derleme zamanı",
|
||||
"downloadUrlNotAvailable": "İndirme URL'si mevcut değil",
|
||||
"cannotOpenDownloadUrl": "İndirme URL'si açılamıyor",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Güncelleme kontrolü yalnızca Android'de kullanılabilir",
|
||||
"youAreRunningTheLatestVersion": "En son sürümü kullanıyorsunuz",
|
||||
"updateAvailableButDownloadUrlNotFound": "Güncelleme mevcut ancak indirme URL'si bulunamadı",
|
||||
"startTictactoe": "Tic-Tac-Toe başlat",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe kullanılamıyor",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: rakip bilinmiyor",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: başlangıç bekleniyor",
|
||||
"acceptsShareLinks": "Paylaşım bağlantılarını kabul eder",
|
||||
"supportsRawHex": "Ham hex destekler",
|
||||
"clipboardfriendly": "Pano dostu",
|
||||
"captured": "Yakalandı",
|
||||
"size": "Boyut",
|
||||
"noCustomChannelsToClear": "Temizlenecek özel kanal yok.",
|
||||
"noDeviceContactsToClear": "Temizlenecek cihaz kişisi yok."
|
||||
}
|
||||
|
||||
@@ -2328,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "Не вдалося очистити маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищено",
|
||||
"contactRouteSet": "Маршрут задано: {route}",
|
||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}"
|
||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Тайм-аут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Корисне навантаження",
|
||||
"hops": "Хопи",
|
||||
"hashSize": "Розмір хешу",
|
||||
"pathBytes": "Байти шляху",
|
||||
"selectedPath": "Обраний шлях",
|
||||
"estimatedTx": "Очікувана передача",
|
||||
"senderToReceipt": "Від відправника до отримання",
|
||||
"receivedCopies": "Отримані копії",
|
||||
"retryCause": "Причина повтору",
|
||||
"retryMode": "Режим повтору",
|
||||
"retryResult": "Результат повтору",
|
||||
"lastRetry": "Остання спроба",
|
||||
"rxPackets": "RX-пакети",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Швидкість",
|
||||
"window": "Вікно",
|
||||
"posttxDelay": "Затримка після передачі",
|
||||
"bandpass": "Смуговий",
|
||||
"bandpassFilterVoice": "Смуговий фільтр голосу",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралія",
|
||||
"australiaNarrow": "Австралія (Вузький)",
|
||||
"australiaQld": "Австралія: QLD",
|
||||
"australiaSaWa": "Австралія: SA, WA",
|
||||
"newZealand": "Нова Зеландія",
|
||||
"newZealandNarrow": "Нова Зеландія (Вузький)",
|
||||
"switzerland": "Швейцарія",
|
||||
"portugal433": "Португалія 433",
|
||||
"portugal868": "Португалія 868",
|
||||
"czechRepublicNarrow": "Чехія (Вузький)",
|
||||
"eu433mhzLongRange": "ЄС 433МГц (Далекий)",
|
||||
"euukDeprecated": "ЄС/Великобританія (Застаріле)",
|
||||
"euukNarrow": "ЄС/Великобританія (Вузький)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендовано)",
|
||||
"vietnamDeprecated": "В'єтнам (Застаріле)",
|
||||
"vietnamNarrow": "В'єтнам (Вузький)",
|
||||
"active": "Активно",
|
||||
"addContact": "Додати контакт",
|
||||
"all": "Усі",
|
||||
"autoResolve": "Автоматичне вирішення",
|
||||
"clearAllLabel": "Очистити все",
|
||||
"clearRelays": "Очистити реле",
|
||||
"clearFilters": "Очистити фільтри",
|
||||
"clearRoute": "Очистити маршрут",
|
||||
"clearMessages": "Очистити повідомлення",
|
||||
"clearScale": "Очистити масштаб",
|
||||
"clearDiscoveries": "Очистити виявлення",
|
||||
"clearOnlineTraceDatabase": "Очистити базу трасувань",
|
||||
"clearAllChannels": "Очистити всі канали",
|
||||
"clearAllContacts": "Очистити всі контакти",
|
||||
"clearChannels": "Очистити канали",
|
||||
"clearContacts": "Очистити контакти",
|
||||
"clearPathOnMaxRetry": "Очистити шлях при макс. спробі",
|
||||
"create": "Створити",
|
||||
"custom": "Користувацький",
|
||||
"defaultValue": "За замовчуванням",
|
||||
"duplicate": "Дублювати",
|
||||
"editName": "Редагувати ім'я",
|
||||
"open": "Відкрити",
|
||||
"paste": "Вставити",
|
||||
"preview": "Попередній перегляд",
|
||||
"remove": "Видалити",
|
||||
"rename": "Перейменувати",
|
||||
"resolveAll": "Вирішити все",
|
||||
"send": "Надіслати",
|
||||
"sendAnyway": "Надіслати все одно",
|
||||
"share": "Поділитися",
|
||||
"shareContact": "Поділитися контактом",
|
||||
"trace": "Трасування",
|
||||
"use": "Використати",
|
||||
"useSelectedFrequency": "Використати обрану частоту",
|
||||
"discovery": "Виявлення",
|
||||
"discoverRepeaters": "Виявити повторювачі",
|
||||
"discoverSensors": "Виявити датчики",
|
||||
"repeaterDiscoverySent": "Виявлення повторювачів надіслано",
|
||||
"sensorDiscoverySent": "Виявлення датчиків надіслано",
|
||||
"clearedPendingDiscoveries": "Очікувані виявлення очищено.",
|
||||
"autoDiscovery": "Автоматичне виявлення",
|
||||
"enableAutomaticAdding": "Увімкнути автоматичне додавання",
|
||||
"autoaddRepeaters": "Автододавання повторювачів",
|
||||
"autoaddRoomServers": "Автододавання серверів кімнат",
|
||||
"autoaddSensors": "Автододавання датчиків",
|
||||
"autoaddUsers": "Автододавання користувачів",
|
||||
"overwriteOldestWhenFull": "Перезаписати найстаріші при заповненні",
|
||||
"storage": "Сховище",
|
||||
"dangerZone": "Небезпечна зона",
|
||||
"profiles": "Профілі",
|
||||
"favourites": "Обране",
|
||||
"sensors": "Датчики",
|
||||
"others": "Інші",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живий трафік",
|
||||
"repeatersMap": "Карта повторювачів",
|
||||
"spectrumScan": "Сканування спектру",
|
||||
"blePacketLogs": "Журнали BLE-пакетів",
|
||||
"onlineTraceDatabase": "База трасувань",
|
||||
"routePathByteSize": "Розмір шляху в байтах",
|
||||
"messageNotifications": "Сповіщення про повідомлення",
|
||||
"sarAlerts": "SAR-сповіщення",
|
||||
"discoveryNotifications": "Сповіщення про виявлення",
|
||||
"updateNotifications": "Сповіщення про оновлення",
|
||||
"muteWhileAppIsOpen": "Без звуку при відкритому додатку",
|
||||
"disableContacts": "Вимкнути контакти",
|
||||
"enableSensorsTab": "Увімкнути вкладку Датчики",
|
||||
"enableProfiles": "Увімкнути профілі",
|
||||
"autoRouteRotation": "Автоматична ротація маршруту",
|
||||
"nearestRepeaterFallback": "Найближчий повторювач як резерв",
|
||||
"deleteAllStoredMessageHistory": "Видалити всю історію повідомлень",
|
||||
"messageFontSize": "Розмір шрифту повідомлень",
|
||||
"rotateMapWithHeading": "Обертати карту за напрямком",
|
||||
"showMapDebugInfo": "Показати налагоджувальну інфо карти",
|
||||
"openMapInFullscreen": "Відкрити карту на повний екран",
|
||||
"showSarMarkersLabel": "Показати SAR-маркери",
|
||||
"displaySarMarkersOnTheMainMap": "Показувати SAR-маркери на головній карті",
|
||||
"showAllContactTrailsLabel": "Показати всі сліди контактів",
|
||||
"hideRepeatersOnMap": "Сховати повторювачі на карті",
|
||||
"setMapScale": "Встановити масштаб карти",
|
||||
"customMapScaleSaved": "Користувацький масштаб карти збережено",
|
||||
"voiceBitrate": "Бітрейт голосу",
|
||||
"voiceCompressor": "Компресор голосу",
|
||||
"balancesQuietAndLoudSpeechLevels": "Вирівнює тихий і гучний рівні мови",
|
||||
"voiceLimiter": "Лімітер голосу",
|
||||
"preventsClippingPeaksBeforeEncoding": "Запобігає обрізанню піків перед кодуванням",
|
||||
"micAutoGain": "Автопідсилення мікрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Дозволяє рекордеру налаштувати рівень входу",
|
||||
"echoCancellation": "Придушення ехо",
|
||||
"noiseSuppression": "Придушення шуму",
|
||||
"trimSilenceInVoiceMessages": "Обрізати тишу в голосових повідомленнях",
|
||||
"compressor": "Компресор",
|
||||
"limiter": "Лімітер",
|
||||
"autoGain": "Автопідсилення",
|
||||
"echoCancel": "Ехо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тиша",
|
||||
"maxImageSize": "Максимальний розмір зображення",
|
||||
"imageCompression": "Стиснення зображення",
|
||||
"grayscale": "Відтінки сірого",
|
||||
"ultraMode": "Ультра режим",
|
||||
"fastPrivateGpsUpdates": "Швидкі приватні GPS-оновлення",
|
||||
"movementThreshold": "Поріг руху",
|
||||
"fastGpsMovementThreshold": "Поріг руху швидкого GPS",
|
||||
"fastGpsActiveuseInterval": "Інтервал активного використання швидкого GPS",
|
||||
"activeuseUpdateInterval": "Інтервал оновлення при активному використанні",
|
||||
"repeatNearbyTraffic": "Повторювати близький трафік",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслювати через повторювачі по мережі",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Лише поблизу, без flood повторювачів",
|
||||
"multihop": "Багатохоповий",
|
||||
"createProfile": "Створити профіль",
|
||||
"renameProfile": "Перейменувати профіль",
|
||||
"newProfile": "Новий профіль",
|
||||
"manageProfiles": "Керувати профілями",
|
||||
"enableProfilesToStartManagingThem": "Увімкніть профілі, щоб почати керувати ними.",
|
||||
"openMessage": "Відкрити повідомлення",
|
||||
"jumpToTheRelatedSarMessage": "Перейти до пов'язаного SAR-повідомлення",
|
||||
"removeSarMarker": "Видалити SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Виберіть призначення для надсилання SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер надіслано на публічний канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер надіслано в кімнату",
|
||||
"loadFromGallery": "Завантажити з галереї",
|
||||
"replaceImage": "Замінити зображення",
|
||||
"selectFromGallery": "Вибрати з галереї",
|
||||
"team": "Команда",
|
||||
"found": "Знайдено",
|
||||
"staging": "Місце збору",
|
||||
"object": "Об'єкт",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Помірно",
|
||||
"busy": "Зайнято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканування спектру не знайшло частот-кандидатів",
|
||||
"searchMessages": "Пошук повідомлень",
|
||||
"sendImageFromGallery": "Надіслати зображення з галереї",
|
||||
"takePhoto": "Зробити фото",
|
||||
"dmOnly": "Лише особисті повідомлення",
|
||||
"allMessages": "Усі повідомлення",
|
||||
"sendToPublicChannel": "Надіслати в публічний канал?",
|
||||
"selectMarkerTypeAndDestination": "Виберіть тип маркера та призначення",
|
||||
"noDestinationsAvailableLabel": "Немає доступних призначень",
|
||||
"image": "Зображення",
|
||||
"format": "Формат",
|
||||
"dimensions": "Розміри",
|
||||
"segments": "Сегменти",
|
||||
"transfers": "Передачі",
|
||||
"downloadedBy": "Завантажено",
|
||||
"saveDiscoverySettings": "Зберегти налаштування виявлення",
|
||||
"savePublicInfo": "Зберегти публічну інформацію",
|
||||
"saveRadioSettings": "Зберегти налаштування радіо",
|
||||
"savePath": "Зберегти шлях",
|
||||
"wipeDeviceData": "Стерти дані пристрою",
|
||||
"wipeDevice": "Стерти пристрій",
|
||||
"destructiveDeviceActions": "Деструктивні дії пристрою.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Оберіть пресет або налаштуйте радіо вручну.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Оберіть ім'я та місцеположення, якими ділиться пристрій.",
|
||||
"availableSpaceOnThisDevice": "Доступний простір на цьому пристрої.",
|
||||
"used": "Використано",
|
||||
"total": "Загалом",
|
||||
"renameValue": "Перейменувати значення",
|
||||
"customizeFields": "Налаштувати поля",
|
||||
"livePreview": "Попередній перегляд наживо",
|
||||
"refreshSchedule": "Розклад оновлення",
|
||||
"noResponse": "Немає відповіді",
|
||||
"refreshing": "Оновлення",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Оберіть реле або вузол для спостереження.",
|
||||
"publicKeyLabel": "Публічний ключ",
|
||||
"alreadyInContacts": "Вже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Підключіться до пристрою перед додаванням контактів",
|
||||
"fromContacts": "З контактів",
|
||||
"onlineOnly": "Лише онлайн",
|
||||
"inBoth": "В обох",
|
||||
"source": "Джерело",
|
||||
"manualRouteEdit": "Ручне редагування маршруту",
|
||||
"observedMeshRoute": "Спостережуваний mesh-маршрут",
|
||||
"allMessagesCleared": "Усі повідомлення очищено",
|
||||
"onlineTraceDatabaseCleared": "Базу трасувань очищено",
|
||||
"packetLogsCleared": "Журнали пакетів очищено",
|
||||
"hexDataCopiedToClipboard": "Hex-дані скопійовано в буфер",
|
||||
"developerModeEnabled": "Режим розробника увімкнено",
|
||||
"developerModeDisabled": "Режим розробника вимкнено",
|
||||
"clipboardIsEmpty": "Буфер обміну порожній",
|
||||
"contactImported": "Контакт імпортовано",
|
||||
"contactLinkCopiedToClipboard": "Посилання на контакт скопійовано в буфер",
|
||||
"failedToExportContact": "Не вдалося експортувати контакт",
|
||||
"noLogsToExport": "Немає журналів для експорту",
|
||||
"exportAsCsv": "Експортувати як CSV",
|
||||
"exportAsText": "Експортувати як текст",
|
||||
"receivedRfc3339": "Отримано (RFC3339)",
|
||||
"buildTime": "Час збірки",
|
||||
"downloadUrlNotAvailable": "URL завантаження недоступний",
|
||||
"cannotOpenDownloadUrl": "Не вдається відкрити URL завантаження",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Перевірка оновлень доступна лише на Android",
|
||||
"youAreRunningTheLatestVersion": "Ви використовуєте останню версію",
|
||||
"updateAvailableButDownloadUrlNotFound": "Оновлення доступне, але URL завантаження не знайдено",
|
||||
"startTictactoe": "Почати Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: суперник невідомий",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: очікування початку",
|
||||
"acceptsShareLinks": "Приймає посилання для обміну",
|
||||
"supportsRawHex": "Підтримує raw hex",
|
||||
"clipboardfriendly": "Зручно для буфера",
|
||||
"captured": "Захоплено",
|
||||
"size": "Розмір",
|
||||
"noCustomChannelsToClear": "Немає користувацьких каналів для очищення.",
|
||||
"noDeviceContactsToClear": "Немає контактів пристрою для очищення."
|
||||
}
|
||||
|
||||
@@ -2228,80 +2228,55 @@
|
||||
"description": "没有可发送的聊天联系人时的消息"
|
||||
},
|
||||
"reply": "回复",
|
||||
"@reply": {
|
||||
},
|
||||
"@reply": {},
|
||||
"technicalDetails": "技术细节",
|
||||
"@technicalDetails": {
|
||||
},
|
||||
"@technicalDetails": {},
|
||||
"messageTechnicalDetails": "消息技术细节",
|
||||
"@messageTechnicalDetails": {
|
||||
},
|
||||
"@messageTechnicalDetails": {},
|
||||
"linkQuality": "链路质量",
|
||||
"@linkQuality": {
|
||||
},
|
||||
"@linkQuality": {},
|
||||
"delivery": "投递",
|
||||
"@delivery": {
|
||||
},
|
||||
"@delivery": {},
|
||||
"status": "状态",
|
||||
"@status": {
|
||||
},
|
||||
"@status": {},
|
||||
"expectedAckTag": "预期的 ACK 标签",
|
||||
"@expectedAckTag": {
|
||||
},
|
||||
"@expectedAckTag": {},
|
||||
"roundTrip": "往返时间",
|
||||
"@roundTrip": {
|
||||
},
|
||||
"@roundTrip": {},
|
||||
"retryAttempt": "重试尝试",
|
||||
"@retryAttempt": {
|
||||
},
|
||||
"@retryAttempt": {},
|
||||
"floodFallback": "泛洪回退",
|
||||
"@floodFallback": {
|
||||
},
|
||||
"@floodFallback": {},
|
||||
"identity": "身份",
|
||||
"@identity": {
|
||||
},
|
||||
"@identity": {},
|
||||
"messageId": "消息 ID",
|
||||
"@messageId": {
|
||||
},
|
||||
"@messageId": {},
|
||||
"sender": "发送者",
|
||||
"@sender": {
|
||||
},
|
||||
"@sender": {},
|
||||
"senderKey": "发送者密钥",
|
||||
"@senderKey": {
|
||||
},
|
||||
"@senderKey": {},
|
||||
"recipient": "接收者",
|
||||
"@recipient": {
|
||||
},
|
||||
"@recipient": {},
|
||||
"recipientKey": "接收者密钥",
|
||||
"@recipientKey": {
|
||||
},
|
||||
"@recipientKey": {},
|
||||
"voice": "语音",
|
||||
"@voice": {
|
||||
},
|
||||
"@voice": {},
|
||||
"voiceId": "语音 ID",
|
||||
"@voiceId": {
|
||||
},
|
||||
"@voiceId": {},
|
||||
"envelope": "信封",
|
||||
"@envelope": {
|
||||
},
|
||||
"@envelope": {},
|
||||
"sessionProgress": "会话进度",
|
||||
"@sessionProgress": {
|
||||
},
|
||||
"@sessionProgress": {},
|
||||
"complete": "完成",
|
||||
"@complete": {
|
||||
},
|
||||
"@complete": {},
|
||||
"rawDump": "原始数据",
|
||||
"@rawDump": {
|
||||
},
|
||||
"@rawDump": {},
|
||||
"cannotRetryMissingRecipient": "无法重试:缺少接收者信息",
|
||||
"@cannotRetryMissingRecipient": {
|
||||
},
|
||||
"@cannotRetryMissingRecipient": {},
|
||||
"voiceUnavailable": "当前语音不可用",
|
||||
"@voiceUnavailable": {
|
||||
},
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "正在请求语音",
|
||||
"@requestingVoice": {
|
||||
},
|
||||
"@requestingVoice": {},
|
||||
"device": "设备",
|
||||
"change": "更改",
|
||||
"wizardOverviewDescription": "此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。",
|
||||
@@ -2353,5 +2328,252 @@
|
||||
"contactFailedToClearRoute": "清除路由失败:{error}",
|
||||
"contactRouteCleared": "路由已清除",
|
||||
"contactRouteSet": "路由已设置:{route}",
|
||||
"contactFailedToSetRoute": "设置路由失败:{error}"
|
||||
"contactFailedToSetRoute": "设置路由失败:{error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK 超时",
|
||||
"opcode": "操作码",
|
||||
"payload": "有效载荷",
|
||||
"hops": "跳数",
|
||||
"hashSize": "哈希大小",
|
||||
"pathBytes": "路径字节",
|
||||
"selectedPath": "选定路径",
|
||||
"estimatedTx": "预计传输",
|
||||
"senderToReceipt": "发送到接收",
|
||||
"receivedCopies": "收到的副本",
|
||||
"retryCause": "重试原因",
|
||||
"retryMode": "重试模式",
|
||||
"retryResult": "重试结果",
|
||||
"lastRetry": "最后重试",
|
||||
"rxPackets": "RX 数据包",
|
||||
"mesh": "Mesh",
|
||||
"rate": "速率",
|
||||
"window": "窗口",
|
||||
"posttxDelay": "发送后延迟",
|
||||
"bandpass": "带通",
|
||||
"bandpassFilterVoice": "带通语音滤波",
|
||||
"frequency": "频率",
|
||||
"australia": "澳大利亚",
|
||||
"australiaNarrow": "澳大利亚(窄带)",
|
||||
"australiaQld": "澳大利亚:QLD",
|
||||
"australiaSaWa": "澳大利亚:SA, WA",
|
||||
"newZealand": "新西兰",
|
||||
"newZealandNarrow": "新西兰(窄带)",
|
||||
"switzerland": "瑞士",
|
||||
"portugal433": "葡萄牙 433",
|
||||
"portugal868": "葡萄牙 868",
|
||||
"czechRepublicNarrow": "捷克(窄带)",
|
||||
"eu433mhzLongRange": "欧盟 433MHz(远距离)",
|
||||
"euukDeprecated": "欧盟/英国(已弃用)",
|
||||
"euukNarrow": "欧盟/英国(窄带)",
|
||||
"usacanadaRecommended": "美国/加拿大(推荐)",
|
||||
"vietnamDeprecated": "越南(已弃用)",
|
||||
"vietnamNarrow": "越南(窄带)",
|
||||
"active": "活动",
|
||||
"addContact": "添加联系人",
|
||||
"all": "全部",
|
||||
"autoResolve": "自动解析",
|
||||
"clearAllLabel": "清除全部",
|
||||
"clearRelays": "清除中继",
|
||||
"clearFilters": "清除过滤器",
|
||||
"clearRoute": "清除路由",
|
||||
"clearMessages": "清除消息",
|
||||
"clearScale": "清除比例",
|
||||
"clearDiscoveries": "清除发现",
|
||||
"clearOnlineTraceDatabase": "清除在线追踪数据库",
|
||||
"clearAllChannels": "清除所有频道",
|
||||
"clearAllContacts": "清除所有联系人",
|
||||
"clearChannels": "清除频道",
|
||||
"clearContacts": "清除联系人",
|
||||
"clearPathOnMaxRetry": "最大重试时清除路径",
|
||||
"create": "创建",
|
||||
"custom": "自定义",
|
||||
"defaultValue": "默认",
|
||||
"duplicate": "复制",
|
||||
"editName": "编辑名称",
|
||||
"open": "打开",
|
||||
"paste": "粘贴",
|
||||
"preview": "预览",
|
||||
"remove": "移除",
|
||||
"rename": "重命名",
|
||||
"resolveAll": "解析全部",
|
||||
"send": "发送",
|
||||
"sendAnyway": "仍然发送",
|
||||
"share": "分享",
|
||||
"shareContact": "分享联系人",
|
||||
"trace": "追踪",
|
||||
"use": "使用",
|
||||
"useSelectedFrequency": "使用选定频率",
|
||||
"discovery": "发现",
|
||||
"discoverRepeaters": "发现中继器",
|
||||
"discoverSensors": "发现传感器",
|
||||
"repeaterDiscoverySent": "中继器发现已发送",
|
||||
"sensorDiscoverySent": "传感器发现已发送",
|
||||
"clearedPendingDiscoveries": "已清除待处理的发现。",
|
||||
"autoDiscovery": "自动发现",
|
||||
"enableAutomaticAdding": "启用自动添加",
|
||||
"autoaddRepeaters": "自动添加中继器",
|
||||
"autoaddRoomServers": "自动添加房间服务器",
|
||||
"autoaddSensors": "自动添加传感器",
|
||||
"autoaddUsers": "自动添加用户",
|
||||
"overwriteOldestWhenFull": "满时覆盖最旧的",
|
||||
"storage": "存储",
|
||||
"dangerZone": "危险区域",
|
||||
"profiles": "配置文件",
|
||||
"favourites": "收藏夹",
|
||||
"sensors": "传感器",
|
||||
"others": "其他",
|
||||
"gpsModule": "GPS 模块",
|
||||
"liveTraffic": "实时流量",
|
||||
"repeatersMap": "中继器地图",
|
||||
"spectrumScan": "频谱扫描",
|
||||
"blePacketLogs": "BLE 数据包日志",
|
||||
"onlineTraceDatabase": "在线追踪数据库",
|
||||
"routePathByteSize": "路由路径字节大小",
|
||||
"messageNotifications": "消息通知",
|
||||
"sarAlerts": "SAR 警报",
|
||||
"discoveryNotifications": "发现通知",
|
||||
"updateNotifications": "更新通知",
|
||||
"muteWhileAppIsOpen": "应用打开时静音",
|
||||
"disableContacts": "禁用联系人",
|
||||
"enableSensorsTab": "启用传感器标签",
|
||||
"enableProfiles": "启用配置文件",
|
||||
"autoRouteRotation": "自动路由轮换",
|
||||
"nearestRepeaterFallback": "最近中继器回退",
|
||||
"deleteAllStoredMessageHistory": "删除所有存储的消息历史",
|
||||
"messageFontSize": "消息字体大小",
|
||||
"rotateMapWithHeading": "随航向旋转地图",
|
||||
"showMapDebugInfo": "显示地图调试信息",
|
||||
"openMapInFullscreen": "全屏打开地图",
|
||||
"showSarMarkersLabel": "显示 SAR 标记",
|
||||
"displaySarMarkersOnTheMainMap": "在主地图上显示 SAR 标记",
|
||||
"showAllContactTrailsLabel": "显示所有联系人轨迹",
|
||||
"hideRepeatersOnMap": "在地图上隐藏中继器",
|
||||
"setMapScale": "设置地图比例",
|
||||
"customMapScaleSaved": "自定义地图比例已保存",
|
||||
"voiceBitrate": "语音比特率",
|
||||
"voiceCompressor": "语音压缩器",
|
||||
"balancesQuietAndLoudSpeechLevels": "平衡安静和响亮的语音级别",
|
||||
"voiceLimiter": "语音限制器",
|
||||
"preventsClippingPeaksBeforeEncoding": "防止编码前的削波",
|
||||
"micAutoGain": "麦克风自动增益",
|
||||
"letsTheRecorderAdjustInputLevel": "让录音器调整输入级别",
|
||||
"echoCancellation": "回声消除",
|
||||
"noiseSuppression": "噪音抑制",
|
||||
"trimSilenceInVoiceMessages": "修剪语音消息中的静音",
|
||||
"compressor": "压缩器",
|
||||
"limiter": "限制器",
|
||||
"autoGain": "自动增益",
|
||||
"echoCancel": "回声",
|
||||
"noiseSuppress": "噪声",
|
||||
"silenceTrim": "静音",
|
||||
"maxImageSize": "最大图片大小",
|
||||
"imageCompression": "图片压缩",
|
||||
"grayscale": "灰度",
|
||||
"ultraMode": "超级模式",
|
||||
"fastPrivateGpsUpdates": "快速私有 GPS 更新",
|
||||
"movementThreshold": "移动阈值",
|
||||
"fastGpsMovementThreshold": "快速 GPS 移动阈值",
|
||||
"fastGpsActiveuseInterval": "快速 GPS 活动使用间隔",
|
||||
"activeuseUpdateInterval": "活动使用更新间隔",
|
||||
"repeatNearbyTraffic": "转发附近流量",
|
||||
"relayThroughRepeatersAcrossTheMesh": "通过网格中的中继器转发",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "仅附近,无中继器泛洪",
|
||||
"multihop": "多跳",
|
||||
"createProfile": "创建配置文件",
|
||||
"renameProfile": "重命名配置文件",
|
||||
"newProfile": "新配置文件",
|
||||
"manageProfiles": "管理配置文件",
|
||||
"enableProfilesToStartManagingThem": "启用配置文件以开始管理它们。",
|
||||
"openMessage": "打开消息",
|
||||
"jumpToTheRelatedSarMessage": "跳转到相关的 SAR 消息",
|
||||
"removeSarMarker": "移除 SAR 标记",
|
||||
"pleaseSelectADestinationToSendSarMarker": "请选择目标以发送 SAR 标记",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR 标记已广播到公共频道",
|
||||
"sarMarkerSentToRoom": "SAR 标记已发送到房间",
|
||||
"loadFromGallery": "从图库加载",
|
||||
"replaceImage": "替换图片",
|
||||
"selectFromGallery": "从图库选择",
|
||||
"team": "团队",
|
||||
"found": "已找到",
|
||||
"staging": "集结区",
|
||||
"object": "物体",
|
||||
"quiet": "安静",
|
||||
"moderate": "中等",
|
||||
"busy": "繁忙",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "频谱扫描未找到候选频率",
|
||||
"searchMessages": "搜索消息",
|
||||
"sendImageFromGallery": "从图库发送图片",
|
||||
"takePhoto": "拍照",
|
||||
"dmOnly": "仅私信",
|
||||
"allMessages": "所有消息",
|
||||
"sendToPublicChannel": "发送到公共频道?",
|
||||
"selectMarkerTypeAndDestination": "选择标记类型和目标",
|
||||
"noDestinationsAvailableLabel": "没有可用的目标",
|
||||
"image": "图片",
|
||||
"format": "格式",
|
||||
"dimensions": "尺寸",
|
||||
"segments": "分段",
|
||||
"transfers": "传输",
|
||||
"downloadedBy": "已下载",
|
||||
"saveDiscoverySettings": "保存发现设置",
|
||||
"savePublicInfo": "保存公共信息",
|
||||
"saveRadioSettings": "保存无线电设置",
|
||||
"savePath": "保存路径",
|
||||
"wipeDeviceData": "擦除设备数据",
|
||||
"wipeDevice": "擦除设备",
|
||||
"destructiveDeviceActions": "破坏性设备操作。",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "选择预设或微调自定义无线电设置。",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "选择此设备共享的名称和位置。",
|
||||
"availableSpaceOnThisDevice": "此设备上的可用空间。",
|
||||
"used": "已使用",
|
||||
"total": "总计",
|
||||
"renameValue": "重命名值",
|
||||
"customizeFields": "自定义字段",
|
||||
"livePreview": "实时预览",
|
||||
"refreshSchedule": "刷新计划",
|
||||
"noResponse": "无响应",
|
||||
"refreshing": "刷新中",
|
||||
"unavailable": "不可用",
|
||||
"pickARelayOrNodeToWatchInSensors": "选择要在传感器中监视的中继或节点。",
|
||||
"publicKeyLabel": "公钥",
|
||||
"alreadyInContacts": "已在联系人中",
|
||||
"connectToADeviceBeforeAddingContacts": "添加联系人前请连接到设备",
|
||||
"fromContacts": "来自联系人",
|
||||
"onlineOnly": "仅在线",
|
||||
"inBoth": "两者中",
|
||||
"source": "来源",
|
||||
"manualRouteEdit": "手动路由编辑",
|
||||
"observedMeshRoute": "观察到的网格路由",
|
||||
"allMessagesCleared": "所有消息已清除",
|
||||
"onlineTraceDatabaseCleared": "在线追踪数据库已清除",
|
||||
"packetLogsCleared": "数据包日志已清除",
|
||||
"hexDataCopiedToClipboard": "十六进制数据已复制到剪贴板",
|
||||
"developerModeEnabled": "开发者模式已启用",
|
||||
"developerModeDisabled": "开发者模式已禁用",
|
||||
"clipboardIsEmpty": "剪贴板为空",
|
||||
"contactImported": "联系人已导入",
|
||||
"contactLinkCopiedToClipboard": "联系人链接已复制到剪贴板",
|
||||
"failedToExportContact": "导出联系人失败",
|
||||
"noLogsToExport": "没有日志可导出",
|
||||
"exportAsCsv": "导出为 CSV",
|
||||
"exportAsText": "导出为文本",
|
||||
"receivedRfc3339": "收到 (RFC3339)",
|
||||
"buildTime": "构建时间",
|
||||
"downloadUrlNotAvailable": "下载链接不可用",
|
||||
"cannotOpenDownloadUrl": "无法打开下载链接",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "更新检查仅在 Android 上可用",
|
||||
"youAreRunningTheLatestVersion": "您正在使用最新版本",
|
||||
"updateAvailableButDownloadUrlNotFound": "有更新可用但未找到下载链接",
|
||||
"startTictactoe": "开始井字棋",
|
||||
"tictactoeUnavailable": "井字棋不可用",
|
||||
"tictactoeOpponentUnknown": "井字棋:对手未知",
|
||||
"tictactoeWaitingForStart": "井字棋:等待开始",
|
||||
"acceptsShareLinks": "接受分享链接",
|
||||
"supportsRawHex": "支持原始十六进制",
|
||||
"clipboardfriendly": "剪贴板友好",
|
||||
"captured": "已捕获",
|
||||
"size": "大小",
|
||||
"noCustomChannelsToClear": "没有自定义频道可清除。",
|
||||
"noDeviceContactsToClear": "没有设备联系人可清除。"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'providers/connection_provider.dart';
|
||||
import 'providers/contacts_provider.dart';
|
||||
import 'providers/messages_provider.dart';
|
||||
@@ -23,6 +24,9 @@ import 'services/voice_player_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'services/locale_preferences.dart';
|
||||
import 'services/mesh_map_nodes_service.dart';
|
||||
import 'services/profile_manager.dart';
|
||||
import 'services/profile_workspace_coordinator.dart';
|
||||
import 'services/profiles_feature_service.dart';
|
||||
import 'services/update_checker_service.dart';
|
||||
import 'services/wizard_preferences.dart';
|
||||
import 'screens/discovery_screen.dart';
|
||||
@@ -31,7 +35,9 @@ import 'screens/welcome_wizard_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await WakelockPlus.enable();
|
||||
runApp(const MeshCoreSarApp());
|
||||
}
|
||||
|
||||
@@ -58,6 +64,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
final activeProfileId =
|
||||
prefs.getString(ProfileManager.activeProfileIdKey) ?? 'default';
|
||||
await ProfileStorageScope.bootstrap(
|
||||
profilesEnabled: profilesEnabled,
|
||||
activeProfileId: activeProfileId,
|
||||
);
|
||||
await _loadThemePreference();
|
||||
await _loadLocalePreference();
|
||||
|
||||
@@ -271,6 +285,13 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) {
|
||||
final manager = ProfileManager();
|
||||
manager.initialize();
|
||||
return manager;
|
||||
},
|
||||
),
|
||||
|
||||
// Voice provider (packet reassembly + playback)
|
||||
ChangeNotifierProvider(
|
||||
@@ -323,6 +344,39 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
imageProvider: context.read<ip.ImageProvider>(),
|
||||
),
|
||||
),
|
||||
ProxyProvider6<
|
||||
ProfileManager,
|
||||
AppProvider,
|
||||
ConnectionProvider,
|
||||
ContactsProvider,
|
||||
MessagesProvider,
|
||||
MapProvider,
|
||||
ProfileWorkspaceCoordinator
|
||||
>(
|
||||
update:
|
||||
(
|
||||
context,
|
||||
profileManager,
|
||||
appProvider,
|
||||
connectionProvider,
|
||||
contactsProvider,
|
||||
messagesProvider,
|
||||
mapProvider,
|
||||
previous,
|
||||
) => ProfileWorkspaceCoordinator(
|
||||
profileManager: profileManager,
|
||||
connectionProvider: connectionProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
messagesProvider: messagesProvider,
|
||||
sensorsProvider: context.read<SensorsProvider>(),
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: context.read<DrawingProvider>(),
|
||||
channelsProvider: context.read<ChannelsProvider>(),
|
||||
voiceProvider: context.read<VoiceProvider>(),
|
||||
imageProvider: context.read<ip.ImageProvider>(),
|
||||
appProvider: appProvider,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _buildMaterialApp(),
|
||||
);
|
||||
|
||||
@@ -57,9 +57,7 @@ class Channel {
|
||||
} else {
|
||||
// Normal channel: require explicit secret
|
||||
if (explicitSecret == null || explicitSecret.length != 16) {
|
||||
throw ArgumentError(
|
||||
'Normal channels require a 16-byte secret',
|
||||
);
|
||||
throw ArgumentError('Normal channels require a 16-byte secret');
|
||||
}
|
||||
return Channel(
|
||||
index: index,
|
||||
@@ -78,6 +76,18 @@ class Channel {
|
||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||
}
|
||||
|
||||
static bool isHashChannelName(String channelName) {
|
||||
return channelName.trim().startsWith('#');
|
||||
}
|
||||
|
||||
static String pskBase64ForHashChannelName(String channelName) {
|
||||
final normalized = channelName.trim();
|
||||
if (!isHashChannelName(normalized)) {
|
||||
throw ArgumentError('Only #channels can export derived psk_base64');
|
||||
}
|
||||
return base64.encode(_generateHashChannelSecret(normalized));
|
||||
}
|
||||
|
||||
/// Create the default public channel (channel 0)
|
||||
/// Uses the well-known pre-shared key from MeshCore
|
||||
factory Channel.publicChannel() {
|
||||
@@ -85,8 +95,22 @@ class Channel {
|
||||
index: 0,
|
||||
name: 'Public Channel',
|
||||
secret: Uint8List.fromList([
|
||||
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
||||
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
|
||||
0x8b,
|
||||
0x33,
|
||||
0x87,
|
||||
0xe9,
|
||||
0xc5,
|
||||
0xcd,
|
||||
0xea,
|
||||
0x6a,
|
||||
0xc9,
|
||||
0xe5,
|
||||
0xed,
|
||||
0xba,
|
||||
0xa1,
|
||||
0x15,
|
||||
0xcd,
|
||||
0x72,
|
||||
]),
|
||||
flags: null,
|
||||
);
|
||||
@@ -95,6 +119,9 @@ class Channel {
|
||||
/// Check if this is a hash-based channel (name starts with '#')
|
||||
bool get isHashChannel => name.startsWith('#');
|
||||
|
||||
/// Base64-encoded PSK for sharing with firmware CLI and related tooling.
|
||||
String get pskBase64 => base64.encode(secret);
|
||||
|
||||
/// Display name for the channel
|
||||
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
||||
String get displayName {
|
||||
@@ -131,12 +158,7 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
Channel copyWith({
|
||||
int? index,
|
||||
String? name,
|
||||
Uint8List? secret,
|
||||
int? flags,
|
||||
}) {
|
||||
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
|
||||
return Channel(
|
||||
index: index ?? this.index,
|
||||
name: name ?? this.name,
|
||||
|
||||
528
lib/models/config_profile.dart
Normal file
528
lib/models/config_profile.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,23 @@ class ContactRouteCodec {
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute direct({int hashSize = 1}) {
|
||||
if (hashSize < 1 || hashSize > maxHashSize) {
|
||||
throw ArgumentError.value(hashSize, 'hashSize', 'Must be 1, 2, or 3.');
|
||||
}
|
||||
|
||||
final encodedPathLen = (hashSize - 1) << 6;
|
||||
return ParsedContactRoute(
|
||||
canonicalText: '',
|
||||
hashSize: hashSize,
|
||||
hopCount: 0,
|
||||
encodedPathLen: encodedPathLen,
|
||||
signedEncodedPathLen: toSignedDescriptor(encodedPathLen),
|
||||
pathBytes: Uint8List(0),
|
||||
paddedPathBytes: Uint8List(maxPathBytes),
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute? fromContact(Contact contact) {
|
||||
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
||||
return null;
|
||||
|
||||
151
lib/models/custom_map_config.dart
Normal file
151
lib/models/custom_map_config.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
class CustomMapConfig {
|
||||
final String filePath;
|
||||
final String displayName;
|
||||
final String mapId;
|
||||
final int imageWidth;
|
||||
final int imageHeight;
|
||||
final double? metersPerPixel;
|
||||
final LatLng? calibrationPointA;
|
||||
final LatLng? calibrationPointB;
|
||||
|
||||
const CustomMapConfig({
|
||||
required this.filePath,
|
||||
required this.displayName,
|
||||
required this.mapId,
|
||||
required this.imageWidth,
|
||||
required this.imageHeight,
|
||||
this.metersPerPixel,
|
||||
this.calibrationPointA,
|
||||
this.calibrationPointB,
|
||||
});
|
||||
|
||||
bool get isCalibrated => metersPerPixel != null && metersPerPixel! > 0;
|
||||
|
||||
double get _displayScale {
|
||||
final heightScale = imageHeight / 90.0;
|
||||
final widthScale = imageWidth / 180.0;
|
||||
return math.max(1.0, math.max(heightScale, widthScale));
|
||||
}
|
||||
|
||||
double get _displayHeight => imageHeight / _displayScale;
|
||||
|
||||
double get _displayWidth => imageWidth / _displayScale;
|
||||
|
||||
LatLngBounds get bounds =>
|
||||
LatLngBounds(const LatLng(0, 0), LatLng(_displayHeight, _displayWidth));
|
||||
|
||||
LatLngBounds get displayBounds =>
|
||||
LatLngBounds(LatLng(_displayHeight, 0), LatLng(0, _displayWidth));
|
||||
|
||||
LatLng toDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
_displayHeight - (point.latitude / _displayScale),
|
||||
point.longitude / _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
LatLng fromDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
(_displayHeight - point.latitude) * _displayScale,
|
||||
point.longitude * _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'filePath': filePath,
|
||||
'displayName': displayName,
|
||||
'mapId': mapId,
|
||||
'imageWidth': imageWidth,
|
||||
'imageHeight': imageHeight,
|
||||
'metersPerPixel': metersPerPixel,
|
||||
'calibrationPointA': calibrationPointA != null
|
||||
? {
|
||||
'lat': calibrationPointA!.latitude,
|
||||
'lon': calibrationPointA!.longitude,
|
||||
}
|
||||
: null,
|
||||
'calibrationPointB': calibrationPointB != null
|
||||
? {
|
||||
'lat': calibrationPointB!.latitude,
|
||||
'lon': calibrationPointB!.longitude,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
CustomMapConfig copyWith({
|
||||
String? filePath,
|
||||
String? displayName,
|
||||
String? mapId,
|
||||
int? imageWidth,
|
||||
int? imageHeight,
|
||||
double? metersPerPixel,
|
||||
bool clearMetersPerPixel = false,
|
||||
LatLng? calibrationPointA,
|
||||
bool clearCalibrationPointA = false,
|
||||
LatLng? calibrationPointB,
|
||||
bool clearCalibrationPointB = false,
|
||||
}) {
|
||||
return CustomMapConfig(
|
||||
filePath: filePath ?? this.filePath,
|
||||
displayName: displayName ?? this.displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? this.mapId,
|
||||
imageWidth: imageWidth ?? this.imageWidth,
|
||||
imageHeight: imageHeight ?? this.imageHeight,
|
||||
metersPerPixel: clearMetersPerPixel
|
||||
? null
|
||||
: (metersPerPixel ?? this.metersPerPixel),
|
||||
calibrationPointA: clearCalibrationPointA
|
||||
? null
|
||||
: (calibrationPointA ?? this.calibrationPointA),
|
||||
calibrationPointB: clearCalibrationPointB
|
||||
? null
|
||||
: (calibrationPointB ?? this.calibrationPointB),
|
||||
);
|
||||
}
|
||||
|
||||
static CustomMapConfig? fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) return null;
|
||||
|
||||
final filePath = json['filePath'];
|
||||
final displayName = json['displayName'];
|
||||
final mapId = json['mapId'];
|
||||
final imageWidth = json['imageWidth'];
|
||||
final imageHeight = json['imageHeight'];
|
||||
if (filePath is! String ||
|
||||
displayName is! String ||
|
||||
mapId is! String ||
|
||||
imageWidth is! int ||
|
||||
imageHeight is! int) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng? parsePoint(dynamic raw) {
|
||||
if (raw is! Map<String, dynamic>) return null;
|
||||
final lat = raw['lat'];
|
||||
final lon = raw['lon'];
|
||||
if (lat is! num || lon is! num) return null;
|
||||
return LatLng(lat.toDouble(), lon.toDouble());
|
||||
}
|
||||
|
||||
final metersPerPixel = json['metersPerPixel'];
|
||||
return CustomMapConfig(
|
||||
filePath: filePath,
|
||||
displayName: displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? mapId,
|
||||
imageWidth: imageWidth,
|
||||
imageHeight: imageHeight,
|
||||
metersPerPixel: metersPerPixel is num ? metersPerPixel.toDouble() : null,
|
||||
calibrationPointA: parsePoint(json['calibrationPointA']),
|
||||
calibrationPointB: parsePoint(json['calibrationPointB']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (Serial)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 'Direct serial connection to MeshCore device';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +71,7 @@ class DeviceInfo {
|
||||
final bool? autoAddRoomServers;
|
||||
final bool? autoAddSensors;
|
||||
final bool? autoAddOverwriteOldest;
|
||||
final int? autoAddMaxHops;
|
||||
final int? radioFreq;
|
||||
final int? radioBw;
|
||||
final int? radioSf;
|
||||
@@ -88,6 +96,7 @@ class DeviceInfo {
|
||||
|
||||
// Repeat mode (firmware v9+)
|
||||
final bool? clientRepeat;
|
||||
final int? pathHashMode;
|
||||
final bool? supportsSpectrumScan;
|
||||
final int? spectrumScanMinKhz;
|
||||
final int? spectrumScanMaxKhz;
|
||||
@@ -116,6 +125,7 @@ class DeviceInfo {
|
||||
this.autoAddRoomServers,
|
||||
this.autoAddSensors,
|
||||
this.autoAddOverwriteOldest,
|
||||
this.autoAddMaxHops,
|
||||
this.radioFreq,
|
||||
this.radioBw,
|
||||
this.radioSf,
|
||||
@@ -132,6 +142,7 @@ class DeviceInfo {
|
||||
this.manufacturerModel,
|
||||
this.semanticVersion,
|
||||
this.clientRepeat,
|
||||
this.pathHashMode,
|
||||
this.supportsSpectrumScan,
|
||||
this.spectrumScanMinKhz,
|
||||
this.spectrumScanMaxKhz,
|
||||
@@ -247,6 +258,7 @@ class DeviceInfo {
|
||||
bool? autoAddRoomServers,
|
||||
bool? autoAddSensors,
|
||||
bool? autoAddOverwriteOldest,
|
||||
int? autoAddMaxHops,
|
||||
int? radioFreq,
|
||||
int? radioBw,
|
||||
int? radioSf,
|
||||
@@ -263,6 +275,7 @@ class DeviceInfo {
|
||||
String? manufacturerModel,
|
||||
String? semanticVersion,
|
||||
bool? clientRepeat,
|
||||
int? pathHashMode,
|
||||
bool? supportsSpectrumScan,
|
||||
int? spectrumScanMinKhz,
|
||||
int? spectrumScanMaxKhz,
|
||||
@@ -292,6 +305,7 @@ class DeviceInfo {
|
||||
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
||||
autoAddOverwriteOldest:
|
||||
autoAddOverwriteOldest ?? this.autoAddOverwriteOldest,
|
||||
autoAddMaxHops: autoAddMaxHops ?? this.autoAddMaxHops,
|
||||
radioFreq: radioFreq ?? this.radioFreq,
|
||||
radioBw: radioBw ?? this.radioBw,
|
||||
radioSf: radioSf ?? this.radioSf,
|
||||
@@ -308,6 +322,7 @@ class DeviceInfo {
|
||||
manufacturerModel: manufacturerModel ?? this.manufacturerModel,
|
||||
semanticVersion: semanticVersion ?? this.semanticVersion,
|
||||
clientRepeat: clientRepeat ?? this.clientRepeat,
|
||||
pathHashMode: pathHashMode ?? this.pathHashMode,
|
||||
supportsSpectrumScan: supportsSpectrumScan ?? this.supportsSpectrumScan,
|
||||
spectrumScanMinKhz: spectrumScanMinKhz ?? this.spectrumScanMinKhz,
|
||||
spectrumScanMaxKhz: spectrumScanMaxKhz ?? this.spectrumScanMaxKhz,
|
||||
|
||||
11
lib/models/map_coordinate_space.dart
Normal file
11
lib/models/map_coordinate_space.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
enum MapCoordinateSpace {
|
||||
geo,
|
||||
customMap;
|
||||
|
||||
static MapCoordinateSpace fromName(String? value) {
|
||||
return MapCoordinateSpace.values.firstWhere(
|
||||
(space) => space.name == value,
|
||||
orElse: () => MapCoordinateSpace.geo,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,26 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
/// Drawing shape type
|
||||
enum DrawingShapeType {
|
||||
line,
|
||||
rectangle,
|
||||
}
|
||||
enum DrawingShapeType { line, rectangle }
|
||||
|
||||
/// Drawing color enum for compact network transmission
|
||||
enum DrawingColor {
|
||||
red, // 0
|
||||
blue, // 1
|
||||
green, // 2
|
||||
yellow, // 3
|
||||
orange, // 4
|
||||
purple, // 5
|
||||
pink, // 6
|
||||
cyan, // 7
|
||||
}
|
||||
enum DrawingColor { red, blue, green, yellow, orange, purple, pink, cyan }
|
||||
|
||||
/// Drawing colors available for user selection
|
||||
class DrawingColors {
|
||||
static const List<Color> palette = [
|
||||
Colors.red, // index 0
|
||||
Colors.blue, // index 1
|
||||
Colors.green, // index 2
|
||||
Colors.yellow, // index 3
|
||||
Colors.orange, // index 4
|
||||
Colors.purple, // index 5
|
||||
Colors.pink, // index 6
|
||||
Colors.cyan, // index 7
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.yellow,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.pink,
|
||||
Colors.cyan,
|
||||
];
|
||||
|
||||
static String colorToName(Color color) {
|
||||
@@ -45,37 +36,36 @@ class DrawingColors {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/// Convert Color to enum index for network transmission
|
||||
static int colorToIndex(Color color) {
|
||||
for (int i = 0; i < palette.length; i++) {
|
||||
if (palette[i].toARGB32() == color.toARGB32()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // Default to red if not found
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Convert enum index to Color for network reception
|
||||
static Color indexToColor(int index) {
|
||||
if (index >= 0 && index < palette.length) {
|
||||
return palette[index];
|
||||
}
|
||||
return palette[0]; // Default to red if invalid index
|
||||
return palette[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for map drawings
|
||||
abstract class MapDrawing {
|
||||
final String id;
|
||||
final DrawingShapeType type;
|
||||
final Color color;
|
||||
final DateTime createdAt;
|
||||
final String? senderName; // Name of sender (null if local drawing)
|
||||
final bool isReceived; // True if drawing was received from another node
|
||||
final String? messageId; // ID of the source message (for navigation)
|
||||
final bool isShared; // Whether drawing has been broadcast over mesh
|
||||
final bool isSent; // Whether this is a sent drawing (vs received)
|
||||
final bool isHidden; // Temporary visibility toggle (session only, not persisted)
|
||||
final String? senderName;
|
||||
final bool isReceived;
|
||||
final String? messageId;
|
||||
final bool isShared;
|
||||
final bool isSent;
|
||||
final bool isHidden;
|
||||
final MapCoordinateSpace coordinateSpace;
|
||||
final String? mapId;
|
||||
|
||||
MapDrawing({
|
||||
required this.id,
|
||||
@@ -88,79 +78,79 @@ abstract class MapDrawing {
|
||||
this.isShared = false,
|
||||
this.isSent = false,
|
||||
this.isHidden = false,
|
||||
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||
this.mapId,
|
||||
});
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
bool get isCustomMap => coordinateSpace == MapCoordinateSpace.customMap;
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
/// Convert to JSON for network transmission (compact format)
|
||||
/// Uses short field names and excludes createdAt to minimize message size
|
||||
/// Sender will be fetched from packet metadata
|
||||
Map<String, dynamic> toNetworkJson();
|
||||
|
||||
/// Parse network JSON (compact format)
|
||||
/// senderName and messageId will be populated from packet metadata
|
||||
static MapDrawing? fromNetworkJson(
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
final typeNum = json['t'] as int?;
|
||||
if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) {
|
||||
if (typeNum == null ||
|
||||
typeNum < 0 ||
|
||||
typeNum >= DrawingShapeType.values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values[typeNum];
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
static MapDrawing? fromJson(Map<String, dynamic> json) {
|
||||
final typeStr = json['type'] as String?;
|
||||
if (typeStr == null) return null;
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values.firstWhere(
|
||||
(e) => e.toString() == 'DrawingShapeType.$typeStr',
|
||||
(value) => value.name == typeStr,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromJson(json);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromJson(json);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the center point of the drawing
|
||||
LatLng getCenter();
|
||||
|
||||
/// Get the bounds of the drawing
|
||||
LatLngBounds getBounds();
|
||||
}
|
||||
|
||||
/// Line drawing on map
|
||||
class LineDrawing extends MapDrawing {
|
||||
final List<LatLng> points;
|
||||
|
||||
@@ -175,6 +165,8 @@ class LineDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.line);
|
||||
|
||||
@override
|
||||
@@ -184,31 +176,51 @@ class LineDrawing extends MapDrawing {
|
||||
'type': type.name,
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'points': points.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(),
|
||||
'points': points
|
||||
.map((point) => {'lat': point.latitude, 'lon': point.longitude})
|
||||
.toList(),
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), p=points
|
||||
// Points are encoded as flat array [lat1,lon1,lat2,lon2,...]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'p': points.expand((p) => [
|
||||
double.parse(p.latitude.toStringAsFixed(5)),
|
||||
double.parse(p.longitude.toStringAsFixed(5)),
|
||||
]).toList(),
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['p'] = points
|
||||
.expand((point) => [point.latitude.round(), point.longitude.round()])
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['p'] = points
|
||||
.expand(
|
||||
(point) => [
|
||||
double.parse(point.latitude.toStringAsFixed(5)),
|
||||
double.parse(point.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
)
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
static LineDrawing fromJson(Map<String, dynamic> json) {
|
||||
final pointsJson = json['points'] as List<dynamic>;
|
||||
final points = pointsJson.map((p) => LatLng(p['lat'] as double, p['lon'] as double)).toList();
|
||||
final points = pointsJson
|
||||
.map(
|
||||
(point) => LatLng(
|
||||
(point['lat'] as num).toDouble(),
|
||||
(point['lon'] as num).toDouble(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final senderName = json['sender'] as String?;
|
||||
|
||||
return LineDrawing(
|
||||
@@ -217,8 +229,12 @@ class LineDrawing extends MapDrawing {
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,42 +242,60 @@ class LineDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
|
||||
final flatPoints = (json['p'] as List<dynamic>).cast<num>();
|
||||
final points = <LatLng>[];
|
||||
for (int i = 0; i < pointsFlat.length; i += 2) {
|
||||
points.add(LatLng(pointsFlat[i], pointsFlat[i + 1]));
|
||||
for (int i = 0; i < flatPoints.length; i += 2) {
|
||||
points.add(
|
||||
LatLng(flatPoints[i].toDouble(), flatPoints[i + 1].toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
return LineDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated points
|
||||
LineDrawing copyWith({List<LatLng>? points}) {
|
||||
LineDrawing copyWith({
|
||||
List<LatLng>? points,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return LineDrawing(
|
||||
id: id,
|
||||
color: color,
|
||||
createdAt: createdAt,
|
||||
points: points ?? this.points,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
if (points.isEmpty) return LatLng(0, 0);
|
||||
if (points.isEmpty) return const LatLng(0, 0);
|
||||
if (points.length == 1) return points[0];
|
||||
|
||||
// Calculate center as average of all points
|
||||
double sumLat = 0;
|
||||
double sumLon = 0;
|
||||
for (final point in points) {
|
||||
@@ -273,8 +307,12 @@ class LineDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
if (points.isEmpty) return LatLngBounds(LatLng(0, 0), LatLng(0, 0));
|
||||
if (points.length == 1) return LatLngBounds(points[0], points[0]);
|
||||
if (points.isEmpty) {
|
||||
return LatLngBounds(const LatLng(0, 0), const LatLng(0, 0));
|
||||
}
|
||||
if (points.length == 1) {
|
||||
return LatLngBounds(points[0], points[0]);
|
||||
}
|
||||
|
||||
double minLat = points[0].latitude;
|
||||
double maxLat = points[0].latitude;
|
||||
@@ -292,7 +330,6 @@ class LineDrawing extends MapDrawing {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle drawing on map
|
||||
class RectangleDrawing extends MapDrawing {
|
||||
final LatLng topLeft;
|
||||
final LatLng bottomRight;
|
||||
@@ -309,16 +346,17 @@ class RectangleDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.rectangle);
|
||||
|
||||
/// Get all corner points for rendering
|
||||
List<LatLng> get corners => [
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude), // top right
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude), // bottom left
|
||||
topLeft, // close the rectangle
|
||||
];
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude),
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude),
|
||||
topLeft,
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -328,27 +366,41 @@ class RectangleDrawing extends MapDrawing {
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
||||
'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude},
|
||||
'bottomRight': {
|
||||
'lat': bottomRight.latitude,
|
||||
'lon': bottomRight.longitude,
|
||||
},
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), b=bounds [lat1,lon1,lat2,lon2]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'b': [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['b'] = [
|
||||
topLeft.latitude.round(),
|
||||
topLeft.longitude.round(),
|
||||
bottomRight.latitude.round(),
|
||||
bottomRight.longitude.round(),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['b'] = [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
static RectangleDrawing fromJson(Map<String, dynamic> json) {
|
||||
@@ -360,11 +412,21 @@ class RectangleDrawing extends MapDrawing {
|
||||
id: json['id'] as String,
|
||||
color: Color(json['color'] as int),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double),
|
||||
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),
|
||||
topLeft: LatLng(
|
||||
(topLeftJson['lat'] as num).toDouble(),
|
||||
(topLeftJson['lon'] as num).toDouble(),
|
||||
),
|
||||
bottomRight: LatLng(
|
||||
(bottomRightJson['lat'] as num).toDouble(),
|
||||
(bottomRightJson['lon'] as num).toDouble(),
|
||||
),
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -372,27 +434,35 @@ class RectangleDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final bounds = (json['b'] as List<dynamic>).cast<double>();
|
||||
final bounds = (json['b'] as List<dynamic>).cast<num>();
|
||||
|
||||
return RectangleDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: LatLng(bounds[0], bounds[1]),
|
||||
bottomRight: LatLng(bounds[2], bounds[3]),
|
||||
topLeft: LatLng(bounds[0].toDouble(), bounds[1].toDouble()),
|
||||
bottomRight: LatLng(bounds[2].toDouble(), bounds[3].toDouble()),
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated corners
|
||||
RectangleDrawing copyWith({
|
||||
LatLng? topLeft,
|
||||
LatLng? bottomRight,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return RectangleDrawing(
|
||||
id: id,
|
||||
@@ -400,12 +470,18 @@ class RectangleDrawing extends MapDrawing {
|
||||
createdAt: createdAt,
|
||||
topLeft: topLeft ?? this.topLeft,
|
||||
bottomRight: bottomRight ?? this.bottomRight,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
// Center is the midpoint between top-left and bottom-right
|
||||
return LatLng(
|
||||
(topLeft.latitude + bottomRight.latitude) / 2,
|
||||
(topLeft.longitude + bottomRight.longitude) / 2,
|
||||
@@ -414,7 +490,6 @@ class RectangleDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
// Bounds are simply the two corners
|
||||
return LatLngBounds(topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export 'package:meshcore_client/meshcore_client.dart'
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'sar_marker.dart';
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
|
||||
extension MessageVoiceExtension on Message {
|
||||
@@ -47,7 +48,7 @@ extension MessageSarExtension on Message {
|
||||
|
||||
/// Convert to a [SarMarker] if this message contains SAR data.
|
||||
SarMarker? toSarMarker() {
|
||||
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
||||
if (!isSarMarker || sarMarkerType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -57,16 +58,26 @@ extension MessageSarExtension on Message {
|
||||
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
||||
debugPrint(' message.sarCustomEmoji: "$sarCustomEmoji"');
|
||||
|
||||
final coordinateSpace = sarCustomMapPoint != null && sarCustomMapId != null
|
||||
? MapCoordinateSpace.customMap
|
||||
: MapCoordinateSpace.geo;
|
||||
final location = sarCustomMapPoint ?? sarGpsCoordinates;
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SarMarker(
|
||||
id: id,
|
||||
type: sarMarkerType!,
|
||||
location: sarGpsCoordinates!,
|
||||
location: location,
|
||||
timestamp: sentAt,
|
||||
senderPublicKey: senderPublicKeyPrefix,
|
||||
senderName: senderName,
|
||||
notes: sarNotes,
|
||||
customEmoji: sarCustomEmoji,
|
||||
colorIndex: sarColorIndex,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: sarCustomMapId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -105,6 +107,15 @@ class _PendingRepeaterOwnerRequest {
|
||||
const _PendingRepeaterOwnerRequest({required this.publicKey});
|
||||
}
|
||||
|
||||
enum ContactsTabSection {
|
||||
favourites,
|
||||
teamMembers,
|
||||
repeaters,
|
||||
sensors,
|
||||
rooms,
|
||||
channels,
|
||||
}
|
||||
|
||||
/// Main App Provider - coordinates all other providers
|
||||
class AppProvider with ChangeNotifier {
|
||||
static const int _maxDirectPayloadHops = 3;
|
||||
@@ -160,6 +171,9 @@ class AppProvider with ChangeNotifier {
|
||||
bool get isContactsEnabled => _isContactsEnabled;
|
||||
bool _isSensorsEnabled = true;
|
||||
bool get isSensorsEnabled => _isSensorsEnabled;
|
||||
final Map<ContactsTabSection, bool> _contactsSectionVisibility = {
|
||||
for (final section in ContactsTabSection.values) section: true,
|
||||
};
|
||||
|
||||
bool _isVoiceSilenceTrimmingEnabled = true;
|
||||
bool get isVoiceSilenceTrimmingEnabled => _isVoiceSilenceTrimmingEnabled;
|
||||
@@ -236,6 +250,7 @@ class AppProvider with ChangeNotifier {
|
||||
_initializeLocationTracking();
|
||||
_loadMapEnabled();
|
||||
_loadContactsEnabled();
|
||||
_loadContactsSectionVisibility();
|
||||
_loadSensorsEnabled();
|
||||
_loadVoiceSilenceTrimmingEnabled();
|
||||
_loadVoiceBandPassFilterEnabled();
|
||||
@@ -253,6 +268,31 @@ class AppProvider with ChangeNotifier {
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
bool isContactsSectionEnabled(ContactsTabSection section) {
|
||||
return _contactsSectionVisibility[section] ?? true;
|
||||
}
|
||||
|
||||
String _contactsSectionVisibilityKey(ContactsTabSection section) {
|
||||
switch (section) {
|
||||
case ContactsTabSection.favourites:
|
||||
return 'contacts_section_favourites_enabled';
|
||||
case ContactsTabSection.teamMembers:
|
||||
return 'contacts_section_team_members_enabled';
|
||||
case ContactsTabSection.repeaters:
|
||||
return 'contacts_section_repeaters_enabled';
|
||||
case ContactsTabSection.sensors:
|
||||
return 'contacts_section_sensors_enabled';
|
||||
case ContactsTabSection.rooms:
|
||||
return 'contacts_section_rooms_enabled';
|
||||
case ContactsTabSection.channels:
|
||||
return 'contacts_section_channels_enabled';
|
||||
}
|
||||
}
|
||||
|
||||
void _startPacketCapturePersistence() {
|
||||
_packetCaptureFlushTimer?.cancel();
|
||||
_packetCaptureFlushTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||
@@ -516,7 +556,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 +568,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 +579,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,18 +592,49 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadContactsSectionVisibility() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
for (final section in ContactsTabSection.values) {
|
||||
_contactsSectionVisibility[section] =
|
||||
prefs.getBool(_scopedKey(_contactsSectionVisibilityKey(section))) ??
|
||||
true;
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error loading contacts section visibility settings: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setContactsSectionEnabled(
|
||||
ContactsTabSection section,
|
||||
bool enabled,
|
||||
) async {
|
||||
try {
|
||||
_contactsSectionVisibility[section] = enabled;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
_scopedKey(_contactsSectionVisibilityKey(section)),
|
||||
enabled,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving contacts section visibility setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load sensors enabled setting from shared preferences
|
||||
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 +646,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 +658,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 +670,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 +685,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 +697,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 +712,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 +724,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 +735,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 +748,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 +759,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 +770,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 +781,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 +792,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 +806,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 +817,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 +830,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 +845,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 +994,12 @@ 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,
|
||||
@@ -1102,16 +1197,31 @@ class AppProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich message with sender name from contacts first
|
||||
// Enrich message with sender name from contacts.
|
||||
// For room-forwarded messages, resolve the original poster's name
|
||||
// from roomPostAuthorPrefix instead of the room server's key.
|
||||
Message enrichedMessage = message;
|
||||
Contact? senderContact;
|
||||
if (message.senderPublicKeyPrefix != null && message.senderName == null) {
|
||||
final contact = contactsProvider.findContactByKey(
|
||||
message.senderPublicKeyPrefix!,
|
||||
);
|
||||
if (contact != null) {
|
||||
senderContact = contact;
|
||||
enrichedMessage = message.copyWith(senderName: contact.advName);
|
||||
if (message.senderName == null) {
|
||||
// Try room post author first (the actual person who posted)
|
||||
if (message.roomPostAuthorPrefix != null) {
|
||||
final author = contactsProvider.findContactByPrefix(
|
||||
message.roomPostAuthorPrefix!,
|
||||
);
|
||||
if (author != null) {
|
||||
senderContact = author;
|
||||
enrichedMessage = message.copyWith(senderName: author.advName);
|
||||
}
|
||||
}
|
||||
// Fall back to sender key (direct messages, or room with unknown author)
|
||||
if (senderContact == null && message.senderPublicKeyPrefix != null) {
|
||||
final contact = contactsProvider.findContactByKey(
|
||||
message.senderPublicKeyPrefix!,
|
||||
);
|
||||
if (contact != null) {
|
||||
senderContact = contact;
|
||||
enrichedMessage = message.copyWith(senderName: contact.advName);
|
||||
}
|
||||
}
|
||||
}
|
||||
senderContact ??= message.senderPublicKeyPrefix != null
|
||||
@@ -1144,6 +1254,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...');
|
||||
@@ -1294,12 +1415,13 @@ class AppProvider with ChangeNotifier {
|
||||
if (_handlePendingRepeaterOwnerResponse(tag, responseData)) {
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry',
|
||||
);
|
||||
// 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);
|
||||
debugPrint('📊 [AppProvider] Binary response (0x8C tag=$tag) received');
|
||||
// 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 +1704,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 +1809,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 +2082,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 +2094,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 +2192,38 @@ 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;
|
||||
@@ -2377,12 +2553,10 @@ class AppProvider with ChangeNotifier {
|
||||
try {
|
||||
_isReconnectSyncInProgress = true;
|
||||
_hasCompletedConnectionBootstrap = false;
|
||||
// Initialize contacts provider with device public key to exclude self
|
||||
// If already initialized (from early load), this will just filter out self-contact
|
||||
// This must happen before getContacts to ensure proper filtering
|
||||
await contactsProvider.initialize(
|
||||
await contactsProvider.prepareForDeviceContactSync(
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
|
||||
// Note: Device clock is automatically synced during connection in MeshCoreBleService
|
||||
// No need to sync it again here
|
||||
@@ -2458,10 +2632,14 @@ class AppProvider with ChangeNotifier {
|
||||
'🔄 [AppProvider] Device reconnected - syncing contacts and missed messages',
|
||||
);
|
||||
|
||||
await contactsProvider.initialize(
|
||||
await contactsProvider.prepareForDeviceContactSync(
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
await connectionProvider.getContacts();
|
||||
await connectionProvider.syncChannels(
|
||||
maxChannels: connectionProvider.deviceInfo.maxChannels,
|
||||
);
|
||||
|
||||
final messageCount = await connectionProvider.syncAllMessages(
|
||||
force: true,
|
||||
@@ -2600,6 +2778,25 @@ class AppProvider with ChangeNotifier {
|
||||
locationTrackingService.setFastLocationActiveUse(isActive);
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedSettings() async {
|
||||
await Future.wait([
|
||||
_loadMapEnabled(),
|
||||
_loadContactsEnabled(),
|
||||
_loadContactsSectionVisibility(),
|
||||
_loadSensorsEnabled(),
|
||||
_loadVoiceSilenceTrimmingEnabled(),
|
||||
_loadVoiceBandPassFilterEnabled(),
|
||||
_loadVoiceCompressorEnabled(),
|
||||
_loadVoiceLimiterEnabled(),
|
||||
_loadVoiceAutoGainEnabled(),
|
||||
_loadVoiceEchoCancellationEnabled(),
|
||||
_loadVoiceNoiseSuppressionEnabled(),
|
||||
_loadMessageFontScale(),
|
||||
_loadMessagingRouteSettings(),
|
||||
locationTrackingService.loadSettings(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _sendFastLocationUpdate(
|
||||
dynamic position, {
|
||||
required String reason,
|
||||
@@ -3447,6 +3644,7 @@ class AppProvider with ChangeNotifier {
|
||||
await connectionProvider.getContacts();
|
||||
|
||||
// Sync all channels so refresh reflects the full device state.
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
await connectionProvider.syncChannels(
|
||||
maxChannels: connectionProvider.deviceInfo.maxChannels,
|
||||
);
|
||||
@@ -3612,6 +3810,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 +3834,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));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ class ChannelsProvider with ChangeNotifier {
|
||||
int _selectedChannelIndex = 0; // Default to public channel
|
||||
|
||||
/// Get all channels
|
||||
List<Channel> get channels => _channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||
List<Channel> get channels =>
|
||||
_channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||
|
||||
/// Get a specific channel by index
|
||||
Channel? getChannel(int index) => _channels[index];
|
||||
@@ -54,12 +55,12 @@ class ChannelsProvider with ChangeNotifier {
|
||||
void removeChannel(int index) {
|
||||
if (_channels.containsKey(index)) {
|
||||
_channels.remove(index);
|
||||
|
||||
|
||||
// If the deleted channel was selected, switch to public channel
|
||||
if (_selectedChannelIndex == index) {
|
||||
_selectedChannelIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -96,6 +97,13 @@ class ChannelsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear runtime channel state before a live device sync begins.
|
||||
void prepareForDeviceSync() {
|
||||
_channels.clear();
|
||||
_selectedChannelIndex = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if channels have been loaded
|
||||
bool get hasChannels => _channels.isNotEmpty;
|
||||
|
||||
|
||||
@@ -72,15 +72,22 @@ class ConnectionProvider with ChangeNotifier {
|
||||
static const int _controlTypeNodeDiscoverReq = 0x80;
|
||||
final MeshCoreBleService _bleService = MeshCoreBleService();
|
||||
MeshCoreTcpService? _tcpService;
|
||||
MeshCoreSerialService? _serialService;
|
||||
Future<void> Function()? _serialDisconnectTransport;
|
||||
|
||||
/// 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 +225,87 @@ 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) {
|
||||
await _disposeSerialService();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disposeTcpService() async {
|
||||
final service = _tcpService;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
_tcpService = null;
|
||||
await service.disconnect();
|
||||
service.dispose();
|
||||
}
|
||||
|
||||
Future<void> _disposeSerialService() async {
|
||||
final service = _serialService;
|
||||
final disconnectTransport = _serialDisconnectTransport;
|
||||
_serialService = null;
|
||||
_serialDisconnectTransport = null;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
service.markDisconnected();
|
||||
service.writeRaw = null;
|
||||
if (disconnectTransport != null) {
|
||||
await disconnectTransport();
|
||||
}
|
||||
service.dispose();
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -459,6 +547,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
manufacturerModel: deviceInfo['manufacturerModel'] as String?,
|
||||
semanticVersion: deviceInfo['semanticVersion'] as String?,
|
||||
clientRepeat: deviceInfo['clientRepeat'] as bool?,
|
||||
pathHashMode: deviceInfo['pathHashMode'] as int?,
|
||||
supportsSpectrumScan: deviceInfo['supportsSpectrumScan'] as bool?,
|
||||
spectrumScanMinKhz: deviceInfo['spectrumScanMinKhz'] as int?,
|
||||
spectrumScanMaxKhz: deviceInfo['spectrumScanMaxKhz'] as int?,
|
||||
@@ -475,6 +564,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
publicKey: selfInfo['publicKey'] as Uint8List?,
|
||||
advLat: selfInfo['advLat'] as int?,
|
||||
advLon: selfInfo['advLon'] as int?,
|
||||
multiAcks: selfInfo['multiAcks'] as int?,
|
||||
advertLocPolicy: selfInfo['advertLocPolicy'] as int?,
|
||||
telemetryModes: selfInfo['telemetryModes'] as int?,
|
||||
manualAddContacts: selfInfo['manualAddContacts'] as bool?,
|
||||
radioFreq: selfInfo['radioFreq'] as int?,
|
||||
radioBw: selfInfo['radioBw'] as int?,
|
||||
@@ -517,6 +609,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: config['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: config['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: config['autoAddOverwriteOldest'] as bool?,
|
||||
autoAddMaxHops: config['autoAddMaxHops'] as int?,
|
||||
);
|
||||
notifyListeners();
|
||||
};
|
||||
@@ -558,6 +651,13 @@ class ConnectionProvider with ChangeNotifier {
|
||||
debugPrint('✅ [Provider] Scan state initialized, notifying listeners');
|
||||
|
||||
try {
|
||||
final adapterError = await _awaitBleAdapterReady();
|
||||
if (adapterError != null) {
|
||||
debugPrint('⚠️ [Provider] BLE adapter not ready: $adapterError');
|
||||
_error = adapterError;
|
||||
return;
|
||||
}
|
||||
|
||||
await for (final scanResult in _bleService.scanForDevices(
|
||||
timeout: const Duration(seconds: 10),
|
||||
)) {
|
||||
@@ -599,6 +699,53 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _awaitBleAdapterReady({
|
||||
Duration timeout = const Duration(seconds: 4),
|
||||
}) async {
|
||||
if (!await FlutterBluePlus.isSupported) {
|
||||
return 'Bluetooth is not supported on this device.';
|
||||
}
|
||||
|
||||
var state = await FlutterBluePlus.adapterState.first;
|
||||
debugPrint('🔵 [Provider] Initial BLE adapter state: $state');
|
||||
|
||||
if (state == BluetoothAdapterState.unknown ||
|
||||
state == BluetoothAdapterState.turningOn) {
|
||||
debugPrint('⏳ [Provider] Waiting for BLE adapter to finish initializing');
|
||||
try {
|
||||
state = await FlutterBluePlus.adapterState
|
||||
.where(
|
||||
(candidate) =>
|
||||
candidate != BluetoothAdapterState.unknown &&
|
||||
candidate != BluetoothAdapterState.turningOn,
|
||||
)
|
||||
.first
|
||||
.timeout(timeout);
|
||||
} on TimeoutException {
|
||||
state = FlutterBluePlus.adapterStateNow;
|
||||
}
|
||||
debugPrint('🔵 [Provider] BLE adapter state after wait: $state');
|
||||
}
|
||||
|
||||
if (state == BluetoothAdapterState.on) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (state) {
|
||||
BluetoothAdapterState.off =>
|
||||
'Bluetooth is turned off. Turn it on and try again.',
|
||||
BluetoothAdapterState.turningOff =>
|
||||
'Bluetooth is turning off. Turn it back on and try again.',
|
||||
BluetoothAdapterState.unauthorized =>
|
||||
'Bluetooth access is not allowed. Check app permissions in System Settings and try again.',
|
||||
BluetoothAdapterState.unavailable =>
|
||||
'Bluetooth hardware is unavailable. On macOS, verify Bluetooth access is enabled for this app target.',
|
||||
BluetoothAdapterState.unknown || BluetoothAdapterState.turningOn =>
|
||||
'Bluetooth is still initializing. Please try again in a moment.',
|
||||
BluetoothAdapterState.on => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Stop scanning
|
||||
Future<void> stopScan() async {
|
||||
await FlutterBluePlus.stopScan();
|
||||
@@ -623,27 +770,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 +798,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 +823,50 @@ 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({
|
||||
required MeshCoreSerialService service,
|
||||
required Future<void> Function() disconnectTransport,
|
||||
required String deviceId,
|
||||
required String deviceName,
|
||||
}) async {
|
||||
debugPrint('🔌 [Provider] connectSerial()');
|
||||
await _prepareForConnectionSwitch(ConnectionMode.usb);
|
||||
await _disposeSerialService();
|
||||
_serialService = service;
|
||||
_serialDisconnectTransport = disconnectTransport;
|
||||
_wireServiceCallbacks(_serialService!);
|
||||
_beginConnectionAttempt(
|
||||
mode: ConnectionMode.usb,
|
||||
deviceId: deviceId,
|
||||
deviceName: deviceName,
|
||||
);
|
||||
|
||||
// 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,
|
||||
);
|
||||
await _disposeSerialService();
|
||||
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 {
|
||||
await _disposeSerialService();
|
||||
_resetConnectionSession();
|
||||
}
|
||||
|
||||
/// Disconnect from device
|
||||
@@ -721,17 +880,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 +1019,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 +1429,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 +1497,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]
|
||||
@@ -1657,11 +1858,13 @@ class ConnectionProvider with ChangeNotifier {
|
||||
return pendingPing;
|
||||
}
|
||||
|
||||
final future = _runSmartPing(
|
||||
contactPublicKey: contactPublicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: onRetryWithFlooding,
|
||||
);
|
||||
final future = _isSelfPublicKey(contactPublicKey)
|
||||
? _runSelfTelemetryPing(contactPublicKey)
|
||||
: _runSmartPing(
|
||||
contactPublicKey: contactPublicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: onRetryWithFlooding,
|
||||
);
|
||||
_pendingSmartPings[pingKey] = future;
|
||||
notifyListeners();
|
||||
|
||||
@@ -1748,10 +1951,53 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<PingResult> _runSelfTelemetryPing(Uint8List devicePublicKey) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return PingResult(success: false, usedFlooding: false, timedOut: true);
|
||||
}
|
||||
|
||||
try {
|
||||
final pingFuture = _pingTracker.trackPing(
|
||||
publicKey: devicePublicKey,
|
||||
wasDirectAttempt: true,
|
||||
);
|
||||
|
||||
// Firmware treats a 4-byte telemetry request as "self telemetry".
|
||||
await _activeService.requestTelemetry(Uint8List(0), zeroHop: true);
|
||||
|
||||
final gotResponse = await pingFuture;
|
||||
return PingResult(
|
||||
success: gotResponse,
|
||||
usedFlooding: false,
|
||||
timedOut: !gotResponse,
|
||||
);
|
||||
} catch (e) {
|
||||
_error = 'Failed to request self telemetry: $e';
|
||||
notifyListeners();
|
||||
return PingResult(success: false, usedFlooding: false, timedOut: true);
|
||||
}
|
||||
}
|
||||
|
||||
String _publicKeyToHex(Uint8List publicKey) {
|
||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||
}
|
||||
|
||||
bool _isSelfPublicKey(Uint8List publicKey) {
|
||||
final selfKey = _deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.length != publicKey.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < publicKey.length; i++) {
|
||||
if (selfKey[i] != publicKey[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Send binary request to contact (modern replacement for requestTelemetry)
|
||||
///
|
||||
/// Supports multiple request types:
|
||||
@@ -2096,6 +2342,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: config['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: config['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: config['autoAddOverwriteOldest'] as bool?,
|
||||
autoAddMaxHops: config['autoAddMaxHops'] as int?,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -2107,6 +2354,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: null,
|
||||
autoAddSensors: null,
|
||||
autoAddOverwriteOldest: null,
|
||||
autoAddMaxHops: null,
|
||||
);
|
||||
notifyListeners();
|
||||
return;
|
||||
@@ -2126,6 +2374,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
required bool autoAddRoomServers,
|
||||
required bool autoAddSensors,
|
||||
required bool overwriteOldest,
|
||||
int maxHops = 0,
|
||||
}) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -2140,6 +2389,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: autoAddRoomServers,
|
||||
autoAddSensors: autoAddSensors,
|
||||
overwriteOldest: overwriteOldest,
|
||||
maxHops: maxHops,
|
||||
);
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
autoAddUsers: autoAddUsers,
|
||||
@@ -2147,6 +2397,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: autoAddRoomServers,
|
||||
autoAddSensors: autoAddSensors,
|
||||
autoAddOverwriteOldest: overwriteOldest,
|
||||
autoAddMaxHops: maxHops,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -2155,7 +2406,63 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request fresh device info (triggers SelfInfo response)
|
||||
Future<void> setPathHashMode(int mode) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _activeService.setPathHashMode(mode);
|
||||
_deviceInfo = _deviceInfo.copyWith(pathHashMode: mode);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to set path hash mode: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@@ -2200,6 +2507,18 @@ class ConnectionProvider with ChangeNotifier {
|
||||
/// - Total storage in KB (if available)
|
||||
///
|
||||
/// Results arrive via onBatteryAndStorage callback and update deviceInfo.
|
||||
/// Request the companion device's own telemetry (GPS, temp, sensors).
|
||||
/// The response arrives via onTelemetryReceived with the device's own key.
|
||||
Future<void> requestSelfTelemetry() async {
|
||||
if (!_activeService.isConnected) return;
|
||||
try {
|
||||
// Firmware expects a 4-byte CMD_SEND_TELEMETRY_REQ frame for "self".
|
||||
await _activeService.requestTelemetry(Uint8List(0), zeroHop: true);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [Provider] requestSelfTelemetry failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getBatteryAndStorage() async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -2590,6 +2909,12 @@ class ConnectionProvider with ChangeNotifier {
|
||||
_rxActivityTimer?.cancel();
|
||||
_txActivityTimer?.cancel();
|
||||
_stopAckCleanupTimer();
|
||||
final disconnectTransport = _serialDisconnectTransport;
|
||||
if (disconnectTransport != null) {
|
||||
unawaited(disconnectTransport());
|
||||
_serialDisconnectTransport = null;
|
||||
}
|
||||
_serialService?.dispose();
|
||||
_bleService.dispose();
|
||||
_tcpService?.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -7,7 +7,9 @@ import '../models/contact_group.dart';
|
||||
import '../models/message_contact_location.dart';
|
||||
import '../services/cayenne_lpp_parser.dart';
|
||||
import '../services/contact_storage_service.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/fast_gps_packet.dart';
|
||||
import '../utils/rssi_location_estimator.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
|
||||
class PendingAdvert {
|
||||
@@ -125,85 +127,99 @@ class _RetainedRoute {
|
||||
class ContactsProvider with ChangeNotifier {
|
||||
static const double _firstHopFallbackOffsetMeters = 100.0;
|
||||
static const String autoGroupIdPrefix = 'auto_group_';
|
||||
static const String _rawTelemetryHexKey = '__raw_lpp_hex';
|
||||
final Map<String, Contact> _contacts = {};
|
||||
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
||||
final Map<String, PendingAdvert> _pendingAdverts = {};
|
||||
final Map<String, LatLng> _estimatedLocations = {};
|
||||
final Map<String, List<RssiObservation>> _rssiObservations = {};
|
||||
final Map<String, Contact> _retainedContactsForSync = {};
|
||||
final ContactStorageService _storageService = ContactStorageService();
|
||||
bool _isInitialized = false;
|
||||
bool _isPersisting = false;
|
||||
bool _persistRequested = false;
|
||||
bool _isPersistingPendingAdverts = false;
|
||||
bool _persistPendingAdvertsRequested = false;
|
||||
String? _storageNamespace;
|
||||
String? _selfPublicKeyHex;
|
||||
ContactTelemetry? _selfTelemetry;
|
||||
|
||||
// Add default public channel on initialization
|
||||
ContactsProvider() {
|
||||
ContactsProvider()
|
||||
: _storageNamespace = ProfileStorageScope.effectiveNamespace {
|
||||
_ensurePublicChannelExists();
|
||||
}
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
String? get storageNamespace => _storageNamespace;
|
||||
|
||||
/// Initialize and load persisted contacts at app startup
|
||||
/// This loads contacts without filtering, allowing offline viewing
|
||||
Future<void> initializeEarly() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
await _loadFromStorage();
|
||||
}
|
||||
|
||||
Future<void> reloadFromStorage({
|
||||
String? namespace,
|
||||
Uint8List? devicePublicKey,
|
||||
}) async {
|
||||
_storageNamespace = namespace;
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
|
||||
}
|
||||
|
||||
Future<void> persistNow() async {
|
||||
await _storageService.saveContacts(
|
||||
_contactsForStorage(),
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
await _storageService.saveContactGroups(
|
||||
_savedContactGroups,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
await _storageService.savePendingAdverts(
|
||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cloneCurrentStorageTo(String? namespace) async {
|
||||
await _storageService.saveContacts(
|
||||
_contactsForStorage(),
|
||||
namespace: namespace,
|
||||
);
|
||||
await _storageService.saveContactGroups(
|
||||
_savedContactGroups,
|
||||
namespace: namespace,
|
||||
);
|
||||
await _storageService.savePendingAdverts(
|
||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||
namespace: namespace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadFromStorage({
|
||||
bool force = false,
|
||||
Uint8List? devicePublicKey,
|
||||
}) async {
|
||||
if (_isInitialized && !force) return;
|
||||
|
||||
try {
|
||||
_resetInMemoryState();
|
||||
debugPrint(
|
||||
'📦 [ContactsProvider] Early loading persisted contacts (no filtering)...',
|
||||
);
|
||||
final storedContacts = await _storageService.loadContacts();
|
||||
final storedGroups = await _storageService.loadContactGroups();
|
||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
||||
|
||||
// Add stored contacts (excluding any with all-zeros public key)
|
||||
const publicChannelKey =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
for (final contact in storedContacts) {
|
||||
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
|
||||
if (contact.publicKeyHex == publicChannelKey) {
|
||||
continue;
|
||||
}
|
||||
_contacts[contact.publicKeyHex] = contact;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
_savedContactGroups
|
||||
..clear()
|
||||
..addAll(storedGroups);
|
||||
_restorePendingAdverts(storedPendingAdverts);
|
||||
debugPrint(
|
||||
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||
);
|
||||
|
||||
// Ensure public channel exists after loading
|
||||
_ensurePublicChannelExists();
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
|
||||
_isInitialized = true; // Mark as initialized even on error
|
||||
_ensurePublicChannelExists();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize and load persisted contacts
|
||||
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
||||
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
||||
if (_isInitialized) {
|
||||
// If already initialized (from early load), just filter out self-contact
|
||||
if (devicePublicKey != null) {
|
||||
_removeSelfContact(devicePublicKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
|
||||
final storedContacts = await _storageService.loadContacts(
|
||||
excludePublicKey: devicePublicKey,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
final storedGroups = await _storageService.loadContactGroups(
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
final storedPendingAdverts = await _storageService.loadPendingAdverts(
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
final storedGroups = await _storageService.loadContactGroups();
|
||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
||||
|
||||
// Add stored contacts (excluding any with all-zeros public key)
|
||||
const publicChannelKey =
|
||||
@@ -225,7 +241,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
devicePublicKey: devicePublicKey,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactsProvider] Loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||
);
|
||||
|
||||
// Ensure public channel exists after loading
|
||||
@@ -233,12 +249,66 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error initializing: $e');
|
||||
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
|
||||
_isInitialized = true; // Mark as initialized even on error
|
||||
_ensurePublicChannelExists();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize and load persisted contacts
|
||||
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
||||
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
if (_isInitialized) {
|
||||
// If already initialized (from early load), just filter out self-contact
|
||||
if (devicePublicKey != null) {
|
||||
_removeSelfContact(devicePublicKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
|
||||
if (devicePublicKey != null) {
|
||||
_removeSelfContact(devicePublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear runtime contact state before a live device contact sync begins.
|
||||
///
|
||||
/// This intentionally does not touch persisted storage. It keeps any saved
|
||||
/// contact groups for the active profile, but removes stale in-memory device
|
||||
/// contacts and discovery state so a newly connected device starts from an
|
||||
/// empty list while sync is in progress.
|
||||
Future<void> prepareForDeviceContactSync({Uint8List? devicePublicKey}) async {
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
if (!_isInitialized) {
|
||||
final storedGroups = await _storageService.loadContactGroups(
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
_savedContactGroups
|
||||
..clear()
|
||||
..addAll(storedGroups);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'🧹 [ContactsProvider] Clearing runtime contacts before device sync',
|
||||
);
|
||||
_retainedContactsForSync
|
||||
..clear()
|
||||
..addEntries(
|
||||
_contactsForStorage().map(
|
||||
(contact) => MapEntry(contact.publicKeyHex, contact),
|
||||
),
|
||||
);
|
||||
_contacts.clear();
|
||||
_pendingAdverts.clear();
|
||||
_estimatedLocations.clear();
|
||||
_rssiObservations.clear();
|
||||
_ensurePublicChannelExists();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
||||
void _removeSelfContact(Uint8List devicePublicKey) {
|
||||
final selfKeyHex = devicePublicKey
|
||||
@@ -287,7 +357,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 +378,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_persistPendingAdvertsRequested = false;
|
||||
await _storageService.savePendingAdverts(
|
||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -315,6 +389,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
List<Contact> get contacts => _contacts.values.toList();
|
||||
ContactTelemetry? get selfTelemetry => _selfTelemetry;
|
||||
List<Contact> get favouriteContacts =>
|
||||
_contacts.values.where((c) => c.isFavourite).toList();
|
||||
List<SavedContactGroup> get savedContactGroups =>
|
||||
List<SavedContactGroup>.from(_savedContactGroups)
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
@@ -436,7 +513,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 +549,73 @@ class ContactsProvider with ChangeNotifier {
|
||||
..sort(_sortByLastSeen);
|
||||
}
|
||||
|
||||
/// All estimated locations (for passing to map marker service).
|
||||
Map<String, LatLng> get estimatedLocations =>
|
||||
Map.unmodifiable(_estimatedLocations);
|
||||
|
||||
/// Get estimated location for a contact (RSSI-based, near last-hop repeater).
|
||||
LatLng? estimatedLocationFor(String publicKeyHex) =>
|
||||
_estimatedLocations[publicKeyHex];
|
||||
|
||||
/// Set an estimated location for a contact that has no GPS.
|
||||
void setEstimatedLocation(String publicKeyHex, LatLng location) {
|
||||
_estimatedLocations[publicKeyHex] = location;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Record an RSSI observation and re-trilaterate the contact's position.
|
||||
///
|
||||
/// Keeps up to 5 observations per unique repeater (latest wins).
|
||||
/// With multiple repeaters, trilateration produces a better estimate.
|
||||
void addRssiObservation({
|
||||
required String contactPublicKeyHex,
|
||||
required List<int> contactPublicKey,
|
||||
required RssiObservation observation,
|
||||
}) {
|
||||
final observations = _rssiObservations.putIfAbsent(
|
||||
contactPublicKeyHex,
|
||||
() => [],
|
||||
);
|
||||
|
||||
// Replace existing observation from the same repeater, or add new
|
||||
final repeaterKey =
|
||||
'${observation.repeaterLocation.latitude},${observation.repeaterLocation.longitude}';
|
||||
observations.removeWhere(
|
||||
(o) =>
|
||||
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
|
||||
repeaterKey,
|
||||
);
|
||||
observations.add(observation);
|
||||
|
||||
// Keep at most 8 observations (most recent per repeater)
|
||||
if (observations.length > 8) {
|
||||
observations.sort((a, b) => b.observedAt.compareTo(a.observedAt));
|
||||
observations.removeRange(8, observations.length);
|
||||
}
|
||||
|
||||
final estimated = RssiLocationEstimator.trilaterate(
|
||||
observations: observations,
|
||||
contactPublicKey: contactPublicKey,
|
||||
);
|
||||
if (estimated != null) {
|
||||
_estimatedLocations[contactPublicKeyHex] = estimated;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective location: own GPS/advert > estimated from RSSI.
|
||||
LatLng? effectiveLocationFor(Contact contact) {
|
||||
return contact.displayLocation ?? _estimatedLocations[contact.publicKeyHex];
|
||||
}
|
||||
|
||||
/// Get contacts with location (for map display)
|
||||
/// Includes contacts with estimated locations from RSSI.
|
||||
List<Contact> get contactsWithLocation =>
|
||||
contacts.where((c) => c.displayLocation != null).toList();
|
||||
contacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||
|
||||
/// Get chat contacts with location (team members on map)
|
||||
List<Contact> get chatContactsWithLocation =>
|
||||
chatContacts.where((c) => c.displayLocation != null).toList();
|
||||
chatContacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||
|
||||
MessageContactLocation? buildMessageContactLocationSnapshot(
|
||||
Contact contact, {
|
||||
@@ -544,7 +684,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// Check if this is a new contact
|
||||
final existingContact = _contacts[contact.publicKeyHex];
|
||||
final existingContact =
|
||||
_contacts[contact.publicKeyHex] ??
|
||||
_retainedContactsForSync[contact.publicKeyHex];
|
||||
final isNewContact = existingContact == null;
|
||||
debugPrint(
|
||||
' isNew: $isNewContact, total contacts before: ${_contacts.length}',
|
||||
@@ -556,7 +698,13 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
_contacts[contact.publicKeyHex] = updatedContact;
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
_retainedContactsForSync.remove(contact.publicKeyHex);
|
||||
// Keep repeaters and sensors in pending adverts so they remain visible
|
||||
// in the discovery list (with a checkmark). Remove others.
|
||||
if (contact.type != ContactType.repeater &&
|
||||
contact.type != ContactType.sensor) {
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
}
|
||||
debugPrint(
|
||||
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
||||
);
|
||||
@@ -580,12 +728,18 @@ class ContactsProvider with ChangeNotifier {
|
||||
excluded++;
|
||||
continue;
|
||||
}
|
||||
final existingContact = _contacts[contact.publicKeyHex];
|
||||
final existingContact =
|
||||
_contacts[contact.publicKeyHex] ??
|
||||
_retainedContactsForSync[contact.publicKeyHex];
|
||||
_contacts[contact.publicKeyHex] = _mergeIncomingContact(
|
||||
incomingContact: contact,
|
||||
existingContact: existingContact,
|
||||
);
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
_retainedContactsForSync.remove(contact.publicKeyHex);
|
||||
if (contact.type != ContactType.repeater &&
|
||||
contact.type != ContactType.sensor) {
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
if (excluded > 0) {
|
||||
debugPrint(
|
||||
@@ -825,6 +979,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,19 +990,46 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
debugPrint(' LPP data size: ${lppData.length} bytes');
|
||||
|
||||
// Deduplicate: same data for same contact within 2 seconds = skip
|
||||
final prefixHex = publicKeyPrefix
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final dataHash = lppData.fold<int>(0, (h, b) => h * 31 + b);
|
||||
final now = DateTime.now();
|
||||
final last = _lastTelemetryHash[prefixHex];
|
||||
if (last != null &&
|
||||
last.$1 == dataHash &&
|
||||
now.difference(last.$2).inSeconds < 2) {
|
||||
debugPrint(' ⏭️ Duplicate telemetry payload, skipping');
|
||||
return;
|
||||
}
|
||||
_lastTelemetryHash[prefixHex] = (dataHash, now);
|
||||
|
||||
// Find contact by public key prefix
|
||||
final contact = _findContactByPrefix(publicKeyPrefix);
|
||||
if (contact == null) {
|
||||
final isSelfTelemetry =
|
||||
contact == null && _matchesSelfPrefix(publicKeyPrefix);
|
||||
if (contact == null && !isSelfTelemetry) {
|
||||
debugPrint(' ❌ Contact not found for this prefix');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(' ✅ Found contact: ${contact.advName}');
|
||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||
if (isSelfTelemetry) {
|
||||
debugPrint(' ✅ Matched self telemetry response');
|
||||
debugPrint(
|
||||
' Old self telemetry timestamp: ${_selfTelemetry?.timestamp}',
|
||||
);
|
||||
} else {
|
||||
debugPrint(' ✅ Found contact: ${contact!.advName}');
|
||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse Cayenne LPP data
|
||||
var telemetry = CayenneLppParser.parse(lppData);
|
||||
var telemetry = _withRawTelemetryHex(
|
||||
CayenneLppParser.parse(lppData),
|
||||
lppData,
|
||||
);
|
||||
debugPrint(' ✅ Parsed new telemetry');
|
||||
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
||||
|
||||
@@ -863,7 +1048,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
final previousTelemetry = contact.telemetry;
|
||||
final previousTelemetry = isSelfTelemetry
|
||||
? _selfTelemetry
|
||||
: contact!.telemetry;
|
||||
|
||||
final mergedTelemetry = _mergeTelemetryForContact(
|
||||
existingTelemetry: previousTelemetry,
|
||||
@@ -875,31 +1062,49 @@ class ContactsProvider with ChangeNotifier {
|
||||
telemetry.gpsLocation,
|
||||
)) {
|
||||
debugPrint(
|
||||
' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps',
|
||||
' ⚠️ Incoming telemetry GPS is invalid/missing; keeping last known contact location as fallback: $incomingGps',
|
||||
);
|
||||
}
|
||||
telemetry = mergedTelemetry;
|
||||
telemetry = ContactTelemetry(
|
||||
gpsLocation: telemetry.gpsLocation,
|
||||
batteryPercentage: mergedTelemetry.batteryPercentage,
|
||||
batteryMilliVolts: mergedTelemetry.batteryMilliVolts,
|
||||
temperature: mergedTelemetry.temperature,
|
||||
timestamp: mergedTelemetry.timestamp,
|
||||
humidity: mergedTelemetry.humidity,
|
||||
pressure: mergedTelemetry.pressure,
|
||||
extraSensorData: mergedTelemetry.extraSensorData,
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelfTelemetry) {
|
||||
_selfTelemetry = telemetry;
|
||||
notifyListeners();
|
||||
debugPrint(' ✅ Updated self telemetry');
|
||||
return;
|
||||
}
|
||||
|
||||
final resolvedContact = contact!;
|
||||
|
||||
// Update contact with new telemetry AND last seen time
|
||||
// lastAdvert is Unix timestamp in seconds
|
||||
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
|
||||
.round();
|
||||
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
||||
debugPrint(' Old lastAdvert: ${resolvedContact.lastAdvert}');
|
||||
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||
|
||||
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
||||
final updatedContact = contact.copyWith(
|
||||
final updatedContact = resolvedContact.copyWith(
|
||||
telemetry: telemetry,
|
||||
lastAdvert: currentTimestamp, // Update last seen time
|
||||
advLat: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.latitude)
|
||||
: contact.advLat,
|
||||
: resolvedContact.advLat,
|
||||
advLon: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.longitude)
|
||||
: contact.advLon,
|
||||
: resolvedContact.advLon,
|
||||
);
|
||||
_contacts[contact.publicKeyHex] = updatedContact;
|
||||
_contacts[resolvedContact.publicKeyHex] = updatedContact;
|
||||
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||
|
||||
_persistContacts();
|
||||
@@ -993,10 +1198,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,16 +1222,82 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
ContactTelemetry _withRawTelemetryHex(
|
||||
ContactTelemetry telemetry,
|
||||
Uint8List lppData,
|
||||
) {
|
||||
final extraSensorData = <String, dynamic>{...?telemetry.extraSensorData};
|
||||
extraSensorData[_rawTelemetryHexKey] = _bytesToHex(lppData);
|
||||
return ContactTelemetry(
|
||||
gpsLocation: telemetry.gpsLocation,
|
||||
batteryPercentage: telemetry.batteryPercentage,
|
||||
batteryMilliVolts: telemetry.batteryMilliVolts,
|
||||
temperature: telemetry.temperature,
|
||||
timestamp: telemetry.timestamp,
|
||||
humidity: telemetry.humidity,
|
||||
pressure: telemetry.pressure,
|
||||
extraSensorData: extraSensorData,
|
||||
);
|
||||
}
|
||||
|
||||
String _bytesToHex(Uint8List bytes) {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
}
|
||||
|
||||
Map<String, dynamic> _mergeExtraSensorData(
|
||||
Map<String, dynamic>? existingExtraSensorData,
|
||||
Map<String, dynamic>? incomingExtraSensorData,
|
||||
) {
|
||||
final merged = <String, dynamic>{...?existingExtraSensorData};
|
||||
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
final incomingMetricFamilies = incomingExtraSensorData.keys
|
||||
.map(_telemetryMetricFamilyForKey)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
if (incomingMetricFamilies.isNotEmpty) {
|
||||
merged.removeWhere((key, _) {
|
||||
final family = _telemetryMetricFamilyForKey(key);
|
||||
return family != null && incomingMetricFamilies.contains(family);
|
||||
});
|
||||
}
|
||||
|
||||
merged.addAll(incomingExtraSensorData);
|
||||
return merged;
|
||||
}
|
||||
|
||||
String? _telemetryMetricFamilyForKey(String key) {
|
||||
const sourcePrefix = '__source_channel:';
|
||||
if (key.startsWith(sourcePrefix)) {
|
||||
return key.substring(sourcePrefix.length);
|
||||
}
|
||||
|
||||
final separatorIndex = key.lastIndexOf('_');
|
||||
if (separatorIndex <= 0 || separatorIndex == key.length - 1) {
|
||||
return key;
|
||||
}
|
||||
|
||||
final suffix = key.substring(separatorIndex + 1);
|
||||
if (int.tryParse(suffix) == null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return key.substring(0, separatorIndex);
|
||||
}
|
||||
|
||||
int _coordinateToAdvertMicrodegrees(double coordinate) {
|
||||
return (coordinate * 1e6).round();
|
||||
}
|
||||
|
||||
/// Find contact by public key prefix (6 bytes)
|
||||
Contact? _findContactByPrefix(Uint8List prefix) {
|
||||
if (prefix.length < 6) return null;
|
||||
if (prefix.isEmpty) return null;
|
||||
|
||||
final takeLen = prefix.length < 6 ? prefix.length : 6;
|
||||
final prefixHex = prefix
|
||||
.sublist(0, 6)
|
||||
.sublist(0, takeLen)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('');
|
||||
|
||||
@@ -1062,6 +1333,36 @@ class ContactsProvider with ChangeNotifier {
|
||||
return _contacts[keyHex];
|
||||
}
|
||||
|
||||
void _setSelfDevicePublicKey(Uint8List? devicePublicKey) {
|
||||
final nextKeyHex = _publicKeyHexOrNull(devicePublicKey);
|
||||
if (_selfPublicKeyHex != nextKeyHex) {
|
||||
_selfTelemetry = null;
|
||||
}
|
||||
_selfPublicKeyHex = nextKeyHex;
|
||||
}
|
||||
|
||||
String? _publicKeyHexOrNull(Uint8List? publicKey) {
|
||||
if (publicKey == null || publicKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||
}
|
||||
|
||||
bool _matchesSelfPrefix(Uint8List prefix) {
|
||||
final selfKeyHex = _selfPublicKeyHex;
|
||||
if (selfKeyHex == null || prefix.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final takeLen = prefix.length < 6 ? prefix.length : 6;
|
||||
final prefixHex = prefix
|
||||
.sublist(0, takeLen)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('');
|
||||
return selfKeyHex.startsWith(prefixHex);
|
||||
}
|
||||
|
||||
/// Clear a contact's learned path locally so the UI and next send both
|
||||
/// prefer flood routing until the radio reports a fresh route.
|
||||
void markPathUnhealthy(Uint8List publicKey) {
|
||||
@@ -1359,6 +1660,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
void clearContacts() {
|
||||
_contacts.clear();
|
||||
_pendingAdverts.clear();
|
||||
_retainedContactsForSync.clear();
|
||||
_ensurePublicChannelExists();
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
@@ -1383,6 +1685,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
// Then remove from local storage
|
||||
_contacts.remove(publicKeyHex);
|
||||
_pendingAdverts.remove(publicKeyHex);
|
||||
_retainedContactsForSync.remove(publicKeyHex);
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
@@ -1390,11 +1693,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 +1701,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_pendingAdverts.clear();
|
||||
await _storageService.clearPendingAdverts();
|
||||
await _storageService.clearPendingAdverts(namespace: _storageNamespace);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1465,6 +1764,16 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_contacts.clear();
|
||||
_savedContactGroups.clear();
|
||||
_pendingAdverts.clear();
|
||||
_estimatedLocations.clear();
|
||||
_rssiObservations.clear();
|
||||
_retainedContactsForSync.clear();
|
||||
_selfTelemetry = null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
|
||||
return {
|
||||
'publicKey': base64Encode(advert.publicKey),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
|
||||
/// Drawing mode state
|
||||
@@ -33,12 +36,18 @@ class DrawingProvider with ChangeNotifier {
|
||||
LatLng? _measurementPoint1;
|
||||
LatLng? _measurementPoint2;
|
||||
double? _measuredDistance; // in meters
|
||||
MapCoordinateSpace _activeCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _activeMapId;
|
||||
double? _activeMetersPerPixel;
|
||||
|
||||
// Getters
|
||||
DrawingMode get drawingMode => _drawingMode;
|
||||
Color get selectedColor => _selectedColor;
|
||||
bool get showReceivedDrawings => _showReceivedDrawings;
|
||||
bool get showSarMarkers => _showSarMarkers;
|
||||
MapCoordinateSpace get activeCoordinateSpace => _activeCoordinateSpace;
|
||||
String? get activeMapId => _activeMapId;
|
||||
double? get activeMetersPerPixel => _activeMetersPerPixel;
|
||||
List<MapDrawing> get drawings {
|
||||
// Filter out hidden drawings first
|
||||
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
||||
@@ -48,8 +57,19 @@ class DrawingProvider with ChangeNotifier {
|
||||
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
|
||||
}
|
||||
|
||||
visibleDrawings = visibleDrawings.where((drawing) {
|
||||
if (drawing.coordinateSpace != _activeCoordinateSpace) {
|
||||
return false;
|
||||
}
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return drawing.mapId != null && drawing.mapId == _activeMapId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return List.unmodifiable(visibleDrawings.toList());
|
||||
}
|
||||
|
||||
MapDrawing? get currentDrawing => _currentDrawing;
|
||||
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
||||
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
||||
@@ -66,6 +86,35 @@ class DrawingProvider with ChangeNotifier {
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _loadPreferences();
|
||||
await _loadDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMapContext({
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
double? metersPerPixel,
|
||||
}) {
|
||||
final changed =
|
||||
coordinateSpace != _activeCoordinateSpace ||
|
||||
mapId != _activeMapId ||
|
||||
metersPerPixel != _activeMetersPerPixel;
|
||||
_activeCoordinateSpace = coordinateSpace;
|
||||
_activeMapId = mapId;
|
||||
_activeMetersPerPixel = metersPerPixel;
|
||||
if (_measurementPoint1 != null && _measurementPoint2 != null) {
|
||||
_measuredDistance = _calculateDistance(
|
||||
_measurementPoint1!,
|
||||
_measurementPoint2!,
|
||||
);
|
||||
}
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set drawing mode
|
||||
void setDrawingMode(DrawingMode mode) {
|
||||
if (_drawingMode != mode) {
|
||||
@@ -98,15 +147,19 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showReceivedDrawings = prefs.getBool(_showReceivedDrawingsKey) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_showSarMarkersKey) ?? true;
|
||||
_showReceivedDrawings =
|
||||
prefs.getBool(_scopedKey(_showReceivedDrawingsKey)) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_scopedKey(_showSarMarkersKey)) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_showReceivedDrawingsKey, _showReceivedDrawings);
|
||||
await prefs.setBool(_showSarMarkersKey, _showSarMarkers);
|
||||
await prefs.setBool(
|
||||
_scopedKey(_showReceivedDrawingsKey),
|
||||
_showReceivedDrawings,
|
||||
);
|
||||
await prefs.setBool(_scopedKey(_showSarMarkersKey), _showSarMarkers);
|
||||
}
|
||||
|
||||
/// Start drawing a line
|
||||
@@ -137,6 +190,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: List.from(_currentLinePoints),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -180,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
? _rectangleStartPoint!.longitude
|
||||
: endPoint.longitude,
|
||||
),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -216,6 +277,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: topLeft,
|
||||
bottomRight: bottomRight,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -237,7 +302,9 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Set second measurement point and calculate distance
|
||||
void setMeasurementPoint2(LatLng point) {
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_measurementPoint2 = point;
|
||||
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
|
||||
@@ -246,6 +313,13 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Calculate distance between two points using Haversine formula
|
||||
double _calculateDistance(LatLng point1, LatLng point2) {
|
||||
if (_activeCoordinateSpace == MapCoordinateSpace.customMap &&
|
||||
_activeMetersPerPixel != null) {
|
||||
final dy = point2.latitude - point1.latitude;
|
||||
final dx = point2.longitude - point1.longitude;
|
||||
final pixelDistance = math.sqrt((dx * dx) + (dy * dy));
|
||||
return pixelDistance * _activeMetersPerPixel!;
|
||||
}
|
||||
const Distance distance = Distance();
|
||||
return distance.as(LengthUnit.Meter, point1, point2);
|
||||
}
|
||||
@@ -302,7 +376,7 @@ class DrawingProvider with ChangeNotifier {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = _drawings.map((d) => d.toJson()).toList();
|
||||
final jsonString = jsonEncode(jsonList);
|
||||
await prefs.setString(_storageKey, jsonString);
|
||||
await prefs.setString(_scopedKey(_storageKey), jsonString);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving drawings: $e');
|
||||
}
|
||||
@@ -312,8 +386,11 @@ class DrawingProvider with ChangeNotifier {
|
||||
Future<void> _loadDrawings() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_storageKey);
|
||||
if (jsonString == null) return;
|
||||
final jsonString = prefs.getString(_scopedKey(_storageKey));
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
_drawings.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
_drawings.clear();
|
||||
@@ -339,6 +416,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: _currentLinePoints,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
} else if (_drawingMode == DrawingMode.rectangle &&
|
||||
_currentDrawing != null) {
|
||||
@@ -376,6 +457,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
return RectangleDrawing(
|
||||
@@ -390,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
return drawing;
|
||||
@@ -404,9 +489,27 @@ class DrawingProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> exportDrawingsJson() {
|
||||
return _drawings.map((drawing) => drawing.toJson()).toList();
|
||||
}
|
||||
|
||||
Future<void> replaceDrawingsFromJson(
|
||||
List<Map<String, dynamic>> jsonList,
|
||||
) async {
|
||||
_drawings
|
||||
..clear()
|
||||
..addAll(jsonList.map(MapDrawing.fromJson).whereType<MapDrawing>());
|
||||
await _saveDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
/// Get all unshared drawings (local drawings not yet sent)
|
||||
List<MapDrawing> getUnsharedDrawings() {
|
||||
return _drawings.where((d) => !d.isShared && !d.isReceived).toList();
|
||||
return drawings.where((d) => !d.isShared && !d.isReceived).toList();
|
||||
}
|
||||
|
||||
/// Mark a drawing as shared
|
||||
@@ -428,6 +531,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -442,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,6 +576,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -483,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../models/message.dart';
|
||||
import '../../models/contact.dart';
|
||||
@@ -19,10 +20,19 @@ class MessageRetryManager {
|
||||
final Map<String, DateTime> _lastRetryTimes = {};
|
||||
final Map<String, int> _pathFailureStreaks = {};
|
||||
|
||||
static const int maxRetryAttempts = 4;
|
||||
/// Max retry attempts when the contact has a known path.
|
||||
/// 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
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
|
||||
@@ -75,6 +76,8 @@ class ImageProvider with ChangeNotifier {
|
||||
_restore();
|
||||
}
|
||||
|
||||
String _scopedStorageKey() => ProfileStorageScope.scopedKey(_storageKey);
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
ImageSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -292,18 +295,22 @@ class ImageProvider with ChangeNotifier {
|
||||
// ── Persistence ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> clearAll() async {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storageKey);
|
||||
await prefs.remove(_scopedStorageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to clear storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_resetInMemoryState();
|
||||
await _restore();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _evictExpiredOutgoing() {
|
||||
final now = DateTime.now();
|
||||
_outgoing.removeWhere((_, s) => now.difference(s.cachedAt) > _outgoingTtl);
|
||||
@@ -343,7 +350,7 @@ class ImageProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
||||
await prefs.setString(_scopedStorageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to persist: $e');
|
||||
}
|
||||
@@ -352,7 +359,7 @@ class ImageProvider with ChangeNotifier {
|
||||
Future<void> _restore() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
final raw = prefs.getString(_scopedStorageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
@@ -428,6 +435,12 @@ class ImageProvider with ChangeNotifier {
|
||||
debugPrint('❌ [ImageProvider] Failed to restore: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingSession {
|
||||
|
||||
@@ -1,30 +1,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (message.isContactMessage) {
|
||||
return existing.senderKeyShort == message.senderKeyShort;
|
||||
// Match by sender key + sender timestamp (matches official app's DB
|
||||
// uniqueness: contactPublicKey + senderTimestamp + text + txtType).
|
||||
if (existing.senderKeyShort == message.senderKeyShort &&
|
||||
existing.senderTimestamp == message.senderTimestamp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback dedup when retransmits surface as separate inbound rows
|
||||
// without a stable timestamp/message id, but still carry the same
|
||||
// visible sender identity and payload.
|
||||
return _matchesDuplicateSenderIdentity(existing, message);
|
||||
}
|
||||
|
||||
if (message.isChannelMessage) {
|
||||
@@ -692,59 +753,43 @@ class MessagesProvider with ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Primary dedup: same senderTimestamp = same message from mesh repeats.
|
||||
// Matches official app's DB uniqueness: (channelSecret, senderTimestamp, text).
|
||||
if (existing.senderTimestamp == message.senderTimestamp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Secondary: catch near-duplicate repeats within a 30s window
|
||||
// (clock drift between nodes).
|
||||
final withinChannelRepeatWindow =
|
||||
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
||||
|
||||
final existingSenderKey = existing.senderKeyShort;
|
||||
final incomingSenderKey = message.senderKeyShort;
|
||||
if (existingSenderKey != null &&
|
||||
incomingSenderKey != null &&
|
||||
existingSenderKey == incomingSenderKey &&
|
||||
withinChannelRepeatWindow) {
|
||||
return true;
|
||||
if (!withinChannelRepeatWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final existingSenderName = _normalizedResolvedSenderName(existing);
|
||||
final incomingSenderName = _normalizedResolvedSenderName(message);
|
||||
if (existing.isChannelMessage && message.isChannelMessage) {
|
||||
debugPrint(
|
||||
'🧪 [MessagesProvider] Channel dedupe check: '
|
||||
'existing(id=${existing.id}, sent=${existing.isSentMessage}, senderName=${existing.senderName ?? "-"}, resolvedSender=${existingSenderName ?? "-"}, senderKey=${existing.senderKeyShort ?? "-"}, ts=${existing.senderTimestamp}, pathLen=${existing.pathLen}) '
|
||||
'incoming(id=${message.id}, sent=${message.isSentMessage}, senderName=${message.senderName ?? "-"}, resolvedSender=${incomingSenderName ?? "-"}, senderKey=${message.senderKeyShort ?? "-"}, ts=${message.senderTimestamp}, pathLen=${message.pathLen}) '
|
||||
'withinWindow=$withinChannelRepeatWindow',
|
||||
);
|
||||
}
|
||||
if (existingSenderName != null &&
|
||||
incomingSenderName != null &&
|
||||
existingSenderName == incomingSenderName) {
|
||||
if (!existing.isSentMessage && !message.isSentMessage) {
|
||||
return withinChannelRepeatWindow;
|
||||
}
|
||||
|
||||
if (!withinChannelRepeatWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mirror meshcore-open behavior for self-authored channel messages:
|
||||
// zero-hop self replays are dropped earlier in AppProvider, while
|
||||
// routed self replays should fold into the local sent bubble when the
|
||||
// public sender name matches within a short window.
|
||||
if (existing.isSentMessage &&
|
||||
!message.isSentMessage &&
|
||||
existingSenderName == incomingSenderName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return _matchesDuplicateSenderIdentity(existing, message);
|
||||
}
|
||||
|
||||
// System messages and other types: never deduplicate by scope alone.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _matchesDuplicateSenderIdentity(Message existing, Message message) {
|
||||
final existingSenderKey = existing.senderKeyShort;
|
||||
final incomingSenderKey = message.senderKeyShort;
|
||||
if (existingSenderKey != null &&
|
||||
incomingSenderKey != null &&
|
||||
existingSenderKey == incomingSenderKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final existingSenderName = _normalizedResolvedSenderName(existing);
|
||||
final incomingSenderName = _normalizedResolvedSenderName(message);
|
||||
return existingSenderName != null &&
|
||||
incomingSenderName != null &&
|
||||
existingSenderName == incomingSenderName;
|
||||
}
|
||||
|
||||
/// Add multiple messages
|
||||
void addMessages(List<Message> messages) {
|
||||
int addedCount = 0;
|
||||
@@ -1051,6 +1096,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
messageReceptionDetails: _messageReceptionDetails,
|
||||
messageTransferDetails: _messageTransferDetails,
|
||||
messageRouteMetadata: _messageRouteMetadata,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1199,7 +1245,10 @@ class MessagesProvider with ChangeNotifier {
|
||||
Future<void> removeSarMarker(String id) async {
|
||||
_sarMarkers.remove(id);
|
||||
_removedSarMarkerIds.add(id);
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
await _storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1324,7 +1373,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
unawaited(
|
||||
_storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1348,23 +1402,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 +1418,29 @@ class MessagesProvider with ChangeNotifier {
|
||||
Future<void> clearSarMarkers() async {
|
||||
_removedSarMarkerIds.addAll(_sarMarkers.keys);
|
||||
_sarMarkers.clear();
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
await _storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all data
|
||||
void clearAll() {
|
||||
_cancelAllTimers();
|
||||
_resetInMemoryState();
|
||||
_retryManager.clearAll();
|
||||
_persistMessages();
|
||||
unawaited(
|
||||
_storageService.saveRemovedSarMarkerIds(
|
||||
_removedSarMarkerIds,
|
||||
namespace: _storageNamespace,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_removedSarMarkerIds.clear();
|
||||
@@ -1393,10 +1455,6 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageAckHistory.clear();
|
||||
_ackHistoryLookup.clear();
|
||||
_completedAckHistory.clear();
|
||||
_retryManager.clearAll();
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int transferCountForSession({
|
||||
@@ -2256,7 +2314,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttempts} for message $messageId',
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttemptsForContact(contact)} for message $messageId',
|
||||
);
|
||||
debugPrint(' Delay: ${delayMs}ms');
|
||||
|
||||
@@ -2296,6 +2354,17 @@ class MessagesProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// On the last attempt, reset the path to force flood mode
|
||||
// (matches official MeshCore app behaviour)
|
||||
if (_retryManager.isLastAttempt(currentMessage, contact)) {
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Last attempt — resetting path to flood for $messageId',
|
||||
);
|
||||
if (resetPathBeforeLastRetryCallback != null) {
|
||||
await resetPathBeforeLastRetryCallback!(contact);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendMessageCallback != null) {
|
||||
final queued = await sendMessageCallback!(
|
||||
contactPublicKey: contact.publicKey,
|
||||
@@ -2439,6 +2508,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
text: message.text,
|
||||
isSarMarker: message.isSarMarker,
|
||||
sarGpsCoordinates: message.sarGpsCoordinates,
|
||||
sarCustomMapPoint: message.sarCustomMapPoint,
|
||||
sarCustomMapId: message.sarCustomMapId,
|
||||
sarNotes: message.sarNotes,
|
||||
sarCustomEmoji: message.sarCustomEmoji,
|
||||
sarColorIndex: message.sarColorIndex,
|
||||
|
||||
@@ -5,6 +5,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';
|
||||
@@ -13,6 +14,7 @@ enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
||||
|
||||
class SensorsProvider with ChangeNotifier {
|
||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||
static const int selfAutoRefreshMinutes = 1;
|
||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
@@ -68,6 +70,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 +82,26 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> _loadWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
|
||||
final storedSpansJson = prefs.getString(_fieldSpanKey);
|
||||
final 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 +169,15 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_isLoaded = false;
|
||||
await _loadWatchedSensors();
|
||||
}
|
||||
|
||||
Future<void> _persistWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys);
|
||||
await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving watched sensors: $e');
|
||||
}
|
||||
@@ -175,7 +190,10 @@ class SensorsProvider with ChangeNotifier {
|
||||
for (final entry in _visibleFieldsBySensor.entries) {
|
||||
encoded[entry.key] = entry.value.toList();
|
||||
}
|
||||
await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded));
|
||||
await prefs.setString(
|
||||
_key(_visibleSensorMetricsKey),
|
||||
jsonEncode(encoded),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving visible sensor metrics: $e');
|
||||
}
|
||||
@@ -184,7 +202,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 +214,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 +226,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 +239,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_autoRefreshMinutesKey,
|
||||
_key(_autoRefreshMinutesKey),
|
||||
jsonEncode(_autoRefreshMinutesBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -347,6 +374,20 @@ class SensorsProvider with ChangeNotifier {
|
||||
return List<String>.unmodifiable(dueKeys);
|
||||
}
|
||||
|
||||
bool _isRefreshDue(
|
||||
String publicKeyHex, {
|
||||
required int minutes,
|
||||
required DateTime refreshTime,
|
||||
}) {
|
||||
if (minutes <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
||||
return lastRefreshAt == null ||
|
||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes);
|
||||
}
|
||||
|
||||
Future<void> toggleMetric(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
@@ -448,6 +489,28 @@ class SensorsProvider with ChangeNotifier {
|
||||
bool isWatched(String publicKeyHex) =>
|
||||
_watchedSensorKeys.contains(publicKeyHex);
|
||||
|
||||
Contact? selfContact(
|
||||
ContactsProvider contactsProvider,
|
||||
ConnectionProvider connectionProvider,
|
||||
) {
|
||||
final selfKey = connectionProvider.deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final existing = contactsProvider.findContactByKey(
|
||||
Uint8List.fromList(selfKey),
|
||||
);
|
||||
final selfTelemetry = contactsProvider.selfTelemetry;
|
||||
if (existing != null) {
|
||||
return selfTelemetry == null
|
||||
? existing
|
||||
: existing.copyWith(telemetry: selfTelemetry);
|
||||
}
|
||||
|
||||
return _buildSelfCandidate(connectionProvider, telemetry: selfTelemetry);
|
||||
}
|
||||
|
||||
Future<void> addSensor(Contact contact) async {
|
||||
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
||||
return;
|
||||
@@ -523,22 +586,117 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
|
||||
Future<void> reorderSensors(int oldIndex, int newIndex) async {
|
||||
if (_watchedSensorKeys.length < 2) {
|
||||
return;
|
||||
}
|
||||
if (oldIndex < 0 ||
|
||||
oldIndex >= _watchedSensorKeys.length ||
|
||||
newIndex < 0 ||
|
||||
newIndex > _watchedSensorKeys.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetIndex = newIndex;
|
||||
if (oldIndex < targetIndex) {
|
||||
targetIndex -= 1;
|
||||
}
|
||||
if (oldIndex == targetIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final movedKey = _watchedSensorKeys.removeAt(oldIndex);
|
||||
_watchedSensorKeys.insert(targetIndex, movedKey);
|
||||
await _persistWatchedSensors();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Contact> availableCandidates(
|
||||
ContactsProvider contactsProvider, {
|
||||
ConnectionProvider? connectionProvider,
|
||||
}) {
|
||||
final candidates = <Contact>[
|
||||
...contactsProvider.chatContacts,
|
||||
...contactsProvider.repeaters,
|
||||
...contactsProvider.sensorContacts,
|
||||
];
|
||||
|
||||
// Add "myself" (own device) as a candidate if it has a public key
|
||||
if (connectionProvider != null) {
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!candidates.any((c) => c.publicKeyHex == self.publicKeyHex)) {
|
||||
candidates.insert(0, self);
|
||||
}
|
||||
}
|
||||
|
||||
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
||||
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
List<String> displaySensorKeys({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
if (_watchedSensorKeys.isNotEmpty) {
|
||||
return List<String>.unmodifiable(_watchedSensorKeys);
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self == null) {
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
return <String>[self.publicKeyHex];
|
||||
}
|
||||
|
||||
String? displaySelfKey({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self == null || _watchedSensorKeys.contains(self.publicKeyHex)) {
|
||||
return null;
|
||||
}
|
||||
return self.publicKeyHex;
|
||||
}
|
||||
|
||||
Contact? contactForDisplay(
|
||||
String publicKeyHex, {
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self?.publicKeyHex == publicKeyHex) {
|
||||
return self;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> refreshAll({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) async {
|
||||
if (_isRefreshingAll || _watchedSensorKeys.isEmpty) {
|
||||
if (_isRefreshingAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
final keysToRefresh = <String>[
|
||||
if (self != null) self.publicKeyHex,
|
||||
..._watchedSensorKeys.where(
|
||||
(key) => self == null || key != self.publicKeyHex,
|
||||
),
|
||||
];
|
||||
if (keysToRefresh.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -546,7 +704,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
for (final key in _watchedSensorKeys) {
|
||||
for (final key in keysToRefresh) {
|
||||
await refreshSensor(
|
||||
publicKeyHex: key,
|
||||
contactsProvider: contactsProvider,
|
||||
@@ -571,13 +729,11 @@ class SensorsProvider with ChangeNotifier {
|
||||
|
||||
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||
|
||||
Contact? contact;
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
contact = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final contact = contactForDisplay(
|
||||
publicKeyHex,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
|
||||
if (contact == null) {
|
||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||
@@ -609,7 +765,17 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = dueAutoRefreshSensorKeys(now: refreshTime);
|
||||
final dueKeys = dueAutoRefreshSensorKeys(now: refreshTime).toList();
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!dueKeys.contains(self.publicKeyHex) &&
|
||||
_isRefreshDue(
|
||||
self.publicKeyHex,
|
||||
minutes: selfAutoRefreshMinutes,
|
||||
refreshTime: refreshTime,
|
||||
)) {
|
||||
dueKeys.insert(0, self.publicKeyHex);
|
||||
}
|
||||
if (dueKeys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -660,4 +826,29 @@ class SensorsProvider with ChangeNotifier {
|
||||
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Contact? _buildSelfCandidate(
|
||||
ConnectionProvider connectionProvider, {
|
||||
ContactTelemetry? telemetry,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final selfKey = deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(selfKey),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
advName: deviceInfo.selfName ?? 'My Device',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: deviceInfo.advLat ?? 0,
|
||||
advLon: deviceInfo.advLon ?? 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
telemetry: telemetry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
@@ -92,6 +93,9 @@ class VoiceProvider with ChangeNotifier {
|
||||
_restorePersistedVoiceData();
|
||||
}
|
||||
|
||||
String _storageKey() =>
|
||||
ProfileStorageScope.scopedKey(_voiceSessionsStorageKey);
|
||||
|
||||
// ── Session accessors ────────────────────────────────────────────────────
|
||||
|
||||
VoiceSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -329,19 +333,22 @@ class VoiceProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearStoredVoiceData() async {
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_playingSessionId = null;
|
||||
await _resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_voiceSessionsStorageKey);
|
||||
await prefs.remove(_storageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to clear stored voice data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _resetInMemoryState();
|
||||
await _restorePersistedVoiceData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -365,7 +372,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_voiceSessionsStorageKey, jsonEncode(payload));
|
||||
await prefs.setString(_storageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to persist voice data: $e');
|
||||
}
|
||||
@@ -374,7 +381,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
Future<void> _restorePersistedVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_voiceSessionsStorageKey);
|
||||
final raw = prefs.getString(_storageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
@@ -437,6 +444,16 @@ class VoiceProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resetInMemoryState() async {
|
||||
if (_playingSessionId != null || _player.isPlaying) {
|
||||
await _player.stop();
|
||||
}
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_playingSessionId = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playerEventsSub.cancel();
|
||||
|
||||
@@ -3,9 +3,12 @@ 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});
|
||||
final String? initialAdvert;
|
||||
|
||||
const AddContactScreen({super.key, this.initialAdvert});
|
||||
|
||||
@override
|
||||
State<AddContactScreen> createState() => _AddContactScreenState();
|
||||
@@ -20,7 +23,12 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadClipboardIfPresent();
|
||||
if (widget.initialAdvert != null &&
|
||||
widget.initialAdvert!.trim().isNotEmpty) {
|
||||
_advertController.text = widget.initialAdvert!.trim();
|
||||
} else {
|
||||
_loadClipboardIfPresent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -78,9 +86,9 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
final text = clipboardData?.text;
|
||||
if (text == null || text.trim().isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Clipboard is empty')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,9 +147,9 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Contact imported')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)),
|
||||
);
|
||||
setState(() {
|
||||
_importSucceeded = true;
|
||||
});
|
||||
@@ -151,107 +159,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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../widgets/compact_signal_indicator.dart' show SignalMetric;
|
||||
|
||||
enum _DiscoveryMenuAction { repeaters, sensors }
|
||||
enum _DiscoveryListFilter { all, repeaters, sensors, others }
|
||||
|
||||
class DiscoveryScreen extends StatefulWidget {
|
||||
const DiscoveryScreen({super.key});
|
||||
@@ -23,7 +23,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
static const int _sensorAdvertType = 4;
|
||||
final Set<String> _resolvingAdvertKeys = <String>{};
|
||||
final Set<int> _runningDiscoveryTypes = <int>{};
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
bool _isResolvingAll = false;
|
||||
String _searchQuery = '';
|
||||
_DiscoveryListFilter _selectedFilter = _DiscoveryListFilter.all;
|
||||
late final Future<List<MeshMapNode>> _cachedNodesFuture;
|
||||
|
||||
@override
|
||||
@@ -34,15 +37,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleMenuAction(_DiscoveryMenuAction action) async {
|
||||
switch (action) {
|
||||
case _DiscoveryMenuAction.repeaters:
|
||||
await _discoverNodeType(_repeaterAdvertType);
|
||||
break;
|
||||
case _DiscoveryMenuAction.sensors:
|
||||
await _discoverNodeType(_sensorAdvertType);
|
||||
break;
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _clearAllDiscoveries() async {
|
||||
@@ -54,7 +52,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 +63,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 +78,9 @@ 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 {
|
||||
@@ -288,8 +293,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
};
|
||||
}
|
||||
|
||||
String? _typeLabelForAdvert(PendingAdvert advert) {
|
||||
return switch (advert.typeValue) {
|
||||
String? _typeLabelForValue(int? typeValue) {
|
||||
return switch (typeValue) {
|
||||
_repeaterAdvertType => 'Repeater',
|
||||
_sensorAdvertType => 'Sensor',
|
||||
3 => 'Room',
|
||||
@@ -298,22 +303,22 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
};
|
||||
}
|
||||
|
||||
String? _resolvedTypeLabelForAdvert(
|
||||
int? _resolvedTypeValueForAdvert(
|
||||
PendingAdvert advert,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
final directType = _typeLabelForAdvert(advert);
|
||||
if (directType != null) {
|
||||
final directType = advert.typeValue;
|
||||
if (directType != null && directType != 0) {
|
||||
return directType;
|
||||
}
|
||||
|
||||
for (final node in cachedNodes) {
|
||||
if (node.publicKey == advert.publicKeyHex.toLowerCase()) {
|
||||
return switch (node.type) {
|
||||
1 => 'Repeater',
|
||||
4 => 'Sensor',
|
||||
3 => 'Room',
|
||||
2 => 'Chat',
|
||||
1 => _repeaterAdvertType,
|
||||
4 => _sensorAdvertType,
|
||||
3 => 3,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -322,6 +327,81 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _resolvedTypeLabelForAdvert(
|
||||
PendingAdvert advert,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
return _typeLabelForValue(_resolvedTypeValueForAdvert(advert, cachedNodes));
|
||||
}
|
||||
|
||||
bool get _hasActiveInlineFilter =>
|
||||
_searchQuery.trim().isNotEmpty ||
|
||||
_selectedFilter != _DiscoveryListFilter.all;
|
||||
|
||||
bool _matchesSelectedFilter(
|
||||
PendingAdvert advert,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
if (_selectedFilter == _DiscoveryListFilter.all) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final resolvedType = _resolvedTypeValueForAdvert(advert, cachedNodes);
|
||||
return switch (_selectedFilter) {
|
||||
_DiscoveryListFilter.all => true,
|
||||
_DiscoveryListFilter.repeaters => resolvedType == _repeaterAdvertType,
|
||||
_DiscoveryListFilter.sensors => resolvedType == _sensorAdvertType,
|
||||
_DiscoveryListFilter.others =>
|
||||
resolvedType != _repeaterAdvertType &&
|
||||
resolvedType != _sensorAdvertType,
|
||||
};
|
||||
}
|
||||
|
||||
bool _matchesSearchQuery(
|
||||
PendingAdvert advert,
|
||||
ContactsProvider contactsProvider,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
final query = _searchQuery.trim().toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final displayName = _displayNameForAdvert(
|
||||
advert,
|
||||
contactsProvider,
|
||||
cachedNodes,
|
||||
).toLowerCase();
|
||||
final typeLabel = (_resolvedTypeLabelForAdvert(advert, cachedNodes) ?? '')
|
||||
.toLowerCase();
|
||||
|
||||
return displayName.contains(query) ||
|
||||
advert.publicKeyHex.toLowerCase().contains(query) ||
|
||||
advert.shortDisplayKey.toLowerCase().contains(query) ||
|
||||
typeLabel.contains(query);
|
||||
}
|
||||
|
||||
List<PendingAdvert> _filteredPendingAdverts(
|
||||
List<PendingAdvert> adverts,
|
||||
ContactsProvider contactsProvider,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
return adverts
|
||||
.where((advert) => _matchesSelectedFilter(advert, cachedNodes))
|
||||
.where(
|
||||
(advert) =>
|
||||
_matchesSearchQuery(advert, contactsProvider, cachedNodes),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _summaryTitle({required int totalCount, required int filteredCount}) {
|
||||
if (filteredCount == totalCount) {
|
||||
return 'Discovered nodes ($totalCount)';
|
||||
}
|
||||
return 'Discovered nodes ($filteredCount/$totalCount)';
|
||||
}
|
||||
|
||||
Widget _buildAdvertTitle(
|
||||
BuildContext context, {
|
||||
required String displayName,
|
||||
@@ -357,270 +437,517 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Discovery'),
|
||||
actions: [
|
||||
Consumer<ConnectionProvider>(
|
||||
builder: (context, connectionProvider, child) {
|
||||
final isConnected = connectionProvider.deviceInfo.isConnected;
|
||||
final repeatersBusy = _runningDiscoveryTypes.contains(
|
||||
_repeaterAdvertType,
|
||||
);
|
||||
final sensorsBusy = _runningDiscoveryTypes.contains(
|
||||
_sensorAdvertType,
|
||||
);
|
||||
|
||||
return PopupMenuButton<_DiscoveryMenuAction>(
|
||||
tooltip: 'Discovery tools',
|
||||
onSelected: _handleMenuAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<_DiscoveryMenuAction>(
|
||||
value: _DiscoveryMenuAction.repeaters,
|
||||
enabled: isConnected && !repeatersBusy,
|
||||
child: Row(
|
||||
children: [
|
||||
repeatersBusy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Discover repeaters'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<_DiscoveryMenuAction>(
|
||||
value: _DiscoveryMenuAction.sensors,
|
||||
enabled: isConnected && !sensorsBusy,
|
||||
child: Row(
|
||||
children: [
|
||||
sensorsBusy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.sensors_outlined),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Discover sensors'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(title: Text(l10n.discovery)),
|
||||
body: FutureBuilder<List<MeshMapNode>>(
|
||||
future: _cachedNodesFuture,
|
||||
builder: (context, nodesSnapshot) => Consumer2<ContactsProvider, ConnectionProvider>(
|
||||
builder: (context, contactsProvider, connectionProvider, child) {
|
||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||
final isConnected = connectionProvider.deviceInfo.isConnected;
|
||||
final cachedNodes = nodesSnapshot.data ?? const <MeshMapNode>[];
|
||||
builder: (context, nodesSnapshot) =>
|
||||
Consumer2<ContactsProvider, ConnectionProvider>(
|
||||
builder: (context, contactsProvider, connectionProvider, child) {
|
||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||
final isConnected = connectionProvider.deviceInfo.isConnected;
|
||||
final cachedNodes = nodesSnapshot.data ?? const <MeshMapNode>[];
|
||||
final repeatersBusy = _runningDiscoveryTypes.contains(
|
||||
_repeaterAdvertType,
|
||||
);
|
||||
final sensorsBusy = _runningDiscoveryTypes.contains(
|
||||
_sensorAdvertType,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
// 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;
|
||||
final filteredAdverts = _filteredPendingAdverts(
|
||||
pendingAdverts,
|
||||
contactsProvider,
|
||||
cachedNodes,
|
||||
);
|
||||
final filteredCount = filteredAdverts.length;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.person_search),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_summaryTitle(
|
||||
totalCount: totalCount,
|
||||
filteredCount: filteredCount,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Resolve entries manually so they do not auto-populate contacts.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (totalCount > 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildInlineSearchField(context),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildTypeFilterChip(
|
||||
context,
|
||||
label: l10n.all,
|
||||
filter: _DiscoveryListFilter.all,
|
||||
),
|
||||
_buildTypeFilterChip(
|
||||
context,
|
||||
label: l10n.repeatersFilter,
|
||||
filter: _DiscoveryListFilter.repeaters,
|
||||
),
|
||||
_buildTypeFilterChip(
|
||||
context,
|
||||
label: l10n.sensors,
|
||||
filter: _DiscoveryListFilter.sensors,
|
||||
),
|
||||
_buildTypeFilterChip(
|
||||
context,
|
||||
label: l10n.others,
|
||||
filter: _DiscoveryListFilter.others,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacing = 10.0;
|
||||
final useTwoColumns =
|
||||
constraints.maxWidth >= 360;
|
||||
final buttonWidth = useTwoColumns
|
||||
? (constraints.maxWidth - spacing) / 2
|
||||
: constraints.maxWidth;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
_buildHeaderActionButton(
|
||||
width: buttonWidth,
|
||||
onPressed: isConnected && !repeatersBusy
|
||||
? () => _discoverNodeType(
|
||||
_repeaterAdvertType,
|
||||
)
|
||||
: null,
|
||||
icon: repeatersBusy
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.router_outlined),
|
||||
label: Text(l10n.discoverRepeaters),
|
||||
),
|
||||
_buildHeaderActionButton(
|
||||
width: buttonWidth,
|
||||
onPressed: isConnected && !sensorsBusy
|
||||
? () => _discoverNodeType(
|
||||
_sensorAdvertType,
|
||||
)
|
||||
: null,
|
||||
icon: sensorsBusy
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.sensors_outlined),
|
||||
label: Text(l10n.discoverSensors),
|
||||
),
|
||||
_buildHeaderActionButton(
|
||||
width: buttonWidth,
|
||||
onPressed:
|
||||
isConnected &&
|
||||
pendingAdverts.isNotEmpty &&
|
||||
!_isResolvingAll
|
||||
? () => _resolveAll(pendingAdverts)
|
||||
: null,
|
||||
icon: _isResolvingAll
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons
|
||||
.download_for_offline_outlined,
|
||||
),
|
||||
label: Text(l10n.resolveAll),
|
||||
),
|
||||
_buildHeaderActionButton(
|
||||
width: buttonWidth,
|
||||
onPressed: pendingAdverts.isNotEmpty
|
||||
? _clearAllDiscoveries
|
||||
: null,
|
||||
icon: const Icon(Icons.clear_all_rounded),
|
||||
label: Text(l10n.clearAllLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (totalCount == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No discovered nodes',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Use the discovery actions above to find repeaters and sensors on the mesh.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasActiveInlineFilter &&
|
||||
totalCount > 0 &&
|
||||
filteredCount == 0)
|
||||
_buildNoFilterResults(context),
|
||||
...filteredAdverts.map(
|
||||
(advert) => _buildPendingAdvertCard(
|
||||
context,
|
||||
advert: advert,
|
||||
contactsProvider: contactsProvider,
|
||||
isConnected: isConnected,
|
||||
isResolved: resolvedKeySet.contains(
|
||||
advert.publicKeyHex,
|
||||
),
|
||||
cachedNodes: cachedNodes,
|
||||
l10n: l10n,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.person_search),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pending discoveries (${pendingAdverts.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
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 _buildInlineSearchField(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final hasFilter = _searchQuery.trim().isNotEmpty;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: hasFilter
|
||||
? colorScheme.primary.withValues(alpha: 0.38)
|
||||
: colorScheme.outline.withValues(alpha: 0.32),
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.025),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: SizedBox(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 8),
|
||||
child: Icon(
|
||||
Icons.search_rounded,
|
||||
size: 17,
|
||||
color: hasFilter
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
cursorColor: colorScheme.primary,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.1,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search discovered nodes',
|
||||
hintStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(
|
||||
alpha: 0.85,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Resolve entries manually so they do not auto-populate contacts.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed:
|
||||
isConnected &&
|
||||
pendingAdverts.isNotEmpty &&
|
||||
!_isResolvingAll
|
||||
? () => _resolveAll(pendingAdverts)
|
||||
: null,
|
||||
icon: _isResolvingAll
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.download_for_offline_outlined,
|
||||
),
|
||||
label: const Text('Resolve all'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: pendingAdverts.isNotEmpty
|
||||
? _clearAllDiscoveries
|
||||
: null,
|
||||
icon: const Icon(Icons.clear_all_rounded),
|
||||
label: const Text('Clear all'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (pendingAdverts.isEmpty)
|
||||
if (hasFilter)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No pending discoveries',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Unknown adverts will appear here until you choose to resolve them.',
|
||||
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,
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Material(
|
||||
color: colorScheme.primary.withValues(alpha: 0.10),
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 14,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
detailLines.join('\n'),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeFilterChip(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required _DiscoveryListFilter filter,
|
||||
}) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final selected = _selectedFilter == filter;
|
||||
final color = selected ? scheme.primary : scheme.outline;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (_selectedFilter == filter) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: selected ? 0.14 : 0.08),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: selected ? 0.45 : 0.22),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoFilterResults(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||
child: Text(
|
||||
'No matches',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderActionButton({
|
||||
required double width,
|
||||
required VoidCallback? onPressed,
|
||||
required Widget icon,
|
||||
required Widget label,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
label: label,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSignalSummary(
|
||||
BuildContext context, {
|
||||
SignalMetric? downMetric,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -6,7 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/device_info.dart' show ConnectionMode;
|
||||
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
@@ -20,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,45 +434,184 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityBadge({
|
||||
required String label,
|
||||
required int count,
|
||||
required bool isActive,
|
||||
required Color activeColor,
|
||||
bool compact = false,
|
||||
}) {
|
||||
final color = isActive ? activeColor : Colors.grey;
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: compact ? 7 : 8,
|
||||
vertical: compact ? 4 : 5,
|
||||
void _showDeviceInfoSheet(BuildContext context, DeviceInfo deviceInfo) {
|
||||
// Request self telemetry so it's fresh
|
||||
context.read<ConnectionProvider>().requestSelfTelemetry();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: compact ? 6 : 7,
|
||||
height: compact ? 6 : 7,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
SizedBox(width: compact ? 5 : 6),
|
||||
Text(
|
||||
'$label:$count',
|
||||
style: TextStyle(
|
||||
fontSize: compact ? 10 : 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
builder: (context) => Consumer<ContactsProvider>(
|
||||
builder: (context, contactsProvider, child) {
|
||||
final telemetry = contactsProvider.selfTelemetry;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
deviceInfo.selfName ?? deviceInfo.deviceName ?? 'Device',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bluetooth,
|
||||
'BLE Signal',
|
||||
deviceInfo.signalRssi != null
|
||||
? '${deviceInfo.signalRssi} dBm'
|
||||
: 'N/A',
|
||||
),
|
||||
if (deviceInfo.batteryPercent != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
'Battery',
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
),
|
||||
if (deviceInfo.batteryMilliVolts != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bolt,
|
||||
'Voltage',
|
||||
'${(deviceInfo.batteryMilliVolts! / 1000).toStringAsFixed(2)}V',
|
||||
),
|
||||
if (deviceInfo.storageUsedKb != null &&
|
||||
deviceInfo.storageTotalKb != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.storage,
|
||||
'Storage',
|
||||
'${deviceInfo.storageUsedKb} / ${deviceInfo.storageTotalKb} KB',
|
||||
),
|
||||
if (deviceInfo.firmwareVersion != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.system_update,
|
||||
'Firmware',
|
||||
'v${deviceInfo.firmwareVersion}',
|
||||
),
|
||||
if (deviceInfo.radioFreq != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.radio,
|
||||
'Frequency',
|
||||
'${(deviceInfo.radioFreq! / 1000).toStringAsFixed(3)} MHz',
|
||||
),
|
||||
if (deviceInfo.txPower != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.power,
|
||||
'TX Power',
|
||||
'${deviceInfo.txPower} dBm',
|
||||
),
|
||||
if (telemetry != null) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Self Telemetry',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (telemetry.temperature != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.thermostat,
|
||||
'Temperature',
|
||||
'${telemetry.temperature!.toStringAsFixed(1)}°C',
|
||||
),
|
||||
if (telemetry.humidity != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.water_drop,
|
||||
'Humidity',
|
||||
'${telemetry.humidity!.toStringAsFixed(1)}%',
|
||||
),
|
||||
if (telemetry.pressure != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.compress,
|
||||
'Pressure',
|
||||
'${telemetry.pressure!.toStringAsFixed(1)} hPa',
|
||||
),
|
||||
if (telemetry.gpsLocation != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.gps_fixed,
|
||||
'GPS',
|
||||
'${telemetry.gpsLocation!.latitude.toStringAsFixed(5)}, ${telemetry.gpsLocation!.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deviceInfoRow(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniSignalBars({required int activeBars, required Color color}) {
|
||||
final inactive = Colors.grey.withValues(alpha: 0.3);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (var i = 0; i < 5; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 1.5),
|
||||
Container(
|
||||
width: 2.5,
|
||||
height: 4.0 + (i * 2),
|
||||
decoration: BoxDecoration(
|
||||
color: i < activeBars ? color : inactive,
|
||||
borderRadius: BorderRadius.circular(1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactActivityIndicator({
|
||||
required bool rxActive,
|
||||
required bool txActive,
|
||||
@@ -590,6 +768,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
|
||||
context.watch<AppProvider>();
|
||||
final activeProfileId = context.select<ProfileManager, String>(
|
||||
(manager) => manager.activeProfileId,
|
||||
);
|
||||
|
||||
final enabledTabs = _enabledTabs;
|
||||
final isMapTabActive = _currentTab == _HomeTab.map;
|
||||
@@ -614,7 +795,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onPressed: () async {
|
||||
await provider.disconnect();
|
||||
},
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
icon: Icon(Icons.power_settings_new),
|
||||
tooltip: AppLocalizations.of(context)!.disconnect,
|
||||
color: Colors.red.shade700,
|
||||
);
|
||||
@@ -626,6 +807,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
final items = <PopupMenuEntry<void>>[];
|
||||
final profilesEnabled = context
|
||||
.read<ProfileManager>()
|
||||
.profilesEnabled;
|
||||
|
||||
if (_isDeveloperModeEnabled) {
|
||||
items.add(
|
||||
@@ -633,8 +817,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.spectrumScan),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -655,11 +839,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.radar_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Live Traffic'),
|
||||
const Icon(Icons.radar_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -690,11 +874,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.router_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Repeaters Map'),
|
||||
const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -751,7 +935,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 +960,31 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
|
||||
if (profilesEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.layers_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.profiles),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfilesScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
),
|
||||
@@ -809,6 +1018,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return MapTab(
|
||||
key: ValueKey<String>('map:$activeProfileId'),
|
||||
onFullscreenChanged: (isFullscreen) {
|
||||
setState(() {
|
||||
_isMapFullscreen = isFullscreen;
|
||||
@@ -865,7 +1075,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return Tab(
|
||||
icon: const Icon(Icons.map),
|
||||
icon: Icon(Icons.map),
|
||||
text: AppLocalizations.of(context)!.map,
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
@@ -890,19 +1100,35 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
||||
|
||||
if (!isConnected) {
|
||||
// Disconnected state: show connect button
|
||||
final buttonLabel = provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
provider.isReconnecting
|
||||
? 'Restoring previous link'
|
||||
: 'No device connected',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: provider.isReconnecting
|
||||
? null
|
||||
: () => _showConnectionDialog(context),
|
||||
@@ -913,22 +1139,19 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.black54,
|
||||
Colors.white70,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth, size: 18),
|
||||
label: Text(
|
||||
provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
: const Icon(Icons.add_link_rounded, size: 18),
|
||||
label: Text(buttonLabel),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -936,7 +1159,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => provider.cancelReconnection(),
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
icon: Icon(Icons.close, size: 20),
|
||||
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
@@ -995,50 +1218,45 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isTcpConnected
|
||||
? Icons.wifi_rounded
|
||||
: Icons.bluetooth_connected_rounded,
|
||||
size: 13,
|
||||
color: signalColor,
|
||||
),
|
||||
if (!isTcpConnected &&
|
||||
deviceInfo.signalRssi != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${deviceInfo.signalRssi}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: signalColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (deviceInfo.batteryPercent != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
_showDeviceInfoSheet(context, deviceInfo),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
isTcpConnected
|
||||
? Icons.wifi_rounded
|
||||
: Icons.bluetooth_connected_rounded,
|
||||
size: 13,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
color: signalColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
if (!isTcpConnected &&
|
||||
deviceInfo.signalRssi != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
_buildMiniSignalBars(
|
||||
activeBars:
|
||||
BatteryDisplayHelper.getSignalBars(
|
||||
deviceInfo.signalRssi!,
|
||||
),
|
||||
color: signalColor,
|
||||
),
|
||||
],
|
||||
if (deviceInfo.batteryPercent != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
size: 13,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -1048,9 +1266,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1147,48 +1365,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isTight
|
||||
? _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
)
|
||||
: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh
|
||||
.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildActivityBadge(
|
||||
label: 'RX',
|
||||
count: provider.rxPacketCount,
|
||||
isActive: provider.rxActivity,
|
||||
activeColor: Colors.green,
|
||||
compact: true,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildActivityBadge(
|
||||
label: 'TX',
|
||||
count: provider.txPacketCount,
|
||||
isActive: provider.txActivity,
|
||||
activeColor: Colors.blue,
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(width: isTight ? 24 : 74),
|
||||
const SizedBox(width: 24),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
@@ -26,7 +27,6 @@ T? _maybeProvider<T>(BuildContext context) {
|
||||
|
||||
class LiveTrafficScreen extends StatefulWidget {
|
||||
final List<BlePacketLog> Function() logReader;
|
||||
final int Function()? rxCountReader;
|
||||
final Listenable? refreshListenable;
|
||||
final DateTime Function() now;
|
||||
final VoidCallback? openPacketLogs;
|
||||
@@ -34,7 +34,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
||||
const LiveTrafficScreen({
|
||||
super.key,
|
||||
required this.logReader,
|
||||
this.rxCountReader,
|
||||
this.refreshListenable,
|
||||
DateTime Function()? now,
|
||||
this.openPacketLogs,
|
||||
@@ -48,7 +47,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
||||
return LiveTrafficScreen(
|
||||
key: key,
|
||||
logReader: () => provider.bleService.packetLogs,
|
||||
rxCountReader: () => provider.rxPacketCount,
|
||||
refreshListenable: provider,
|
||||
openPacketLogs: openPacketLogs,
|
||||
);
|
||||
@@ -117,15 +115,17 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logs = widget.logReader();
|
||||
final totalRxDataCount = LiveTrafficSummary.countRxDataLogs(logs);
|
||||
final unfilteredSnapshot = LiveTrafficSummary.fromLogs(
|
||||
widget.logReader(),
|
||||
logs,
|
||||
now: widget.now(),
|
||||
clearedAt: _clearedAt,
|
||||
preferredHashSize: _preferredHashSize,
|
||||
window: _selectedWindow,
|
||||
);
|
||||
final snapshot = LiveTrafficSummary.fromLogs(
|
||||
widget.logReader(),
|
||||
logs,
|
||||
now: widget.now(),
|
||||
clearedAt: _clearedAt,
|
||||
preferredHashSize: _preferredHashSize,
|
||||
@@ -147,7 +147,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(
|
||||
@@ -176,7 +176,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
children: [
|
||||
_SummaryPanel(
|
||||
snapshot: snapshot,
|
||||
totalRxCount: widget.rxCountReader?.call(),
|
||||
totalRxCount: totalRxDataCount,
|
||||
routeHashCounts: routeHashCounts,
|
||||
onWindowTap: () => _showWindowPicker(context),
|
||||
),
|
||||
@@ -305,8 +305,48 @@ class _SummaryPanel extends StatelessWidget {
|
||||
LiveTrafficBusyness.active => 'Active',
|
||||
LiveTrafficBusyness.busy => 'Busy',
|
||||
};
|
||||
final metrics = [
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:rxPackets'),
|
||||
label: AppLocalizations.of(context)!.rxPackets,
|
||||
value: '${snapshot.rxCount}',
|
||||
subtitle: totalRxCount == null
|
||||
? _windowSummaryLabel(snapshot.windowDuration)
|
||||
: 'Device total $totalRxCount',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:rssi'),
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
value: snapshot.latestRssiDbm == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestRssiDbm} dBm',
|
||||
subtitle: snapshot.avgRssiDbm == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:snr'),
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
value: snapshot.latestSnrDb == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||
subtitle: snapshot.avgSnrDb == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||
),
|
||||
_MetricTile(
|
||||
tileKey: const ValueKey('liveTrafficMetric:multihop'),
|
||||
label: AppLocalizations.of(context)!.multihop,
|
||||
value: '${snapshot.multiHopCount}',
|
||||
subtitle: routeHashCounts.summaryLabel,
|
||||
footer: snapshot.avgHopCount == null
|
||||
? 'No routes yet'
|
||||
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
||||
),
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -327,17 +367,17 @@ class _SummaryPanel extends StatelessWidget {
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
_SummaryBadge(
|
||||
label: 'Mesh',
|
||||
label: AppLocalizations.of(context)!.mesh,
|
||||
value: busynessLabel,
|
||||
color: busynessColor,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Rate',
|
||||
label: AppLocalizations.of(context)!.rate,
|
||||
value: '${snapshot.packetsPerMinute} pkt/min',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Window',
|
||||
label: AppLocalizations.of(context)!.window,
|
||||
value: _windowLabel(snapshot.windowDuration),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onTap: onWindowTap,
|
||||
@@ -345,44 +385,26 @@ class _SummaryPanel extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
_MetricTile(
|
||||
label: 'RX packets',
|
||||
value: '${snapshot.rxCount}',
|
||||
subtitle: totalRxCount == null
|
||||
? _windowSummaryLabel(snapshot.windowDuration)
|
||||
: 'Device total $totalRxCount',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'RSSI',
|
||||
value: snapshot.latestRssiDbm == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestRssiDbm} dBm',
|
||||
subtitle: snapshot.avgRssiDbm == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'SNR',
|
||||
value: snapshot.latestSnrDb == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||
subtitle: snapshot.avgSnrDb == null
|
||||
? 'No average yet'
|
||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'Multi-hop',
|
||||
value: '${snapshot.multiHopCount}',
|
||||
subtitle: routeHashCounts.summaryLabel,
|
||||
footer: snapshot.avgHopCount == null
|
||||
? 'No routes yet'
|
||||
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
||||
),
|
||||
],
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacing = 10.0;
|
||||
const minTileWidth = 150.0;
|
||||
final columns =
|
||||
((constraints.maxWidth + spacing) / (minTileWidth + spacing))
|
||||
.floor()
|
||||
.clamp(1, metrics.length);
|
||||
final tileWidth =
|
||||
(constraints.maxWidth - (spacing * (columns - 1))) / columns;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
for (final metric in metrics)
|
||||
SizedBox(width: tileWidth, child: metric),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -455,7 +477,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
_FilterChip(
|
||||
label: 'All',
|
||||
label: AppLocalizations.of(context)!.all,
|
||||
selected: selectedType == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
@@ -524,12 +546,14 @@ String _windowSummaryLabel(Duration duration) {
|
||||
}
|
||||
|
||||
class _MetricTile extends StatelessWidget {
|
||||
final Key? tileKey;
|
||||
final String label;
|
||||
final String value;
|
||||
final String subtitle;
|
||||
final String? footer;
|
||||
|
||||
const _MetricTile({
|
||||
this.tileKey,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
@@ -539,7 +563,7 @@ class _MetricTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 160,
|
||||
key: tileKey,
|
||||
height: 136,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -948,7 +972,6 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _LiveTrafficPacketDetails {
|
||||
@@ -1104,7 +1127,6 @@ class _PacketInfoLine extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _GeoPoint {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import '../services/voice_recorder_service.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import '../utils/contact_sorting.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
import '../utils/tictactoe_message_parser.dart';
|
||||
@@ -47,6 +48,110 @@ class MessagesTab extends StatefulWidget {
|
||||
State<MessagesTab> createState() => _MessagesTabState();
|
||||
}
|
||||
|
||||
class _ComposerActionTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Color color;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ComposerActionTile({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.color,
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final effectiveColor = enabled
|
||||
? color
|
||||
: colorScheme.onSurfaceVariant.withValues(alpha: 0.45);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
enabled
|
||||
? colorScheme.surfaceContainerLow
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
enabled
|
||||
? effectiveColor.withValues(alpha: 0.08)
|
||||
: colorScheme.surfaceContainerHigh,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: enabled
|
||||
? effectiveColor.withValues(alpha: 0.16)
|
||||
: colorScheme.outlineVariant.withValues(alpha: 0.16),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: effectiveColor.withValues(alpha: enabled ? 0.08 : 0.03),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveColor.withValues(alpha: enabled ? 0.14 : 0.10),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: effectiveColor, size: 20),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
color: enabled
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MessagesTabState extends State<MessagesTab> {
|
||||
static const int _maxContactMessageBytes = 156;
|
||||
static const int _maxChannelMessageBytes = 127;
|
||||
@@ -299,27 +404,27 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
final contacts =
|
||||
contactsProvider.contacts
|
||||
.where((contact) => contact.type == ContactType.chat)
|
||||
.where((contact) {
|
||||
if (normalizedQuery.isEmpty) return true;
|
||||
return contact.displayName.toLowerCase().contains(
|
||||
normalizedQuery,
|
||||
);
|
||||
})
|
||||
.toList()
|
||||
..sort((a, b) {
|
||||
final aName = a.displayName.toLowerCase();
|
||||
final bName = b.displayName.toLowerCase();
|
||||
final aStarts =
|
||||
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
|
||||
final bStarts =
|
||||
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
|
||||
if (aStarts != bStarts) {
|
||||
return aStarts ? -1 : 1;
|
||||
}
|
||||
return aName.compareTo(bName);
|
||||
});
|
||||
contactsProvider.chatContacts.where((contact) {
|
||||
if (normalizedQuery.isEmpty) return true;
|
||||
return contact.displayName.toLowerCase().contains(normalizedQuery);
|
||||
}).toList()..sort((a, b) {
|
||||
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
final aName = a.displayName.toLowerCase();
|
||||
final bName = b.displayName.toLowerCase();
|
||||
final aStarts =
|
||||
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
|
||||
final bStarts =
|
||||
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
|
||||
if (aStarts != bStarts) {
|
||||
return aStarts ? -1 : 1;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
|
||||
return contacts.take(8).toList(growable: false);
|
||||
}
|
||||
@@ -408,16 +513,33 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
|
||||
// Filter contacts by type
|
||||
final contacts = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.chat)
|
||||
.toList();
|
||||
final rooms = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.room)
|
||||
.toList();
|
||||
final channels = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.channel)
|
||||
.toList();
|
||||
final contacts = List<Contact>.from(contactsProvider.chatContacts)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
final rooms = List<Contact>.from(contactsProvider.rooms)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
final channels = List<Contact>.from(contactsProvider.channels)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -516,7 +638,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 +656,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}).firstOrNull;
|
||||
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -564,13 +688,19 @@ 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;
|
||||
}
|
||||
|
||||
@@ -641,7 +771,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
|
||||
_textController.clear();
|
||||
_focusNode.unfocus();
|
||||
|
||||
try {
|
||||
// Check destination type and send accordingly
|
||||
@@ -1376,7 +1505,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final decision = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Send to Public Channel?'),
|
||||
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
|
||||
content: Text(
|
||||
'You are about to send $mediaType to the Public Channel. '
|
||||
'This is not advised because everyone on the mesh may receive it. '
|
||||
@@ -1389,7 +1518,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Send anyway'),
|
||||
child: Text(AppLocalizations.of(context)!.sendAnyway),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1443,81 +1572,207 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
void _showComposerActions() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) {
|
||||
final theme = Theme.of(sheetContext);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
Future<void> runAction(Future<void> Function() action) async {
|
||||
await _runAfterSheetDismissal(sheetContext, action);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.search),
|
||||
title: const Text('Search messages'),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
_showFilteredMessageSearch();
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_location_alt),
|
||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
_showSarDialog();
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_voiceSupported)
|
||||
ListTile(
|
||||
enabled: !_isSendingVoice,
|
||||
leading: Icon(_isRecording ? Icons.stop : Icons.mic),
|
||||
title: Text(_isRecording ? 'Stop recording' : 'Record voice'),
|
||||
onTap: _isSendingVoice
|
||||
? null
|
||||
: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
if (_isRecording) {
|
||||
await _stopAndSendVoice();
|
||||
} else {
|
||||
await _startVoiceRecording();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(sheetContext).size.height * 0.72,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surfaceContainerLow,
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Send image from gallery'),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
await _pickAndSendImage(source: ImageSource.gallery);
|
||||
});
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(32),
|
||||
bottom: Radius.circular(28),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.14),
|
||||
blurRadius: 28,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'More actions',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Search, share, or start something from this chat.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(
|
||||
alpha: 0.18,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.pop(sheetContext),
|
||||
tooltip: l10n.close,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final actions = <Widget>[
|
||||
_ComposerActionTile(
|
||||
icon: Icons.search_rounded,
|
||||
title: l10n.searchMessages,
|
||||
subtitle: 'Find text in the current conversation',
|
||||
color: const Color(0xFF2B6CB0),
|
||||
onTap: () => runAction(() async {
|
||||
_showFilteredMessageSearch();
|
||||
}),
|
||||
),
|
||||
_ComposerActionTile(
|
||||
icon: Icons.add_location_alt_rounded,
|
||||
title: l10n.sendSarMarker,
|
||||
subtitle: 'Share a marker with coordinates',
|
||||
color: const Color(0xFFB45309),
|
||||
onTap: () => runAction(() async {
|
||||
_showSarDialog();
|
||||
}),
|
||||
),
|
||||
if (_voiceSupported)
|
||||
_ComposerActionTile(
|
||||
icon: _isRecording
|
||||
? Icons.stop_rounded
|
||||
: Icons.mic_rounded,
|
||||
title: _isRecording
|
||||
? 'Stop recording'
|
||||
: 'Record voice',
|
||||
subtitle: _isSendingVoice
|
||||
? 'Voice message is sending'
|
||||
: _isRecording
|
||||
? 'Finish and send your clip'
|
||||
: 'Capture and send a voice note',
|
||||
color: const Color(0xFF7C3AED),
|
||||
enabled: !_isSendingVoice,
|
||||
onTap: !_isSendingVoice
|
||||
? () => runAction(() async {
|
||||
if (_isRecording) {
|
||||
await _stopAndSendVoice();
|
||||
} else {
|
||||
await _startVoiceRecording();
|
||||
}
|
||||
})
|
||||
: null,
|
||||
),
|
||||
_ComposerActionTile(
|
||||
icon: Icons.photo_library_rounded,
|
||||
title: l10n.sendImageFromGallery,
|
||||
subtitle: 'Choose an image from your library',
|
||||
color: const Color(0xFF0F766E),
|
||||
enabled: !_isSendingImage,
|
||||
onTap: !_isSendingImage
|
||||
? () => runAction(() async {
|
||||
await _pickAndSendImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
})
|
||||
: null,
|
||||
),
|
||||
_ComposerActionTile(
|
||||
icon: Icons.camera_alt_rounded,
|
||||
title: l10n.takePhoto,
|
||||
subtitle: 'Capture something right now',
|
||||
color: const Color(0xFF2563EB),
|
||||
enabled: !_isSendingImage,
|
||||
onTap: !_isSendingImage
|
||||
? () => runAction(() async {
|
||||
await _pickAndSendImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
})
|
||||
: null,
|
||||
),
|
||||
_ComposerActionTile(
|
||||
icon: Icons.grid_3x3_rounded,
|
||||
title: l10n.startTictactoe,
|
||||
subtitle: l10n.dmOnly,
|
||||
color: const Color(0xFFBE185D),
|
||||
onTap: () => runAction(() async {
|
||||
await _startTicTacToeGame();
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: actions.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisExtent: 126,
|
||||
),
|
||||
itemBuilder: (context, index) => actions[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Take photo'),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
await _pickAndSendImage(source: ImageSource.camera);
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.grid_3x3),
|
||||
title: const Text('Start Tic-Tac-Toe'),
|
||||
subtitle: const Text('DM only'),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
await _startTicTacToeGame();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -2020,12 +2275,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
void _handleMessageTap(Message message) {
|
||||
if (widget.onNavigateToMap == null) return;
|
||||
|
||||
if (message.isSarMarker && message.sarGpsCoordinates != null) {
|
||||
if (message.isSarMarker) {
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
mapProvider.navigateToLocation(
|
||||
location: message.sarGpsCoordinates!,
|
||||
zoom: 15.0,
|
||||
);
|
||||
final marker = message.toSarMarker();
|
||||
if (marker == null) {
|
||||
return;
|
||||
}
|
||||
final error = mapProvider.navigateToSarMarker(marker);
|
||||
if (error != null && mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
return;
|
||||
}
|
||||
widget.onNavigateToMap?.call();
|
||||
return;
|
||||
}
|
||||
@@ -2034,7 +2296,16 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
mapProvider.navigateToDrawing(message.drawingId!, drawingProvider);
|
||||
final error = mapProvider.navigateToDrawing(
|
||||
message.drawingId!,
|
||||
drawingProvider,
|
||||
);
|
||||
if (error != null && mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
return;
|
||||
}
|
||||
widget.onNavigateToMap?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -155,8 +155,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
||||
Clipboard.setData(ClipboardData(text: log.hexData));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Hex data copied to clipboard'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.hexDataCopiedToClipboard),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -184,7 +184,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
setState(() {});
|
||||
if (parentContext.mounted) {
|
||||
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||
const SnackBar(content: Text('Packet logs cleared')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.packetLogsCleared)),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -205,7 +205,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('BLE Packet Logs'),
|
||||
Text(AppLocalizations.of(context)!.blePacketLogs),
|
||||
Text(
|
||||
'${logs.length} packets',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -316,23 +316,23 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Export logs',
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'csv',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.table_chart),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as CSV'),
|
||||
const Icon(Icons.table_chart),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsCsv),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'txt',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.text_snippet),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as Text'),
|
||||
const Icon(Icons.text_snippet),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsText),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -411,8 +411,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
_filterDirection = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear_all),
|
||||
label: const Text('Clear filters'),
|
||||
icon: Icon(Icons.clear_all),
|
||||
label: Text(AppLocalizations.of(context)!.clearFilters),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -519,24 +519,24 @@ class _PacketLogCard extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: isRx ? Icons.call_received : Icons.call_made,
|
||||
label: 'Direction',
|
||||
label: AppLocalizations.of(context)!.direction,
|
||||
value: isRx ? 'RX' : 'TX',
|
||||
accent: directionColor,
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.data_object,
|
||||
label: 'Size',
|
||||
label: AppLocalizations.of(context)!.size,
|
||||
value: '${log.rawData.length} bytes',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Captured',
|
||||
label: AppLocalizations.of(context)!.captured,
|
||||
value: _formatTimestamp(log.timestamp),
|
||||
),
|
||||
if (log.responseCode != null)
|
||||
_FactCard(
|
||||
icon: Icons.sell,
|
||||
label: 'Opcode',
|
||||
label: AppLocalizations.of(context)!.opcode,
|
||||
value: log.opcodeName,
|
||||
),
|
||||
],
|
||||
@@ -565,7 +565,7 @@ class _PacketLogCard extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
if (rxInfo?.rssiDbm != null)
|
||||
_SignalMeter(
|
||||
label: 'RSSI',
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
||||
normalized: _normalizeRssi(
|
||||
rxInfo.rssiDbm!.toDouble(),
|
||||
@@ -573,9 +573,9 @@ class _PacketLogCard extends StatelessWidget {
|
||||
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
||||
),
|
||||
if (rxInfo?.snrDb != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
_SignalMeter(
|
||||
label: 'SNR',
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
valueLabel:
|
||||
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
||||
normalized: _normalizeSnr(rxInfo.snrDb!),
|
||||
@@ -854,17 +854,17 @@ class _RouteSection extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: Icons.route,
|
||||
label: 'Payload',
|
||||
label: AppLocalizations.of(context)!.payload,
|
||||
value: _payloadTypeLabel(route.payloadType),
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.hub,
|
||||
label: 'Hops',
|
||||
label: AppLocalizations.of(context)!.hops,
|
||||
value: '${route.hopCount}',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.tag,
|
||||
label: 'Hash size',
|
||||
label: AppLocalizations.of(context)!.hashSize,
|
||||
value:
|
||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
||||
),
|
||||
|
||||
251
lib/screens/profiles_screen.dart
Normal file
251
lib/screens/profiles_screen.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class ProfilesScreen extends StatelessWidget {
|
||||
const ProfilesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
final profiles = profileManager.visibleProfiles;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.profiles),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.importProfileFromFile();
|
||||
},
|
||||
icon: const Icon(Icons.file_open),
|
||||
tooltip: 'Import profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _createProfile(context),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newProfile),
|
||||
),
|
||||
body: profiles.isEmpty
|
||||
? Center(
|
||||
child: Text(AppLocalizations.of(context)!.enableProfilesToStartManagingThem),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
final isActive =
|
||||
profileManager.activeProfileId == profile.id;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
profile.name,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (isActive)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(AppLocalizations.of(context)!.active),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(_summary(profile)),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.openProfile(profile.id);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.open),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final resolved =
|
||||
profile.id ==
|
||||
ConfigProfile.defaultProfileId
|
||||
? await context
|
||||
.read<
|
||||
ProfileWorkspaceCoordinator
|
||||
>()
|
||||
.snapshotCurrentProfile(
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
)
|
||||
: profile;
|
||||
if (!context.mounted) return;
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.exportProfile(resolved);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.share),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case 'duplicate':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.duplicateProfile(profile);
|
||||
break;
|
||||
case 'rename':
|
||||
await _renameProfile(context, profile);
|
||||
break;
|
||||
case 'delete':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.deleteProfile(profile);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Text(AppLocalizations.of(context)!.duplicate),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
PopupMenuItem(
|
||||
value: 'rename',
|
||||
child: Text(AppLocalizations.of(context)!.rename),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _summary(ConfigProfile profile) {
|
||||
if (profile.isDefault) {
|
||||
return 'Current app state and history.';
|
||||
}
|
||||
|
||||
final sections = <String>[];
|
||||
if (profile.sections.deviceConfig?.isEmpty == false) {
|
||||
sections.add('Device');
|
||||
}
|
||||
if (profile.sections.channels.isNotEmpty) {
|
||||
sections.add('${profile.sections.channels.length} channels');
|
||||
}
|
||||
if (profile.sections.appSettings?.isEmpty == false) {
|
||||
sections.add('App settings');
|
||||
}
|
||||
if (profile.sections.mapWorkspace?.isEmpty == false) {
|
||||
sections.add('Map workspace');
|
||||
}
|
||||
return sections.isEmpty ? 'Empty profile' : sections.join(' | ');
|
||||
}
|
||||
|
||||
Future<void> _createProfile(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.createProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: Text(AppLocalizations.of(context)!.create),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().createProfileFromCurrent(
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _renameProfile(
|
||||
BuildContext context,
|
||||
ConfigProfile profile,
|
||||
) async {
|
||||
final controller = TextEditingController(text: profile.name);
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.renameProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().renameProfile(
|
||||
profile,
|
||||
name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RepeatersMapScreen extends StatefulWidget {
|
||||
const RepeatersMapScreen({super.key});
|
||||
@@ -191,18 +192,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 12),
|
||||
_InfoLine(
|
||||
label: 'Public key',
|
||||
label: AppLocalizations.of(context)!.publicKeyLabel,
|
||||
value: repeater.publicKey,
|
||||
monospace: true,
|
||||
),
|
||||
_InfoLine(
|
||||
label: 'Coordinates',
|
||||
label: AppLocalizations.of(context)!.coordinates,
|
||||
value:
|
||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
_InfoLine(label: 'Source', value: repeater.sourceLabel),
|
||||
_InfoLine(label: AppLocalizations.of(context)!.source, value: repeater.sourceLabel),
|
||||
const SizedBox(height: 12),
|
||||
if (canAdd)
|
||||
SizedBox(
|
||||
@@ -233,8 +234,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Already in contacts'),
|
||||
icon: Icon(Icons.check_circle),
|
||||
label: Text(AppLocalizations.of(context)!.alreadyInContacts),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -253,8 +254,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Connect to a device before adding contacts'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectToADeviceBeforeAddingContacts),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -366,7 +367,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Repeaters Map'),
|
||||
title: Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _isLoading
|
||||
@@ -446,18 +447,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
children: [
|
||||
_LegendRow(
|
||||
color: Color(0xFF7C3AED),
|
||||
label: 'From contacts',
|
||||
color: const Color(0xFF7C3AED),
|
||||
label: AppLocalizations.of(context)!.fromContacts,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(
|
||||
color: Color(0xFFE8681D),
|
||||
label: 'Online only',
|
||||
color: const Color(0xFFE8681D),
|
||||
label: AppLocalizations.of(context)!.onlineOnly,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_LegendRow(color: Color(0xFF1B8F4F), label: 'In both'),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(color: const Color(0xFF1B8F4F), label: AppLocalizations.of(context)!.inBoth),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -240,7 +240,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'import',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
leading: Icon(Icons.download),
|
||||
title: Text(l10n.importFromClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.upload),
|
||||
leading: Icon(Icons.upload),
|
||||
title: Text(l10n.exportToClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -257,7 +257,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.restart_alt),
|
||||
leading: Icon(Icons.restart_alt),
|
||||
title: Text(l10n.resetToDefaults),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -283,14 +283,14 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noTemplates,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tapAddToCreate,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
@@ -317,7 +317,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _addTemplate,
|
||||
icon: const Icon(Icons.add),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(l10n.addTemplate),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,9 @@ import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../widgets/sensors/bthome_met_history_sheet.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class SensorsTab extends StatefulWidget {
|
||||
final bool isActive;
|
||||
@@ -99,71 +101,124 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
Future<void> _showAddSensorSheet(BuildContext context) async {
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final candidates = sensorsProvider.availableCandidates(contactsProvider);
|
||||
final candidates = sensorsProvider.availableCandidates(
|
||||
contactsProvider,
|
||||
connectionProvider: context.read<ConnectionProvider>(),
|
||||
);
|
||||
final searchController = TextEditingController();
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) {
|
||||
if (candidates.isEmpty) {
|
||||
return const SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'No eligible nodes available. Discover a relay or node first.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text(
|
||||
'Add sensor node',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
try {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) {
|
||||
if (candidates.isEmpty) {
|
||||
return const SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'No eligible nodes available. Discover a relay or node first.',
|
||||
),
|
||||
subtitle: Text('Pick a relay or node to watch in Sensors.'),
|
||||
),
|
||||
...candidates.map(
|
||||
(contact) => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 4,
|
||||
);
|
||||
}
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
final query = searchController.text.trim().toLowerCase();
|
||||
final filteredCandidates = candidates.where((contact) {
|
||||
if (query.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return contact.displayName.toLowerCase().contains(query) ||
|
||||
contact.publicKeyHex.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: const Color(0xFFDDEAF8),
|
||||
child: Icon(
|
||||
_typeIcon(contact),
|
||||
color: const Color(0xFF1E4F7A),
|
||||
),
|
||||
),
|
||||
title: Text(contact.displayName),
|
||||
subtitle: _SensorCandidatePreview(contact: contact),
|
||||
isThreeLine: true,
|
||||
onTap: () async {
|
||||
await sensorsProvider.addSensor(contact);
|
||||
if (!sheetContext.mounted) return;
|
||||
Navigator.of(sheetContext).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${contact.displayName} added to Sensors',
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Add sensor node',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.pickARelayOrNodeToWatchInSensors,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: (_) => setModalState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.search),
|
||||
hintText: 'Search sensors',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (filteredCandidates.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Text(
|
||||
'No sensor candidates match your search.',
|
||||
),
|
||||
),
|
||||
...filteredCandidates.map(
|
||||
(contact) => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 4,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: const Color(0xFFDDEAF8),
|
||||
child: Icon(
|
||||
_typeIcon(contact),
|
||||
color: const Color(0xFF1E4F7A),
|
||||
),
|
||||
),
|
||||
title: Text(contact.displayName),
|
||||
subtitle: _SensorCandidatePreview(contact: contact),
|
||||
isThreeLine: true,
|
||||
onTap: () async {
|
||||
await sensorsProvider.addSensor(contact);
|
||||
if (!sheetContext.mounted) return;
|
||||
Navigator.of(sheetContext).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${contact.displayName} added to Sensors',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
searchController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showMetricSelector(
|
||||
@@ -209,7 +264,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,
|
||||
@@ -274,30 +329,50 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
onPressed: () => _showAddSensorSheet(context),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: Consumer2<SensorsProvider, ContactsProvider>(
|
||||
builder: (context, sensorsProvider, contactsProvider, child) {
|
||||
final watchedKeys = sensorsProvider.watchedSensorKeys;
|
||||
body: Consumer3<SensorsProvider, ContactsProvider, ConnectionProvider>(
|
||||
builder:
|
||||
(
|
||||
context,
|
||||
sensorsProvider,
|
||||
contactsProvider,
|
||||
connectionProvider,
|
||||
child,
|
||||
) {
|
||||
final watchedKeys = sensorsProvider.watchedSensorKeys;
|
||||
final hasPersistedSensors = watchedKeys.isNotEmpty;
|
||||
final selfDisplayKey = sensorsProvider.displaySelfKey(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
final hasAnyCards =
|
||||
selfDisplayKey != null || watchedKeys.isNotEmpty;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _refreshAll(context),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||
children: [
|
||||
if (watchedKeys.isEmpty)
|
||||
const _EmptySensorsState()
|
||||
else
|
||||
...watchedKeys.map((key) {
|
||||
Contact? contact;
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == key) {
|
||||
contact = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final availableFieldKeys = sensorMetricKeysFor(contact);
|
||||
final visibleFields = sensorsProvider
|
||||
.effectiveVisibleFieldsFor(key, availableFieldKeys);
|
||||
return SensorTelemetryCard(
|
||||
Widget buildSensorCard(
|
||||
String key, {
|
||||
required int index,
|
||||
required int totalCount,
|
||||
required bool isWatchedCard,
|
||||
}) {
|
||||
final contact = sensorsProvider.contactForDisplay(
|
||||
key,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
final availableFieldKeys = sensorMetricKeysFor(contact);
|
||||
final visibleFields = sensorsProvider.effectiveVisibleFieldsFor(
|
||||
key,
|
||||
availableFieldKeys,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
key: ValueKey<String>('sensor_card_$key'),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: index == totalCount - 1 ? 0 : 12,
|
||||
),
|
||||
child: ReorderableDelayedDragStartListener(
|
||||
enabled: isWatchedCard,
|
||||
index: index,
|
||||
child: SensorTelemetryCard(
|
||||
contact: contact,
|
||||
state: sensorsProvider.stateFor(key),
|
||||
visibleFields: visibleFields,
|
||||
@@ -310,22 +385,90 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
for (final field in visibleFields)
|
||||
field: sensorsProvider.fieldSpanFor(key, field),
|
||||
},
|
||||
onRemove: () async {
|
||||
await sensorsProvider.removeSensor(key);
|
||||
},
|
||||
onRemove: isWatchedCard
|
||||
? () async {
|
||||
await sensorsProvider.removeSensor(key);
|
||||
}
|
||||
: null,
|
||||
onCustomize: () =>
|
||||
_showMetricSelector(context, key, contact),
|
||||
onShowMetHistory: (contact) =>
|
||||
showBTHomeMetHistorySheet(context, contact: contact),
|
||||
onMoveUp: isWatchedCard && index > 0
|
||||
? () =>
|
||||
sensorsProvider.reorderSensors(index, index - 1)
|
||||
: null,
|
||||
onMoveDown: isWatchedCard && index < totalCount - 1
|
||||
? () =>
|
||||
sensorsProvider.reorderSensors(index, index + 2)
|
||||
: null,
|
||||
onRefresh: () => sensorsProvider.refreshSensor(
|
||||
publicKeyHex: key,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: context.read<ConnectionProvider>(),
|
||||
connectionProvider: connectionProvider,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _refreshAll(context),
|
||||
child: !hasAnyCards
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||
children: const [_EmptySensorsState()],
|
||||
)
|
||||
: hasPersistedSensors && selfDisplayKey == null
|
||||
? ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||
itemCount: watchedKeys.length,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
sensorsProvider.reorderSensors(oldIndex, newIndex),
|
||||
itemBuilder: (context, index) => buildSensorCard(
|
||||
watchedKeys[index],
|
||||
index: index,
|
||||
totalCount: watchedKeys.length,
|
||||
isWatchedCard: true,
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||
children: [
|
||||
if (selfDisplayKey != null)
|
||||
buildSensorCard(
|
||||
selfDisplayKey,
|
||||
index: 0,
|
||||
totalCount: watchedKeys.isEmpty ? 1 : 2,
|
||||
isWatchedCard: false,
|
||||
),
|
||||
if (selfDisplayKey != null && watchedKeys.isEmpty)
|
||||
_SelfOnlySensorsCta(
|
||||
onAddSensor: () => _showAddSensorSheet(context),
|
||||
),
|
||||
if (watchedKeys.isNotEmpty)
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: watchedKeys.length,
|
||||
onReorder: (oldIndex, newIndex) => sensorsProvider
|
||||
.reorderSensors(oldIndex, newIndex),
|
||||
itemBuilder: (context, index) => buildSensorCard(
|
||||
watchedKeys[index],
|
||||
index: index,
|
||||
totalCount: watchedKeys.length,
|
||||
isWatchedCard: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -388,10 +531,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(
|
||||
@@ -405,6 +548,12 @@ class _SensorCustomizeView extends StatelessWidget {
|
||||
labelOverrides: sensorsProvider.labelOverridesFor(
|
||||
publicKeyHex,
|
||||
),
|
||||
onShowMetHistory: contact == null
|
||||
? null
|
||||
: (contact) => showBTHomeMetHistorySheet(
|
||||
context,
|
||||
contact: contact,
|
||||
),
|
||||
fieldSpans: {
|
||||
for (final field in visibleFields)
|
||||
field: sensorsProvider.fieldSpanFor(publicKeyHex, field),
|
||||
@@ -414,7 +563,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 +851,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,9 +1016,14 @@ class _SensorCandidatePreview extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptySensorsState extends StatelessWidget {
|
||||
class _EmptySensorsState extends StatefulWidget {
|
||||
const _EmptySensorsState();
|
||||
|
||||
@override
|
||||
State<_EmptySensorsState> createState() => _EmptySensorsStateState();
|
||||
}
|
||||
|
||||
class _EmptySensorsStateState extends State<_EmptySensorsState> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -898,7 +1052,7 @@ class _EmptySensorsState extends StatelessWidget {
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
'Use + to add discovered relays or nodes. Pull down to refresh telemetry after adding them.',
|
||||
'Use + to add discovered relays or nodes. Your device will appear here automatically when available.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@@ -908,6 +1062,81 @@ class _EmptySensorsState extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SelfOnlySensorsCta extends StatelessWidget {
|
||||
final VoidCallback onAddSensor;
|
||||
|
||||
const _SelfOnlySensorsCta({required this.onAddSensor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add_chart_rounded,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Add another device',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Bring in weather stations, repeaters, or other devices to watch them here.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onAddSensor,
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('Choose'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IconData _typeIcon(Contact contact) {
|
||||
if (contact.isSensor) {
|
||||
return Icons.sensors;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/device_info.dart';
|
||||
import '../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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
156
lib/services/app_config_snapshot_service.dart
Normal file
156
lib/services/app_config_snapshot_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import 'image_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'route_hash_preferences.dart';
|
||||
import 'voice_bitrate_preferences.dart';
|
||||
|
||||
class AppConfigSnapshotService {
|
||||
Future<AppSettingsProfileSection> capture(AppProvider appProvider) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
return AppSettingsProfileSection(
|
||||
mapEnabled: appProvider.isMapEnabled,
|
||||
contactsEnabled: appProvider.isContactsEnabled,
|
||||
sensorsEnabled: appProvider.isSensorsEnabled,
|
||||
voiceSilenceTrimmingEnabled: appProvider.isVoiceSilenceTrimmingEnabled,
|
||||
voiceBandPassFilterEnabled: appProvider.isVoiceBandPassFilterEnabled,
|
||||
voiceCompressorEnabled: appProvider.isVoiceCompressorEnabled,
|
||||
voiceLimiterEnabled: appProvider.isVoiceLimiterEnabled,
|
||||
voiceAutoGainEnabled: appProvider.isVoiceAutoGainEnabled,
|
||||
voiceEchoCancellationEnabled: appProvider.isVoiceEchoCancellationEnabled,
|
||||
voiceNoiseSuppressionEnabled: appProvider.isVoiceNoiseSuppressionEnabled,
|
||||
messageFontScale: appProvider.messageFontScale,
|
||||
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
||||
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
||||
nearestRelayFallbackEnabled: appProvider.nearestRelayFallbackEnabled,
|
||||
voiceBitrate: await VoiceBitratePreferences.getBitrate(),
|
||||
routeHashSize: await RouteHashPreferences.getHashSize(),
|
||||
imageMaxSize: await ImagePreferences.getMaxSize(),
|
||||
imageCompression: await ImagePreferences.getCompression(),
|
||||
imageGrayscale: await ImagePreferences.getGrayscale(),
|
||||
imageUltraMode: await ImagePreferences.getUltraMode(),
|
||||
showRxTxIndicators:
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
) ??
|
||||
true,
|
||||
fastLocationUpdatesEnabled: locationTracking.fastLocationUpdatesEnabled,
|
||||
fastLocationMovementThresholdMeters:
|
||||
locationTracking.fastLocationMovementThresholdMeters,
|
||||
fastLocationActiveCadenceSeconds:
|
||||
locationTracking.fastLocationActiveCadenceSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
AppSettingsProfileSection? section,
|
||||
AppProvider appProvider,
|
||||
) async {
|
||||
if (section == null || section.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.mapEnabled != null) {
|
||||
await appProvider.toggleMapEnabled(section.mapEnabled!);
|
||||
}
|
||||
if (section.contactsEnabled != null) {
|
||||
await appProvider.toggleContactsEnabled(section.contactsEnabled!);
|
||||
}
|
||||
if (section.sensorsEnabled != null) {
|
||||
await appProvider.toggleSensorsEnabled(section.sensorsEnabled!);
|
||||
}
|
||||
if (section.voiceSilenceTrimmingEnabled != null) {
|
||||
await appProvider.toggleVoiceSilenceTrimmingEnabled(
|
||||
section.voiceSilenceTrimmingEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBandPassFilterEnabled != null) {
|
||||
await appProvider.toggleVoiceBandPassFilterEnabled(
|
||||
section.voiceBandPassFilterEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceCompressorEnabled != null) {
|
||||
await appProvider.toggleVoiceCompressorEnabled(
|
||||
section.voiceCompressorEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceLimiterEnabled != null) {
|
||||
await appProvider.toggleVoiceLimiterEnabled(section.voiceLimiterEnabled!);
|
||||
}
|
||||
if (section.voiceAutoGainEnabled != null) {
|
||||
await appProvider.toggleVoiceAutoGainEnabled(
|
||||
section.voiceAutoGainEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceEchoCancellationEnabled != null) {
|
||||
await appProvider.toggleVoiceEchoCancellationEnabled(
|
||||
section.voiceEchoCancellationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceNoiseSuppressionEnabled != null) {
|
||||
await appProvider.toggleVoiceNoiseSuppressionEnabled(
|
||||
section.voiceNoiseSuppressionEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.messageFontScale != null) {
|
||||
await appProvider.setMessageFontScale(section.messageFontScale!);
|
||||
}
|
||||
if (section.autoRouteRotationEnabled != null) {
|
||||
await appProvider.toggleAutoRouteRotationEnabled(
|
||||
section.autoRouteRotationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.clearPathOnMaxRetry != null) {
|
||||
await appProvider.toggleClearPathOnMaxRetry(section.clearPathOnMaxRetry!);
|
||||
}
|
||||
if (section.nearestRelayFallbackEnabled != null) {
|
||||
await appProvider.toggleNearestRelayFallbackEnabled(
|
||||
section.nearestRelayFallbackEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBitrate != null) {
|
||||
await VoiceBitratePreferences.setBitrate(section.voiceBitrate!);
|
||||
}
|
||||
if (section.routeHashSize != null) {
|
||||
await RouteHashPreferences.setHashSize(section.routeHashSize!);
|
||||
}
|
||||
if (section.imageMaxSize != null) {
|
||||
await ImagePreferences.setMaxSize(section.imageMaxSize!);
|
||||
}
|
||||
if (section.imageCompression != null) {
|
||||
await ImagePreferences.setCompression(section.imageCompression!);
|
||||
}
|
||||
if (section.imageGrayscale != null) {
|
||||
await ImagePreferences.setGrayscale(section.imageGrayscale!);
|
||||
}
|
||||
if (section.imageUltraMode != null) {
|
||||
await ImagePreferences.setUltraMode(section.imageUltraMode!);
|
||||
}
|
||||
if (section.showRxTxIndicators != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
section.showRxTxIndicators!,
|
||||
);
|
||||
}
|
||||
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
if (section.fastLocationUpdatesEnabled != null) {
|
||||
locationTracking.fastLocationUpdatesEnabled =
|
||||
section.fastLocationUpdatesEnabled!;
|
||||
}
|
||||
if (section.fastLocationMovementThresholdMeters != null) {
|
||||
locationTracking.fastLocationMovementThresholdMeters =
|
||||
section.fastLocationMovementThresholdMeters!;
|
||||
}
|
||||
if (section.fastLocationActiveCadenceSeconds != null) {
|
||||
locationTracking.fastLocationActiveCadenceSeconds =
|
||||
section.fastLocationActiveCadenceSeconds!;
|
||||
}
|
||||
await locationTracking.saveSettings();
|
||||
await appProvider.reloadProfileScopedSettings();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Background location tracking service for SAR operations
|
||||
/// Tracks user location and sends periodic updates via MeshCore BLE
|
||||
@@ -15,6 +16,11 @@ class BackgroundLocationService {
|
||||
static const String _prefKeyLastLon = 'background_last_lon';
|
||||
|
||||
MeshCoreBleService? _bleService;
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
bool _isInitialized = false;
|
||||
StreamSubscription<Position>? _positionSubscription;
|
||||
|
||||
@@ -71,8 +77,8 @@ class BackgroundLocationService {
|
||||
|
||||
// Save settings
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, true);
|
||||
await prefs.setDouble(_prefKeyDistance, distanceThreshold);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), true);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distanceThreshold);
|
||||
|
||||
// Start listening to position updates
|
||||
Position? lastPosition;
|
||||
@@ -110,8 +116,14 @@ class BackgroundLocationService {
|
||||
lastPosition = position;
|
||||
|
||||
// Save to preferences
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLat),
|
||||
position.latitude,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLon),
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
// Update device's advertised location
|
||||
if (_bleService != null && _bleService!.isConnected) {
|
||||
@@ -161,7 +173,7 @@ class BackgroundLocationService {
|
||||
_positionSubscription = null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, false);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||
debugPrint('✅ [BackgroundLocation] Tracking stopped');
|
||||
}
|
||||
|
||||
@@ -169,13 +181,13 @@ class BackgroundLocationService {
|
||||
/// Note: This will restart tracking with the new threshold
|
||||
Future<void> updateDistanceThreshold(double distance) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyDistance, distance);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distance);
|
||||
debugPrint(
|
||||
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
||||
);
|
||||
|
||||
// Restart tracking if currently enabled
|
||||
final isEnabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
||||
final isEnabled = prefs.getBool(_scopedKey(_prefKeyEnabled)) ?? false;
|
||||
if (isEnabled && _bleService != null) {
|
||||
await stopTracking();
|
||||
await startTracking(distanceThreshold: distance);
|
||||
|
||||
178
lib/services/bthome_met_history.dart
Normal file
178
lib/services/bthome_met_history.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import '../models/contact.dart';
|
||||
|
||||
enum BTHomeMetMeasurement {
|
||||
temperature(1, 'Temperature', '°C'),
|
||||
humidity(2, 'Humidity', '%'),
|
||||
windSpeed(3, 'Wind speed', 'm/s'),
|
||||
gust(4, 'Wind gust', 'm/s'),
|
||||
rain(5, 'Rain', 'mm');
|
||||
|
||||
const BTHomeMetMeasurement(this.id, this.label, this.unit);
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final String unit;
|
||||
|
||||
static BTHomeMetMeasurement? fromId(int id) {
|
||||
for (final value in BTHomeMetMeasurement.values) {
|
||||
if (value.id == id) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryPage {
|
||||
const BTHomeMetHistoryPage({
|
||||
required this.measurement,
|
||||
required this.page,
|
||||
required this.values,
|
||||
});
|
||||
|
||||
final BTHomeMetMeasurement measurement;
|
||||
final int page;
|
||||
final List<double> values;
|
||||
|
||||
double? get latest => values.isEmpty ? null : values.last;
|
||||
double? get minimum => values.isEmpty
|
||||
? null
|
||||
: values.reduce((left, right) => left < right ? left : right);
|
||||
double? get maximum => values.isEmpty
|
||||
? null
|
||||
: values.reduce((left, right) => left > right ? left : right);
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryFormatException implements Exception {
|
||||
const BTHomeMetHistoryFormatException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class BTHomeMetHistoryParser {
|
||||
static BTHomeMetHistoryPage parse(String text) {
|
||||
final parts = text
|
||||
.trim()
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.toList(growable: false);
|
||||
if (parts.length < 3) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history response is too short.',
|
||||
);
|
||||
}
|
||||
|
||||
final measurementId = int.tryParse(parts[0]);
|
||||
final page = int.tryParse(parts[1]);
|
||||
final count = int.tryParse(parts[2]);
|
||||
if (measurementId == null || page == null || count == null) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history header is invalid.',
|
||||
);
|
||||
}
|
||||
|
||||
final measurement = BTHomeMetMeasurement.fromId(measurementId);
|
||||
if (measurement == null) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'Unsupported MET history measurement id: $measurementId',
|
||||
);
|
||||
}
|
||||
if (count < 0) {
|
||||
throw const BTHomeMetHistoryFormatException(
|
||||
'MET history sample count is invalid.',
|
||||
);
|
||||
}
|
||||
if (parts.length != count + 3) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'MET history sample count mismatch: expected $count values, got ${parts.length - 3}.',
|
||||
);
|
||||
}
|
||||
|
||||
final values = <double>[];
|
||||
for (final part in parts.skip(3)) {
|
||||
final value = double.tryParse(part);
|
||||
if (value == null) {
|
||||
throw BTHomeMetHistoryFormatException(
|
||||
'Invalid MET history sample value: $part',
|
||||
);
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
return BTHomeMetHistoryPage(
|
||||
measurement: measurement,
|
||||
page: page,
|
||||
values: List<double>.unmodifiable(values),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTruthyCapabilityValue(Object? value) {
|
||||
if (value is num) {
|
||||
return value > 0;
|
||||
}
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _hasBTHomeMetCapability(Contact? contact) {
|
||||
final extraSensorData = contact?.telemetry?.extraSensorData;
|
||||
if (extraSensorData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// MET history is only enabled when the node advertises an explicit
|
||||
// capability marker on channel 1. Accept a small set of channel-1 marker
|
||||
// keys so the app remains tolerant while firmware-side encoding settles.
|
||||
const capabilityKeys = <String>[
|
||||
'met_capability',
|
||||
'met_capability_1',
|
||||
'generic_sensor_1',
|
||||
'light_level_1',
|
||||
];
|
||||
|
||||
for (final key in capabilityKeys) {
|
||||
if (_isTruthyCapabilityValue(extraSensorData[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<BTHomeMetMeasurement> bTHomeMetMeasurementsForContact(Contact? contact) {
|
||||
final telemetry = contact?.telemetry;
|
||||
if (telemetry == null || !_hasBTHomeMetCapability(contact)) {
|
||||
return const <BTHomeMetMeasurement>[];
|
||||
}
|
||||
|
||||
final measurements = <BTHomeMetMeasurement>[
|
||||
if (telemetry.temperature != null) BTHomeMetMeasurement.temperature,
|
||||
if (telemetry.humidity != null) BTHomeMetMeasurement.humidity,
|
||||
];
|
||||
|
||||
final extraSensorData = telemetry.extraSensorData;
|
||||
if (extraSensorData != null) {
|
||||
if (extraSensorData.keys.any(
|
||||
(key) => key.startsWith('speed_') || key.startsWith('signed_speed_'),
|
||||
)) {
|
||||
measurements.add(BTHomeMetMeasurement.windSpeed);
|
||||
}
|
||||
if (extraSensorData.keys.any((key) => key.startsWith('gust_'))) {
|
||||
measurements.add(BTHomeMetMeasurement.gust);
|
||||
}
|
||||
if (extraSensorData.keys.any((key) => key.startsWith('rain_'))) {
|
||||
measurements.add(BTHomeMetMeasurement.rain);
|
||||
}
|
||||
}
|
||||
|
||||
return List<BTHomeMetMeasurement>.unmodifiable(measurements);
|
||||
}
|
||||
|
||||
bool supportsBTHomeMetHistory(Contact? contact) =>
|
||||
bTHomeMetMeasurementsForContact(contact).isNotEmpty;
|
||||
@@ -88,10 +88,13 @@ class CayenneLppParser {
|
||||
LatLng? gpsLocation;
|
||||
double? batteryPercentage;
|
||||
double? batteryMilliVolts;
|
||||
double? deferredBatteryMilliVolts;
|
||||
double? deferredBatteryPercentage;
|
||||
double? temperature;
|
||||
double? humidity;
|
||||
double? pressure;
|
||||
final extraSensorData = <String, dynamic>{};
|
||||
bool sawNonSelfChannel = false;
|
||||
|
||||
int fieldCount = 0;
|
||||
while (reader.hasRemaining) {
|
||||
@@ -112,6 +115,9 @@ class CayenneLppParser {
|
||||
|
||||
final channel = reader.readByte();
|
||||
debugPrint(' Channel: $channel');
|
||||
if (channel != _selfTelemetryChannel) {
|
||||
sawNonSelfChannel = true;
|
||||
}
|
||||
|
||||
final type = reader.readByte();
|
||||
debugPrint(
|
||||
@@ -136,7 +142,7 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Analog Input (raw): $rawValue');
|
||||
debugPrint(' Analog Input (volts): ${value}V');
|
||||
if (_isBatteryChannel(channel)) {
|
||||
if (_isDedicatedBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
@@ -144,6 +150,10 @@ class CayenneLppParser {
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else if (_isDeferredBatteryChannel(channel)) {
|
||||
deferredBatteryMilliVolts = value * 1000;
|
||||
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
} else {
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
}
|
||||
@@ -237,7 +247,7 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Voltage (raw): $rawValue');
|
||||
debugPrint(' Voltage: ${value}V');
|
||||
if (_isBatteryChannel(channel)) {
|
||||
if (_isDedicatedBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
@@ -245,6 +255,10 @@ class CayenneLppParser {
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else if (_isDeferredBatteryChannel(channel)) {
|
||||
deferredBatteryMilliVolts = value * 1000;
|
||||
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData['voltage_$channel'] = value;
|
||||
} else {
|
||||
extraSensorData['voltage_$channel'] = value;
|
||||
}
|
||||
@@ -327,7 +341,7 @@ class CayenneLppParser {
|
||||
case _lppPercentage:
|
||||
final value = reader.readByte().toDouble();
|
||||
debugPrint(' Percentage: $value%');
|
||||
if (_isBatteryChannel(channel)) {
|
||||
if (_isDedicatedBatteryChannel(channel) || _isDeferredBatteryChannel(channel)) {
|
||||
batteryPercentage = value;
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
} else {
|
||||
@@ -631,6 +645,21 @@ class CayenneLppParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (batteryMilliVolts == null &&
|
||||
batteryPercentage == null &&
|
||||
deferredBatteryMilliVolts != null &&
|
||||
deferredBatteryPercentage != null &&
|
||||
!sawNonSelfChannel) {
|
||||
batteryMilliVolts = deferredBatteryMilliVolts;
|
||||
batteryPercentage = deferredBatteryPercentage;
|
||||
extraSensorData[_sourceChannelKey('battery')] = _selfTelemetryChannel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = _selfTelemetryChannel;
|
||||
debugPrint(
|
||||
' Promoted self-channel voltage to battery: '
|
||||
'${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(' Parsed $fieldCount fields');
|
||||
debugPrint(' ✅ [CayenneLPP] Parsing complete');
|
||||
debugPrint(
|
||||
@@ -677,8 +706,10 @@ class CayenneLppParser {
|
||||
return ((voltage - 3.0) / 1.2) * 100.0;
|
||||
}
|
||||
|
||||
static bool _isBatteryChannel(int channel) =>
|
||||
channel == 0 || channel == _selfTelemetryChannel;
|
||||
static bool _isDedicatedBatteryChannel(int channel) => channel == 0;
|
||||
|
||||
static bool _isDeferredBatteryChannel(int channel) =>
|
||||
channel == _selfTelemetryChannel;
|
||||
|
||||
static int _readUInt32BE(BufferReader reader) {
|
||||
final bytes = reader.readBytes(4);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/contact_group.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
@@ -14,8 +15,12 @@ class ContactStorageService {
|
||||
static const int _maxStoredContacts = 500; // Store up to 500 contacts
|
||||
static const int _maxStoredPendingAdverts = 500;
|
||||
|
||||
String _key(String baseKey, {String? namespace}) {
|
||||
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
|
||||
}
|
||||
|
||||
/// Save contacts to persistent storage
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -30,7 +35,10 @@ class ContactStorageService {
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_contactsKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
||||
@@ -42,10 +50,15 @@ class ContactStorageService {
|
||||
|
||||
/// Load contacts from persistent storage
|
||||
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
|
||||
Future<List<Contact>> loadContacts({Uint8List? excludePublicKey}) async {
|
||||
Future<List<Contact>> loadContacts({
|
||||
Uint8List? excludePublicKey,
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
debugPrint('ℹ️ [ContactStorage] No stored contacts found');
|
||||
@@ -84,23 +97,29 @@ class ContactStorageService {
|
||||
}
|
||||
|
||||
/// Clear all stored contacts
|
||||
Future<void> clearContacts() async {
|
||||
Future<void> clearContacts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_contactsKey);
|
||||
await prefs.remove(_key(_contactsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveContactGroups(List<SavedContactGroup> groups) async {
|
||||
Future<void> saveContactGroups(
|
||||
List<SavedContactGroup> groups, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = jsonEncode(
|
||||
groups.map((group) => _contactGroupToJson(group)).toList(),
|
||||
);
|
||||
await prefs.setString(_contactGroupsKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
|
||||
);
|
||||
@@ -109,10 +128,12 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SavedContactGroup>> loadContactGroups() async {
|
||||
Future<List<SavedContactGroup>> loadContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactGroupsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
@@ -128,13 +149,29 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePendingAdverts(List<Map<String, dynamic>> adverts) async {
|
||||
Future<void> clearContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_key(_contactGroupsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all contact groups');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contact groups: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePendingAdverts(
|
||||
List<Map<String, dynamic>> adverts, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final limitedList = adverts.length > _maxStoredPendingAdverts
|
||||
? adverts.sublist(adverts.length - _maxStoredPendingAdverts)
|
||||
: adverts;
|
||||
await prefs.setString(_pendingAdvertsKey, jsonEncode(limitedList));
|
||||
await prefs.setString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
jsonEncode(limitedList),
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} pending adverts to storage',
|
||||
);
|
||||
@@ -143,10 +180,14 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> loadPendingAdverts() async {
|
||||
Future<List<Map<String, dynamic>>> loadPendingAdverts({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_pendingAdvertsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
@@ -159,21 +200,55 @@ class ContactStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearPendingAdverts() async {
|
||||
Future<void> clearPendingAdverts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_pendingAdvertsKey);
|
||||
await prefs.remove(_key(_pendingAdvertsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored pending adverts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing pending adverts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
// ── Favorites ───────────────────────────────────────────────────────────────
|
||||
|
||||
static const String _favoritesKey = 'contact_favorites';
|
||||
|
||||
Future<void> saveFavorites(
|
||||
Set<String> publicKeyHexes, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
await prefs.setStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
publicKeyHexes.toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>> loadFavorites({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
);
|
||||
return list?.toSet() ?? {};
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
|
||||
140
lib/services/device_config_applicator.dart
Normal file
140
lib/services/device_config_applicator.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
|
||||
class DeviceConfigApplicator {
|
||||
ConfigProfileSections capture({
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
return ConfigProfileSections(
|
||||
deviceConfig: DeviceConfigProfileSection(
|
||||
frequencyKhz: deviceInfo.radioFreq,
|
||||
bandwidth: deviceInfo.radioBw,
|
||||
spreadingFactor: deviceInfo.radioSf,
|
||||
codingRate: deviceInfo.radioCr,
|
||||
repeatEnabled: deviceInfo.clientRepeat,
|
||||
txPower: deviceInfo.txPower,
|
||||
telemetryModes: deviceInfo.telemetryModes,
|
||||
advertLocationPolicy: deviceInfo.advertLocPolicy,
|
||||
multiAcks: deviceInfo.multiAcks,
|
||||
manualAddContacts: deviceInfo.manualAddContacts,
|
||||
autoAddUsers: deviceInfo.autoAddUsers,
|
||||
autoAddRepeaters: deviceInfo.autoAddRepeaters,
|
||||
autoAddRoomServers: deviceInfo.autoAddRoomServers,
|
||||
autoAddSensors: deviceInfo.autoAddSensors,
|
||||
autoAddOverwriteOldest: deviceInfo.autoAddOverwriteOldest,
|
||||
publicLatitude: _decodeAdvertCoordinate(deviceInfo.advLat),
|
||||
publicLongitude: _decodeAdvertCoordinate(deviceInfo.advLon),
|
||||
),
|
||||
channels: channelsProvider.channels,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
ConfigProfile profile, {
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final deviceConfig = profile.sections.deviceConfig;
|
||||
if (deviceConfig != null && !deviceConfig.isEmpty) {
|
||||
if (deviceConfig.txPower != null) {
|
||||
await connectionProvider.setTxPower(deviceConfig.txPower!);
|
||||
}
|
||||
if (deviceConfig.telemetryModes != null ||
|
||||
deviceConfig.advertLocationPolicy != null ||
|
||||
deviceConfig.manualAddContacts != null ||
|
||||
deviceConfig.multiAcks != null) {
|
||||
await connectionProvider.setOtherParams(
|
||||
manualAddContacts: (deviceConfig.manualAddContacts ?? false) ? 1 : 0,
|
||||
telemetryModes: deviceConfig.telemetryModes ?? 0,
|
||||
advertLocationPolicy: deviceConfig.advertLocationPolicy ?? 0,
|
||||
multiAcks: deviceConfig.multiAcks ?? 0,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.autoAddUsers != null &&
|
||||
deviceConfig.autoAddRepeaters != null &&
|
||||
deviceConfig.autoAddRoomServers != null &&
|
||||
deviceConfig.autoAddSensors != null &&
|
||||
deviceConfig.autoAddOverwriteOldest != null) {
|
||||
await connectionProvider.setAutoaddConfig(
|
||||
autoAddUsers: deviceConfig.autoAddUsers!,
|
||||
autoAddRepeaters: deviceConfig.autoAddRepeaters!,
|
||||
autoAddRoomServers: deviceConfig.autoAddRoomServers!,
|
||||
autoAddSensors: deviceConfig.autoAddSensors!,
|
||||
overwriteOldest: deviceConfig.autoAddOverwriteOldest!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.publicLatitude != null &&
|
||||
deviceConfig.publicLongitude != null) {
|
||||
await connectionProvider.setAdvertLatLon(
|
||||
latitude: deviceConfig.publicLatitude!,
|
||||
longitude: deviceConfig.publicLongitude!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.frequencyKhz != null &&
|
||||
deviceConfig.bandwidth != null &&
|
||||
deviceConfig.spreadingFactor != null &&
|
||||
deviceConfig.codingRate != null) {
|
||||
await connectionProvider.setRadioParams(
|
||||
frequency: deviceConfig.frequencyKhz!,
|
||||
bandwidth: deviceConfig.bandwidth!,
|
||||
spreadingFactor: deviceConfig.spreadingFactor!,
|
||||
codingRate: deviceConfig.codingRate!,
|
||||
repeat: deviceConfig.repeatEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _applyChannels(
|
||||
channels: profile.sections.channels,
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
}
|
||||
|
||||
Future<void> _applyChannels({
|
||||
required List<Channel> channels,
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (channels.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final desired = {for (final channel in channels) channel.index: channel};
|
||||
|
||||
for (final channel in channels) {
|
||||
await connectionProvider.setChannelSlot(
|
||||
channelIdx: channel.index,
|
||||
channelName: channel.name,
|
||||
secret: Uint8List.fromList(channel.secret),
|
||||
);
|
||||
channelsProvider.addOrUpdateChannelObject(channel);
|
||||
}
|
||||
|
||||
for (final existing in channelsProvider.channels) {
|
||||
if (existing.index == 0 || desired.containsKey(existing.index)) {
|
||||
continue;
|
||||
}
|
||||
await connectionProvider.deleteChannel(existing.index);
|
||||
channelsProvider.removeChannel(existing.index);
|
||||
}
|
||||
}
|
||||
|
||||
double? _decodeAdvertCoordinate(int? value) {
|
||||
if (value == null || value == 0) {
|
||||
return null;
|
||||
}
|
||||
return value / 1e6;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Stores user-selected image compression settings.
|
||||
class ImagePreferences {
|
||||
@@ -17,44 +18,53 @@ class ImagePreferences {
|
||||
|
||||
static Future<int> getMaxSize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_maxSizeKey) ?? defaultMaxSize;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_maxSizeKey)) ??
|
||||
defaultMaxSize;
|
||||
return supportedSizes.contains(value) ? value : defaultMaxSize;
|
||||
}
|
||||
|
||||
static Future<void> setMaxSize(int size) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_maxSizeKey, size);
|
||||
await prefs.setInt(ProfileStorageScope.scopedKey(_maxSizeKey), size);
|
||||
}
|
||||
|
||||
static Future<int> getCompression() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_qualityKey) ?? defaultQuality;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_qualityKey)) ??
|
||||
defaultQuality;
|
||||
return value.clamp(10, 90);
|
||||
}
|
||||
|
||||
static Future<void> setCompression(int compression) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_qualityKey, compression.clamp(10, 90));
|
||||
await prefs.setInt(
|
||||
ProfileStorageScope.scopedKey(_qualityKey),
|
||||
compression.clamp(10, 90),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getGrayscale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_grayscaleKey) ?? defaultGrayscale;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_grayscaleKey)) ??
|
||||
defaultGrayscale;
|
||||
}
|
||||
|
||||
static Future<void> setGrayscale(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_grayscaleKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_grayscaleKey), value);
|
||||
}
|
||||
|
||||
static Future<bool> getUltraMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_ultraModeKey) ?? defaultUltraMode;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_ultraModeKey)) ??
|
||||
defaultUltraMode;
|
||||
}
|
||||
|
||||
static Future<void> setUltraMode(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_ultraModeKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_ultraModeKey), value);
|
||||
}
|
||||
|
||||
static int effectiveMaxSize(
|
||||
|
||||
@@ -141,6 +141,18 @@ class LiveTrafficSummary {
|
||||
|
||||
const LiveTrafficSummary._();
|
||||
|
||||
static bool isRxDataLog(BlePacketLog log) {
|
||||
return log.direction == PacketDirection.rx &&
|
||||
log.responseCode == logRxDataResponseCode;
|
||||
}
|
||||
|
||||
static int countRxDataLogs(Iterable<BlePacketLog> logs, {DateTime? since}) {
|
||||
return logs.where((log) {
|
||||
return isRxDataLog(log) &&
|
||||
(since == null || !log.timestamp.isBefore(since));
|
||||
}).length;
|
||||
}
|
||||
|
||||
static LiveTrafficSnapshot fromLogs(
|
||||
Iterable<BlePacketLog> logs, {
|
||||
required DateTime now,
|
||||
@@ -154,15 +166,14 @@ class LiveTrafficSummary {
|
||||
? clearedAt
|
||||
: windowStart;
|
||||
|
||||
final recentLogs = logs
|
||||
.where(
|
||||
(log) =>
|
||||
log.direction == PacketDirection.rx &&
|
||||
log.responseCode == logRxDataResponseCode &&
|
||||
!log.timestamp.isBefore(effectiveStart),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final recentLogs =
|
||||
logs
|
||||
.where(
|
||||
(log) =>
|
||||
isRxDataLog(log) && !log.timestamp.isBefore(effectiveStart),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
final entries = <LiveTrafficEntry>[];
|
||||
for (final log in recentLogs) {
|
||||
@@ -215,7 +226,9 @@ class LiveTrafficSummary {
|
||||
}
|
||||
}
|
||||
|
||||
final visibleEntries = filteredEntries.reversed.take(maxVisibleEntries).toList();
|
||||
final visibleEntries = filteredEntries.reversed
|
||||
.take(maxVisibleEntries)
|
||||
.toList();
|
||||
const txCount = 0;
|
||||
final totalCount = rxCount;
|
||||
final packetsPerMinute =
|
||||
@@ -233,7 +246,9 @@ class LiveTrafficSummary {
|
||||
avgRssiDbm: rssiCount == 0 ? null : rssiSum / rssiCount,
|
||||
latestRssiDbm: latestRssiDbm,
|
||||
multiHopCount: multiHopCount,
|
||||
avgHopCount: hopCountSamples == 0 ? null : hopCountTotal / hopCountSamples,
|
||||
avgHopCount: hopCountSamples == 0
|
||||
? null
|
||||
: hopCountTotal / hopCountSamples,
|
||||
visibleEntries: visibleEntries,
|
||||
busyness: _busynessForPacketsPerMinute(packetsPerMinute),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
100
lib/services/map_workspace_snapshot_service.dart
Normal file
100
lib/services/map_workspace_snapshot_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/services/profile_device_key_resolver.dart
Normal file
21
lib/services/profile_device_key_resolver.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import '../models/device_info.dart';
|
||||
|
||||
class ProfileDeviceKeyResolver {
|
||||
static String? resolve({
|
||||
required DeviceInfo deviceInfo,
|
||||
required ConnectionMode connectionMode,
|
||||
}) {
|
||||
// Always prefer the public key — it's the only truly stable identifier.
|
||||
// Don't fall back to deviceId to avoid creating duplicate profiles when
|
||||
// publicKey arrives late (after initial connection but before DeviceInfo).
|
||||
final publicKey = deviceInfo.publicKey;
|
||||
if (publicKey == null || publicKey.isEmpty) {
|
||||
return null; // Wait until publicKey is available
|
||||
}
|
||||
|
||||
final hex = publicKey
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
return hex.isNotEmpty ? 'pk:$hex' : null;
|
||||
}
|
||||
}
|
||||
185
lib/services/profile_manager.dart
Normal file
185
lib/services/profile_manager.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class ProfileManager with ChangeNotifier {
|
||||
static const String _profilesKey = 'profiles_library';
|
||||
static const String activeProfileIdKey = 'profiles_active_profile_id';
|
||||
static const String _deviceProfileDefaultsKey =
|
||||
'profiles_device_active_profile_ids';
|
||||
static const String _transferHistoryKey = 'profiles_transfer_history';
|
||||
|
||||
final List<ConfigProfile> _customProfiles = <ConfigProfile>[];
|
||||
final List<ProfileTransferRecord> _transferHistory =
|
||||
<ProfileTransferRecord>[];
|
||||
final Map<String, String> _deviceProfileDefaults = <String, String>{};
|
||||
bool _isInitialized = false;
|
||||
bool _profilesEnabled = false;
|
||||
String _activeProfileId = ConfigProfile.defaultProfileId;
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get profilesEnabled => _profilesEnabled;
|
||||
String get activeProfileId => _activeProfileId;
|
||||
List<ConfigProfile> get customProfiles => List.unmodifiable(_customProfiles);
|
||||
List<ProfileTransferRecord> get transferHistory =>
|
||||
List.unmodifiable(_transferHistory);
|
||||
|
||||
List<ConfigProfile> get visibleProfiles => [
|
||||
if (_profilesEnabled) ConfigProfile.defaultProfile(),
|
||||
..._customProfiles,
|
||||
];
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
_activeProfileId =
|
||||
prefs.getString(activeProfileIdKey) ?? ConfigProfile.defaultProfileId;
|
||||
|
||||
final deviceDefaultsJson = prefs.getString(_deviceProfileDefaultsKey);
|
||||
if (deviceDefaultsJson != null && deviceDefaultsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(deviceDefaultsJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
_deviceProfileDefaults
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.map((key, value) => MapEntry(key, value?.toString() ?? ''))
|
||||
..removeWhere((key, value) => value.isEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final profilesJson = prefs.getString(_profilesKey);
|
||||
if (profilesJson != null && profilesJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(profilesJson) as List<dynamic>;
|
||||
_customProfiles
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(ConfigProfile.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
final historyJson = prefs.getString(_transferHistoryKey);
|
||||
if (historyJson != null && historyJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(historyJson) as List<dynamic>;
|
||||
_transferHistory
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(
|
||||
ProfileTransferRecord.fromJson,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ConfigProfile? getProfile(String id) {
|
||||
if (id == ConfigProfile.defaultProfileId) {
|
||||
return ConfigProfile.defaultProfile();
|
||||
}
|
||||
for (final profile in _customProfiles) {
|
||||
if (profile.id == id) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> setProfilesEnabled(bool enabled) async {
|
||||
_profilesEnabled = enabled;
|
||||
await ProfilesFeatureService.setEnabled(enabled);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileId(String id) async {
|
||||
await setActiveProfileIdForDevice(id);
|
||||
}
|
||||
|
||||
String profileIdForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return _activeProfileId;
|
||||
}
|
||||
return _deviceProfileDefaults[deviceKey] ?? ConfigProfile.defaultProfileId;
|
||||
}
|
||||
|
||||
bool hasProfileForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return _deviceProfileDefaults.containsKey(deviceKey);
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileIdForDevice(
|
||||
String id, {
|
||||
String? deviceKey,
|
||||
}) async {
|
||||
_activeProfileId = id;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(activeProfileIdKey, id);
|
||||
if (deviceKey != null && deviceKey.isNotEmpty) {
|
||||
_deviceProfileDefaults[deviceKey] = id;
|
||||
await prefs.setString(
|
||||
_deviceProfileDefaultsKey,
|
||||
jsonEncode(_deviceProfileDefaults),
|
||||
);
|
||||
}
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> upsertProfile(ConfigProfile profile) async {
|
||||
final index = _customProfiles.indexWhere((item) => item.id == profile.id);
|
||||
if (index == -1) {
|
||||
_customProfiles.add(profile);
|
||||
} else {
|
||||
_customProfiles[index] = profile;
|
||||
}
|
||||
_customProfiles.sort((a, b) => a.name.compareTo(b.name));
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteProfile(String id) async {
|
||||
_customProfiles.removeWhere((profile) => profile.id == id);
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> recordTransfer(ProfileTransferRecord record) async {
|
||||
_transferHistory.insert(0, record);
|
||||
if (_transferHistory.length > 100) {
|
||||
_transferHistory.removeRange(100, _transferHistory.length);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_transferHistoryKey,
|
||||
jsonEncode(_transferHistory.map((item) => item.toJson()).toList()),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistProfiles() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_profilesKey,
|
||||
jsonEncode(_customProfiles.map((profile) => profile.toJson()).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/services/profile_transport.dart
Normal file
10
lib/services/profile_transport.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
abstract class ProfileTransport {
|
||||
String get label;
|
||||
}
|
||||
|
||||
class FileProfileTransport implements ProfileTransport {
|
||||
const FileProfileTransport();
|
||||
|
||||
@override
|
||||
String get label => 'File';
|
||||
}
|
||||
499
lib/services/profile_workspace_coordinator.dart
Normal file
499
lib/services/profile_workspace_coordinator.dart
Normal file
@@ -0,0 +1,499 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/drawing_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../providers/voice_provider.dart';
|
||||
import '../providers/image_provider.dart' as ip;
|
||||
import 'app_config_snapshot_service.dart';
|
||||
import 'contact_storage_service.dart';
|
||||
import 'device_config_applicator.dart';
|
||||
import 'message_storage_service.dart';
|
||||
import 'profile_manager.dart';
|
||||
import 'profile_device_key_resolver.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'map_workspace_snapshot_service.dart';
|
||||
|
||||
class ProfileWorkspaceCoordinator {
|
||||
ProfileWorkspaceCoordinator({
|
||||
required this.profileManager,
|
||||
required this.connectionProvider,
|
||||
required this.contactsProvider,
|
||||
required this.messagesProvider,
|
||||
required this.sensorsProvider,
|
||||
required this.mapProvider,
|
||||
required this.drawingProvider,
|
||||
required this.channelsProvider,
|
||||
required this.voiceProvider,
|
||||
required this.imageProvider,
|
||||
required this.appProvider,
|
||||
AppConfigSnapshotService? appConfigSnapshotService,
|
||||
MapWorkspaceSnapshotService? mapWorkspaceSnapshotService,
|
||||
DeviceConfigApplicator? deviceConfigApplicator,
|
||||
MessageStorageService? messageStorageService,
|
||||
ContactStorageService? contactStorageService,
|
||||
}) : _appConfigSnapshotService =
|
||||
appConfigSnapshotService ?? AppConfigSnapshotService(),
|
||||
_mapWorkspaceSnapshotService =
|
||||
mapWorkspaceSnapshotService ?? MapWorkspaceSnapshotService(),
|
||||
_deviceConfigApplicator =
|
||||
deviceConfigApplicator ?? DeviceConfigApplicator(),
|
||||
_messageStorageService =
|
||||
messageStorageService ?? MessageStorageService(),
|
||||
_contactStorageService =
|
||||
contactStorageService ?? ContactStorageService();
|
||||
|
||||
final ProfileManager profileManager;
|
||||
final ConnectionProvider connectionProvider;
|
||||
final ContactsProvider contactsProvider;
|
||||
final MessagesProvider messagesProvider;
|
||||
final SensorsProvider sensorsProvider;
|
||||
final MapProvider mapProvider;
|
||||
final DrawingProvider drawingProvider;
|
||||
final ChannelsProvider channelsProvider;
|
||||
final VoiceProvider voiceProvider;
|
||||
final ip.ImageProvider imageProvider;
|
||||
final AppProvider appProvider;
|
||||
final AppConfigSnapshotService _appConfigSnapshotService;
|
||||
final MapWorkspaceSnapshotService _mapWorkspaceSnapshotService;
|
||||
final DeviceConfigApplicator _deviceConfigApplicator;
|
||||
final MessageStorageService _messageStorageService;
|
||||
final ContactStorageService _contactStorageService;
|
||||
Future<void>? _deviceProfileSyncFuture;
|
||||
|
||||
Future<void> setProfilesEnabled(bool enabled) async {
|
||||
final wasEnabled = profileManager.profilesEnabled;
|
||||
if (wasEnabled && !enabled) {
|
||||
await _saveActiveCustomProfileSnapshot();
|
||||
} else {
|
||||
await _persistCurrentState();
|
||||
}
|
||||
await profileManager.setProfilesEnabled(enabled);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: enabled,
|
||||
activeProfileId: enabled ? profileManager.activeProfileId : 'default',
|
||||
);
|
||||
if (enabled) {
|
||||
await _ensureProfileForCurrentDevice();
|
||||
if (wasEnabled) {
|
||||
await openProfile(profileManager.activeProfileId);
|
||||
} else {
|
||||
await _switchRuntimeScope(profileManager.activeProfileId);
|
||||
final profile = await resolveProfile(profileManager.activeProfileId);
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _switchRuntimeScope('default');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConfigProfile> snapshotCurrentProfile({
|
||||
required String id,
|
||||
required String name,
|
||||
String? notes,
|
||||
}) async {
|
||||
final deviceSections = _deviceConfigApplicator.capture(
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
final appSettings = await _appConfigSnapshotService.capture(appProvider);
|
||||
final mapWorkspace = await _mapWorkspaceSnapshotService.capture(
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
return ConfigProfile(
|
||||
id: id,
|
||||
name: name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
notes: notes,
|
||||
sections: ConfigProfileSections(
|
||||
deviceConfig: deviceSections.deviceConfig,
|
||||
channels: deviceSections.channels,
|
||||
appSettings: appSettings,
|
||||
mapWorkspace: mapWorkspace,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> createProfileFromCurrent({
|
||||
required String name,
|
||||
String? notes,
|
||||
}) async {
|
||||
final profileId = 'profile_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final profile = await snapshotCurrentProfile(
|
||||
id: profileId,
|
||||
name: name,
|
||||
notes: notes,
|
||||
);
|
||||
await _initializeEmptyStorageNamespace(profileId);
|
||||
await profileManager.upsertProfile(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
Future<ConfigProfile> duplicateProfile(ConfigProfile source) async {
|
||||
final duplicate = await snapshotCurrentProfile(
|
||||
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '${source.name} Copy',
|
||||
notes: source.notes,
|
||||
);
|
||||
final sourceNamespace = source.id == ConfigProfile.defaultProfileId
|
||||
? null
|
||||
: source.id;
|
||||
await _copyStorageNamespace(sourceNamespace, duplicate.id);
|
||||
await profileManager.upsertProfile(
|
||||
duplicate.copyWith(
|
||||
sections: source.id == profileManager.activeProfileId
|
||||
? duplicate.sections
|
||||
: source.sections,
|
||||
),
|
||||
);
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
Future<void> renameProfile(ConfigProfile profile, String name) async {
|
||||
await profileManager.upsertProfile(
|
||||
profile.copyWith(name: name, updatedAt: DateTime.now()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteProfile(ConfigProfile profile) async {
|
||||
if (profile.isDefault) return;
|
||||
if (profile.id == profileManager.activeProfileId) {
|
||||
await openProfile(ConfigProfile.defaultProfileId);
|
||||
}
|
||||
await _messageStorageService.clearMessages(namespace: profile.id);
|
||||
await _contactStorageService.clearContacts(namespace: profile.id);
|
||||
await _contactStorageService.clearContactGroups(namespace: profile.id);
|
||||
await _contactStorageService.clearPendingAdverts(namespace: profile.id);
|
||||
await profileManager.deleteProfile(profile.id);
|
||||
}
|
||||
|
||||
Future<void> openProfile(String profileId) async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
await _saveActiveCustomProfileSnapshot();
|
||||
await connectionProvider.disconnect();
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
profileId,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
await _switchRuntimeScope(profileId);
|
||||
|
||||
final profile = await resolveProfile(profileId);
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> applyProfile(String profileId) async {
|
||||
await openProfile(profileId);
|
||||
final profile = await resolveProfile(profileId);
|
||||
await _deviceConfigApplicator.apply(
|
||||
profile,
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> resolveProfile(String profileId) async {
|
||||
if (profileId == ConfigProfile.defaultProfileId) {
|
||||
return snapshotCurrentProfile(id: profileId, name: 'Default');
|
||||
}
|
||||
return profileManager.getProfile(profileId) ??
|
||||
await snapshotCurrentProfile(id: profileId, name: 'Unknown');
|
||||
}
|
||||
|
||||
Future<void> exportProfile(ConfigProfile profile) async {
|
||||
final resolved = profile.id == ConfigProfile.defaultProfileId
|
||||
? await snapshotCurrentProfile(id: profile.id, name: profile.name)
|
||||
: profile;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File(
|
||||
'${tempDir.path}/${resolved.name.replaceAll(' ', '_').toLowerCase()}_${resolved.id}.meshcore_profile.json',
|
||||
);
|
||||
await file.writeAsString(
|
||||
const JsonEncoder.withIndent(' ').convert(resolved.toJson()),
|
||||
);
|
||||
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
||||
await profileManager.recordTransfer(
|
||||
ProfileTransferRecord(
|
||||
profileId: resolved.id,
|
||||
direction: 'export',
|
||||
timestamp: DateTime.now(),
|
||||
detail: 'Shared as file',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile?> importProfileFromFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
withData: true,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final file = result.files.first;
|
||||
if (file.bytes == null) return null;
|
||||
|
||||
final decoded = jsonDecode(utf8.decode(file.bytes!));
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final imported = ConfigProfile.fromJson(decoded).copyWith(
|
||||
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
await profileManager.upsertProfile(imported);
|
||||
await profileManager.recordTransfer(
|
||||
ProfileTransferRecord(
|
||||
profileId: imported.id,
|
||||
direction: 'import',
|
||||
timestamp: DateTime.now(),
|
||||
detail: file.name,
|
||||
),
|
||||
);
|
||||
return imported;
|
||||
}
|
||||
|
||||
Future<void> syncActiveProfileForCurrentDevice() async {
|
||||
if (!profileManager.profilesEnabled) {
|
||||
return;
|
||||
}
|
||||
final inFlightSync = _deviceProfileSyncFuture;
|
||||
if (inFlightSync != null) {
|
||||
await inFlightSync;
|
||||
return;
|
||||
}
|
||||
|
||||
final syncFuture = _syncActiveProfileForCurrentDeviceInternal();
|
||||
_deviceProfileSyncFuture = syncFuture;
|
||||
try {
|
||||
await syncFuture;
|
||||
} finally {
|
||||
if (identical(_deviceProfileSyncFuture, syncFuture)) {
|
||||
_deviceProfileSyncFuture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncActiveProfileForCurrentDeviceInternal() async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
if (deviceKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final profile = await _ensureProfileForCurrentDevice();
|
||||
final targetProfileId = profile.id;
|
||||
if (targetProfileId == profileManager.activeProfileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _persistCurrentState();
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
targetProfileId,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
await _switchRuntimeScope(targetProfileId);
|
||||
|
||||
await _appConfigSnapshotService.apply(
|
||||
profile.sections.appSettings,
|
||||
appProvider,
|
||||
);
|
||||
await _mapWorkspaceSnapshotService.apply(
|
||||
profile.sections.mapWorkspace,
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: drawingProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ConfigProfile> _ensureProfileForCurrentDevice() async {
|
||||
final deviceKey = _currentDeviceProfileKey;
|
||||
final targetProfileId = profileManager.profileIdForDevice(deviceKey);
|
||||
final existingProfile = profileManager.getProfile(targetProfileId);
|
||||
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||
existingProfile != null) {
|
||||
return existingProfile;
|
||||
}
|
||||
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||
targetProfileId == ConfigProfile.defaultProfileId) {
|
||||
return ConfigProfile.defaultProfile();
|
||||
}
|
||||
if (deviceKey == null) {
|
||||
return await resolveProfile(profileManager.activeProfileId);
|
||||
}
|
||||
|
||||
final profile = await createProfileFromCurrent(
|
||||
name: _buildDeviceProfileName(),
|
||||
);
|
||||
await profileManager.setActiveProfileIdForDevice(
|
||||
profile.id,
|
||||
deviceKey: deviceKey,
|
||||
);
|
||||
return profile;
|
||||
}
|
||||
|
||||
String _buildDeviceProfileName() {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final name = deviceInfo.selfName?.trim();
|
||||
if (name != null && name.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(name)}';
|
||||
}
|
||||
|
||||
final displayName = deviceInfo.displayName?.trim();
|
||||
if (displayName != null && displayName.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(displayName)}';
|
||||
}
|
||||
|
||||
final deviceId = deviceInfo.deviceId?.trim();
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
return 'Device ${_sanitizeProfileLabel(deviceId)}';
|
||||
}
|
||||
|
||||
return 'Device Profile';
|
||||
}
|
||||
|
||||
String _sanitizeProfileLabel(String value) {
|
||||
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
}
|
||||
|
||||
String? get _currentDeviceProfileKey => ProfileDeviceKeyResolver.resolve(
|
||||
deviceInfo: connectionProvider.deviceInfo,
|
||||
connectionMode: connectionProvider.connectionMode,
|
||||
);
|
||||
|
||||
Future<void> _switchRuntimeScope(String profileId) async {
|
||||
final runtimeProfilesEnabled = profileManager.profilesEnabled;
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: runtimeProfilesEnabled,
|
||||
activeProfileId: profileId,
|
||||
);
|
||||
await messagesProvider.reloadFromStorage(
|
||||
namespace: ProfileStorageScope.effectiveNamespace,
|
||||
);
|
||||
await contactsProvider.reloadFromStorage(
|
||||
namespace: ProfileStorageScope.effectiveNamespace,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
await sensorsProvider.reloadProfileScopedState();
|
||||
await drawingProvider.reloadProfileScopedState();
|
||||
await mapProvider.reloadProfileScopedState();
|
||||
await voiceProvider.reloadProfileScopedState();
|
||||
await imageProvider.reloadProfileScopedState();
|
||||
await appProvider.reloadProfileScopedSettings();
|
||||
}
|
||||
|
||||
Future<void> _persistCurrentState() async {
|
||||
await messagesProvider.persistNow();
|
||||
await contactsProvider.persistNow();
|
||||
}
|
||||
|
||||
Future<void> _saveActiveCustomProfileSnapshot() async {
|
||||
if (profileManager.activeProfileId == ConfigProfile.defaultProfileId) {
|
||||
await _persistCurrentState();
|
||||
return;
|
||||
}
|
||||
final current = profileManager.getProfile(profileManager.activeProfileId);
|
||||
if (current == null) {
|
||||
await _persistCurrentState();
|
||||
return;
|
||||
}
|
||||
final snapshot = await snapshotCurrentProfile(
|
||||
id: current.id,
|
||||
name: current.name,
|
||||
notes: current.notes,
|
||||
);
|
||||
await profileManager.upsertProfile(snapshot);
|
||||
await _persistCurrentState();
|
||||
}
|
||||
|
||||
Future<void> _copyStorageNamespace(
|
||||
String? sourceNamespace,
|
||||
String targetNamespace,
|
||||
) async {
|
||||
final messages = await _messageStorageService.loadMessages(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final contactLocations = await _messageStorageService
|
||||
.loadMessageContactLocations(namespace: sourceNamespace);
|
||||
final receptionDetails = await _messageStorageService
|
||||
.loadMessageReceptionDetails(namespace: sourceNamespace);
|
||||
final transferDetails = await _messageStorageService
|
||||
.loadMessageTransferDetails(namespace: sourceNamespace);
|
||||
final routeMetadata = await _messageStorageService.loadMessageRouteMetadata(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final removedIds = await _messageStorageService.loadRemovedSarMarkerIds(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
await _messageStorageService.saveMessages(
|
||||
messages,
|
||||
messageContactLocations: contactLocations,
|
||||
messageReceptionDetails: receptionDetails,
|
||||
messageTransferDetails: transferDetails,
|
||||
messageRouteMetadata: routeMetadata,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _messageStorageService.saveRemovedSarMarkerIds(
|
||||
removedIds,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
|
||||
final contacts = await _contactStorageService.loadContacts(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final groups = await _contactStorageService.loadContactGroups(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
final pending = await _contactStorageService.loadPendingAdverts(
|
||||
namespace: sourceNamespace,
|
||||
);
|
||||
await _contactStorageService.saveContacts(
|
||||
contacts,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _contactStorageService.saveContactGroups(
|
||||
groups,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
await _contactStorageService.savePendingAdverts(
|
||||
pending,
|
||||
namespace: targetNamespace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initializeEmptyStorageNamespace(String namespace) async {
|
||||
await _messageStorageService.clearMessages(namespace: namespace);
|
||||
await _contactStorageService.clearContacts(namespace: namespace);
|
||||
await _contactStorageService.clearContactGroups(namespace: namespace);
|
||||
await _contactStorageService.clearPendingAdverts(namespace: namespace);
|
||||
}
|
||||
}
|
||||
54
lib/services/profiles_feature_service.dart
Normal file
54
lib/services/profiles_feature_service.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ProfilesFeatureService {
|
||||
static const String enabledKey = 'profiles_enabled';
|
||||
|
||||
static Future<bool> isEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(enabledKey) ?? true;
|
||||
}
|
||||
|
||||
static Future<void> setEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(enabledKey, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileStorageScope {
|
||||
static bool _profilesEnabled = true;
|
||||
static String _activeProfileId = 'default';
|
||||
|
||||
static Future<void> bootstrap({
|
||||
required bool profilesEnabled,
|
||||
required String activeProfileId,
|
||||
}) async {
|
||||
_profilesEnabled = profilesEnabled;
|
||||
_activeProfileId = activeProfileId;
|
||||
}
|
||||
|
||||
static void setScope({
|
||||
required bool profilesEnabled,
|
||||
required String activeProfileId,
|
||||
}) {
|
||||
_profilesEnabled = profilesEnabled;
|
||||
_activeProfileId = activeProfileId;
|
||||
}
|
||||
|
||||
static bool get profilesEnabled => _profilesEnabled;
|
||||
static String get activeProfileId => _activeProfileId;
|
||||
|
||||
static String? get effectiveNamespace {
|
||||
if (!_profilesEnabled || _activeProfileId == 'default') {
|
||||
return null;
|
||||
}
|
||||
return _activeProfileId;
|
||||
}
|
||||
|
||||
static String scopedKey(String baseKey, {String? namespace}) {
|
||||
final scope = namespace ?? effectiveNamespace;
|
||||
if (scope == null || scope.isEmpty) {
|
||||
return baseKey;
|
||||
}
|
||||
return 'profile.$scope.$baseKey';
|
||||
}
|
||||
}
|
||||
48
lib/services/relay_candidate_sorter.dart
Normal file
48
lib/services/relay_candidate_sorter.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
|
||||
class RelayCandidateSorter {
|
||||
const RelayCandidateSorter();
|
||||
|
||||
List<Contact> sortByDistanceFromSelf(
|
||||
List<Contact> contacts, {
|
||||
required LatLng? selfPoint,
|
||||
}) {
|
||||
final sorted = List<Contact>.from(contacts);
|
||||
sorted.sort((a, b) {
|
||||
if (selfPoint != null) {
|
||||
final distanceCompare = _distanceFrom(
|
||||
selfPoint,
|
||||
a,
|
||||
).compareTo(_distanceFrom(selfPoint, b));
|
||||
if (distanceCompare != 0) {
|
||||
return distanceCompare;
|
||||
}
|
||||
}
|
||||
|
||||
final nameCompare = a.displayName.compareTo(b.displayName);
|
||||
if (nameCompare != 0) {
|
||||
return nameCompare;
|
||||
}
|
||||
|
||||
return a.publicKeyHex.compareTo(b.publicKeyHex);
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
double _distanceFrom(LatLng selfPoint, Contact contact) {
|
||||
final location = contact.displayLocation;
|
||||
if (location == null) {
|
||||
return double.infinity;
|
||||
}
|
||||
|
||||
return Geolocator.distanceBetween(
|
||||
selfPoint.latitude,
|
||||
selfPoint.longitude,
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '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;
|
||||
}
|
||||
}
|
||||
|
||||
50
lib/services/serial/serial_transport.dart
Normal file
50
lib/services/serial/serial_transport.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
|
||||
import 'serial_transport_stub.dart'
|
||||
if (dart.library.io) 'serial_transport_io.dart'
|
||||
if (dart.library.js_interop) 'serial_transport_web.dart';
|
||||
|
||||
const int kSerialBaudRate = 115200;
|
||||
|
||||
class SerialDeviceInfo {
|
||||
final String id;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Object handle;
|
||||
|
||||
const SerialDeviceInfo({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.handle,
|
||||
});
|
||||
}
|
||||
|
||||
class SerialConnection {
|
||||
final MeshCoreSerialService service;
|
||||
final String deviceId;
|
||||
final String deviceName;
|
||||
final Future<void> Function() disconnect;
|
||||
|
||||
const SerialConnection({
|
||||
required this.service,
|
||||
required this.deviceId,
|
||||
required this.deviceName,
|
||||
required this.disconnect,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class SerialTransport {
|
||||
bool get isSupported;
|
||||
bool get canRequestDevice;
|
||||
String get actionLabel;
|
||||
String get emptyStateTitle;
|
||||
String get unsupportedMessage;
|
||||
|
||||
Future<List<SerialDeviceInfo>> listDevices();
|
||||
Future<SerialDeviceInfo?> requestDevice();
|
||||
Future<SerialConnection> connect(SerialDeviceInfo device);
|
||||
void dispose();
|
||||
}
|
||||
|
||||
SerialTransport createSerialTransport() => createSerialTransportImpl();
|
||||
283
lib/services/serial/serial_transport_io.dart
Normal file
283
lib/services/serial/serial_transport_io.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_libserialport/flutter_libserialport.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'package:usb_serial/usb_serial.dart';
|
||||
|
||||
import 'serial_transport.dart';
|
||||
|
||||
class _IoSerialTransport implements SerialTransport {
|
||||
@override
|
||||
bool get isSupported =>
|
||||
Platform.isAndroid || Platform.isMacOS || Platform.isWindows;
|
||||
|
||||
@override
|
||||
bool get canRequestDevice => false;
|
||||
|
||||
@override
|
||||
String get actionLabel =>
|
||||
Platform.isAndroid ? 'Scan USB devices' : 'Scan serial ports';
|
||||
|
||||
@override
|
||||
String get emptyStateTitle => Platform.isAndroid
|
||||
? 'No USB serial devices found.\nConnect a MeshCore device via OTG cable.'
|
||||
: 'No serial ports found.\nConnect a MeshCore device over USB serial.';
|
||||
|
||||
@override
|
||||
String get unsupportedMessage =>
|
||||
'Serial is supported on Android, macOS, Windows, and compatible web browsers.';
|
||||
|
||||
@override
|
||||
Future<List<SerialDeviceInfo>> listDevices() async {
|
||||
if (Platform.isAndroid) {
|
||||
final devices = await UsbSerial.listDevices();
|
||||
return devices.map(_androidDeviceInfo).toList(growable: false);
|
||||
}
|
||||
if (Platform.isMacOS || Platform.isWindows) {
|
||||
return _desktopSerialDevices();
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SerialDeviceInfo?> requestDevice() async => null;
|
||||
|
||||
@override
|
||||
Future<SerialConnection> connect(SerialDeviceInfo device) async {
|
||||
if (Platform.isAndroid) {
|
||||
return _connectAndroid(device);
|
||||
}
|
||||
if (Platform.isMacOS || Platform.isWindows) {
|
||||
return _connectDesktop(device);
|
||||
}
|
||||
throw UnsupportedError(unsupportedMessage);
|
||||
}
|
||||
|
||||
SerialDeviceInfo _androidDeviceInfo(UsbDevice device) {
|
||||
final title = _firstNonEmpty([
|
||||
device.productName,
|
||||
device.manufacturerName,
|
||||
'USB Serial Device',
|
||||
]);
|
||||
final subtitleParts = <String>[
|
||||
if ((device.manufacturerName ?? '').trim().isNotEmpty)
|
||||
device.manufacturerName!.trim(),
|
||||
if (device.vid != null && device.pid != null)
|
||||
'VID:${_hex(device.vid!)} PID:${_hex(device.pid!)}',
|
||||
];
|
||||
|
||||
return SerialDeviceInfo(
|
||||
id: '${device.deviceId}:${device.vid ?? 'na'}:${device.pid ?? 'na'}:${device.productName ?? 'usb'}',
|
||||
title: title,
|
||||
subtitle: subtitleParts.isEmpty
|
||||
? 'Ready over OTG serial'
|
||||
: subtitleParts.join(' • '),
|
||||
handle: device,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SerialDeviceInfo>> _desktopSerialDevices() async {
|
||||
final ports = <SerialDeviceInfo>[];
|
||||
for (final address in SerialPort.availablePorts) {
|
||||
final port = SerialPort(address);
|
||||
try {
|
||||
final title = _firstNonEmpty([
|
||||
port.productName,
|
||||
port.description,
|
||||
address,
|
||||
]);
|
||||
final subtitleParts = <String>[
|
||||
address,
|
||||
if ((port.manufacturer ?? '').trim().isNotEmpty)
|
||||
port.manufacturer!.trim(),
|
||||
if (port.vendorId != null && port.productId != null)
|
||||
'VID:${_hex(port.vendorId!)} PID:${_hex(port.productId!)}',
|
||||
];
|
||||
ports.add(
|
||||
SerialDeviceInfo(
|
||||
id: address,
|
||||
title: title,
|
||||
subtitle: subtitleParts.join(' • '),
|
||||
handle: address,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
port.dispose();
|
||||
}
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
Future<SerialConnection> _connectAndroid(SerialDeviceInfo device) async {
|
||||
final usbDevice = device.handle as UsbDevice;
|
||||
final service = MeshCoreSerialService(appName: 'MeshCore SAR');
|
||||
StreamSubscription<Uint8List>? inputSubscription;
|
||||
UsbPort? port;
|
||||
|
||||
try {
|
||||
port = await usbDevice.create();
|
||||
if (port == null) {
|
||||
throw Exception('Failed to create USB port');
|
||||
}
|
||||
final opened = await port.open();
|
||||
if (!opened) {
|
||||
throw Exception('Failed to open USB port');
|
||||
}
|
||||
|
||||
await port.setDTR(true);
|
||||
await port.setRTS(true);
|
||||
await port.setPortParameters(
|
||||
kSerialBaudRate,
|
||||
UsbPort.DATABITS_8,
|
||||
UsbPort.STOPBITS_1,
|
||||
UsbPort.PARITY_NONE,
|
||||
);
|
||||
|
||||
service.writeRaw = (data) async {
|
||||
await port!.write(data);
|
||||
};
|
||||
|
||||
inputSubscription = port.inputStream?.listen(
|
||||
(data) => service.feedRawBytes(data),
|
||||
onError: (error) {
|
||||
debugPrint('❌ [Serial/Android] Read error: $error');
|
||||
service.markDisconnected();
|
||||
},
|
||||
onDone: () {
|
||||
debugPrint('⚠️ [Serial/Android] Port closed');
|
||||
service.markDisconnected();
|
||||
},
|
||||
);
|
||||
|
||||
final connected = await service.markConnected();
|
||||
if (!connected) {
|
||||
throw Exception('Serial session initialization failed');
|
||||
}
|
||||
|
||||
return SerialConnection(
|
||||
service: service,
|
||||
deviceId:
|
||||
'${usbDevice.deviceId}:${usbDevice.vid ?? 'na'}:${usbDevice.pid ?? 'na'}',
|
||||
deviceName: device.title,
|
||||
disconnect: () async {
|
||||
await inputSubscription?.cancel();
|
||||
inputSubscription = null;
|
||||
service.writeRaw = null;
|
||||
await port?.close();
|
||||
port = null;
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
await inputSubscription?.cancel();
|
||||
service.writeRaw = null;
|
||||
await port?.close();
|
||||
service.dispose();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<SerialConnection> _connectDesktop(SerialDeviceInfo device) async {
|
||||
final portName = device.handle as String;
|
||||
final port = SerialPort(portName);
|
||||
final config = SerialPortConfig();
|
||||
final service = MeshCoreSerialService(appName: 'MeshCore SAR');
|
||||
SerialPortReader? reader;
|
||||
StreamSubscription<Uint8List>? inputSubscription;
|
||||
|
||||
try {
|
||||
if (!port.openReadWrite()) {
|
||||
throw Exception(
|
||||
SerialPort.lastError?.message ?? 'Failed to open serial port',
|
||||
);
|
||||
}
|
||||
|
||||
config.baudRate = kSerialBaudRate;
|
||||
config.bits = 8;
|
||||
config.stopBits = 1;
|
||||
config.parity = SerialPortParity.none;
|
||||
config.setFlowControl(SerialPortFlowControl.none);
|
||||
config.dtr = SerialPortDtr.on;
|
||||
config.rts = SerialPortRts.on;
|
||||
port.config = config;
|
||||
|
||||
service.writeRaw = (data) async {
|
||||
var offset = 0;
|
||||
while (offset < data.length) {
|
||||
final written = port.write(data.sublist(offset));
|
||||
if (written <= 0) {
|
||||
throw Exception(
|
||||
SerialPort.lastError?.message ?? 'Failed to write to serial port',
|
||||
);
|
||||
}
|
||||
offset += written;
|
||||
}
|
||||
};
|
||||
|
||||
reader = SerialPortReader(port);
|
||||
inputSubscription = reader.stream.listen(
|
||||
(data) => service.feedRawBytes(data),
|
||||
onError: (error) {
|
||||
debugPrint('❌ [Serial/Desktop] Read error: $error');
|
||||
service.markDisconnected();
|
||||
},
|
||||
onDone: () {
|
||||
debugPrint('⚠️ [Serial/Desktop] Reader closed');
|
||||
service.markDisconnected();
|
||||
},
|
||||
);
|
||||
|
||||
final connected = await service.markConnected();
|
||||
if (!connected) {
|
||||
throw Exception('Serial session initialization failed');
|
||||
}
|
||||
|
||||
return SerialConnection(
|
||||
service: service,
|
||||
deviceId: portName,
|
||||
deviceName: device.title,
|
||||
disconnect: () async {
|
||||
await inputSubscription?.cancel();
|
||||
inputSubscription = null;
|
||||
reader?.close();
|
||||
reader = null;
|
||||
service.writeRaw = null;
|
||||
if (port.isOpen) {
|
||||
port.close();
|
||||
}
|
||||
port.dispose();
|
||||
config.dispose();
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
await inputSubscription?.cancel();
|
||||
reader?.close();
|
||||
service.writeRaw = null;
|
||||
if (port.isOpen) {
|
||||
port.close();
|
||||
}
|
||||
port.dispose();
|
||||
config.dispose();
|
||||
service.dispose();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
String _firstNonEmpty(List<String?> values) {
|
||||
for (final value in values) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed != null && trimmed.isNotEmpty) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return 'Serial Device';
|
||||
}
|
||||
|
||||
String _hex(int value) => value.toRadixString(16).padLeft(4, '0');
|
||||
|
||||
SerialTransport createSerialTransportImpl() => _IoSerialTransport();
|
||||
35
lib/services/serial/serial_transport_stub.dart
Normal file
35
lib/services/serial/serial_transport_stub.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'serial_transport.dart';
|
||||
|
||||
class _UnsupportedSerialTransport implements SerialTransport {
|
||||
@override
|
||||
bool get isSupported => false;
|
||||
|
||||
@override
|
||||
bool get canRequestDevice => false;
|
||||
|
||||
@override
|
||||
String get actionLabel => 'Serial unsupported';
|
||||
|
||||
@override
|
||||
String get emptyStateTitle => 'Serial is unavailable on this platform.';
|
||||
|
||||
@override
|
||||
String get unsupportedMessage =>
|
||||
'Serial is supported on Android, macOS, Windows, and compatible web browsers.';
|
||||
|
||||
@override
|
||||
Future<List<SerialDeviceInfo>> listDevices() async => const [];
|
||||
|
||||
@override
|
||||
Future<SerialDeviceInfo?> requestDevice() async => null;
|
||||
|
||||
@override
|
||||
Future<SerialConnection> connect(SerialDeviceInfo device) async {
|
||||
throw UnsupportedError(unsupportedMessage);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
SerialTransport createSerialTransportImpl() => _UnsupportedSerialTransport();
|
||||
209
lib/services/serial/serial_transport_web.dart
Normal file
209
lib/services/serial/serial_transport_web.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'package:webserial/webserial.dart';
|
||||
|
||||
import 'serial_transport.dart';
|
||||
|
||||
class _WebSerialTransport implements SerialTransport {
|
||||
@override
|
||||
bool get isSupported =>
|
||||
web.window.navigator.hasProperty('serial'.toJS).toDart;
|
||||
|
||||
@override
|
||||
bool get canRequestDevice => true;
|
||||
|
||||
@override
|
||||
String get actionLabel => 'Choose serial device';
|
||||
|
||||
@override
|
||||
String get emptyStateTitle =>
|
||||
'No serial devices granted yet.\nChoose a device and approve access in your browser.';
|
||||
|
||||
@override
|
||||
String get unsupportedMessage =>
|
||||
'Web Serial requires a compatible browser such as Chrome or Edge and a secure context (HTTPS or localhost).';
|
||||
|
||||
@override
|
||||
Future<List<SerialDeviceInfo>> listDevices() async {
|
||||
if (!isSupported) {
|
||||
return const [];
|
||||
}
|
||||
final ports = await serial.getPorts().toDart;
|
||||
return ports
|
||||
.toDart
|
||||
.whereType<JSSerialPort>()
|
||||
.map(_deviceInfoForPort)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SerialDeviceInfo?> requestDevice() async {
|
||||
if (!isSupported) {
|
||||
return null;
|
||||
}
|
||||
final port = await requestWebSerialPort(null);
|
||||
if (port == null) {
|
||||
return null;
|
||||
}
|
||||
return _deviceInfoForPort(port);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SerialConnection> connect(SerialDeviceInfo device) async {
|
||||
final port = device.handle as JSSerialPort;
|
||||
final service = MeshCoreSerialService(appName: 'MeshCore SAR');
|
||||
web.ReadableStreamDefaultReader? reader;
|
||||
bool keepReading = false;
|
||||
|
||||
try {
|
||||
await port
|
||||
.open(
|
||||
JSSerialOptions(
|
||||
baudRate: kSerialBaudRate,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: 'none',
|
||||
bufferSize: 1024,
|
||||
flowControl: 'none',
|
||||
),
|
||||
)
|
||||
.toDart;
|
||||
|
||||
await port
|
||||
.setSignals(
|
||||
JSSerialOutputSignals(
|
||||
dataTerminalReady: true,
|
||||
requestToSend: true,
|
||||
),
|
||||
)
|
||||
.toDart;
|
||||
|
||||
service.writeRaw = (data) async {
|
||||
final writable = port.writable;
|
||||
if (writable == null) {
|
||||
throw Exception('Serial port is not writable');
|
||||
}
|
||||
final writer =
|
||||
writable.getWriter() as web.WritableStreamDefaultWriter?;
|
||||
if (writer == null) {
|
||||
throw Exception('Failed to acquire serial writer');
|
||||
}
|
||||
try {
|
||||
await writer.write(data.toJS).toDart;
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
};
|
||||
|
||||
final readable = port.readable;
|
||||
if (readable == null) {
|
||||
throw Exception('Serial port is not readable');
|
||||
}
|
||||
|
||||
reader = readable.getReader() as web.ReadableStreamDefaultReader?;
|
||||
if (reader == null) {
|
||||
throw Exception('Failed to acquire serial reader');
|
||||
}
|
||||
|
||||
keepReading = true;
|
||||
unawaited(_readLoop(reader, service, () => keepReading));
|
||||
|
||||
final connected = await service.markConnected();
|
||||
if (!connected) {
|
||||
throw Exception('Serial session initialization failed');
|
||||
}
|
||||
|
||||
return SerialConnection(
|
||||
service: service,
|
||||
deviceId: device.id,
|
||||
deviceName: device.title,
|
||||
disconnect: () async {
|
||||
keepReading = false;
|
||||
try {
|
||||
await reader?.cancel().toDart;
|
||||
} catch (_) {}
|
||||
try {
|
||||
reader?.releaseLock();
|
||||
} catch (_) {}
|
||||
reader = null;
|
||||
service.writeRaw = null;
|
||||
try {
|
||||
await port.close().toDart;
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
keepReading = false;
|
||||
try {
|
||||
await reader?.cancel().toDart;
|
||||
} catch (_) {}
|
||||
try {
|
||||
reader?.releaseLock();
|
||||
} catch (_) {}
|
||||
service.writeRaw = null;
|
||||
try {
|
||||
await port.close().toDart;
|
||||
} catch (_) {}
|
||||
service.dispose();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _readLoop(
|
||||
web.ReadableStreamDefaultReader reader,
|
||||
MeshCoreSerialService service,
|
||||
bool Function() shouldContinue,
|
||||
) async {
|
||||
while (shouldContinue()) {
|
||||
try {
|
||||
final result = await reader.read().toDart;
|
||||
if (result.done) {
|
||||
break;
|
||||
}
|
||||
final value = result.value;
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
service.feedRawBytes((value as JSUint8Array).toDart);
|
||||
} catch (_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
service.markDisconnected();
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
SerialDeviceInfo _deviceInfoForPort(JSSerialPort port) {
|
||||
final info = port.getInfo();
|
||||
final vendorId = info.usbVendorId;
|
||||
final productId = info.usbProductId;
|
||||
final title = (vendorId != 0 || productId != 0)
|
||||
? 'USB ${_hex(vendorId)}:${_hex(productId)}'
|
||||
: 'Granted serial device';
|
||||
final subtitle = [
|
||||
if (vendorId != 0 || productId != 0)
|
||||
'VID:${_hex(vendorId)} PID:${_hex(productId)}',
|
||||
'Web Serial',
|
||||
].join(' • ');
|
||||
|
||||
return SerialDeviceInfo(
|
||||
id: '$title::$subtitle',
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
handle: port,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
String _hex(int value) => value.toRadixString(16).padLeft(4, '0');
|
||||
|
||||
SerialTransport createSerialTransportImpl() => _WebSerialTransport();
|
||||
100
lib/services/serial/usb_serial_transport.dart
Normal file
100
lib/services/serial/usb_serial_transport.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
20
lib/services/serial/web_serial_transport.dart
Normal file
20
lib/services/serial/web_serial_transport.dart
Normal 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() {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user