mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 15:32:58 +10:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0135d56ddc | |||
| bdb1eb6b42 | |||
| f2ccec2926 | |||
| 31671958d5 | |||
| ea379ce50b | |||
| 50af2e0bc9 | |||
| d5ac84430c | |||
| 190fd3b353 | |||
| a2d1cb2a99 | |||
| 83386a8cde | |||
| acc0fff2dc | |||
| a26055c93f | |||
| 5a70ed48cf | |||
| a42cf77a70 | |||
| 31db565ebf | |||
| 515b9c1f29 | |||
| ea1d728d4f | |||
| 86bde1d178 | |||
| de63733bb9 | |||
| c880c2d107 | |||
| 2a7cc28a3a | |||
| 8a16024642 | |||
| 0f17e2382c | |||
| 6065059241 | |||
| faefef14ff | |||
| ddc87f3a27 | |||
| 2188b49726 | |||
| 1a9b7b0d55 | |||
| 74e29a6c0f | |||
| 972ae809e3 | |||
| deb46553f3 | |||
| 58fc55df13 | |||
| ea2f35ec2e | |||
| e2585c0992 | |||
| 78f1a7b28e | |||
| 0121b5f649 | |||
| ec14870aed | |||
| c0516a475d | |||
| b998186430 | |||
| 16b2c24983 | |||
| c8ff0cc943 | |||
| 64bf307d09 | |||
| 88f8066ed3 | |||
| bf5fadd15e | |||
| 3730b2a6c2 | |||
| 173fdf7168 | |||
| 7465e81996 | |||
| 677b25908a | |||
| fc55fb98ce | |||
| 2bdd9d35cc | |||
| 1f816f7e08 | |||
| bd27c90216 | |||
| 9bcb8b9ca6 | |||
| aaf79c90c9 | |||
| 08edd2696e | |||
| 0f2d18d6fa | |||
| 298951f8bc | |||
| f3db63ceea | |||
| 47044ae14e | |||
| f4dd76a459 | |||
| ab76a52d71 | |||
| 332bb5ef3a | |||
| 81a423d096 | |||
| 700e85b13d | |||
| 9a27953a6e | |||
| abde4a5e46 | |||
| 6e1cb0482f | |||
| c28b38a233 | |||
| 722caf774e | |||
| d269e181c3 | |||
| 35498c1b90 | |||
| bf4f52a4e3 | |||
| 3ca53e967c | |||
| 096e0a4184 | |||
| c7b33f1d1b | |||
| 7288f11c88 | |||
| 2306269384 | |||
| 41ff2353a4 | |||
| b3ad54f296 | |||
| 7cb4c5a334 | |||
| bb8ad70cb9 | |||
| 8fe4129204 | |||
| 2feff809ff | |||
| 51d70ce086 | |||
| b05b62eeee | |||
| 061b715694 | |||
| f38b8b0319 | |||
| 304c389669 | |||
| 7acfe47fd7 | |||
| f4b18d97a1 | |||
| d2b693e5ce | |||
| ba2763a3f6 | |||
| 0c4910e149 | |||
| 4bf2519559 | |||
| 19edeab9d5 | |||
| 0e81d75cce | |||
| 9437846127 | |||
| 50ab46ed40 | |||
| dc193be8ed | |||
| 8a804a3706 | |||
| 1dc90d0e89 | |||
| 5f2312e086 | |||
| 4239fb11ed | |||
| 5fae2e5f73 | |||
| 947fafbbb7 | |||
| a9fbf8c7f5 | |||
| 72f0aa7208 | |||
| f87d4896ab | |||
| 9250dfec31 | |||
| 37db955ab2 | |||
| 739d9475c0 | |||
| b526175be4 | |||
| 73081862ad | |||
| fac062a100 | |||
| ef6bd78632 | |||
| 01c8390989 | |||
| c05f813d65 | |||
| c52b19b09f | |||
| 6a666839b6 | |||
| bc77f7e287 | |||
| 9332d8126f | |||
| 9ce00556ec | |||
| 4995f5f380 | |||
| 4e6e7b6061 | |||
| aa350aa4ae | |||
| dfd38b19e9 | |||
| 4afab3f629 | |||
| 67816130ac | |||
| d573f0c312 | |||
| 5b699cd624 | |||
| a4d3d248a5 | |||
| 2a3f2b3a24 | |||
| 675083fa01 | |||
| 5fc4b80b16 | |||
| 84a32c1e67 | |||
| 607583060a | |||
| 71cf556b61 | |||
| c26174ad18 | |||
| 04021a39a1 | |||
| fe23e9f7a0 | |||
| d7ec1876af |
@@ -30,6 +30,7 @@ migrate_working_dir/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
pubspec.lock
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
@@ -65,6 +66,7 @@ secrets.dart
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
|
||||
# Android
|
||||
.gradle/
|
||||
**/android/.gradle/
|
||||
**/android/captures/
|
||||
**/android/local.properties
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
4.0.0
|
||||
@@ -78,6 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- ✅ **Android**: Full support (API 21+)
|
||||
- ✅ **iOS**: Full support (iOS 12+)
|
||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
||||
- 🚧 **Web**: Under construction (Chrome)
|
||||
|
||||
### Dependencies
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ android {
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -83,5 +83,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>
|
||||
|
After Width: | Height: | Size: 253 B |
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
description = "MeshCore Flutter Application";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Flutter and Dart
|
||||
flutter
|
||||
dart
|
||||
|
||||
# Java (required for Android development)
|
||||
jdk17
|
||||
|
||||
# Android development tools
|
||||
android-tools
|
||||
gradle
|
||||
|
||||
# For the shell hook to set up the environment for Flutter development
|
||||
gtk3
|
||||
glib
|
||||
sysprof
|
||||
libclang
|
||||
cmake
|
||||
ninja
|
||||
pkg-config
|
||||
libdatrie
|
||||
|
||||
# Additional tools for installing Android SDK if not present
|
||||
curl
|
||||
unzip
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "MeshCore Flutter Development Environment"
|
||||
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
|
||||
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
|
||||
|
||||
# Setup Android SDK in home directory (standard location)
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
|
||||
|
||||
echo "Android SDK: $ANDROID_HOME"
|
||||
echo ""
|
||||
|
||||
# Check if Android SDK exists and offer to download if not
|
||||
if [ ! -d "$ANDROID_HOME" ]; then
|
||||
echo "WARNING: Android SDK not found at $ANDROID_HOME"
|
||||
echo ""
|
||||
echo "To download and set up the Android SDK, run this command:"
|
||||
echo ""
|
||||
cat << 'EOF'
|
||||
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
|
||||
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
|
||||
unzip -q cmdline-tools.zip && \
|
||||
mkdir -p cmdline-tools/latest && \
|
||||
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
|
||||
rm cmdline-tools.zip && \
|
||||
cd cmdline-tools/latest/bin && \
|
||||
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
|
||||
echo "Android SDK setup complete!"
|
||||
EOF
|
||||
echo ""
|
||||
echo "Then run 'flutter doctor' again to verify."
|
||||
echo ""
|
||||
else
|
||||
echo "Android SDK found at $ANDROID_HOME"
|
||||
fi
|
||||
|
||||
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
@@ -30,6 +29,7 @@ import '../storage/contact_store.dart';
|
||||
import '../storage/message_store.dart';
|
||||
import '../storage/unread_store.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import 'meshcore_protocol.dart';
|
||||
|
||||
class MeshCoreUuids {
|
||||
@@ -38,6 +38,42 @@ class MeshCoreUuids {
|
||||
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
}
|
||||
|
||||
class DirectRepeater {
|
||||
static const int maxAgeMinutes = 30; // Max age for direct repeater info
|
||||
final int pubkeyFirstByte;
|
||||
double snr;
|
||||
DateTime lastUpdated;
|
||||
|
||||
DirectRepeater({
|
||||
required this.pubkeyFirstByte,
|
||||
required this.snr,
|
||||
DateTime? lastUpdated,
|
||||
}) : lastUpdated = lastUpdated ?? DateTime.now();
|
||||
|
||||
void update(double newSNR) {
|
||||
snr = newSNR;
|
||||
lastUpdated = DateTime.now();
|
||||
}
|
||||
|
||||
int get ranking {
|
||||
if (isStale()) {
|
||||
return -1; // Stale repeaters get lowest rank
|
||||
}
|
||||
// Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties.
|
||||
final ageMs =
|
||||
DateTime.now().millisecondsSinceEpoch -
|
||||
lastUpdated.millisecondsSinceEpoch;
|
||||
final maxAgeMs = maxAgeMinutes * 60 * 1000;
|
||||
final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs);
|
||||
return ((snr - 31.75) * 1000).round() + recencyScore;
|
||||
}
|
||||
|
||||
bool isStale() {
|
||||
return DateTime.now().difference(lastUpdated) >
|
||||
const Duration(minutes: maxAgeMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
enum MeshCoreConnectionState {
|
||||
disconnected,
|
||||
scanning,
|
||||
@@ -46,6 +82,18 @@ enum MeshCoreConnectionState {
|
||||
disconnecting,
|
||||
}
|
||||
|
||||
class RepeaterBatterySnapshot {
|
||||
final int millivolts;
|
||||
final DateTime updatedAt;
|
||||
final String source;
|
||||
|
||||
const RepeaterBatterySnapshot({
|
||||
required this.millivolts,
|
||||
required this.updatedAt,
|
||||
required this.source,
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreConnector extends ChangeNotifier {
|
||||
// Message windowing to limit memory usage
|
||||
static const int _messageWindowSize = 200;
|
||||
@@ -66,6 +114,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final List<Channel> _channels = [];
|
||||
final Map<String, List<Message>> _conversations = {};
|
||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||
final List<String> _pendingChannelSentQueue = [];
|
||||
final List<_PendingCommandAck> _pendingGenericAckQueue = [];
|
||||
static const String _reactionSendQueuePrefix = '__reaction_send__';
|
||||
int _reactionSendQueueSequence = 0;
|
||||
final Set<String> _loadedConversationKeys = {};
|
||||
final Map<int, Set<String>> _processedChannelReactions =
|
||||
{}; // channelIndex -> Set of "targetHash_emoji"
|
||||
@@ -91,9 +143,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? _currentBwHz;
|
||||
int? _currentSf;
|
||||
int? _currentCr;
|
||||
bool? _clientRepeat;
|
||||
int? _firmwareVerCode;
|
||||
int? _batteryMillivolts;
|
||||
double? _selfLatitude;
|
||||
double? _selfLongitude;
|
||||
final List<DirectRepeater> _directRepeaters = List.empty(growable: true);
|
||||
bool _isLoadingContacts = false;
|
||||
bool _isLoadingChannels = false;
|
||||
bool _hasLoadedChannels = false;
|
||||
@@ -149,6 +204,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final Map<String, bool> _contactSmazEnabled = {};
|
||||
final Set<String> _knownContactKeys = {};
|
||||
final Map<String, int> _contactUnreadCount = {};
|
||||
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
|
||||
bool _unreadStateLoaded = false;
|
||||
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
|
||||
String? _activeContactKey;
|
||||
@@ -195,12 +251,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String? get selfName => _selfName;
|
||||
double? get selfLatitude => _selfLatitude;
|
||||
double? get selfLongitude => _selfLongitude;
|
||||
List<DirectRepeater> get directRepeaters => _directRepeaters;
|
||||
int? get currentTxPower => _currentTxPower;
|
||||
int? get maxTxPower => _maxTxPower;
|
||||
int? get currentFreqHz => _currentFreqHz;
|
||||
int? get currentBwHz => _currentBwHz;
|
||||
int? get currentSf => _currentSf;
|
||||
int? get currentCr => _currentCr;
|
||||
bool? get clientRepeat => _clientRepeat;
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int get maxContacts => _maxContacts;
|
||||
@@ -213,10 +272,32 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
: 0;
|
||||
int? get batteryPercent => _batteryMillivolts == null
|
||||
? null
|
||||
: _estimateBatteryPercent(
|
||||
: estimateBatteryPercentFromMillivolts(
|
||||
_batteryMillivolts!,
|
||||
_batteryChemistryForDevice(),
|
||||
);
|
||||
RepeaterBatterySnapshot? getRepeaterBatterySnapshot(String contactKeyHex) =>
|
||||
_repeaterBatterySnapshots[contactKeyHex];
|
||||
int? getRepeaterBatteryMillivolts(String contactKeyHex) =>
|
||||
_repeaterBatterySnapshots[contactKeyHex]?.millivolts;
|
||||
|
||||
void updateRepeaterBatterySnapshot(
|
||||
String contactKeyHex,
|
||||
int millivolts, {
|
||||
String source = 'unknown',
|
||||
}) {
|
||||
if (contactKeyHex.isEmpty || millivolts <= 0) return;
|
||||
final previous = _repeaterBatterySnapshots[contactKeyHex];
|
||||
final snapshot = RepeaterBatterySnapshot(
|
||||
millivolts: millivolts,
|
||||
updatedAt: DateTime.now(),
|
||||
source: source,
|
||||
);
|
||||
_repeaterBatterySnapshots[contactKeyHex] = snapshot;
|
||||
if (previous?.millivolts != millivolts) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String _batteryChemistryForDevice() {
|
||||
final deviceId = _device?.remoteId.toString();
|
||||
@@ -224,27 +305,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return _appSettingsService!.batteryChemistryForDevice(deviceId);
|
||||
}
|
||||
|
||||
int _estimateBatteryPercent(int millivolts, String chemistry) {
|
||||
final range = _batteryVoltageRange(chemistry);
|
||||
final minMv = range.$1;
|
||||
final maxMv = range.$2;
|
||||
if (millivolts <= minMv) return 0;
|
||||
if (millivolts >= maxMv) return 100;
|
||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
||||
}
|
||||
|
||||
(int, int) _batteryVoltageRange(String chemistry) {
|
||||
switch (chemistry) {
|
||||
case 'lifepo4':
|
||||
return (2600, 3650);
|
||||
case 'lipo':
|
||||
return (3000, 4200);
|
||||
case 'nmc':
|
||||
default:
|
||||
return (3000, 4200);
|
||||
}
|
||||
}
|
||||
|
||||
List<Message> getMessages(Contact contact) {
|
||||
return _conversations[contact.publicKeyHex] ?? [];
|
||||
}
|
||||
@@ -609,6 +669,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
publicKey: contact.publicKey,
|
||||
name: contact.name,
|
||||
type: contact.type,
|
||||
flags: contact.flags,
|
||||
pathLength: selection.hopCount >= 0
|
||||
? selection.hopCount
|
||||
: contact.pathLength,
|
||||
@@ -658,7 +719,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_scanResults.clear();
|
||||
for (var result in results) {
|
||||
if (result.device.platformName.startsWith("MeshCore-") ||
|
||||
result.advertisementData.advName.startsWith("MeshCore-")) {
|
||||
result.advertisementData.advName.startsWith("MeshCore-") ||
|
||||
result.advertisementData.advName.startsWith("Whisper-")) {
|
||||
_scanResults.add(result);
|
||||
}
|
||||
}
|
||||
@@ -775,9 +837,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
|
||||
// Enable wake lock to prevent BLE disconnection when screen turns off
|
||||
await WakelockPlus.enable();
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
@@ -886,9 +945,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_setState(MeshCoreConnectionState.disconnecting);
|
||||
_stopBatteryPolling();
|
||||
|
||||
// Disable wake lock when disconnecting
|
||||
await WakelockPlus.disable();
|
||||
|
||||
await _notifySubscription?.cancel();
|
||||
_notifySubscription = null;
|
||||
|
||||
@@ -922,7 +978,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfName = null;
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_clientRepeat = null;
|
||||
_firmwareVerCode = null;
|
||||
_batteryMillivolts = null;
|
||||
_repeaterBatterySnapshots.clear();
|
||||
_batteryRequested = false;
|
||||
_awaitingSelfInfo = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
@@ -934,6 +993,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_hasLoadedChannels = false;
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
if (!manual) {
|
||||
@@ -941,7 +1003,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendFrame(Uint8List data) async {
|
||||
Future<void> sendFrame(
|
||||
Uint8List data, {
|
||||
String? channelSendQueueId,
|
||||
bool expectsGenericAck = false,
|
||||
}) async {
|
||||
if (!isConnected || _rxCharacteristic == null) {
|
||||
throw Exception("Not connected to a MeshCore device");
|
||||
}
|
||||
@@ -960,6 +1026,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
data.toList(),
|
||||
withoutResponse: canWriteWithoutResponse,
|
||||
);
|
||||
_trackPendingGenericAck(
|
||||
data,
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
expectsGenericAck: expectsGenericAck,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> requestBatteryStatus({bool force = false}) async {
|
||||
@@ -1115,11 +1186,78 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
customPath,
|
||||
pathLen,
|
||||
type: contact.type,
|
||||
flags: contact.flags,
|
||||
name: contact.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
|
||||
if (!isConnected) return;
|
||||
final latestContact =
|
||||
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
|
||||
final updatedFlags = isFavorite
|
||||
? (latestContact.flags | contactFlagFavorite)
|
||||
: (latestContact.flags & ~contactFlagFavorite);
|
||||
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
latestContact.publicKey,
|
||||
latestContact.path,
|
||||
latestContact.pathLength,
|
||||
type: latestContact.type,
|
||||
flags: updatedFlags,
|
||||
name: latestContact.name,
|
||||
),
|
||||
);
|
||||
|
||||
final index = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
if (index >= 0) {
|
||||
_contacts[index] = _contacts[index].copyWith(
|
||||
type: latestContact.type,
|
||||
name: latestContact.name,
|
||||
pathLength: latestContact.pathLength,
|
||||
path: latestContact.path,
|
||||
flags: updatedFlags,
|
||||
);
|
||||
notifyListeners();
|
||||
unawaited(_persistContacts());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Contact?> _fetchContactSnapshotFromDevice(
|
||||
Uint8List pubKey, {
|
||||
Duration timeout = const Duration(seconds: 3),
|
||||
}) async {
|
||||
if (!isConnected) return null;
|
||||
final expectedKeyHex = pubKeyToHex(pubKey);
|
||||
final completer = Completer<Contact?>();
|
||||
|
||||
void finish(Contact? result) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
final subscription = receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty || frame[0] != respCodeContact) return;
|
||||
final parsed = Contact.fromFrame(frame);
|
||||
if (parsed == null || parsed.publicKeyHex != expectedKeyHex) return;
|
||||
finish(parsed);
|
||||
});
|
||||
|
||||
final timer = Timer(timeout, () => finish(null));
|
||||
try {
|
||||
await getContactByKey(pubKey);
|
||||
return await completer.future;
|
||||
} finally {
|
||||
timer.cancel();
|
||||
await subscription.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set path override for a contact (persists across contact refreshes)
|
||||
/// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path
|
||||
Future<void> setPathOverride(
|
||||
@@ -1315,7 +1453,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
// Send the reaction to the device (don't add as a visible message)
|
||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, text));
|
||||
final reactionQueueId = _nextReactionSendQueueId();
|
||||
_pendingChannelSentQueue.add(reactionQueueId);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, text),
|
||||
channelSendQueueId: reactionQueueId,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1325,6 +1469,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
channel.index,
|
||||
);
|
||||
_addChannelMessage(channel.index, message);
|
||||
_pendingChannelSentQueue.add(message.messageId);
|
||||
notifyListeners();
|
||||
|
||||
final trimmed = text.trim();
|
||||
@@ -1334,7 +1479,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
channelSendQueueId: message.messageId,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeContact(Contact contact) async {
|
||||
@@ -1681,6 +1830,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
debugPrint('RX frame: code=$code len=${frame.length}');
|
||||
|
||||
switch (code) {
|
||||
case respCodeOk:
|
||||
_handleOk();
|
||||
break;
|
||||
case respCodeDeviceInfo:
|
||||
_handleDeviceInfo(frame);
|
||||
break;
|
||||
@@ -1696,6 +1848,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isLoadingContacts = true;
|
||||
notifyListeners();
|
||||
break;
|
||||
case pushCodeNewAdvert:
|
||||
debugPrint('Got New CONTACT');
|
||||
// It's the same format as respCodeContact, so we can reuse the handler
|
||||
_handleContact(frame);
|
||||
break;
|
||||
case respCodeContact:
|
||||
debugPrint('Got CONTACT');
|
||||
_handleContact(frame);
|
||||
@@ -1740,6 +1897,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case pushCodeStatusResponse:
|
||||
break;
|
||||
case pushCodeLogRxData:
|
||||
_handleRxData(frame);
|
||||
_handleLogRxData(frame);
|
||||
break;
|
||||
case respCodeChannelInfo:
|
||||
@@ -1753,11 +1911,35 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
break;
|
||||
case respCodeCustomVars:
|
||||
_handleCustomVars(frame);
|
||||
break;
|
||||
// RESP_CODE_ERR is a defined firmware response (code 1), not an unknown frame.
|
||||
case respCodeErr:
|
||||
_handleErrorFrame(frame);
|
||||
break;
|
||||
default:
|
||||
debugPrint('Unknown frame code: $code');
|
||||
}
|
||||
}
|
||||
|
||||
void _handleErrorFrame(Uint8List frame) {
|
||||
final errCode = frame.length > 1 ? frame[1] : -1;
|
||||
_appDebugLogService?.warn(
|
||||
'Firmware responded with error code: $errCode',
|
||||
tag: 'Protocol',
|
||||
);
|
||||
|
||||
if (_pendingGenericAckQueue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final failedAck = _pendingGenericAckQueue.removeAt(0);
|
||||
if (failedAck.commandCode != cmdSendChannelTxtMsg ||
|
||||
failedAck.channelSendQueueId == null) {
|
||||
return;
|
||||
}
|
||||
_pendingChannelSentQueue.remove(failedAck.channelSendQueueId);
|
||||
}
|
||||
|
||||
void _handlePathUpdated(Uint8List frame) {
|
||||
// Frame format: [0]=code, [1-32]=pub_key
|
||||
if (frame.length >= 33 && _pathHistoryService != null) {
|
||||
@@ -1826,6 +2008,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
void _handleDeviceInfo(Uint8List frame) {
|
||||
if (frame.length < 4) return;
|
||||
_firmwareVerCode = frame[1];
|
||||
|
||||
// Parse client_repeat from firmware v9+ (byte 80)
|
||||
if (frame.length >= 81) {
|
||||
_clientRepeat = frame[80] != 0;
|
||||
}
|
||||
|
||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||
final reportedContacts = frame[2];
|
||||
final reportedChannels = frame[3];
|
||||
@@ -1846,8 +2035,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleNoMoreMessages() {
|
||||
@@ -2008,6 +2197,80 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleContactAdvert(Contact contact) {
|
||||
if (listEquals(contact.publicKey, _selfPublicKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contact.type == advTypeRepeater) {
|
||||
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
);
|
||||
}
|
||||
// Check if this is a new contact
|
||||
final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex);
|
||||
final existingIndex = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
final existing = _contacts[existingIndex];
|
||||
final mergedLastMessageAt =
|
||||
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||
? existing.lastMessageAt
|
||||
: contact.lastMessageAt;
|
||||
|
||||
appLogger.info(
|
||||
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||
_contacts[existingIndex] = contact.copyWith(
|
||||
lastMessageAt: mergedLastMessageAt,
|
||||
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||
pathOverrideBytes: existing.pathOverrideBytes,
|
||||
);
|
||||
|
||||
appLogger.info(
|
||||
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
} else {
|
||||
_contacts.add(contact);
|
||||
appLogger.info(
|
||||
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
_knownContactKeys.add(contact.publicKeyHex);
|
||||
_loadMessagesForContact(contact.publicKeyHex);
|
||||
|
||||
// Add path to history if we have a valid path
|
||||
if (_pathHistoryService != null && contact.pathLength >= 0) {
|
||||
_pathHistoryService!.handlePathUpdated(contact);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// Show notification for new contact (advertisement)
|
||||
if (isNewContact && _appSettingsService != null) {
|
||||
final settings = _appSettingsService!.settings;
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isLoadingContacts) {
|
||||
unawaited(_persistContacts());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistContacts() async {
|
||||
await _contactStore.saveContacts(_contacts);
|
||||
}
|
||||
@@ -2334,6 +2597,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final label = channelName ?? _channelDisplayName(channelIndex);
|
||||
if (_appSettingsService!.isChannelMuted(label)) return;
|
||||
|
||||
_notificationService.showChannelMessageNotification(
|
||||
channelName: label,
|
||||
message: message.text,
|
||||
@@ -2457,8 +2722,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_retryService != null) {
|
||||
_retryService!.updateMessageFromSent(ackHash, timeoutMs);
|
||||
final retryService = _retryService;
|
||||
if (retryService != null &&
|
||||
retryService.updateMessageFromSent(
|
||||
ackHash,
|
||||
timeoutMs,
|
||||
allowQueueFallback: false,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_markNextPendingChannelMessageSent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryService != null) {
|
||||
retryService.updateMessageFromSent(ackHash, timeoutMs);
|
||||
}
|
||||
} else {
|
||||
// Fallback to old behavior
|
||||
@@ -2475,6 +2754,64 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _markNextPendingChannelMessageSent() {
|
||||
while (_pendingChannelSentQueue.isNotEmpty) {
|
||||
final queuedMessageId = _pendingChannelSentQueue.removeAt(0);
|
||||
if (_isReactionSendQueueId(queuedMessageId)) {
|
||||
return true;
|
||||
}
|
||||
if (_markPendingChannelMessageSentById(queuedMessageId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _markPendingChannelMessageSentById(String messageId) {
|
||||
for (final entry in _channelMessages.entries) {
|
||||
final channelMessages = entry.value;
|
||||
for (int i = channelMessages.length - 1; i >= 0; i--) {
|
||||
final message = channelMessages[i];
|
||||
if (message.messageId != messageId) {
|
||||
continue;
|
||||
}
|
||||
if (!message.isOutgoing ||
|
||||
message.status != ChannelMessageStatus.pending) {
|
||||
return false;
|
||||
}
|
||||
channelMessages[i] = message.copyWith(
|
||||
status: ChannelMessageStatus.sent,
|
||||
);
|
||||
_pendingChannelSentQueue.remove(messageId);
|
||||
unawaited(
|
||||
_channelMessageStore.saveChannelMessages(entry.key, channelMessages),
|
||||
);
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleOk() {
|
||||
if (_pendingGenericAckQueue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingAck = _pendingGenericAckQueue.removeAt(0);
|
||||
if (pendingAck.commandCode != cmdSendChannelTxtMsg ||
|
||||
pendingAck.channelSendQueueId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final queueId = pendingAck.channelSendQueueId!;
|
||||
_pendingChannelSentQueue.remove(queueId);
|
||||
if (_isReactionSendQueueId(queueId)) {
|
||||
return;
|
||||
}
|
||||
_markPendingChannelMessageSentById(queueId);
|
||||
}
|
||||
|
||||
void _handleSendConfirmed(Uint8List frame) {
|
||||
// Frame format from C++:
|
||||
// [0] = PUSH_CODE_SEND_CONFIRMED
|
||||
@@ -3053,18 +3390,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
mergedPathBytes.length,
|
||||
);
|
||||
final newRepeatCount = existing.repeatCount + 1;
|
||||
final promotedFromPending =
|
||||
newRepeatCount == 1 &&
|
||||
existing.status == ChannelMessageStatus.pending;
|
||||
messages[existingIndex] = existing.copyWith(
|
||||
repeatCount: newRepeatCount,
|
||||
pathLength: mergedPathLength,
|
||||
pathBytes: mergedPathBytes,
|
||||
pathVariants: mergedPathVariants,
|
||||
// Mark as sent when first repeat is heard
|
||||
status:
|
||||
newRepeatCount == 1 &&
|
||||
existing.status == ChannelMessageStatus.pending
|
||||
status: promotedFromPending
|
||||
? ChannelMessageStatus.sent
|
||||
: existing.status,
|
||||
);
|
||||
if (promotedFromPending) {
|
||||
_pendingChannelSentQueue.remove(existing.messageId);
|
||||
}
|
||||
} else {
|
||||
messages.add(processedMessage);
|
||||
}
|
||||
@@ -3214,8 +3555,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleDisconnection() {
|
||||
// Disable wake lock when connection is lost
|
||||
WakelockPlus.disable();
|
||||
_stopBatteryPolling();
|
||||
|
||||
for (final entry in _pendingRepeaterAcks.values) {
|
||||
@@ -3239,11 +3578,37 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _trackPendingGenericAck(
|
||||
Uint8List data, {
|
||||
String? channelSendQueueId,
|
||||
required bool expectsGenericAck,
|
||||
}) {
|
||||
if (!expectsGenericAck || data.isEmpty) return;
|
||||
_pendingGenericAckQueue.add(
|
||||
_PendingCommandAck(
|
||||
commandCode: data[0],
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _nextReactionSendQueueId() {
|
||||
_reactionSendQueueSequence++;
|
||||
return '$_reactionSendQueuePrefix$_reactionSendQueueSequence';
|
||||
}
|
||||
|
||||
bool _isReactionSendQueueId(String queueId) {
|
||||
return queueId.startsWith(_reactionSendQueuePrefix);
|
||||
}
|
||||
|
||||
Map<String, String> _parseKeyValueString(String input) {
|
||||
final result = <String, String>{};
|
||||
|
||||
@@ -3269,7 +3634,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
void _handleCustomVars(Uint8List frame) {
|
||||
final buf = BufferReader(frame.sublist(1));
|
||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||
try {
|
||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
|
||||
}
|
||||
}
|
||||
|
||||
void _setState(MeshCoreConnectionState newState) {
|
||||
@@ -3293,6 +3662,191 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleRxData(Uint8List frame) {
|
||||
final packet = BufferReader(frame);
|
||||
double snr = 0.0;
|
||||
int routeType = 0;
|
||||
int payloadType = 0;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
Uint8List payload = Uint8List(0);
|
||||
try {
|
||||
packet.skipBytes(1); // Skip frame type byte
|
||||
snr = packet.readInt8() / 4.0;
|
||||
packet.skipBytes(1); // Skip RSSI byte
|
||||
//final rssi = packet.readByte();
|
||||
final header = packet.readByte();
|
||||
routeType = header & 0x03;
|
||||
payloadType = (header >> 2) & 0x0F;
|
||||
//final payloadVer = (header >> 6) & 0x03;
|
||||
final pathLen = packet.readByte();
|
||||
pathBytes = packet.readBytes(pathLen);
|
||||
payload = packet.readBytes(packet.remaining);
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (payloadType) {
|
||||
case payloadTypeADVERT:
|
||||
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePayloadAdvertReceived(
|
||||
Uint8List frame,
|
||||
Uint8List path,
|
||||
int routeType,
|
||||
double snr,
|
||||
) {
|
||||
final advert = BufferReader(frame);
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
String name = '';
|
||||
String contactKeyHex = '';
|
||||
Uint8List publicKey = Uint8List(0);
|
||||
int type = 0;
|
||||
int timestamp = 0;
|
||||
bool hasLocation = false;
|
||||
bool hasName = false;
|
||||
try {
|
||||
publicKey = advert.readBytes(32);
|
||||
contactKeyHex = publicKey
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
|
||||
timestamp = advert.readInt32LE();
|
||||
//TODO add signature verification
|
||||
advert.skipBytes(64); // Skip signature for now
|
||||
final flags = advert.readByte();
|
||||
type = flags & 0x0F;
|
||||
hasLocation = (flags & 0x10) != 0;
|
||||
// For future use:
|
||||
//final hasFeature1 = (flags & 0x20) != 0;
|
||||
//final hasFeature2 = (flags & 0x40) != 0;
|
||||
hasName = (flags & 0x80) != 0;
|
||||
if (hasLocation && advert.remaining >= 8) {
|
||||
latitude = advert.readInt32LE() / 1e6;
|
||||
longitude = advert.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && advert.remaining > 0) {
|
||||
name = advert.readString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
if (listEquals(publicKey, _selfPublicKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new contact
|
||||
final isNewContact = !_knownContactKeys.contains(contactKeyHex);
|
||||
|
||||
if (isNewContact) {
|
||||
final newContact = Contact(
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
type: type,
|
||||
pathLength: path.length,
|
||||
path: Uint8List.fromList(
|
||||
path.reversed.toList(),
|
||||
), // Store path in reverse for easier use in outgoing messages
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
);
|
||||
_handleContactAdvert(newContact);
|
||||
_updateDirectRepeater(newContact, snr, path);
|
||||
return;
|
||||
}
|
||||
|
||||
final existingIndex = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contactKeyHex,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
final existing = _contacts[existingIndex];
|
||||
final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now())
|
||||
? DateTime.now()
|
||||
: existing.lastMessageAt;
|
||||
|
||||
appLogger.info(
|
||||
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||
_contacts[existingIndex] = existing.copyWith(
|
||||
latitude: hasLocation ? latitude : existing.latitude,
|
||||
longitude: hasLocation ? longitude : existing.longitude,
|
||||
name: hasName ? name : existing.name,
|
||||
path: Uint8List.fromList(path.reversed.toList()),
|
||||
pathLength: path.length,
|
||||
lastMessageAt: mergedLastMessageAt,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||
pathOverrideBytes: existing.pathOverrideBytes,
|
||||
);
|
||||
|
||||
// Add path to history if we have a valid path
|
||||
if (_pathHistoryService != null &&
|
||||
_contacts[existingIndex].pathLength >= 0) {
|
||||
_pathHistoryService!.handlePathUpdated(_contacts[existingIndex]);
|
||||
}
|
||||
|
||||
_updateDirectRepeater(_contacts[existingIndex], snr, path);
|
||||
|
||||
appLogger.info(
|
||||
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
|
||||
final pubkeyFirstByte = path.isNotEmpty
|
||||
? path.last
|
||||
: contact.publicKey.first;
|
||||
|
||||
_directRepeaters.removeWhere((r) => r.isStale());
|
||||
|
||||
//We can use adverts from chat and sensor nodes, but only if the advert has a path to get the last hop.
|
||||
if ((contact.type == advTypeChat || contact.type == advTypeSensor) &&
|
||||
path.isEmpty) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final isTracked = _directRepeaters.where(
|
||||
(r) => r.pubkeyFirstByte == pubkeyFirstByte,
|
||||
);
|
||||
|
||||
final sortedRepeaters = List<DirectRepeater>.from(_directRepeaters)
|
||||
..sort((a, b) => b.snr.compareTo(a.snr));
|
||||
final weakestRepeater = sortedRepeaters.isNotEmpty
|
||||
? sortedRepeaters.last
|
||||
: null;
|
||||
|
||||
if (_directRepeaters.length >= 5 &&
|
||||
weakestRepeater != null &&
|
||||
isTracked.isEmpty) {
|
||||
_directRepeaters.remove(weakestRepeater);
|
||||
}
|
||||
|
||||
if (isTracked.isNotEmpty) {
|
||||
final repeater = isTracked.first;
|
||||
repeater.update(snr);
|
||||
} else if (_directRepeaters.length < 5) {
|
||||
_directRepeaters.add(
|
||||
DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
const int _phRouteMask = 0x03;
|
||||
@@ -3350,3 +3904,10 @@ class _RepeaterAckContext {
|
||||
required this.messageBytes,
|
||||
});
|
||||
}
|
||||
|
||||
class _PendingCommandAck {
|
||||
final int commandCode;
|
||||
final String? channelSendQueueId;
|
||||
|
||||
_PendingCommandAck({required this.commandCode, this.channelSendQueueId});
|
||||
}
|
||||
|
||||
@@ -13,12 +13,22 @@ class BufferReader {
|
||||
int readByte() => readBytes(1)[0];
|
||||
|
||||
Uint8List readBytes(int count) {
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
);
|
||||
}
|
||||
final data = _buffer.sublist(_pointer, _pointer + count);
|
||||
_pointer += count;
|
||||
return data;
|
||||
}
|
||||
|
||||
void skipBytes(int count) {
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
);
|
||||
}
|
||||
_pointer += count;
|
||||
}
|
||||
|
||||
@@ -151,6 +161,7 @@ const int cmdGetContactByKey = 30;
|
||||
const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
@@ -166,7 +177,7 @@ const int reqTypeGetStatus = 0x01;
|
||||
const int reqTypeKeepAlive = 0x02;
|
||||
const int reqTypeGetTelemetry = 0x03;
|
||||
const int reqTypeGetAccessList = 0x05;
|
||||
const int reqTypeGetNeighbours = 0x06;
|
||||
const int reqTypeGetNeighbors = 0x06;
|
||||
|
||||
// Repeater response codes
|
||||
const int respServerLoginOk = 0;
|
||||
@@ -212,6 +223,30 @@ const int advTypeRepeater = 2;
|
||||
const int advTypeRoom = 3;
|
||||
const int advTypeSensor = 4;
|
||||
|
||||
// Payload Types
|
||||
const int payloadTypeREQ =
|
||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeRESPONSE =
|
||||
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeTXTMSG =
|
||||
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||
const int payloadTypeACK = 0x03; // a simple ack
|
||||
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||
const int payloadTypeGRPTXT =
|
||||
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||
const int payloadTypeGRPDATA =
|
||||
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeANONREQ =
|
||||
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||
const int payloadTypePATH =
|
||||
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||
//...
|
||||
const int payloadTypeRawCustom =
|
||||
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int maxPathSize = 64;
|
||||
@@ -255,6 +290,7 @@ int _minPositive(int a, int b) {
|
||||
const int contactPubKeyOffset = 1;
|
||||
const int contactTypeOffset = 33;
|
||||
const int contactFlagsOffset = 34;
|
||||
const int contactFlagFavorite = 0x01;
|
||||
const int contactPathLenOffset = 35;
|
||||
const int contactPathOffset = 36;
|
||||
const int contactNameOffset = 100;
|
||||
@@ -550,18 +586,29 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
|
||||
}
|
||||
|
||||
// Build CMD_SET_RADIO_PARAMS frame
|
||||
// Format: [cmd][freq x4][bw x4][sf][cr]
|
||||
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
|
||||
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
|
||||
// freq: frequency in Hz (300000-2500000)
|
||||
// bw: bandwidth in Hz (7000-500000)
|
||||
// sf: spreading factor (5-12)
|
||||
// cr: coding rate (5-8)
|
||||
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
|
||||
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
|
||||
Uint8List buildSetRadioParamsFrame(
|
||||
int freqHz,
|
||||
int bwHz,
|
||||
int sf,
|
||||
int cr, {
|
||||
bool? clientRepeat,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetRadioParams);
|
||||
writer.writeUInt32LE(freqHz);
|
||||
writer.writeUInt32LE(bwHz);
|
||||
writer.writeByte(sf);
|
||||
writer.writeByte(cr);
|
||||
if (clientRepeat != null) {
|
||||
writer.writeByte(clientRepeat ? 1 : 0);
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -777,3 +824,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||
writer.writeBytes(pubKey);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_SET_OTHER_PARAMS frame
|
||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||
Uint8List buildSetOtherParamsFrame(
|
||||
bool allowAutoAddContacts,
|
||||
int allowTelemetryFlags,
|
||||
int advertLocationPolicy,
|
||||
int multiAcks,
|
||||
) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetOtherParams);
|
||||
writer.writeByte(
|
||||
allowAutoAddContacts ? 0x00 : 0x01,
|
||||
); // Allow Auto Add Contacts
|
||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
+175
-161
@@ -1,4 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class CayenneLpp {
|
||||
@@ -84,180 +86,192 @@ class CayenneLpp {
|
||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||
final buffer = BufferReader(bytes);
|
||||
final telemetry = <Map<String, dynamic>>[];
|
||||
try {
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppLuminosity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPresence:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppTemperature:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppCurrent:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppConcentration:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPower:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'latitude': buffer.readInt24BE() / 10000,
|
||||
'longitude': buffer.readInt24BE() / 10000,
|
||||
'altitude': buffer.readInt24BE() / 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return telemetry;
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppLuminosity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPresence:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppTemperature:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppCurrent:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppConcentration:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPower:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'latitude': buffer.readInt24BE() / 10000,
|
||||
'longitude': buffer.readInt24BE() / 10000,
|
||||
'altitude': buffer.readInt24BE() / 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return telemetry;
|
||||
}
|
||||
}
|
||||
return telemetry;
|
||||
} catch (e) {
|
||||
// Handle parsing errors, possibly due to malformed data
|
||||
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||
// Return any telemetry parsed so far to preserve partial data
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
||||
final buffer = BufferReader(bytes);
|
||||
final Map<int, Map<String, dynamic>> channels = {};
|
||||
try {
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
// Optional: stop on padding (00 00)
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Optional: stop on padding (00 00)
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
final channelData = channels.putIfAbsent(
|
||||
channel,
|
||||
() => {'channel': channel, 'values': <String, dynamic>{}},
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||
break;
|
||||
case lppLuminosity:
|
||||
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppPresence:
|
||||
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
||||
break;
|
||||
case lppTemperature:
|
||||
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||
break;
|
||||
case lppVoltage:
|
||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||
break;
|
||||
case lppCurrent:
|
||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||
break;
|
||||
case lppPercentage:
|
||||
channelData['values']['percentage'] = buffer.readUInt8();
|
||||
break;
|
||||
case lppConcentration:
|
||||
channelData['values']['concentration'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppPower:
|
||||
channelData['values']['power'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppGps:
|
||||
channelData['values']['gps'] = {
|
||||
'latitude': buffer.readInt24BE() / 10000.0,
|
||||
'longitude': buffer.readInt24BE() / 10000.0,
|
||||
'altitude': buffer.readInt24BE() / 100.0,
|
||||
};
|
||||
break;
|
||||
// Add more types as needed...
|
||||
default:
|
||||
//Stopped parsing to avoid misalignment
|
||||
return channels.values.toList();
|
||||
}
|
||||
}
|
||||
|
||||
final channelData = channels.putIfAbsent(
|
||||
channel,
|
||||
() => {'channel': channel, 'values': <String, dynamic>{}},
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||
break;
|
||||
case lppLuminosity:
|
||||
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppPresence:
|
||||
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
||||
break;
|
||||
case lppTemperature:
|
||||
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||
break;
|
||||
case lppVoltage:
|
||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||
break;
|
||||
case lppCurrent:
|
||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||
break;
|
||||
case lppPercentage:
|
||||
channelData['values']['percentage'] = buffer.readUInt8();
|
||||
break;
|
||||
case lppConcentration:
|
||||
channelData['values']['concentration'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppPower:
|
||||
channelData['values']['power'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppGps:
|
||||
channelData['values']['gps'] = {
|
||||
'latitude': buffer.readInt24BE() / 10000.0,
|
||||
'longitude': buffer.readInt24BE() / 10000.0,
|
||||
'altitude': buffer.readInt24BE() / 100.0,
|
||||
};
|
||||
break;
|
||||
// Add more types as needed...
|
||||
default:
|
||||
// Unknown type: skip or handle error?
|
||||
continue;
|
||||
}
|
||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||
return channelsOut;
|
||||
} catch (e) {
|
||||
// Handle parsing errors, possibly due to malformed data
|
||||
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||
return <
|
||||
Map<String, dynamic>
|
||||
>[]; // Return an empty list on error to avoid crashing the app
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||
return channelsOut;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class LosIcon extends StatelessWidget {
|
||||
final double size;
|
||||
final Color? color;
|
||||
|
||||
const LosIcon({super.key, this.size = 24, this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final iconTheme = IconTheme.of(context);
|
||||
final iconColor =
|
||||
color ??
|
||||
iconTheme.color ??
|
||||
theme.iconTheme.color ??
|
||||
theme.colorScheme.onSurface;
|
||||
|
||||
return Icon(Symbols.elevation, size: size, color: iconColor);
|
||||
}
|
||||
}
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "bg",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Контакти",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Брой контакти",
|
||||
"settings_infoChannelCount": "Брой канали",
|
||||
"settings_presets": "Предварителни настройки",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Честота (MHz)",
|
||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Мощност (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
|
||||
"settings_longRange": "Дълъг обхват",
|
||||
"settings_fastSpeed": "Бърза скорост",
|
||||
"settings_error": "Грешка: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Публичен канал",
|
||||
"channels_privateChannel": "Частен канал",
|
||||
"channels_editChannel": "Редактирай канал",
|
||||
"channels_muteChannel": "Заглуши канала",
|
||||
"channels_unmuteChannel": "Включи известията на канала",
|
||||
"channels_deleteChannel": "Изтрий канала",
|
||||
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||
"repeater_neighbours": "Съседи",
|
||||
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||
"repeater_neighbors": "Съседи",
|
||||
"neighbors_receivedData": "Получени данни за съседи",
|
||||
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
||||
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
||||
"neighbors_repeatersNeighbours": "Повторители Съседи",
|
||||
"neighbors_repeatersNeighbors": "Повторители Съседи",
|
||||
"neighbors_noData": "Няма налични данни за съседи.",
|
||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||
@@ -1557,6 +1562,8 @@
|
||||
"contacts_clipboardEmpty": "Клипборда е празна.",
|
||||
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
||||
"appSettings_languageRu": "Руски",
|
||||
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
|
||||
"contacts_contactImported": "Контактът е импортиран.",
|
||||
"contacts_zeroHopAdvert": "Реклама без скок",
|
||||
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
|
||||
"notification_newTypeDiscovered": "Открит нов {contactType}",
|
||||
"notification_receivedNewMessage": "Получено ново съобщение",
|
||||
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
|
||||
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
|
||||
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
|
||||
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
|
||||
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
|
||||
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
|
||||
"map_pathTraceCancelled": "Отменен е следването на пътя.",
|
||||
"pathTrace_clearTooltip": "Изчисти пътя",
|
||||
"map_removeLast": "Премахни Последно",
|
||||
"map_runTrace": "Изпълни Път на Следване",
|
||||
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
|
||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||
"snrIndicator_lastSeen": "Последно видян",
|
||||
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
||||
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
||||
"settings_clientRepeat": "Без електричество – повторение",
|
||||
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "единици",
|
||||
"appSettings_unitsMetric": "Метрика (m / km)",
|
||||
"appSettings_unitsImperial": "Имперска (ft / mi)",
|
||||
"map_lineOfSight": "Линия на видимост",
|
||||
"map_losScreenTitle": "Линия на видимост",
|
||||
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
|
||||
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Изчистете всички точки",
|
||||
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
|
||||
"losMenuTitle": "LOS меню",
|
||||
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
|
||||
"losShowDisplayNodes": "Показване на възли на дисплея",
|
||||
"losCustomPoints": "Персонализирани точки",
|
||||
"losCustomPointLabel": "Персонализирано {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Точка А",
|
||||
"losPointB": "Точка Б",
|
||||
"losAntennaA": "Антена A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Антена B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Стартирайте LOS",
|
||||
"losNoElevationData": "Няма данни за надморска височина",
|
||||
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: проверка...",
|
||||
"losStatusNoData": "LOS: няма данни",
|
||||
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
|
||||
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
|
||||
"losRenameCustomPoint": "Преименувайте персонализирана точка",
|
||||
"losPointName": "Име на точката",
|
||||
"losShowPanelTooltip": "Показване на LOS панел",
|
||||
"losHidePanelTooltip": "Скриване на LOS панела",
|
||||
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Радиохоризонт",
|
||||
"losLegendLosBeam": "Линия на видимост",
|
||||
"losLegendTerrain": "Терен",
|
||||
"losFrequencyLabel": "Честота",
|
||||
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
|
||||
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
|
||||
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
|
||||
"listFilter_addToFavorites": "Добави към любими",
|
||||
"listFilter_favorites": "Любими",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
|
||||
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
|
||||
"contacts_unread": "Непрочетено",
|
||||
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
|
||||
}
|
||||
|
||||
+267
-53
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "de",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakte",
|
||||
@@ -96,14 +104,14 @@
|
||||
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
||||
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
||||
"settings_actions": "Aktionen",
|
||||
"settings_sendAdvertisement": "Sende eine Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
|
||||
"settings_sendAdvertisement": "Sende Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
|
||||
"settings_advertisementSent": "Ankündigung gesendet",
|
||||
"settings_syncTime": "Zeitsynchronisierung",
|
||||
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
|
||||
"settings_timeSynchronized": "Zeit synchronisiert",
|
||||
"settings_refreshContacts": "Kontakte aktualisieren",
|
||||
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
|
||||
"settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden",
|
||||
"settings_rebootDevice": "Gerät neu starten",
|
||||
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
|
||||
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Anzahl Kontakte",
|
||||
"settings_infoChannelCount": "Anzahl Kanäle",
|
||||
"settings_presets": "Voreinstellungen",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequenz (MHz)",
|
||||
"settings_frequencyHelper": "300,00 - 2.500,00",
|
||||
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX-Leistung (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
|
||||
"settings_longRange": "Grosse Reichweite",
|
||||
"settings_fastSpeed": "Schnelle Geschwindigkeit",
|
||||
"settings_error": "Fehler: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Öffentlicher Kanal",
|
||||
"channels_privateChannel": "Privater Kanal",
|
||||
"channels_editChannel": "Kanal bearbeiten",
|
||||
"channels_muteChannel": "Kanal stummschalten",
|
||||
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
|
||||
"channels_deleteChannel": "Lösche den Kanal",
|
||||
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -540,7 +545,7 @@
|
||||
"chat_routingMode": "Routenmodus",
|
||||
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
|
||||
"chat_forceFloodMode": "Flut-Modus erzwingen",
|
||||
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
|
||||
"chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):",
|
||||
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
|
||||
"chat_hopSingular": "Sprung",
|
||||
"chat_hopPlural": "Sprünge",
|
||||
@@ -554,7 +559,7 @@
|
||||
},
|
||||
"chat_successes": "Erfolgreich",
|
||||
"chat_removePath": "Pfad entfernen",
|
||||
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
||||
"chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
||||
"chat_pathActions": "Pfadaktionen:",
|
||||
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
|
||||
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
|
||||
@@ -717,7 +722,7 @@
|
||||
"mapCache_cacheArea": "Zwischenspeicherbereich",
|
||||
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
|
||||
"mapCache_zoomRange": "Zoom Bereich",
|
||||
"mapCache_estimatedTiles": "Geschätzte Fliesen: {count}",
|
||||
"mapCache_estimatedTiles": "Geschätzte Kacheln: {count}",
|
||||
"@mapCache_estimatedTiles": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -854,7 +859,7 @@
|
||||
},
|
||||
"path_enterCustomPath": "Gebe Pfad ein",
|
||||
"path_currentPathLabel": "Aktueller Pfad",
|
||||
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
|
||||
"path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.",
|
||||
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
|
||||
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
|
||||
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
|
||||
@@ -887,7 +892,7 @@
|
||||
"repeater_forceFloodMode": "Flut-Modus erzwingen",
|
||||
"repeater_pathManagement": "Pfadverwaltung",
|
||||
"repeater_refresh": "Aktualisieren",
|
||||
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
|
||||
"repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.",
|
||||
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
|
||||
"@repeater_errorLoadingStatus": {
|
||||
"placeholders": {
|
||||
@@ -957,7 +962,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}",
|
||||
"repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}",
|
||||
"@repeater_duplicatesFloodDirect": {
|
||||
"placeholders": {
|
||||
"flood": {
|
||||
@@ -983,7 +988,7 @@
|
||||
"repeater_adminPassword": "Admin-Passwort",
|
||||
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
|
||||
"repeater_guestPassword": "Gast-Passwort",
|
||||
"repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort",
|
||||
"repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort",
|
||||
"repeater_radioSettings": "Funk Einstellungen",
|
||||
"repeater_frequencyMhz": "Frequenz (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
@@ -1086,11 +1091,11 @@
|
||||
}
|
||||
},
|
||||
"repeater_cliTitle": "Repeater CLI",
|
||||
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
|
||||
"repeater_debugNextCommand": "Fehlersuche des nächsten Befehls",
|
||||
"repeater_commandHelp": "Hilfe",
|
||||
"repeater_clearHistory": "Löschen der Historie",
|
||||
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
|
||||
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
|
||||
"repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle",
|
||||
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
|
||||
"repeater_previousCommand": "Vorhergehende Aktion",
|
||||
"repeater_nextCommand": "Nächste Aktion",
|
||||
@@ -1132,7 +1137,7 @@
|
||||
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
|
||||
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
|
||||
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
|
||||
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
||||
"repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
||||
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
|
||||
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
|
||||
@@ -1143,14 +1148,14 @@
|
||||
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
|
||||
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
|
||||
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
|
||||
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.",
|
||||
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.",
|
||||
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
|
||||
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
|
||||
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
|
||||
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
|
||||
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
|
||||
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
|
||||
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
|
||||
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.",
|
||||
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
|
||||
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
|
||||
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
|
||||
@@ -1243,7 +1248,7 @@
|
||||
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
|
||||
"channelPath_repeaterHops": "Repeater-Sprünge",
|
||||
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
|
||||
"channelPath_messageDetails": "Nachrichtsdetails",
|
||||
"channelPath_messageDetails": "Nachrichtendetails",
|
||||
"channelPath_senderLabel": "Sender",
|
||||
"channelPath_timeLabel": "Zeit",
|
||||
"channelPath_repeatsLabel": "Wiederholungen",
|
||||
@@ -1344,10 +1349,13 @@
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_filters": "Filtere",
|
||||
"listFilter_all": "Alle",
|
||||
"listFilter_favorites": "Favoriten",
|
||||
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
|
||||
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
|
||||
"listFilter_users": "Benutzer",
|
||||
"listFilter_repeaters": "Repeater",
|
||||
"listFilter_roomServers": "Raumserver",
|
||||
"listFilter_unreadOnly": "Nur nicht gelesen",
|
||||
"listFilter_unreadOnly": "Nicht gelesen",
|
||||
"listFilter_newGroup": "Neue Gruppe",
|
||||
"@neighbors_errorLoading": {
|
||||
"placeholders": {
|
||||
@@ -1356,13 +1364,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Nachbarn",
|
||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||
"neighbors_receivedData": "Empfangene Nachbarendaten",
|
||||
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
|
||||
"repeater_neighbors": "Nachbarn",
|
||||
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||
"neighbors_repeatersNeighbours": "Nachbarn",
|
||||
"neighbors_noData": "Keine Nachbardaten verfügbar.",
|
||||
"neighbors_repeatersNeighbors": "Nachbarn",
|
||||
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
|
||||
@@ -1389,8 +1397,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"neighbors_heardAgo": "Hörte: {time} vor her.",
|
||||
"neighbors_unknownContact": "Unbekannte {pubkey}",
|
||||
"neighbors_heardAgo": "Gehört vor: {time}",
|
||||
"neighbors_unknownContact": "Unbekannt {pubkey}",
|
||||
"settings_locationGPSEnable": "GPS aktivieren",
|
||||
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
|
||||
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
|
||||
@@ -1493,9 +1501,9 @@
|
||||
"community_deleted": "Community \"{name}\" verlassen",
|
||||
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
|
||||
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
|
||||
"community_selectCommunity": "Wählen Sie Community",
|
||||
"community_selectCommunity": "Wählen Sie eine Community",
|
||||
"community_regularHashtag": "Regulärer Hashtag",
|
||||
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
|
||||
"community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)",
|
||||
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
|
||||
"community_forCommunity": "Für {name}",
|
||||
"community_communityHashtag": "Community Hashtag",
|
||||
@@ -1557,61 +1565,267 @@
|
||||
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
||||
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
||||
"appSettings_languageUk": "Ukrainisch",
|
||||
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
|
||||
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
|
||||
"contacts_contactImported": "Kontakt wurde importiert.",
|
||||
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
||||
"contacts_zeroHopAdvert": "Zero-Hop-Anzeige",
|
||||
"contacts_floodAdvert": "Überflutungsanzeige",
|
||||
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
||||
"contacts_floodAdvert": "Flut-Ankündigung",
|
||||
"contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen",
|
||||
"contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen",
|
||||
"contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren",
|
||||
"contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren",
|
||||
"contacts_ShareContact": "Kontakt in die Zwischenablage kopieren",
|
||||
"contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.",
|
||||
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
|
||||
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.",
|
||||
|
||||
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
|
||||
"notification_activityTitle": "MeshCore Aktivität",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
|
||||
"@notification_messagesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
|
||||
"@notification_channelMessagesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
|
||||
"@notification_newNodesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
|
||||
"@notification_newTypeDiscovered": {
|
||||
"placeholders": {
|
||||
"contactType": {"type": "String"}
|
||||
"contactType": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_receivedNewMessage": "Neue Nachricht empfangen",
|
||||
"contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.",
|
||||
"settings_gpxExportAll": "Alle Kontakte nach GPX exportieren",
|
||||
"settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.",
|
||||
"settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren",
|
||||
"settings_gpxExportContacts": "Begleiter nach GPX exportieren",
|
||||
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
|
||||
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
|
||||
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
|
||||
"settings_gpxExportContacts": "Kontakte als GPX exportieren",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.",
|
||||
"settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.",
|
||||
"settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.",
|
||||
"settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte",
|
||||
"settings_gpxExportChat": "Begleiterstandorte",
|
||||
"settings_gpxExportChat": "Kontaktstandorte",
|
||||
"settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.",
|
||||
"settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.",
|
||||
"settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt",
|
||||
"settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.",
|
||||
"settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.",
|
||||
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren",
|
||||
"settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert",
|
||||
"pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!"
|
||||
|
||||
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
|
||||
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
|
||||
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
|
||||
"map_removeLast": "Letztes Entfernen",
|
||||
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
|
||||
"map_runTrace": "Pfadverlauf ausführen",
|
||||
"pathTrace_clearTooltip": "Pfad löschen",
|
||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||
"snrIndicator_lastSeen": "Zuletzt gesehen",
|
||||
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater",
|
||||
"chat_ShowAllPaths": "Alle Pfade anzeigen",
|
||||
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
||||
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Einheiten",
|
||||
"appSettings_unitsMetric": "Metrisch (m/km)",
|
||||
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||
"map_lineOfSight": "Sichtlinie",
|
||||
"map_losScreenTitle": "Sichtlinie",
|
||||
"losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.",
|
||||
"losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Löschen Sie alle Punkte",
|
||||
"losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen",
|
||||
"losMenuTitle": "LOS-Menü",
|
||||
"losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen",
|
||||
"losShowDisplayNodes": "Anzeigeknoten anzeigen",
|
||||
"losCustomPoints": "Benutzerdefinierte Punkte",
|
||||
"losCustomPointLabel": "Benutzerdefiniert {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punkt A",
|
||||
"losPointB": "Punkt B",
|
||||
"losAntennaA": "Antenne A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenne B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Führen Sie LOS aus",
|
||||
"losNoElevationData": "Keine Höhendaten",
|
||||
"losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: Überprüfen...",
|
||||
"losStatusNoData": "LOS: keine Daten",
|
||||
"losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.",
|
||||
"losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.",
|
||||
"losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um",
|
||||
"losPointName": "Punktname",
|
||||
"losShowPanelTooltip": "LOS-Panel anzeigen",
|
||||
"losHidePanelTooltip": "LOS-Panel ausblenden",
|
||||
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Funkhorizont",
|
||||
"losLegendLosBeam": "Sichtlinie",
|
||||
"losLegendTerrain": "Gelände",
|
||||
"losFrequencyLabel": "Frequenz",
|
||||
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
|
||||
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
|
||||
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Ungelesen",
|
||||
"contacts_searchContactsNoNumber": "Kontakte suchen...",
|
||||
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
|
||||
}
|
||||
|
||||
+627
-177
File diff suppressed because it is too large
Load Diff
+230
-16
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "es",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contactos",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Número de contactos",
|
||||
"settings_infoChannelCount": "Número de canales",
|
||||
"settings_presets": "Preajustes",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frecuencia (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potencia (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
|
||||
"settings_longRange": "Largo Alcance",
|
||||
"settings_fastSpeed": "Velocidad Rápida",
|
||||
"settings_error": "Error: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal público",
|
||||
"channels_privateChannel": "Canal privado",
|
||||
"channels_editChannel": "Editar canal",
|
||||
"channels_muteChannel": "Silenciar canal",
|
||||
"channels_unmuteChannel": "Activar canal",
|
||||
"channels_deleteChannel": "Eliminar canal",
|
||||
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vecinos",
|
||||
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
|
||||
"repeater_neighbors": "Vecinos",
|
||||
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
|
||||
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
||||
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
||||
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
||||
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
|
||||
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
|
||||
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||
@@ -1556,6 +1561,8 @@
|
||||
"appSettings_languageUk": "Ucraniano",
|
||||
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
||||
"appSettings_languageRu": "Ruso",
|
||||
"appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes",
|
||||
"appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes",
|
||||
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
||||
"contacts_floodAdvert": "Anuncio de inundación",
|
||||
"contacts_contactImported": "El contacto ha sido importado.",
|
||||
@@ -1569,34 +1576,40 @@
|
||||
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
|
||||
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
|
||||
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
|
||||
|
||||
"notification_activityTitle": "Actividad de MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
|
||||
"@notification_messagesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
|
||||
"@notification_channelMessagesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
|
||||
"@notification_newNodesCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
|
||||
"@notification_newTypeDiscovered": {
|
||||
"placeholders": {
|
||||
"contactType": {"type": "String"}
|
||||
"contactType": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_receivedNewMessage": "Nuevo mensaje recibido",
|
||||
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
|
||||
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
|
||||
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
|
||||
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
|
||||
@@ -1612,6 +1625,207 @@
|
||||
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
|
||||
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
|
||||
"pathTrace_clearTooltip": "Borrar ruta",
|
||||
"map_runTrace": "Ejecutar Rastreo de Ruta",
|
||||
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
|
||||
"map_removeLast": "Eliminar último",
|
||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
|
||||
"snrIndicator_lastSeen": "Visto por última vez",
|
||||
"chat_ShowAllPaths": "Mostrar todos los caminos",
|
||||
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
||||
"settings_clientRepeat": "Repetir sin conexión",
|
||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
|
||||
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unidades",
|
||||
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||
"appSettings_unitsImperial": "Imperial (pies/millas)",
|
||||
"map_lineOfSight": "Línea de visión",
|
||||
"map_losScreenTitle": "Línea de visión",
|
||||
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
|
||||
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Borrar todos los puntos",
|
||||
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
|
||||
"losMenuTitle": "Menú LOS",
|
||||
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
|
||||
"losShowDisplayNodes": "Mostrar nodos de visualización",
|
||||
"losCustomPoints": "Puntos personalizados",
|
||||
"losCustomPointLabel": "Personalizado {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punto A",
|
||||
"losPointB": "Punto B",
|
||||
"losAntennaA": "Antena A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antena B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Ejecutar LOS",
|
||||
"losNoElevationData": "Sin datos de elevación",
|
||||
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: comprobando...",
|
||||
"losStatusNoData": "LOS: sin datos",
|
||||
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
|
||||
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
|
||||
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
|
||||
"losPointName": "Nombre del punto",
|
||||
"losShowPanelTooltip": "Mostrar panel LOS",
|
||||
"losHidePanelTooltip": "Ocultar panel LOS",
|
||||
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horizonte radioeléctrico",
|
||||
"losLegendLosBeam": "Línea de visión",
|
||||
"losLegendTerrain": "Terreno",
|
||||
"losFrequencyLabel": "Frecuencia",
|
||||
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
|
||||
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
|
||||
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_favorites": "Favoritos",
|
||||
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
|
||||
"listFilter_addToFavorites": "Añadir a favoritos",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchContactsNoNumber": "Buscar contactos...",
|
||||
"contacts_unread": "No leído",
|
||||
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
|
||||
}
|
||||
|
||||
+255
-48
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "fr",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contacts",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Nombre de contacts",
|
||||
"settings_infoChannelCount": "Nombre de canaux",
|
||||
"settings_presets": "Préréglages",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Fréquence (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Puissance (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||
"settings_longRange": "Portée Longue",
|
||||
"settings_fastSpeed": "Vitesse Rapide",
|
||||
"settings_error": "Erreur : {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -210,8 +213,8 @@
|
||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
|
||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||
"appSettings_mapDisplay": "Affichage de la carte",
|
||||
"appSettings_showRepeaters": "Afficher les répétiteurs",
|
||||
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte",
|
||||
"appSettings_showRepeaters": "Afficher les répéteurs",
|
||||
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte",
|
||||
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
|
||||
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
|
||||
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
|
||||
@@ -266,8 +269,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_manageRepeater": "Gérer le répétiteur",
|
||||
"contacts_roomLogin": "Connexion Salle",
|
||||
"contacts_manageRepeater": "Gérer le répéteur",
|
||||
"contacts_roomLogin": "Connexion Room Server",
|
||||
"contacts_openChat": "Ouverture du Chat",
|
||||
"contacts_editGroup": "Modifier le groupe",
|
||||
"contacts_deleteGroup": "Supprimer le groupe",
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal public",
|
||||
"channels_privateChannel": "Canal privé",
|
||||
"channels_editChannel": "Modifier le canal",
|
||||
"channels_muteChannel": "Désactiver les notifications du canal",
|
||||
"channels_unmuteChannel": "Réactiver les notifications du canal",
|
||||
"channels_deleteChannel": "Supprimer le canal",
|
||||
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -542,9 +547,9 @@
|
||||
"chat_forceFloodMode": "Mode tout le réseau forcé",
|
||||
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
|
||||
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
|
||||
"chat_hopSingular": "Sautez",
|
||||
"chat_hopPlural": "sautez",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopSingular": "saut",
|
||||
"chat_hopPlural": "sauts",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -636,7 +641,7 @@
|
||||
}
|
||||
},
|
||||
"map_chat": "Chat",
|
||||
"map_repeater": "Répétiteur",
|
||||
"map_repeater": "Répéteur",
|
||||
"map_room": "Salle",
|
||||
"map_sensor": "Capteur",
|
||||
"map_pinDm": "Clé (DM)",
|
||||
@@ -677,7 +682,7 @@
|
||||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_joinRoom": "Rejoindre la salle",
|
||||
"map_manageRepeater": "Gérer le répétiteur",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
|
||||
@@ -800,13 +805,13 @@
|
||||
"time_allTime": "Tout le temps",
|
||||
"dialog_disconnect": "Déconnecter",
|
||||
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
||||
"login_repeaterLogin": "Connexion au répétiteur",
|
||||
"login_roomLogin": "Connexion Salle",
|
||||
"login_repeaterLogin": "Connexion au répéteur",
|
||||
"login_roomLogin": "Connexion Room Server",
|
||||
"login_password": "Mot de passe",
|
||||
"login_enterPassword": "Entrez votre mot de passe",
|
||||
"login_savePassword": "Sauvegarder le mot de passe",
|
||||
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
|
||||
"login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.",
|
||||
"login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.",
|
||||
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
|
||||
"login_routing": "Redirection",
|
||||
"login_routingMode": "Mode de routage",
|
||||
@@ -871,17 +876,17 @@
|
||||
},
|
||||
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
|
||||
"path_setPath": "Définir le chemin",
|
||||
"repeater_management": "Gestion des répétiteurs",
|
||||
"repeater_management": "Gestion des répéteurs",
|
||||
"repeater_managementTools": "Outils de Gestion",
|
||||
"repeater_status": "État",
|
||||
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
|
||||
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
|
||||
"repeater_telemetry": "Télémetrie",
|
||||
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
|
||||
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
|
||||
"repeater_settings": "Paramètres",
|
||||
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
|
||||
"repeater_statusTitle": "État du répétiteur",
|
||||
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
|
||||
"repeater_statusTitle": "État du répéteur",
|
||||
"repeater_routingMode": "Mode de routage",
|
||||
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
|
||||
"repeater_forceFloodMode": "Mode tout le réseau forcé",
|
||||
@@ -976,10 +981,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsTitle": "Paramètres du répétiteur",
|
||||
"repeater_settingsTitle": "Paramètres du répéteur",
|
||||
"repeater_basicSettings": "Paramètres de base",
|
||||
"repeater_repeaterName": "Nom du répétiteur",
|
||||
"repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur",
|
||||
"repeater_repeaterName": "Nom du répéteur",
|
||||
"repeater_repeaterNameHelper": "Afficher le nom de ce répéteur",
|
||||
"repeater_adminPassword": "Mot de passe Administrateur",
|
||||
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
|
||||
"repeater_guestPassword": "Mot de passe invité",
|
||||
@@ -999,7 +1004,7 @@
|
||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||
"repeater_features": "Fonctionnalités",
|
||||
"repeater_packetForwarding": "Transfert de paquets",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||
"repeater_guestAccess": "Accès Invité",
|
||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||
"repeater_privacyMode": "Mode de confidentialité",
|
||||
@@ -1026,14 +1031,14 @@
|
||||
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
|
||||
"repeater_dangerZone": "Zone dangereuse",
|
||||
"repeater_rebootRepeater": "Redémarrer Répéteur",
|
||||
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
|
||||
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
|
||||
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur",
|
||||
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?",
|
||||
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?",
|
||||
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
|
||||
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur",
|
||||
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !",
|
||||
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur",
|
||||
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !",
|
||||
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
|
||||
"repeater_commandSent": "Commande envoyée : {command}",
|
||||
"@repeater_commandSent": {
|
||||
@@ -1085,7 +1090,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliTitle": "Répétiteur CLI",
|
||||
"repeater_cliTitle": "Répéteur CLI",
|
||||
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
|
||||
"repeater_commandHelp": "Aide",
|
||||
"repeater_clearHistory": "Effacer l'historique",
|
||||
@@ -1119,13 +1124,13 @@
|
||||
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
|
||||
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
|
||||
"repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
|
||||
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
|
||||
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
|
||||
"repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).",
|
||||
"repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.",
|
||||
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
|
||||
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
|
||||
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
|
||||
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
|
||||
@@ -1147,7 +1152,7 @@
|
||||
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
|
||||
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
|
||||
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
|
||||
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
||||
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
||||
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
|
||||
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
|
||||
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
|
||||
@@ -1171,8 +1176,8 @@
|
||||
"repeater_settingsCategory": "Paramètres",
|
||||
"repeater_bridge": "Pont",
|
||||
"repeater_logging": "Journalisation",
|
||||
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)",
|
||||
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)",
|
||||
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)",
|
||||
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)",
|
||||
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
|
||||
"repeater_gpsManagement": "Gestion GPS",
|
||||
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
|
||||
@@ -1241,7 +1246,7 @@
|
||||
"channelPath_title": "Chemin de paquet",
|
||||
"channelPath_viewMap": "Afficher la carte",
|
||||
"channelPath_otherObservedPaths": "Autres chemins observés",
|
||||
"channelPath_repeaterHops": "Sauts du répétiteur",
|
||||
"channelPath_repeaterHops": "Sauts du répéteur",
|
||||
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
|
||||
"channelPath_messageDetails": "Détails du message",
|
||||
"channelPath_senderLabel": "Expéditeur",
|
||||
@@ -1306,7 +1311,7 @@
|
||||
}
|
||||
},
|
||||
"channelPath_mapTitle": "Carte du chemin",
|
||||
"channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.",
|
||||
"channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.",
|
||||
"channelPath_primaryPath": "Chemin {index} (Principal)",
|
||||
"@channelPath_primaryPath": {
|
||||
"placeholders": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Voisins",
|
||||
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
|
||||
"repeater_neighbors": "Voisins",
|
||||
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
|
||||
"neighbors_receivedData": "Données des voisins reçues",
|
||||
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
||||
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
||||
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
|
||||
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
|
||||
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||
@@ -1396,7 +1401,7 @@
|
||||
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
||||
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
||||
"contacts_manageRoom": "Gérer le Room Server",
|
||||
"room_management": "Administración del Servidor de Habitación",
|
||||
"room_management": "Administrattion Room Server",
|
||||
"@community_joinConfirmation": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -1556,11 +1561,13 @@
|
||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||
"appSettings_languageUk": "Ukrainien",
|
||||
"appSettings_languageRu": "Russe",
|
||||
"appSettings_enableMessageTracing": "Activer le traçage des messages",
|
||||
"appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages",
|
||||
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
||||
"contacts_contactImported": "Le contact a été importé.",
|
||||
"contacts_floodAdvert": "Annonce de crue",
|
||||
"contacts_floodAdvert": "Annonce à tout le réseau",
|
||||
"contacts_contactImportFailed": "Échec de l'importation du contact.",
|
||||
"contacts_zeroHopAdvert": "Annonce Zero Hop",
|
||||
"contacts_zeroHopAdvert": "Annonce Zero saut",
|
||||
"contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers",
|
||||
"contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers",
|
||||
"contacts_ShareContact": "Copier le contact dans le presse-papiers",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||
"contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
|
||||
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
|
||||
"pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
|
||||
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
|
||||
"pathTrace_clearTooltip": "Effacer le chemin",
|
||||
"map_pathTraceCancelled": "Traçage de chemin annulé",
|
||||
"map_removeLast": "Supprimer le dernier",
|
||||
"map_runTrace": "Exécuter la traçage de chemin",
|
||||
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||
"snrIndicator_lastSeen": "Dernière fois vu",
|
||||
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
|
||||
"chat_ShowAllPaths": "Afficher tous les chemins",
|
||||
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
||||
"settings_clientRepeat": "Répétition hors réseau",
|
||||
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unités",
|
||||
"appSettings_unitsMetric": "Métrique (m/km)",
|
||||
"appSettings_unitsImperial": "Impérial (ft / mi)",
|
||||
"map_lineOfSight": "Ligne de vue",
|
||||
"map_losScreenTitle": "Ligne de vue",
|
||||
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
|
||||
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Effacer tous les points",
|
||||
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
|
||||
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
|
||||
"losCustomPoints": "Points personnalisés",
|
||||
"losCustomPointLabel": "Personnalisé {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Point A",
|
||||
"losPointB": "Point B",
|
||||
"losAntennaA": "Antenne A : {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenne B : {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Exécuter la LOS",
|
||||
"losNoElevationData": "Aucune donnée d'altitude",
|
||||
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS : vérification...",
|
||||
"losStatusNoData": "LOS : aucune donnée",
|
||||
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
|
||||
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
|
||||
"losRenameCustomPoint": "Renommer le point personnalisé",
|
||||
"losPointName": "Nom du point",
|
||||
"losShowPanelTooltip": "Afficher le panneau LOS",
|
||||
"losHidePanelTooltip": "Masquer le panneau LOS",
|
||||
"losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horizon radio",
|
||||
"losLegendLosBeam": "Ligne de visée",
|
||||
"losLegendTerrain": "Terrain",
|
||||
"losFrequencyLabel": "Fréquence",
|
||||
"losFrequencyInfoTooltip": "Voir les détails du calcul",
|
||||
"losFrequencyDialogTitle": "Calcul de l’horizon radio",
|
||||
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_addToFavorites": "Ajouter à mes favoris",
|
||||
"listFilter_removeFromFavorites": "Supprimer des favoris",
|
||||
"listFilter_favorites": "Préférences",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Non lu",
|
||||
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "it",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contatti",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Numero contatti",
|
||||
"settings_infoChannelCount": "Numero Canale",
|
||||
"settings_presets": "Preset",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequenza (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potenza (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
|
||||
"settings_longRange": "Lungo Raggio",
|
||||
"settings_fastSpeed": "Velocità Rapida",
|
||||
"settings_error": "Errore: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canale pubblico",
|
||||
"channels_privateChannel": "Canale privato",
|
||||
"channels_editChannel": "Modifica canale",
|
||||
"channels_muteChannel": "Silenzia canale",
|
||||
"channels_unmuteChannel": "Attiva notifiche canale",
|
||||
"channels_deleteChannel": "Elimina canale",
|
||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vicini",
|
||||
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||
"repeater_neighbors": "Vicini",
|
||||
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||
"neighbors_receivedData": "Ricevute dati vicini",
|
||||
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
||||
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
||||
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
|
||||
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
|
||||
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||
@@ -1556,6 +1561,8 @@
|
||||
"appSettings_languageRu": "Russo",
|
||||
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
||||
"appSettings_languageUk": "Ucraino",
|
||||
"appSettings_enableMessageTracing": "Abilita tracciamento messaggi",
|
||||
"appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi",
|
||||
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
||||
"contacts_floodAdvert": "Annuncio alluvionale",
|
||||
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
|
||||
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
|
||||
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
|
||||
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
|
||||
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
|
||||
"settings_gpxExportContacts": "Esporta compagni in GPX",
|
||||
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
|
||||
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
|
||||
"map_removeLast": "Rimuovi ultimo",
|
||||
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
|
||||
"pathTrace_clearTooltip": "Pulisci percorso",
|
||||
"map_runTrace": "Esegui Path Trace",
|
||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||
"chat_ShowAllPaths": "Mostra tutti i percorsi",
|
||||
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
|
||||
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unità",
|
||||
"appSettings_unitsMetric": "Metrico (m/km)",
|
||||
"appSettings_unitsImperial": "Imperiale (ft / mi)",
|
||||
"map_lineOfSight": "Linea di vista",
|
||||
"map_losScreenTitle": "Linea di vista",
|
||||
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
|
||||
"losRunFailed": "Controllo della linea di vista fallito: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Cancella tutti i punti",
|
||||
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
|
||||
"losMenuTitle": "Menù LOS",
|
||||
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
|
||||
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
|
||||
"losCustomPoints": "Punti personalizzati",
|
||||
"losCustomPointLabel": "Personalizzato {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punto A",
|
||||
"losPointB": "Punto B",
|
||||
"losAntennaA": "Antenna A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenna B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Esegui LOS",
|
||||
"losNoElevationData": "Nessun dato di elevazione",
|
||||
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: controllo...",
|
||||
"losStatusNoData": "LOS: nessun dato",
|
||||
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
|
||||
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
|
||||
"losRenameCustomPoint": "Rinomina punto personalizzato",
|
||||
"losPointName": "Nome del punto",
|
||||
"losShowPanelTooltip": "Mostra il pannello LOS",
|
||||
"losHidePanelTooltip": "Nascondi il pannello LOS",
|
||||
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Orizzonte radio",
|
||||
"losLegendLosBeam": "Linea di vista",
|
||||
"losLegendTerrain": "Terreno",
|
||||
"losFrequencyLabel": "Frequenza",
|
||||
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
|
||||
"losFrequencyDialogTitle": "Calcolo dell’orizzonte radio",
|
||||
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_addToFavorites": "Aggiungi ai preferiti",
|
||||
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
|
||||
"listFilter_favorites": "Preferiti",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
|
||||
"contacts_searchContactsNoNumber": "Cerca Contatti...",
|
||||
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||
"contacts_unread": "Non letti",
|
||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
|
||||
}
|
||||
|
||||
+429
-36
@@ -376,6 +376,24 @@ abstract class AppLocalizations {
|
||||
/// **'Scan'**
|
||||
String get scanner_scan;
|
||||
|
||||
/// No description provided for @scanner_bluetoothOff.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bluetooth is off'**
|
||||
String get scanner_bluetoothOff;
|
||||
|
||||
/// No description provided for @scanner_bluetoothOffMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please turn on Bluetooth to scan for devices'**
|
||||
String get scanner_bluetoothOffMessage;
|
||||
|
||||
/// No description provided for @scanner_enableBluetooth.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable Bluetooth'**
|
||||
String get scanner_enableBluetooth;
|
||||
|
||||
/// No description provided for @device_quickSwitch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -682,6 +700,12 @@ abstract class AppLocalizations {
|
||||
/// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'**
|
||||
String get settings_aboutDescription;
|
||||
|
||||
/// No description provided for @settings_aboutOpenMeteoAttribution.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS elevation data: Open-Meteo (CC BY 4.0)'**
|
||||
String get settings_aboutOpenMeteoAttribution;
|
||||
|
||||
/// No description provided for @settings_infoName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -730,24 +754,6 @@ abstract class AppLocalizations {
|
||||
/// **'Presets'**
|
||||
String get settings_presets;
|
||||
|
||||
/// No description provided for @settings_preset915Mhz.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'915 MHz'**
|
||||
String get settings_preset915Mhz;
|
||||
|
||||
/// No description provided for @settings_preset868Mhz.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'868 MHz'**
|
||||
String get settings_preset868Mhz;
|
||||
|
||||
/// No description provided for @settings_preset433Mhz.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'433 MHz'**
|
||||
String get settings_preset433Mhz;
|
||||
|
||||
/// No description provided for @settings_frequency.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -802,17 +808,23 @@ abstract class AppLocalizations {
|
||||
/// **'Invalid TX power (0-22 dBm)'**
|
||||
String get settings_txPowerInvalid;
|
||||
|
||||
/// No description provided for @settings_longRange.
|
||||
/// No description provided for @settings_clientRepeat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Long Range'**
|
||||
String get settings_longRange;
|
||||
/// **'Off-Grid Repeat'**
|
||||
String get settings_clientRepeat;
|
||||
|
||||
/// No description provided for @settings_fastSpeed.
|
||||
/// No description provided for @settings_clientRepeatSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fast Speed'**
|
||||
String get settings_fastSpeed;
|
||||
/// **'Allow this device to repeat mesh packets for others'**
|
||||
String get settings_clientRepeatSubtitle;
|
||||
|
||||
/// No description provided for @settings_clientRepeatFreqWarning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'**
|
||||
String get settings_clientRepeatFreqWarning;
|
||||
|
||||
/// No description provided for @settings_error.
|
||||
///
|
||||
@@ -958,6 +970,18 @@ abstract class AppLocalizations {
|
||||
/// **'Українська'**
|
||||
String get appSettings_languageUk;
|
||||
|
||||
/// No description provided for @appSettings_enableMessageTracing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable Message Tracing'**
|
||||
String get appSettings_enableMessageTracing;
|
||||
|
||||
/// No description provided for @appSettings_enableMessageTracingSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show detailed routing and timing metadata for messages'**
|
||||
String get appSettings_enableMessageTracingSubtitle;
|
||||
|
||||
/// No description provided for @appSettings_notifications.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1234,6 +1258,24 @@ abstract class AppLocalizations {
|
||||
/// **'Offline Map Cache'**
|
||||
String get appSettings_offlineMapCache;
|
||||
|
||||
/// No description provided for @appSettings_unitsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Units'**
|
||||
String get appSettings_unitsTitle;
|
||||
|
||||
/// No description provided for @appSettings_unitsMetric.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Metric (m / km)'**
|
||||
String get appSettings_unitsMetric;
|
||||
|
||||
/// No description provided for @appSettings_unitsImperial.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Imperial (ft / mi)'**
|
||||
String get appSettings_unitsImperial;
|
||||
|
||||
/// No description provided for @appSettings_noAreaSelected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1294,11 +1336,47 @@ abstract class AppLocalizations {
|
||||
/// **'Contacts will appear when devices advertise'**
|
||||
String get contacts_contactsWillAppear;
|
||||
|
||||
/// No description provided for @contacts_unread.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unread'**
|
||||
String get contacts_unread;
|
||||
|
||||
/// No description provided for @contacts_searchContactsNoNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search Contacts...'**
|
||||
String get contacts_searchContactsNoNumber;
|
||||
|
||||
/// No description provided for @contacts_searchContacts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search contacts...'**
|
||||
String get contacts_searchContacts;
|
||||
/// **'Search {number}{str} Contacts...'**
|
||||
String contacts_searchContacts(int number, String str);
|
||||
|
||||
/// No description provided for @contacts_searchFavorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search {number}{str} Favorites...'**
|
||||
String contacts_searchFavorites(int number, String str);
|
||||
|
||||
/// No description provided for @contacts_searchUsers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search {number}{str} Users...'**
|
||||
String contacts_searchUsers(int number, String str);
|
||||
|
||||
/// No description provided for @contacts_searchRepeaters.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search {number}{str} Repeaters...'**
|
||||
String contacts_searchRepeaters(int number, String str);
|
||||
|
||||
/// No description provided for @contacts_searchRoomServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search {number}{str} Room servers...'**
|
||||
String contacts_searchRoomServers(int number, String str);
|
||||
|
||||
/// No description provided for @contacts_noUnreadContacts.
|
||||
///
|
||||
@@ -1516,6 +1594,18 @@ abstract class AppLocalizations {
|
||||
/// **'Edit channel'**
|
||||
String get channels_editChannel;
|
||||
|
||||
/// No description provided for @channels_muteChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mute channel'**
|
||||
String get channels_muteChannel;
|
||||
|
||||
/// No description provided for @channels_unmuteChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unmute channel'**
|
||||
String get channels_unmuteChannel;
|
||||
|
||||
/// No description provided for @channels_deleteChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1528,6 +1618,12 @@ abstract class AppLocalizations {
|
||||
/// **'Delete \"{name}\"? This cannot be undone.'**
|
||||
String channels_deleteChannelConfirm(String name);
|
||||
|
||||
/// No description provided for @channels_channelDeleteFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to delete channel \"{name}\"'**
|
||||
String channels_channelDeleteFailed(String name);
|
||||
|
||||
/// No description provided for @channels_channelDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2026,6 +2122,12 @@ abstract class AppLocalizations {
|
||||
/// **'Path Management'**
|
||||
String get chat_pathManagement;
|
||||
|
||||
/// No description provided for @chat_ShowAllPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show all paths'**
|
||||
String get chat_ShowAllPaths;
|
||||
|
||||
/// No description provided for @chat_routingMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2278,6 +2380,18 @@ abstract class AppLocalizations {
|
||||
/// **'Node Map'**
|
||||
String get map_title;
|
||||
|
||||
/// No description provided for @map_lineOfSight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Line of Sight'**
|
||||
String get map_lineOfSight;
|
||||
|
||||
/// No description provided for @map_losScreenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Line of Sight'**
|
||||
String get map_losScreenTitle;
|
||||
|
||||
/// No description provided for @map_noNodesWithLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2518,6 +2632,30 @@ abstract class AppLocalizations {
|
||||
/// **'Manage Repeater'**
|
||||
String get map_manageRepeater;
|
||||
|
||||
/// No description provided for @map_tapToAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap on nodes to add them to the path.'**
|
||||
String get map_tapToAdd;
|
||||
|
||||
/// No description provided for @map_runTrace.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run Path Trace'**
|
||||
String get map_runTrace;
|
||||
|
||||
/// No description provided for @map_removeLast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove Last'**
|
||||
String get map_removeLast;
|
||||
|
||||
/// No description provided for @map_pathTraceCancelled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path trace cancelled.'**
|
||||
String get map_pathTraceCancelled;
|
||||
|
||||
/// No description provided for @mapCache_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2997,17 +3135,17 @@ abstract class AppLocalizations {
|
||||
/// **'Send commands to the repeater'**
|
||||
String get repeater_cliSubtitle;
|
||||
|
||||
/// No description provided for @repeater_neighbours.
|
||||
/// No description provided for @repeater_neighbors.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Neighbors'**
|
||||
String get repeater_neighbours;
|
||||
String get repeater_neighbors;
|
||||
|
||||
/// No description provided for @repeater_neighboursSubtitle.
|
||||
/// No description provided for @repeater_neighborsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'View zero hop neighbors.'**
|
||||
String get repeater_neighboursSubtitle;
|
||||
String get repeater_neighborsSubtitle;
|
||||
|
||||
/// No description provided for @repeater_settings.
|
||||
///
|
||||
@@ -4151,13 +4289,13 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @neighbors_receivedData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Received Neighbours Data'**
|
||||
/// **'Received Neighbors Data'**
|
||||
String get neighbors_receivedData;
|
||||
|
||||
/// No description provided for @neighbors_requestTimedOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Neighbours request timed out.'**
|
||||
/// **'Neighbors request timed out.'**
|
||||
String get neighbors_requestTimedOut;
|
||||
|
||||
/// No description provided for @neighbors_errorLoading.
|
||||
@@ -4166,16 +4304,16 @@ abstract class AppLocalizations {
|
||||
/// **'Error loading neighbors: {error}'**
|
||||
String neighbors_errorLoading(String error);
|
||||
|
||||
/// No description provided for @neighbors_repeatersNeighbours.
|
||||
/// No description provided for @neighbors_repeatersNeighbors.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Repeaters Neighbours'**
|
||||
String get neighbors_repeatersNeighbours;
|
||||
/// **'Repeaters Neighbors'**
|
||||
String get neighbors_repeatersNeighbors;
|
||||
|
||||
/// No description provided for @neighbors_noData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No neighbours data available.'**
|
||||
/// **'No neighbors data available.'**
|
||||
String get neighbors_noData;
|
||||
|
||||
/// No description provided for @neighbors_unknownContact.
|
||||
@@ -4670,6 +4808,24 @@ abstract class AppLocalizations {
|
||||
/// **'All'**
|
||||
String get listFilter_all;
|
||||
|
||||
/// No description provided for @listFilter_favorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Favorites'**
|
||||
String get listFilter_favorites;
|
||||
|
||||
/// No description provided for @listFilter_addToFavorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add to favorites'**
|
||||
String get listFilter_addToFavorites;
|
||||
|
||||
/// No description provided for @listFilter_removeFromFavorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove from favorites'**
|
||||
String get listFilter_removeFromFavorites;
|
||||
|
||||
/// No description provided for @listFilter_users.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -4730,6 +4886,231 @@ abstract class AppLocalizations {
|
||||
/// **'One or more of the hops is missing a location!'**
|
||||
String get pathTrace_someHopsNoLocation;
|
||||
|
||||
/// No description provided for @pathTrace_clearTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear path.'**
|
||||
String get pathTrace_clearTooltip;
|
||||
|
||||
/// No description provided for @losSelectStartEnd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select start and end nodes for LOS.'**
|
||||
String get losSelectStartEnd;
|
||||
|
||||
/// No description provided for @losRunFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Line-of-sight check failed: {error}'**
|
||||
String losRunFailed(String error);
|
||||
|
||||
/// No description provided for @losClearAllPoints.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear all points'**
|
||||
String get losClearAllPoints;
|
||||
|
||||
/// No description provided for @losRunToViewElevationProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run LOS to view elevation profile'**
|
||||
String get losRunToViewElevationProfile;
|
||||
|
||||
/// No description provided for @losMenuTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS Menu'**
|
||||
String get losMenuTitle;
|
||||
|
||||
/// No description provided for @losMenuSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap nodes or long-press map for custom points'**
|
||||
String get losMenuSubtitle;
|
||||
|
||||
/// No description provided for @losShowDisplayNodes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show display nodes'**
|
||||
String get losShowDisplayNodes;
|
||||
|
||||
/// No description provided for @losCustomPoints.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Custom points'**
|
||||
String get losCustomPoints;
|
||||
|
||||
/// No description provided for @losCustomPointLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Custom {index}'**
|
||||
String losCustomPointLabel(int index);
|
||||
|
||||
/// No description provided for @losPointA.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Point A'**
|
||||
String get losPointA;
|
||||
|
||||
/// No description provided for @losPointB.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Point B'**
|
||||
String get losPointB;
|
||||
|
||||
/// No description provided for @losAntennaA.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Antenna A: {value} {unit}'**
|
||||
String losAntennaA(String value, String unit);
|
||||
|
||||
/// No description provided for @losAntennaB.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Antenna B: {value} {unit}'**
|
||||
String losAntennaB(String value, String unit);
|
||||
|
||||
/// No description provided for @losRun.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run LOS'**
|
||||
String get losRun;
|
||||
|
||||
/// No description provided for @losNoElevationData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No elevation data'**
|
||||
String get losNoElevationData;
|
||||
|
||||
/// No description provided for @losProfileClear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}'**
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
);
|
||||
|
||||
/// No description provided for @losProfileBlocked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}'**
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
);
|
||||
|
||||
/// No description provided for @losStatusChecking.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS: checking...'**
|
||||
String get losStatusChecking;
|
||||
|
||||
/// No description provided for @losStatusNoData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS: no data'**
|
||||
String get losStatusNoData;
|
||||
|
||||
/// No description provided for @losStatusSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown'**
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown);
|
||||
|
||||
/// No description provided for @losErrorElevationUnavailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Elevation data unavailable for one or more samples.'**
|
||||
String get losErrorElevationUnavailable;
|
||||
|
||||
/// No description provided for @losErrorInvalidInput.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid points/elevation data for LOS calculation.'**
|
||||
String get losErrorInvalidInput;
|
||||
|
||||
/// No description provided for @losRenameCustomPoint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Rename custom point'**
|
||||
String get losRenameCustomPoint;
|
||||
|
||||
/// No description provided for @losPointName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Point name'**
|
||||
String get losPointName;
|
||||
|
||||
/// No description provided for @losShowPanelTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show LOS panel'**
|
||||
String get losShowPanelTooltip;
|
||||
|
||||
/// No description provided for @losHidePanelTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hide LOS panel'**
|
||||
String get losHidePanelTooltip;
|
||||
|
||||
/// No description provided for @losElevationAttribution.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Elevation data: Open-Meteo (CC BY 4.0)'**
|
||||
String get losElevationAttribution;
|
||||
|
||||
/// No description provided for @losLegendRadioHorizon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio horizon'**
|
||||
String get losLegendRadioHorizon;
|
||||
|
||||
/// No description provided for @losLegendLosBeam.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LOS beam'**
|
||||
String get losLegendLosBeam;
|
||||
|
||||
/// No description provided for @losLegendTerrain.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Terrain'**
|
||||
String get losLegendTerrain;
|
||||
|
||||
/// No description provided for @losFrequencyLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Frequency'**
|
||||
String get losFrequencyLabel;
|
||||
|
||||
/// No description provided for @losFrequencyInfoTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'View calculation details'**
|
||||
String get losFrequencyInfoTooltip;
|
||||
|
||||
/// No description provided for @losFrequencyDialogTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio horizon calculation'**
|
||||
String get losFrequencyDialogTitle;
|
||||
|
||||
/// Explain how the calculation uses the baseline frequency and derived k-factor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'**
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
);
|
||||
|
||||
/// No description provided for @contacts_pathTrace.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -4987,6 +5368,18 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'meshcore-open GPX map data export'**
|
||||
String get settings_gpxExportShareSubject;
|
||||
|
||||
/// No description provided for @snrIndicator_nearByRepeaters.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nearby Repeaters'**
|
||||
String get snrIndicator_nearByRepeaters;
|
||||
|
||||
/// No description provided for @snrIndicator_lastSeen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last seen'**
|
||||
String get snrIndicator_lastSeen;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Сканирай';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Бързо превключване';
|
||||
|
||||
@@ -316,6 +326,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Име';
|
||||
|
||||
@@ -340,15 +354,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Предварителни настройки';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Честота (MHz)';
|
||||
|
||||
@@ -377,10 +382,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Дълъг обхват';
|
||||
String get settings_clientRepeat => 'Без електричество – повторение';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Бърза скорост';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -456,6 +466,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Украински';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Разрешаване на проследяване на съобщения';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Уведомления';
|
||||
|
||||
@@ -616,6 +634,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'единици';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Метрика (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Няма избрана област';
|
||||
|
||||
@@ -654,7 +681,35 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
'Контактите ще се появят, когато устройствата рекламират.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Търсене на контакти...';
|
||||
String get contacts_unread => 'Непрочетено';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Търси контакти...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Търсене на контакти...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Търсене на $number$str любими...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Търсене на $number$str потребители...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Търсене на $number$str повтарящи се...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Търсене на $number$str сървъри в стаята...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
||||
@@ -779,6 +834,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Редактирай канал';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Заглуши канала';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Включи известията на канала';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Изтрий канала';
|
||||
|
||||
@@ -787,6 +848,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Неуспешно изтриване на канала \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Каналът \"$name\" е изтрит';
|
||||
@@ -1074,6 +1140,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Управление на пътища';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Покажи всички пътища';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим на маршрутизиране';
|
||||
|
||||
@@ -1234,6 +1303,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Карта на възлите';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Линия на видимост';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Линия на видимост';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
||||
|
||||
@@ -1363,6 +1438,19 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Натиснете върху възлите, за да ги добавите към пътя.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Изпълни Път на Следване';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Премахни Последно';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Кеш на офлайн карти';
|
||||
|
||||
@@ -1658,10 +1746,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Съседи';
|
||||
String get repeater_neighbors => 'Съседи';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Преглед на съседни възли с нулев скок.';
|
||||
|
||||
@override
|
||||
@@ -2361,7 +2449,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
|
||||
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Няма налични данни за съседи.';
|
||||
@@ -2668,6 +2756,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Всички';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Любими';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Добави към любими';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Потребители';
|
||||
|
||||
@@ -2699,6 +2796,147 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Един или повече от хмелите липсва местоположение!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Проверката на пряката видимост е неуспешна: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Изчистете всички точки';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Стартирайте LOS, за да видите профила на надморската височина';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS меню';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Докоснете възли или натиснете продължително карта за персонализирани точки';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Показване на възли на дисплея';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Персонализирани точки';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Персонализирано $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Точка А';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Точка Б';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Антена A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Антена B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Стартирайте LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Няма данни за надморска височина';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: проверка...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: няма данни';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Няма налични данни за надморска височина за една или повече проби.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Име на точката';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Показване на LOS панел';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Скриване на LOS панела';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Радиохоризонт';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Линия на видимост';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Терен';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Честота';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Пътен проследяване';
|
||||
|
||||
@@ -2868,4 +3106,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open износ на данни за карта в формат GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последно видян';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scannen';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Schnelles Umschalten';
|
||||
|
||||
@@ -244,10 +254,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Sende eine Ankündigung';
|
||||
String get settings_sendAdvertisement => 'Sende Ankündigung';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung';
|
||||
String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung';
|
||||
|
||||
@override
|
||||
String get settings_advertisementSent => 'Ankündigung gesendet';
|
||||
@@ -267,7 +277,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_refreshContactsSubtitle =>
|
||||
'Kontakte-Liste vom Gerät neu laden';
|
||||
'Kontakt-Liste vom Gerät neu laden';
|
||||
|
||||
@override
|
||||
String get settings_rebootDevice => 'Gerät neu starten';
|
||||
@@ -310,6 +320,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS-Höhendaten: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Name';
|
||||
|
||||
@@ -334,15 +348,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Voreinstellungen';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequenz (MHz)';
|
||||
|
||||
@@ -371,10 +376,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Grosse Reichweite';
|
||||
String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Schnelle Geschwindigkeit';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -450,6 +460,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainisch';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Nachrichtenverfolgung aktivieren';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Benachrichtigungen';
|
||||
|
||||
@@ -613,6 +631,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Einheiten';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrisch (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt';
|
||||
|
||||
@@ -650,7 +677,35 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Suche Kontakte...';
|
||||
String get contacts_unread => 'Ungelesen';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Kontakte suchen...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Suche Kontakte...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Suche $number$str Favoriten...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Suche $number$str Benutzer...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Suche $number$str Repeater...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Suche $number$str Raumserver...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte';
|
||||
@@ -776,6 +831,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Kanal bearbeiten';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Kanal stummschalten';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Lösche den Kanal';
|
||||
|
||||
@@ -784,6 +845,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kanal $name konnte nicht gelöscht werden';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanal \"$name\" gelöscht';
|
||||
@@ -1074,6 +1140,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Pfadverwaltung';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routenmodus';
|
||||
|
||||
@@ -1086,7 +1155,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Aktuelle ACK-Pfade (tasten, um zu verwenden):';
|
||||
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
@@ -1117,7 +1186,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
|
||||
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Pfadaktionen:';
|
||||
@@ -1233,6 +1302,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Karte';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Sichtlinie';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Sichtlinie';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten';
|
||||
|
||||
@@ -1362,6 +1437,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Repeater verwalten';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Pfadverlauf ausführen';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Letztes Entfernen';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline-Karten-Cache';
|
||||
|
||||
@@ -1418,7 +1506,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String mapCache_estimatedTiles(int count) {
|
||||
return 'Geschätzte Fliesen: $count';
|
||||
return 'Geschätzte Kacheln: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1592,7 +1680,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.';
|
||||
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
@@ -1657,10 +1745,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Nachbarn';
|
||||
String get repeater_neighbors => 'Nachbarn';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
||||
String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Einstellungen';
|
||||
@@ -1689,7 +1777,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_statusRequestTimeout =>
|
||||
'Statusanfrage zeitweise fehlgeschlagen.';
|
||||
'Statusanfrage durch Timeout fehlgeschlagen.';
|
||||
|
||||
@override
|
||||
String repeater_errorLoadingStatus(String error) {
|
||||
@@ -1766,7 +1854,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String repeater_duplicatesFloodDirect(String flood, String direct) {
|
||||
return 'Überflut: $flood, Direkt: $direct';
|
||||
return 'Flut: $flood, Direkt: $direct';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1797,7 +1885,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_guestPasswordHelper =>
|
||||
'Schreibgeschützter Zugriffspasswort';
|
||||
'Schreibgeschütztes Zugriffspasswort';
|
||||
|
||||
@override
|
||||
String get repeater_radioSettings => 'Funk Einstellungen';
|
||||
@@ -1992,7 +2080,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get repeater_cliTitle => 'Repeater CLI';
|
||||
|
||||
@override
|
||||
String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl';
|
||||
String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls';
|
||||
|
||||
@override
|
||||
String get repeater_commandHelp => 'Hilfe';
|
||||
@@ -2005,7 +2093,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_typeCommandOrUseQuick =>
|
||||
'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle';
|
||||
'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle';
|
||||
|
||||
@override
|
||||
String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...';
|
||||
@@ -2131,7 +2219,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetRxDelay =>
|
||||
'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
|
||||
'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetTxDelay =>
|
||||
@@ -2175,7 +2263,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetBridgeType =>
|
||||
'Ruft Brückentyp none, rs232, espnow ab.';
|
||||
'Ruft Brückentyp: none, rs232, espnow ab.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpLogStart =>
|
||||
@@ -2202,7 +2290,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionLoad =>
|
||||
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.';
|
||||
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionGet =>
|
||||
@@ -2351,10 +2439,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_receivedData => 'Empfangene Nachbarendaten';
|
||||
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
|
||||
|
||||
@override
|
||||
String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.';
|
||||
String get neighbors_requestTimedOut =>
|
||||
'Anfrage durch Timeout fehlgeschlagen.';
|
||||
|
||||
@override
|
||||
String neighbors_errorLoading(String error) {
|
||||
@@ -2362,19 +2451,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Nachbarn';
|
||||
String get neighbors_repeatersNeighbors => 'Nachbarn';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
|
||||
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
||||
|
||||
@override
|
||||
String neighbors_unknownContact(String pubkey) {
|
||||
return 'Unbekannte $pubkey';
|
||||
return 'Unbekannt $pubkey';
|
||||
}
|
||||
|
||||
@override
|
||||
String neighbors_heardAgo(String time) {
|
||||
return 'Hörte: $time vor her.';
|
||||
return 'Gehört vor: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2394,7 +2483,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Die Detailangaben für dieses Paket sind nicht verfügbar.';
|
||||
|
||||
@override
|
||||
String get channelPath_messageDetails => 'Nachrichtsdetails';
|
||||
String get channelPath_messageDetails => 'Nachrichtendetails';
|
||||
|
||||
@override
|
||||
String get channelPath_senderLabel => 'Sender';
|
||||
@@ -2630,14 +2719,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Füge einen Hashtag-Kanal für diese Community hinzu';
|
||||
|
||||
@override
|
||||
String get community_selectCommunity => 'Wählen Sie Community';
|
||||
String get community_selectCommunity => 'Wählen Sie eine Community';
|
||||
|
||||
@override
|
||||
String get community_regularHashtag => 'Regulärer Hashtag';
|
||||
|
||||
@override
|
||||
String get community_regularHashtagDesc =>
|
||||
'Öffentliches Hashtag (jeder kann teilnehmen)';
|
||||
'Öffentlicher Hashtag (jeder kann teilnehmen)';
|
||||
|
||||
@override
|
||||
String get community_communityHashtag => 'Community Hashtag';
|
||||
@@ -2672,6 +2761,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Alle';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favoriten';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Benutzer';
|
||||
|
||||
@@ -2682,7 +2780,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get listFilter_roomServers => 'Raumserver';
|
||||
|
||||
@override
|
||||
String get listFilter_unreadOnly => 'Nur nicht gelesen';
|
||||
String get listFilter_unreadOnly => 'Nicht gelesen';
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Neue Gruppe';
|
||||
@@ -2701,7 +2799,149 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Eine oder mehrere der Hopfen fehlen einen Standort!';
|
||||
'Bei einer oder mehreren Knoten fehlt der Standort!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Pfad löschen';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Wählen Sie Start- und Endknoten für LOS aus.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Sichtlinienprüfung fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Löschen Sie alle Punkte';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Führen Sie LOS aus, um das Höhenprofil anzuzeigen';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS-Menü';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Anzeigeknoten anzeigen';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Benutzerdefinierte Punkte';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Benutzerdefiniert $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punkt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punkt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenne A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenne B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Führen Sie LOS aus';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Keine Höhendaten';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: Überprüfen...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: keine Daten';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Ungültige Punkte/Höhendaten für die LOS-Berechnung.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint =>
|
||||
'Benennen Sie den benutzerdefinierten Punkt um';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Punktname';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'LOS-Panel anzeigen';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'LOS-Panel ausblenden';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Funkhorizont';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Sichtlinie';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Gelände';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenz';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Pfadverfolgung';
|
||||
@@ -2743,14 +2983,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Kontakt konnte nicht importiert werden';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige';
|
||||
String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Überflutungsanzeige';
|
||||
String get contacts_floodAdvert => 'Flut-Ankündigung';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard =>
|
||||
'Werbung in die Zwischenablage kopieren';
|
||||
'Ankündigung in die Zwischenablage kopieren';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
@@ -2776,7 +3016,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.';
|
||||
'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'MeshCore Aktivität';
|
||||
@@ -2824,28 +3064,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Repeater und Raumserver nach GPX exportieren';
|
||||
'Repeater und Raumserver als GPX exportieren';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren';
|
||||
String get settings_gpxExportContacts => 'Kontakte als GPX exportieren';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exportiert Begleiter mit einem Ort in eine GPX-Datei.';
|
||||
'Exportiert Kontakte mit einem Ort in eine GPX-Datei.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren';
|
||||
String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.';
|
||||
'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.';
|
||||
String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.';
|
||||
@@ -2863,16 +3103,22 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Repeater- und Raumserver-Standorte';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Begleiterstandorte';
|
||||
String get settings_gpxExportChat => 'Kontaktstandorte';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Kartendaten aus meshcore-open exportiert';
|
||||
'GPX-Kartendaten aus meshcore-open exportiert';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open GPX-Kartendaten exportieren';
|
||||
'GPX-Kartendaten aus meshcore-open exportieren';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scan';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth is off';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Please turn on Bluetooth to scan for devices';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Quick switch';
|
||||
|
||||
@@ -308,6 +318,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS elevation data: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Name';
|
||||
|
||||
@@ -332,15 +346,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequency (MHz)';
|
||||
|
||||
@@ -369,10 +374,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Long Range';
|
||||
String get settings_clientRepeat => 'Off-Grid Repeat';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Fast Speed';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Allow this device to repeat mesh packets for others';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -448,6 +458,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Show detailed routing and timing metadata for messages';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notifications';
|
||||
|
||||
@@ -608,6 +625,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Units';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metric (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'No area selected';
|
||||
|
||||
@@ -644,7 +670,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'Contacts will appear when devices advertise';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Search contacts...';
|
||||
String get contacts_unread => 'Unread';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Search Contacts...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Search $number$str Contacts...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Search $number$str Favorites...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Search $number$str Users...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Search $number$str Repeaters...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Search $number$str Room servers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'No unread contacts';
|
||||
@@ -768,6 +822,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Edit channel';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Mute channel';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Unmute channel';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Delete channel';
|
||||
|
||||
@@ -776,6 +836,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Delete \"$name\"? This cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Failed to delete channel \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Channel \"$name\" deleted';
|
||||
@@ -1059,6 +1124,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Path Management';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Show all paths';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routing mode';
|
||||
|
||||
@@ -1213,6 +1281,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Node Map';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'No nodes with location data';
|
||||
|
||||
@@ -1342,6 +1416,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Manage Repeater';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Run Path Trace';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remove Last';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Path trace cancelled.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Map Cache';
|
||||
|
||||
@@ -1632,10 +1718,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Neighbors';
|
||||
String get repeater_neighbors => 'Neighbors';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
|
||||
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Settings';
|
||||
@@ -2311,10 +2397,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_receivedData => 'Received Neighbours Data';
|
||||
String get neighbors_receivedData => 'Received Neighbors Data';
|
||||
|
||||
@override
|
||||
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
|
||||
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
|
||||
|
||||
@override
|
||||
String neighbors_errorLoading(String error) {
|
||||
@@ -2322,10 +2408,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
|
||||
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'No neighbours data available.';
|
||||
String get neighbors_noData => 'No neighbors data available.';
|
||||
|
||||
@override
|
||||
String neighbors_unknownContact(String pubkey) {
|
||||
@@ -2628,6 +2714,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'All';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Add to favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Remove from favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Users';
|
||||
|
||||
@@ -2659,6 +2754,146 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'One or more of the hops is missing a location!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Clear path.';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Line-of-sight check failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Clear all points';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Run LOS to view elevation profile';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS Menu';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Show display nodes';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Custom points';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Custom $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Point A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Point B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenna A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenna B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Run LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'No elevation data';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: checking...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: no data';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Elevation data unavailable for one or more samples.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Invalid points/elevation data for LOS calculation.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Rename custom point';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Point name';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Show LOS panel';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Hide LOS panel';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Elevation data: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radio horizon';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'LOS beam';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequency';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'View calculation details';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Radio horizon calculation';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Path Trace';
|
||||
|
||||
@@ -2824,4 +3059,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open GPX map data export';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Last seen';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Escanea';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Cambiar rápidamente';
|
||||
|
||||
@@ -313,6 +323,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Nombre';
|
||||
|
||||
@@ -337,15 +351,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preajustes';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frecuencia (MHz)';
|
||||
|
||||
@@ -374,10 +379,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Largo Alcance';
|
||||
String get settings_clientRepeat => 'Repetir sin conexión';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocidad Rápida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -453,6 +463,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Habilitar seguimiento de mensajes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notificaciones';
|
||||
|
||||
@@ -614,6 +632,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unidades';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
|
||||
|
||||
@@ -651,7 +678,35 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Los contactos aparecerán cuando los dispositivos anuncien.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Buscar contactos...';
|
||||
String get contacts_unread => 'No leído';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Buscar contactos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Buscar $number$str Favoritos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Buscar $number$str Usuarios...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Buscar $number$str Repetidores...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Buscar $number$str servidores de sala...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'No contactos sin leer';
|
||||
@@ -777,6 +832,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Editar canal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenciar canal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Activar canal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Eliminar canal';
|
||||
|
||||
@@ -785,6 +846,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'No se pudo eliminar el canal \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canal \"$name\" eliminado';
|
||||
@@ -1073,6 +1139,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestión de Rutas';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de enrutamiento';
|
||||
|
||||
@@ -1231,6 +1300,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mapa de Nodos';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
|
||||
|
||||
@@ -1360,6 +1435,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gestionar Repetidor';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Eliminar último';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Caché de Mapa Offline';
|
||||
|
||||
@@ -1656,10 +1743,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vecinos';
|
||||
String get repeater_neighbors => 'Vecinos';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
|
||||
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Configuración';
|
||||
@@ -2358,7 +2445,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
|
||||
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
||||
@@ -2667,6 +2754,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Todas';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Añadir a favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Usuarios';
|
||||
|
||||
@@ -2698,6 +2794,149 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Uno o más de los lúpulos carecen de una ubicación';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Borrar ruta';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Seleccione los nodos de inicio y fin para LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Error en la comprobación de la línea de visión: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Borrar todos los puntos';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Ejecute LOS para ver el perfil de elevación';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menú LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Puntos personalizados';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Personalizado $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punto A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punto B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Ejecutar LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Sin datos de elevación';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: comprobando...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: sin datos';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Datos de elevación no disponibles para una o más muestras.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint =>
|
||||
'Cambiar el nombre del punto personalizado';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nombre del punto';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Mostrar panel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Ocultar panel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Datos de elevación: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frecuencia';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Rastreo de caminos';
|
||||
|
||||
@@ -2868,4 +3107,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open exportación de datos de mapa GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scanner';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Basculement rapide';
|
||||
|
||||
@@ -314,6 +324,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Nom';
|
||||
|
||||
@@ -338,15 +352,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Préréglages';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Fréquence (MHz)';
|
||||
|
||||
@@ -375,10 +380,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Portée Longue';
|
||||
String get settings_clientRepeat => 'Répétition hors réseau';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Vitesse Rapide';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permettez à cet appareil de répéter les paquets de données pour les autres.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -454,6 +464,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainien';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Activer le traçage des messages';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Afficher les métadonnées détaillées de routage et de synchronisation des messages';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notifications';
|
||||
|
||||
@@ -560,11 +578,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get appSettings_mapDisplay => 'Affichage de la carte';
|
||||
|
||||
@override
|
||||
String get appSettings_showRepeaters => 'Afficher les répétiteurs';
|
||||
String get appSettings_showRepeaters => 'Afficher les répéteurs';
|
||||
|
||||
@override
|
||||
String get appSettings_showRepeatersSubtitle =>
|
||||
'Afficher les nœuds répétiteurs sur la carte';
|
||||
'Afficher les nœuds répéteurs sur la carte';
|
||||
|
||||
@override
|
||||
String get appSettings_showChatNodes => 'Afficher les nœuds de discussion';
|
||||
@@ -616,6 +634,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unités';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Métrique (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Impérial (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Aucune zone sélectionnée';
|
||||
|
||||
@@ -654,7 +681,35 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Les contacts apparaîtront lorsque les appareils font leur annonce.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Rechercher des contacts...';
|
||||
String get contacts_unread => 'Non lu';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Rechercher des contacts...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Rechercher des contacts...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Rechercher $number$str Favoris...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Rechercher $number$str utilisateurs...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Rechercher $number$str Répéteurs...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Rechercher $number$str serveurs de salle...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Aucun contact non lu';
|
||||
@@ -671,13 +726,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_manageRepeater => 'Gérer le répétiteur';
|
||||
String get contacts_manageRepeater => 'Gérer le répéteur';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gérer le Room Server';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Connexion Salle';
|
||||
String get contacts_roomLogin => 'Connexion Room Server';
|
||||
|
||||
@override
|
||||
String get contacts_openChat => 'Ouverture du Chat';
|
||||
@@ -779,6 +834,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Modifier le canal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Désactiver les notifications du canal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Réactiver les notifications du canal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Supprimer le canal';
|
||||
|
||||
@@ -787,6 +848,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Supprimer $name? Cela ne peut pas être annulé.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Échec de la suppression de la chaîne \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Le canal \"$name\" a été supprimé';
|
||||
@@ -1076,6 +1142,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestion des chemins';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Afficher tous les chemins';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Mode de routage';
|
||||
|
||||
@@ -1094,18 +1163,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'Sautez';
|
||||
String get chat_hopSingular => 'saut';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'sautez';
|
||||
String get chat_hopPlural => 'sauts';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
other: 'sauts',
|
||||
one: 'saut',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
@@ -1237,6 +1306,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Carte des nœuds';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Ligne de vue';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Ligne de vue';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation =>
|
||||
'Aucun nœud avec des données de localisation';
|
||||
@@ -1259,7 +1334,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_chat => 'Chat';
|
||||
|
||||
@override
|
||||
String get map_repeater => 'Répétiteur';
|
||||
String get map_repeater => 'Répéteur';
|
||||
|
||||
@override
|
||||
String get map_room => 'Salle';
|
||||
@@ -1365,7 +1440,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_joinRoom => 'Rejoindre la salle';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Gérer le répétiteur';
|
||||
String get map_manageRepeater => 'Gérer le répéteur';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Appuyez sur les nœuds pour les ajouter au chemin.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Exécuter la traçage de chemin';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Supprimer le dernier';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Traçage de chemin annulé';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Cache de Carte Hors Ligne';
|
||||
@@ -1509,10 +1597,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
|
||||
|
||||
@override
|
||||
String get login_repeaterLogin => 'Connexion au répétiteur';
|
||||
String get login_repeaterLogin => 'Connexion au répéteur';
|
||||
|
||||
@override
|
||||
String get login_roomLogin => 'Connexion Salle';
|
||||
String get login_roomLogin => 'Connexion Room Server';
|
||||
|
||||
@override
|
||||
String get login_password => 'Mot de passe';
|
||||
@@ -1529,7 +1617,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_repeaterDescription =>
|
||||
'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.';
|
||||
'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.';
|
||||
|
||||
@override
|
||||
String get login_roomDescription =>
|
||||
@@ -1634,10 +1722,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get path_setPath => 'Définir le chemin';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Gestion des répétiteurs';
|
||||
String get repeater_management => 'Gestion des répéteurs';
|
||||
|
||||
@override
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
String get room_management => 'Administrattion Room Server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Outils de Gestion';
|
||||
@@ -1647,7 +1735,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_statusSubtitle =>
|
||||
'Afficher l\'état, les statistiques et les voisins du répétiteur';
|
||||
'Afficher l\'état, les statistiques et les voisins du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_telemetry => 'Télémetrie';
|
||||
@@ -1660,24 +1748,23 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_cli => 'CLI';
|
||||
|
||||
@override
|
||||
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
|
||||
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Voisins';
|
||||
String get repeater_neighbors => 'Voisins';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
'Afficher les voisins de saut nuls.';
|
||||
String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Paramètres';
|
||||
|
||||
@override
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Configurer les paramètres du répétiteur';
|
||||
'Configurer les paramètres du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'État du répétiteur';
|
||||
String get repeater_statusTitle => 'État du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_routingMode => 'Mode de routage';
|
||||
@@ -1783,16 +1870,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_settingsTitle => 'Paramètres du répétiteur';
|
||||
String get repeater_settingsTitle => 'Paramètres du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_basicSettings => 'Paramètres de base';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterName => 'Nom du répétiteur';
|
||||
String get repeater_repeaterName => 'Nom du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur';
|
||||
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_adminPassword => 'Mot de passe Administrateur';
|
||||
@@ -1856,7 +1943,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_packetForwardingSubtitle =>
|
||||
'Activer le répétiteur pour transmettre des paquets';
|
||||
'Activer le répéteur pour transmettre des paquets';
|
||||
|
||||
@override
|
||||
String get repeater_guestAccess => 'Accès Invité';
|
||||
@@ -1905,11 +1992,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_rebootRepeaterSubtitle =>
|
||||
'Réinitialiser l\'appareil répétiteur';
|
||||
'Réinitialiser l\'appareil répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_rebootRepeaterConfirm =>
|
||||
'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?';
|
||||
'Êtes-vous sûr de vouloir redémarrer ce répéteur ?';
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité';
|
||||
@@ -1920,18 +2007,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKeyConfirm =>
|
||||
'Cela générera une nouvelle identité pour le répétiteur. Continuer ?';
|
||||
'Cela générera une nouvelle identité pour le répéteur. Continuer ?';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystem => 'Supprimer le système de fichiers';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystemSubtitle =>
|
||||
'Formater le système de fichiers du répétiteur';
|
||||
'Formater le système de fichiers du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystemConfirm =>
|
||||
'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !';
|
||||
'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !';
|
||||
|
||||
@override
|
||||
String get repeater_eraseSerialOnly =>
|
||||
@@ -1999,7 +2086,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_cliTitle => 'Répétiteur CLI';
|
||||
String get repeater_cliTitle => 'Répéteur CLI';
|
||||
|
||||
@override
|
||||
String get repeater_debugNextCommand => 'Déboguer Prochaine Commande';
|
||||
@@ -2091,7 +2178,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetRepeat =>
|
||||
'Active ou désactive le rôle du répétiteur pour ce nœud.';
|
||||
'Active ou désactive le rôle du répéteur pour ce nœud.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAllowReadOnly =>
|
||||
@@ -2115,7 +2202,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAdvertInterval =>
|
||||
'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.';
|
||||
'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetFloodAdvertInterval =>
|
||||
@@ -2201,7 +2288,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighbors =>
|
||||
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
|
||||
'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighborRemove =>
|
||||
@@ -2289,12 +2376,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_logging => 'Journalisation';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsRepeaterOnly =>
|
||||
'Voisins (Uniquement répétiteur)';
|
||||
String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)';
|
||||
|
||||
@override
|
||||
String get repeater_regionManagementRepeaterOnly =>
|
||||
'Gestion des régions (uniquement pour le répétiteur)';
|
||||
'Gestion des régions (uniquement pour le répéteur)';
|
||||
|
||||
@override
|
||||
String get repeater_regionNote =>
|
||||
@@ -2373,7 +2459,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
|
||||
String get neighbors_repeatersNeighbors => 'Répéteurs Voisins';
|
||||
|
||||
@override
|
||||
String get neighbors_noData =>
|
||||
@@ -2399,7 +2485,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get channelPath_otherObservedPaths => 'Autres chemins observés';
|
||||
|
||||
@override
|
||||
String get channelPath_repeaterHops => 'Sauts du répétiteur';
|
||||
String get channelPath_repeaterHops => 'Sauts du répéteur';
|
||||
|
||||
@override
|
||||
String get channelPath_noHopDetails =>
|
||||
@@ -2467,7 +2553,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get channelPath_noRepeaterLocations =>
|
||||
'Aucune position de répétiteur disponible pour ce chemin.';
|
||||
'Aucune position de répéteur disponible pour ce chemin.';
|
||||
|
||||
@override
|
||||
String channelPath_primaryPath(int index) {
|
||||
@@ -2684,6 +2770,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Tout';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Préférences';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Ajouter à mes favoris';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Supprimer des favoris';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Utilisateurs';
|
||||
|
||||
@@ -2713,7 +2808,149 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Une ou plusieurs des houblons manquent d\'une localisation !';
|
||||
'Un ou plusieurs des sauts manquent d\'une localisation !';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Effacer le chemin';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Sélectionnez les nœuds de début et de fin pour LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Échec de la vérification de la ligne de vue : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Effacer tous les points';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Exécutez LOS pour afficher le profil d\'altitude';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menu LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Points personnalisés';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Personnalisé $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Point A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Point B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenne A : $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenne B : $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Exécuter la LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Aucune donnée d\'altitude';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, bloqué par $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS : vérification...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS : aucune donnée';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Données d\'altitude indisponibles pour un ou plusieurs échantillons.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Données de points/d\'altitude non valides pour le calcul de la LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Renommer le point personnalisé';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nom du point';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Afficher le panneau LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Masquer le panneau LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Données d’altitude : Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horizon radio';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Ligne de visée';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Fréquence';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Voir les détails du calcul';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Calcul de l’horizon radio';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Traçage de chemin';
|
||||
@@ -2756,10 +2993,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Échec de l\'importation du contact.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Annonce Zero Hop';
|
||||
String get contacts_zeroHopAdvert => 'Annonce Zero saut';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Annonce de crue';
|
||||
String get contacts_floodAdvert => 'Annonce à tout le réseau';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard =>
|
||||
@@ -2892,4 +3129,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open exporter les données de carte GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scansiona';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Passa velocemente';
|
||||
|
||||
@@ -312,6 +322,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Nome';
|
||||
|
||||
@@ -336,15 +350,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preset';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequenza (MHz)';
|
||||
|
||||
@@ -373,10 +378,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lungo Raggio';
|
||||
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocità Rapida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -452,6 +462,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraino';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Abilita tracciamento messaggi';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Mostra metadati dettagliati su instradamento e tempi per i messaggi';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notifiche';
|
||||
|
||||
@@ -613,6 +631,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unità';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrico (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
||||
|
||||
@@ -650,7 +677,35 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Cerca contatti...';
|
||||
String get contacts_unread => 'Non letti';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Cerca contatti...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Cerca $number$str Preferiti...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Cerca $number$str Utenti...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Cerca $number$str Ripetitori...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Cerca $number$str server Room...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
||||
@@ -775,6 +830,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Modifica canale';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenzia canale';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Attiva notifiche canale';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Elimina canale';
|
||||
|
||||
@@ -783,6 +844,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Impossibile eliminare il canale \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canale \"$name\" eliminato';
|
||||
@@ -1071,6 +1137,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestione Percorsi';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modalità di routing';
|
||||
|
||||
@@ -1230,6 +1299,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mappa Nodi';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linea di vista';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linea di vista';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
||||
|
||||
@@ -1359,6 +1434,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gestisci Ripetitore';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Esegui Path Trace';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Rimuovi ultimo';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Cache Mappa Offline';
|
||||
|
||||
@@ -1654,10 +1741,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vicini';
|
||||
String get repeater_neighbors => 'Vicini';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Visualizza vicini di salto pari a zero.';
|
||||
|
||||
@override
|
||||
@@ -2358,7 +2445,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
|
||||
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
||||
@@ -2667,6 +2754,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Tutti';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Preferiti';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Utenti';
|
||||
|
||||
@@ -2699,6 +2795,148 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Uno o più dei luppoli mancano di una posizione!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Seleziona i nodi iniziali e finali per la LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Controllo della linea di vista fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Cancella tutti i punti';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Eseguire LOS per visualizzare il profilo altimetrico';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menù LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Punti personalizzati';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Personalizzato $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punto A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punto B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenna A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenna B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Esegui LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Nessun dato di elevazione';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: controllo...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: nessun dato';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Dati di elevazione non disponibili per uno o più campioni.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Dati punti/elevazione non validi per il calcolo della LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nome del punto';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Mostra il pannello LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Orizzonte radio';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Linea di vista';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenza';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Traccia Percorso';
|
||||
|
||||
@@ -2872,4 +3110,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open esportazione dati mappa GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scan';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snelle overschakeling';
|
||||
|
||||
@@ -310,6 +320,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Naam';
|
||||
|
||||
@@ -334,15 +348,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequentie (MHz)';
|
||||
|
||||
@@ -371,10 +376,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lange Afstand';
|
||||
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Hoge Snelheid';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -450,6 +460,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Gedetailleerde routerings- en timing-metadata voor berichten weergeven';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notificaties';
|
||||
|
||||
@@ -611,6 +628,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Eenheden';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrisch (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperiaal (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
|
||||
|
||||
@@ -648,7 +674,35 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Contacten verschijnen wanneer apparaten zich aanbieden.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Zoek contacten...';
|
||||
String get contacts_unread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Zoek contacten...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Zoek contacten...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Zoek $number$str favorieten...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Zoek $number$str gebruikers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Zoek $number$str Repeaters...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Zoek $number$str Room servers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
|
||||
@@ -773,6 +827,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Kanaal bewerken';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Kanaal dempen';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Kanaal dempen opheffen';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Kanaal verwijderen';
|
||||
|
||||
@@ -781,6 +841,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kan kanaal $name niet verwijderen';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanaal \"$name\" verwijderd';
|
||||
@@ -1068,6 +1133,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Beheer van Paden';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Toon alle paden';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routeerwijze';
|
||||
|
||||
@@ -1226,6 +1294,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Node Map';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
|
||||
|
||||
@@ -1355,6 +1429,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Beheer Repeater';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Tik op knooppunten om ze toe te voegen aan het pad';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Padeshulp traceren';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Verwijder Laatste';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Kaarten Cache';
|
||||
|
||||
@@ -1649,10 +1736,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Buren';
|
||||
String get repeater_neighbors => 'Buren';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Instellingen';
|
||||
@@ -2348,7 +2435,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
|
||||
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||
@@ -2658,6 +2745,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Alles';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Gebruikers';
|
||||
|
||||
@@ -2689,6 +2785,148 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Een of meer van de hops ontbreken een locatie!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Weg wissen';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Selecteer begin- en eindknooppunten voor LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Zichtlijncontrole mislukt: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Wis alle punten';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Voer LOS uit om het hoogteprofiel te bekijken';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS-menu';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tik op knooppunten of druk lang op de kaart voor aangepaste punten';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Toon weergaveknooppunten';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Aangepaste punten';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Aangepast $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenne A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenne B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Voer LOS uit';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Geen hoogtegegevens';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: controleren...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: geen gegevens';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Hoogtegegevens niet beschikbaar voor een of meer monsters.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Ongeldige punten/hoogtegegevens voor LOS-berekening.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Hernoem aangepast punt';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Puntnaam';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Toon LOS-paneel';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'LOS-paneel verbergen';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radiohorizon';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrein';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequentie';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Pad Traceren';
|
||||
|
||||
@@ -2859,4 +3097,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open GPX kaartgegevens exporteren';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Przeskanuj';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Włącz Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Szybka zmiana';
|
||||
|
||||
@@ -313,6 +323,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Imię';
|
||||
|
||||
@@ -337,15 +351,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preset';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Częstotliwość (MHz)';
|
||||
|
||||
@@ -375,10 +380,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Długi zasięg';
|
||||
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Szybka prędkość';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -454,6 +464,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukraińska';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Powiadomienia';
|
||||
|
||||
@@ -615,6 +632,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Bufor Map Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Jednostki';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metryczne (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
|
||||
|
||||
@@ -652,7 +678,35 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Wyszukaj kontakty...';
|
||||
String get contacts_unread => 'Nieprzeczytane';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Wyszukaj kontakty...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Wyszukaj $number$str ulubione...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Wyszukaj $number$str Użytkowników...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Wyszukaj $number$str powtórników...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Wyszukaj $number$str serwerów Room...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
|
||||
@@ -778,6 +832,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Edytuj kanał';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Wycisz kanał';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Usuń kanał';
|
||||
|
||||
@@ -786,6 +846,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Usuń \"$name\"? Nie można tego cofnąć.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Nie udało się usunąć kanału \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanał \"$name\" usunięto';
|
||||
@@ -1073,6 +1138,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Zarządzanie ścieżkami';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Tryb routingu';
|
||||
|
||||
@@ -1232,6 +1300,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mapa węzłów';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linia wzroku';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linia wzroku';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
|
||||
|
||||
@@ -1361,6 +1435,18 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Zarządzaj Powtórzami';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Uruchom ślad ścieżki';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Usuń ostatni';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Bufor Map Offline';
|
||||
|
||||
@@ -1658,10 +1744,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Sąsiedzi';
|
||||
String get repeater_neighbors => 'Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||
|
||||
@override
|
||||
@@ -2357,7 +2443,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
|
||||
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
||||
@@ -2666,6 +2752,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Wszystko';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Ulubione';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Użytkownicy';
|
||||
|
||||
@@ -2697,6 +2792,147 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Uruchom LOS, aby wyświetlić profil wysokości';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menu LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Punkty niestandardowe';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Niestandardowe $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punkt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punkt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Uruchom LOS-a';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Brak danych o wysokości';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: sprawdzam...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: brak danych';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nazwa punktu';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Pokaż panel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Ukryj panel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horyzont radiowy';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Linia widoczności';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Częstotliwość';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Śledzenie Ścieżek';
|
||||
|
||||
@@ -2874,4 +3110,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'Eksport danych mapy GPX meshcore-open';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Digitalizar';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth está desativado';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Mudar rapidamente';
|
||||
|
||||
@@ -314,6 +324,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Nome';
|
||||
|
||||
@@ -338,15 +352,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequência (MHz)';
|
||||
|
||||
@@ -375,10 +380,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Alcance Longo';
|
||||
String get settings_clientRepeat => 'Repetição sem rede';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocidade Rápida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -454,6 +464,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Ativar rastreamento de mensagens';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Mostrar metadados detalhados de roteamento e tempo para as mensagens';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notificações';
|
||||
|
||||
@@ -614,6 +632,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unidades';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
||||
|
||||
@@ -652,7 +679,35 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Pesquisar contatos...';
|
||||
String get contacts_unread => 'Não lido';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Pesquisar contatos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Pesquisar $number$str Favoritos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Pesquisar $number$str Usuários...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Pesquisar $number$str Repetidores...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Pesquisar $number$str servidores de sala...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
||||
@@ -778,6 +833,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Editar canal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenciar canal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Ativar canal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Excluir canal';
|
||||
|
||||
@@ -786,6 +847,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Falha ao excluir o canal \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canal \"$name\" excluído';
|
||||
@@ -1073,6 +1139,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de roteamento';
|
||||
|
||||
@@ -1231,6 +1300,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mapa de Nós';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linha de visão';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linha de visão';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation =>
|
||||
'Não existem nós com dados de localização.';
|
||||
@@ -1361,6 +1436,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gerenciar Repetidor';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Executar Traçado de Caminho';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remover Último';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Cache de Mapa Offline';
|
||||
|
||||
@@ -1656,11 +1743,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vizinhos';
|
||||
String get repeater_neighbors => 'Vizinhos';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
'Visualizar vizinhos de salto zero.';
|
||||
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Configurações';
|
||||
@@ -2359,7 +2445,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
|
||||
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
||||
@@ -2669,6 +2755,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Tudo';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Usuários';
|
||||
|
||||
@@ -2700,6 +2795,147 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Um ou mais dos lúpulos estão sem localização!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Limpar caminho';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Falha na verificação da linha de visão: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Limpe todos os pontos';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Execute o LOS para visualizar o perfil de elevação';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menu LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Mostrar nós de exibição';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Pontos personalizados';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return '$index personalizado';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Ponto A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Ponto B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Executar LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Sem dados de elevação';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: verificando...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: sem dados';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Dados de elevação indisponíveis para uma ou mais amostras.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Dados de pontos/elevação inválidos para cálculo de LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Renomear ponto personalizado';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nome do ponto';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Mostrar painel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Ocultar painel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Dados de elevação: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horizonte de rádio';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Linha de visada';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequência';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Traçado de Caminho';
|
||||
|
||||
@@ -2869,4 +3105,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open exportação de dados de mapa GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Сканирование';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth выключен';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Быстрое переключение';
|
||||
|
||||
@@ -311,6 +321,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Данные о высоте LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Имя';
|
||||
|
||||
@@ -335,15 +349,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Пресеты';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 МГц';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 МГц';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 МГц';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Частота (МГц)';
|
||||
|
||||
@@ -373,10 +378,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Недопустимая мощность передачи (0–22 дБм)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Дальний радиус';
|
||||
String get settings_clientRepeat => 'Повторение \"вне сети\"';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Высокая скорость';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Позвольте этому устройству повторять пакеты данных для других устройств.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -452,6 +462,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Включить трассировку сообщений';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Показывать подробные метаданные о маршрутизации и времени для сообщений';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Уведомления';
|
||||
|
||||
@@ -614,6 +632,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Единицы';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Метрическая (м/км)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Имперская (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Область не выбрана';
|
||||
|
||||
@@ -651,7 +678,35 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Контакты появятся, когда устройства начнут рассылать оповещения';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Поиск контактов...';
|
||||
String get contacts_unread => 'Непрочитанное';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Поиск контактов...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Поиск контактов...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Поиск $number$str избранного...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Поиск $number$str пользователей...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Поиск $number$str ретрансляторов...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Поиск $number$str серверов комнат...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
|
||||
@@ -776,6 +831,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Изменить канал';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Отключить уведомления канала';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Включить уведомления канала';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Удалить канал';
|
||||
|
||||
@@ -784,6 +845,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Удалить \"$name\"? Это действие нельзя отменить.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Не удалось удалить канал $name.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Канал \"$name\" удалён';
|
||||
@@ -1071,6 +1137,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Управление маршрутами';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Показать все пути';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим маршрутизации';
|
||||
|
||||
@@ -1233,6 +1302,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Карта нод';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Линия видимости';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Линия видимости';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
|
||||
|
||||
@@ -1362,6 +1437,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Управление репитером';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Запустить трассировку пути';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Удалить последний';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Отмена трассировки пути';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Кэш офлайн-карты';
|
||||
|
||||
@@ -1658,10 +1745,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Соседи';
|
||||
String get repeater_neighbors => 'Соседи';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
|
||||
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Настройки';
|
||||
@@ -2361,7 +2448,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
|
||||
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Данные о соседях недоступны.';
|
||||
@@ -2671,6 +2758,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Все';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Избранное';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Добавить в избранное';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Удалить из избранного';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Пользователи';
|
||||
|
||||
@@ -2702,6 +2798,147 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Одному или нескольким хмелям не указано местоположение!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Очистить путь';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Проверка прямой видимости не удалась: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Очистить все точки';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Запустите LOS, чтобы просмотреть профиль высот.';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'ЛОС Меню';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Показать узлы отображения';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Пользовательские точки';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Пользовательский $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Точка А';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Точка Б';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Антенна А: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Антенна Б: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Запустить ЛОС';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Нет данных о высоте';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, заблокирован $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'ЛОС: проверяю...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'ЛОС: нет данных';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Данные о высоте недоступны для одного или нескольких образцов.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Неверные данные о точках/высоте для расчета LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Переименовать пользовательскую точку';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Имя точки';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Показать панель LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Скрыть панель LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Данные о высоте: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Радиогоризонт';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Линия прямой видимости';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Рельеф';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Частота';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Трассировка пути';
|
||||
|
||||
@@ -2880,4 +3117,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open экспорт данных карты GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Skončiť';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Rýchle prepínač';
|
||||
|
||||
@@ -310,6 +320,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Meno';
|
||||
|
||||
@@ -334,15 +348,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Prednastavenia';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frekvencia (MHz)';
|
||||
|
||||
@@ -371,10 +376,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Dlhý dosah';
|
||||
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Rýchla rýchlosť';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -450,6 +460,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinská';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Zobraziť podrobné metadáta o smerovaní a časovaní správ';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Upozornenia';
|
||||
|
||||
@@ -608,6 +625,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Jednotky';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrické (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
||||
|
||||
@@ -645,7 +671,35 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
|
||||
String get contacts_unread => 'Neprečítané';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Vyhľadávajte kontakty...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Hľadať $number$str obľúbené...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Hľadať $number$str používateľov...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Hľadať $number$str opakovače...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Hľadaj $number$str serverov miestností...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
||||
@@ -773,6 +827,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Upraviť kanál';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Stlmiť kanál';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Odstrániť kanál';
|
||||
|
||||
@@ -781,6 +841,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kanál \"$name\" sa nepodarilo odstrániť';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanál \"$name\" bol odstránený';
|
||||
@@ -1068,6 +1133,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Správa ciest';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Režim trasy';
|
||||
|
||||
@@ -1227,6 +1295,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mapa uzlov';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
||||
|
||||
@@ -1356,6 +1430,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Spravovať Opakovanie';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Spustiť trasovaním cesty';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Odstrániť posledný';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Mapa Pamäť';
|
||||
|
||||
@@ -1651,10 +1737,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Súsezný';
|
||||
String get repeater_neighbors => 'Súsezný';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Nastavenia';
|
||||
@@ -2345,7 +2431,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
|
||||
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
|
||||
|
||||
@override
|
||||
String get neighbors_noData =>
|
||||
@@ -2654,6 +2740,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Všetko';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Obľúbené';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Používatelia';
|
||||
|
||||
@@ -2685,6 +2780,147 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Jedna alebo viac chmeľov chýba lokalita!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Kontrola priamej viditeľnosti zlyhala: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Vymazať všetky body';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Ak chcete zobraziť výškový profil, spustite LOS';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menu LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Vlastné body';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Vlastné $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Bod A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Bod B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Anténa A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Anténa B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Spustite LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: kontrolujem...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: žiadne údaje';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Premenovať vlastný bod';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Názov bodu';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Zobraziť panel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Skryť panel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Rádiový horizont';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Priama viditeľnosť';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terén';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvencia';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Sledovanie lúčov';
|
||||
|
||||
@@ -2856,4 +3092,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open export dát GPX mapových údajov';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Skeniraj';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth je izklopljen';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Hitro preklop';
|
||||
|
||||
@@ -309,6 +319,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Podatki o višini LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Ime';
|
||||
|
||||
@@ -333,15 +347,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Prednastavitve';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frekvenca (MHz)';
|
||||
|
||||
@@ -370,10 +375,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'DDolg doseg';
|
||||
String get settings_clientRepeat => 'Neovadno ponavljanje';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Visoka hitrost';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Omogočite temu naprave, da ponavlja paketne sporočila za druge.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -449,6 +459,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinsko';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Obvestila';
|
||||
|
||||
@@ -609,6 +626,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Enote';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrična (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperialno (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Območje ni izbrano';
|
||||
|
||||
@@ -646,7 +672,35 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
'Stiki se bodo prikazali, ko se naprave oglasijo.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Iskanje stikov...';
|
||||
String get contacts_unread => 'Neprebrano';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Iskanje stikov...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Iskanje stikov...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Iskanje $number$str priljubljenih...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Išči $number$str uporabnikov...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Išči $number$str ponavljalnike...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Išči $number$str strežnikov sob...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
|
||||
@@ -771,6 +825,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Uredi kanal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Utišaj kanal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Vklopi obvestila kanala';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Pošlji kanal';
|
||||
|
||||
@@ -779,6 +839,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Izbrišem \"$name\"? To se ne da povrniti.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kanala $name ni bilo mogoče izbrisati';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanal \"$name\" izbrisan.';
|
||||
@@ -1066,6 +1131,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Upravljanje poti';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Prikaži vse poti';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
||||
|
||||
@@ -1222,6 +1290,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Mapa omrežja';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linija vida';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linija vida';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation =>
|
||||
'Nihče od notranjih elementov nima podatkov o lokaciji.';
|
||||
@@ -1352,6 +1426,18 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Upravljajte Ponovitve';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Zaženi sledenje poti';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Odstrani Zadnji';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Spremljanje poti je prekinjeno.';
|
||||
|
||||
@override
|
||||
String get mapCache_title =>
|
||||
'Omrezni predpomnilnik zemljeških zemljejevskih slik';
|
||||
@@ -1650,10 +1736,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
'Pošlji ukazne povelje na ponovitveno enoto.';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Sosedi';
|
||||
String get repeater_neighbors => 'Sosedi';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
|
||||
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Nastavitve';
|
||||
@@ -2349,7 +2435,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
|
||||
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
||||
@@ -2657,6 +2743,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Vse';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Priljubljene';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Uporabniki';
|
||||
|
||||
@@ -2688,6 +2783,147 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Ena ali več hmelju manjka lokacija!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Počisti pot';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Preverjanje vidnega polja ni uspelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Počisti vse točke';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Zaženite LOS za ogled višinskega profila';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS meni';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Pokaži prikazna vozlišča';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Točke po meri';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Po meri $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Točka A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Točka B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Zaženi LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Ni podatkov o višini';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blokiral $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: preverjam ...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: ni podatkov';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Neveljavni podatki o točkah/višini za izračun LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Preimenujte točko po meri';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Ime točke';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Pokaži ploščo LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Skrij ploščo LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Podatki o višini: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radijski horizont';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Linija vidnosti';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvenca';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Sledenje poti';
|
||||
|
||||
@@ -2861,4 +3097,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open izvoz podatkov GPX karte';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Skanna';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snabb växling';
|
||||
|
||||
@@ -307,6 +317,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Namn';
|
||||
|
||||
@@ -331,15 +345,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Fördefinierade inställningar';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frekvens (MHz)';
|
||||
|
||||
@@ -368,10 +373,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lång räckvidd';
|
||||
String get settings_clientRepeat => 'Upprepa utan elnät';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Snabb hastighet';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Låt enheten repetera nätpaket för andra användare.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -447,6 +457,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainska';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Meddelanden';
|
||||
|
||||
@@ -604,6 +621,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Enheter';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metriskt (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
||||
|
||||
@@ -641,7 +667,35 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
'Kontakter kommer att visas när enheter annonserar.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Sök kontakter...';
|
||||
String get contacts_unread => 'Oläst';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Sök kontakter...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Sök $number$str Favoriter...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Sök $number$str användare...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Sök $number$str upprepningsenheter...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Sök $number$str Room-servrar...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
||||
@@ -767,6 +821,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Redigera kanal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Tysta kanal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Slå på ljud för kanal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Ta bort kanal';
|
||||
|
||||
@@ -775,6 +835,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Radera \"$name\"? Detta kan inte ångras.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Det gick inte att ta bort kanalen \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanalen \"$name\" raderad';
|
||||
@@ -1063,6 +1128,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Stigarhantering';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Visa alla vägar';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Ruttläge';
|
||||
|
||||
@@ -1219,6 +1287,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Nodkarta';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Synlinje';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Synlinje';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
||||
|
||||
@@ -1348,6 +1422,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Hantera Upprepare';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Kör spårsökning';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Ta bort sista';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Kartcache';
|
||||
|
||||
@@ -1640,10 +1726,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Grannar';
|
||||
String get repeater_neighbors => 'Grannar';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
|
||||
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Inställningar';
|
||||
@@ -2334,7 +2420,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
|
||||
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
||||
@@ -2642,6 +2728,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Alla';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favoriter';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Lägg till i favoriter';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Användare';
|
||||
|
||||
@@ -2673,6 +2768,145 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'En eller flera av humlen saknar en plats!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Rensa väg';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Synlinjekontroll misslyckades: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Rensa alla punkter';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS-menyn';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Visa displaynoder';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Anpassade poäng';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Anpassad $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punkt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punkt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenn A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenn B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Kör LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Inga höjddata';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: kollar...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: inga data';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Höjddata är inte tillgänglig för ett eller flera prover.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Ogiltiga poäng/höjddata för LOS-beräkning.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Punktnamn';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Visa LOS-panelen';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Dölj LOS-panelen';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radiohorisont';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Siktlinje';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terräng';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvens';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Path Trace';
|
||||
|
||||
@@ -2841,4 +3075,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open export av GPX-kartdata';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Сканувати';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth вимкнено';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Швидке перемикання';
|
||||
|
||||
@@ -312,6 +322,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Дані про висоту LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Ім\'я';
|
||||
|
||||
@@ -336,15 +350,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Попередні налаштування';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 МГц';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 МГц';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 МГц';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Частота (МГц)';
|
||||
|
||||
@@ -373,10 +378,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Дальній діапазон';
|
||||
String get settings_clientRepeat => 'Автономна система';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Висока швидкість';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -452,6 +462,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Увімкнути відстеження повідомлень';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Показувати детальні метадані про маршрутизацію та час для повідомлень';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Сповіщення';
|
||||
|
||||
@@ -612,6 +630,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'одиниці';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Метричний (м / км)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Імперська (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Область не вибрано';
|
||||
|
||||
@@ -649,7 +676,35 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
'Контакти з\'являться, коли пристрої надішлють оголошення.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Пошук контактів...';
|
||||
String get contacts_unread => 'Непрочитане';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Пошук контактів...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Пошук контактів...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Пошук $number$str улюблених...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Пошук $number$str користувачів...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Пошук $number$str ретрансляторів...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Пошук $number$str серверів кімнат...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
|
||||
@@ -774,6 +829,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Редагувати канал';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Вимкнути сповіщення каналу';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Увімкнути сповіщення каналу';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Видалити канал';
|
||||
|
||||
@@ -782,6 +843,11 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'Видалити $name? Це не можна скасувати.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Не вдалося видалити канал \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Канал «$name» видалено';
|
||||
@@ -1069,6 +1135,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Керування шляхами';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Показати всі шляхи';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим маршрутизації';
|
||||
|
||||
@@ -1231,6 +1300,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_title => 'Карта вузлів';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Пряма видимість';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Пряма видимість';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation =>
|
||||
'Немає вузлів з даними про розташування';
|
||||
@@ -1361,6 +1436,18 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Керувати ретранслятором';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Виконати трасування шляху';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Видалити останній';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Відмінується трасування шляху';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Офлайн-кеш карти';
|
||||
|
||||
@@ -1657,10 +1744,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Сусіди';
|
||||
String get repeater_neighbors => 'Сусіди';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Показати сусідів нульового стрибка.';
|
||||
|
||||
@override
|
||||
@@ -2362,7 +2449,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
|
||||
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
||||
@@ -2678,6 +2765,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Все';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Улюблені';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Додати до улюблених';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Користувачі';
|
||||
|
||||
@@ -2709,6 +2805,148 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Одне або більше хмелів відсутнє місце розташування!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Очистити шлях';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Виберіть початковий і кінцевий вузли для LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Помилка перевірки прямої видимості: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Очистити всі пункти';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Запустіть LOS, щоб переглянути профіль висоти';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Меню LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Торкніться вузлів або утримуйте карту, щоб отримати власні точки';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Показати вузли відображення';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Користувальницькі точки';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Спеціальний $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Точка А';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Точка Б';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Антена A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Антена B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Запустіть LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Немає даних про висоту';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, заблоковано $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: перевірка...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: немає даних';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Дані про висоту недоступні для одного чи кількох зразків.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Недійсні дані про точки/висоту для розрахунку LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Перейменуйте спеціальну точку';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Назва точки';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Показати панель LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Приховати панель LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Дані про висоту: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Радіогоризонт';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Лінія прямої видимості';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Рельєф';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Частота';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Трасування шляхів';
|
||||
|
||||
@@ -2886,4 +3124,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'експорт даних карти meshcore-open у форматі GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||
}
|
||||
|
||||
+725
-530
File diff suppressed because it is too large
Load Diff
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "nl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contacten",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Aantal Contacten",
|
||||
"settings_infoChannelCount": "Aantal Kanalen",
|
||||
"settings_presets": "Presets",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequentie (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Vermogen (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||
"settings_longRange": "Lange Afstand",
|
||||
"settings_fastSpeed": "Hoge Snelheid",
|
||||
"settings_error": "Fout: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Open kanaal",
|
||||
"channels_privateChannel": "Private kanaal",
|
||||
"channels_editChannel": "Kanaal bewerken",
|
||||
"channels_muteChannel": "Kanaal dempen",
|
||||
"channels_unmuteChannel": "Kanaal dempen opheffen",
|
||||
"channels_deleteChannel": "Kanaal verwijderen",
|
||||
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Buren",
|
||||
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
|
||||
"repeater_neighbors": "Buren",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||
"neighbors_repeatersNeighbours": "Herhalingen Buren",
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
@@ -1560,6 +1565,8 @@
|
||||
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||
"appSettings_languageRu": "Russisch",
|
||||
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
|
||||
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
|
||||
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
|
||||
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
|
||||
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
|
||||
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
|
||||
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Alle contactlocaties",
|
||||
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
|
||||
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
|
||||
"map_removeLast": "Verwijder Laatste",
|
||||
"pathTrace_clearTooltip": "Weg wissen",
|
||||
"map_pathTraceCancelled": "Pad traceren geannuleerd",
|
||||
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
|
||||
"map_runTrace": "Padeshulp traceren",
|
||||
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
||||
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
||||
"snrIndicator_lastSeen": "Laatst gezien",
|
||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||
"chat_ShowAllPaths": "Toon alle paden",
|
||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Eenheden",
|
||||
"appSettings_unitsMetric": "Metrisch (m / km)",
|
||||
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
|
||||
"map_lineOfSight": "Zichtlijn",
|
||||
"map_losScreenTitle": "Zichtlijn",
|
||||
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
|
||||
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Wis alle punten",
|
||||
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
|
||||
"losMenuTitle": "LOS-menu",
|
||||
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
|
||||
"losShowDisplayNodes": "Toon weergaveknooppunten",
|
||||
"losCustomPoints": "Aangepaste punten",
|
||||
"losCustomPointLabel": "Aangepast {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punt A",
|
||||
"losPointB": "Punt B",
|
||||
"losAntennaA": "Antenne A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenne B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Voer LOS uit",
|
||||
"losNoElevationData": "Geen hoogtegegevens",
|
||||
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: controleren...",
|
||||
"losStatusNoData": "LOS: geen gegevens",
|
||||
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
|
||||
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
|
||||
"losRenameCustomPoint": "Hernoem aangepast punt",
|
||||
"losPointName": "Puntnaam",
|
||||
"losShowPanelTooltip": "Toon LOS-paneel",
|
||||
"losHidePanelTooltip": "LOS-paneel verbergen",
|
||||
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Radiohorizon",
|
||||
"losLegendLosBeam": "Zichtlijn",
|
||||
"losLegendTerrain": "Terrein",
|
||||
"losFrequencyLabel": "Frequentie",
|
||||
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
|
||||
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
|
||||
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
|
||||
"listFilter_favorites": "Favorieten",
|
||||
"listFilter_addToFavorites": "Toevoegen aan favorieten",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Ongelezen",
|
||||
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
|
||||
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "pl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakty",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Liczba kontaktów",
|
||||
"settings_infoChannelCount": "Liczba kanałów",
|
||||
"settings_presets": "Preset",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Częstotliwość (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Moc (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
|
||||
"settings_longRange": "Długi zasięg",
|
||||
"settings_fastSpeed": "Szybka prędkość",
|
||||
"settings_error": "Błąd: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Kanał publiczny",
|
||||
"channels_privateChannel": "Prywatny kanał",
|
||||
"channels_editChannel": "Edytuj kanał",
|
||||
"channels_muteChannel": "Wycisz kanał",
|
||||
"channels_unmuteChannel": "Wyłącz wyciszenie kanału",
|
||||
"channels_deleteChannel": "Usuń kanał",
|
||||
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Sąsiedzi",
|
||||
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"repeater_neighbors": "Sąsiedzi",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
|
||||
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
|
||||
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
||||
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
||||
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
||||
@@ -1555,6 +1560,8 @@
|
||||
"contacts_chatTraceRoute": "Śledź trasę promienia",
|
||||
"appSettings_languageRu": "Rosyjski",
|
||||
"appSettings_languageUk": "Ukraińska",
|
||||
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
|
||||
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
|
||||
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
|
||||
"contacts_zeroHopAdvert": "Reklama Zero Hop",
|
||||
"contacts_floodAdvert": "Reklama powodziowa",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
|
||||
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
|
||||
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
|
||||
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
|
||||
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
|
||||
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportChat": "Lokalizacje towarzyszy",
|
||||
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
|
||||
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
|
||||
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
|
||||
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
|
||||
"map_runTrace": "Uruchom ślad ścieżki",
|
||||
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
|
||||
"map_removeLast": "Usuń ostatni",
|
||||
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
||||
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
||||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
|
||||
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
|
||||
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
|
||||
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Jednostki",
|
||||
"appSettings_unitsMetric": "Metryczne (m / km)",
|
||||
"appSettings_unitsImperial": "Imperialne (ft / mi)",
|
||||
"map_lineOfSight": "Linia wzroku",
|
||||
"map_losScreenTitle": "Linia wzroku",
|
||||
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
|
||||
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Wyczyść wszystkie punkty",
|
||||
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
|
||||
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
|
||||
"losCustomPoints": "Punkty niestandardowe",
|
||||
"losCustomPointLabel": "Niestandardowe {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punkt A",
|
||||
"losPointB": "Punkt B",
|
||||
"losAntennaA": "Antena A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antena B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Uruchom LOS-a",
|
||||
"losNoElevationData": "Brak danych o wysokości",
|
||||
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: sprawdzam...",
|
||||
"losStatusNoData": "LOS: brak danych",
|
||||
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
|
||||
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
|
||||
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
|
||||
"losPointName": "Nazwa punktu",
|
||||
"losShowPanelTooltip": "Pokaż panel LOS",
|
||||
"losHidePanelTooltip": "Ukryj panel LOS",
|
||||
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horyzont radiowy",
|
||||
"losLegendLosBeam": "Linia widoczności",
|
||||
"losLegendTerrain": "Teren",
|
||||
"losFrequencyLabel": "Częstotliwość",
|
||||
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
|
||||
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
|
||||
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Usuń z ulubionych",
|
||||
"listFilter_addToFavorites": "Dodaj do ulubionych",
|
||||
"listFilter_favorites": "Ulubione",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Nieprzeczytane",
|
||||
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
|
||||
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
||||
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
||||
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
|
||||
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "pt",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contactos",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Número de Contatos",
|
||||
"settings_infoChannelCount": "Número do Canal",
|
||||
"settings_presets": "Presets",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequência (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potência (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
|
||||
"settings_longRange": "Alcance Longo",
|
||||
"settings_fastSpeed": "Velocidade Rápida",
|
||||
"settings_error": "Erro: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal público",
|
||||
"channels_privateChannel": "Canal privado",
|
||||
"channels_editChannel": "Editar canal",
|
||||
"channels_muteChannel": "Silenciar canal",
|
||||
"channels_unmuteChannel": "Ativar canal",
|
||||
"channels_deleteChannel": "Excluir canal",
|
||||
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vizinhos",
|
||||
"repeater_neighbors": "Vizinhos",
|
||||
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
||||
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
|
||||
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
|
||||
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
||||
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
||||
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
|
||||
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
|
||||
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||
@@ -1561,6 +1566,8 @@
|
||||
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
||||
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
||||
"appSettings_languageRu": "Russo",
|
||||
"appSettings_enableMessageTracing": "Ativar rastreamento de mensagens",
|
||||
"appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens",
|
||||
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
||||
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
||||
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
|
||||
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
|
||||
"notification_receivedNewMessage": "Nova mensagem recebida",
|
||||
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
|
||||
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
|
||||
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Todos os locais de contatos",
|
||||
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
|
||||
"map_runTrace": "Executar Traçado de Caminho",
|
||||
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
|
||||
"pathTrace_clearTooltip": "Limpar caminho",
|
||||
"map_removeLast": "Remover Último",
|
||||
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
|
||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
|
||||
"snrIndicator_lastSeen": "Visto pela última vez",
|
||||
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
||||
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
||||
"settings_clientRepeat": "Repetição sem rede",
|
||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
|
||||
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unidades",
|
||||
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||
"map_lineOfSight": "Linha de visão",
|
||||
"map_losScreenTitle": "Linha de visão",
|
||||
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
|
||||
"losRunFailed": "Falha na verificação da linha de visão: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Limpe todos os pontos",
|
||||
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
|
||||
"losShowDisplayNodes": "Mostrar nós de exibição",
|
||||
"losCustomPoints": "Pontos personalizados",
|
||||
"losCustomPointLabel": "{index} personalizado",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Ponto A",
|
||||
"losPointB": "Ponto B",
|
||||
"losAntennaA": "Antena A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antena B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Executar LOS",
|
||||
"losNoElevationData": "Sem dados de elevação",
|
||||
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: verificando...",
|
||||
"losStatusNoData": "LOS: sem dados",
|
||||
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
|
||||
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
|
||||
"losRenameCustomPoint": "Renomear ponto personalizado",
|
||||
"losPointName": "Nome do ponto",
|
||||
"losShowPanelTooltip": "Mostrar painel LOS",
|
||||
"losHidePanelTooltip": "Ocultar painel LOS",
|
||||
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horizonte de rádio",
|
||||
"losLegendLosBeam": "Linha de visada",
|
||||
"losLegendTerrain": "Terreno",
|
||||
"losFrequencyLabel": "Frequência",
|
||||
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
|
||||
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
|
||||
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_addToFavorites": "Adicionar aos favoritos",
|
||||
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
|
||||
"listFilter_favorites": "Favoritos",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
|
||||
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
|
||||
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||
"contacts_unread": "Não lido",
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "ru",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Контакты",
|
||||
@@ -101,9 +109,6 @@
|
||||
"settings_infoContactsCount": "Количество контактов",
|
||||
"settings_infoChannelCount": "Количество каналов",
|
||||
"settings_presets": "Пресеты",
|
||||
"settings_preset915Mhz": "915 МГц",
|
||||
"settings_preset868Mhz": "868 МГц",
|
||||
"settings_preset433Mhz": "433 МГц",
|
||||
"settings_frequency": "Частота (МГц)",
|
||||
"settings_frequencyHelper": "300.0 – 2500.0",
|
||||
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
|
||||
@@ -113,8 +118,6 @@
|
||||
"settings_txPower": "Мощность передачи (дБм)",
|
||||
"settings_txPowerHelper": "0 – 22",
|
||||
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
|
||||
"settings_longRange": "Дальний радиус",
|
||||
"settings_fastSpeed": "Высокая скорость",
|
||||
"settings_error": "Ошибка: {message}",
|
||||
"appSettings_title": "Настройки приложения",
|
||||
"appSettings_appearance": "Внешний вид",
|
||||
@@ -231,6 +234,8 @@
|
||||
"channels_publicChannel": "Публичный канал",
|
||||
"channels_privateChannel": "Приватный канал",
|
||||
"channels_editChannel": "Изменить канал",
|
||||
"channels_muteChannel": "Отключить уведомления канала",
|
||||
"channels_unmuteChannel": "Включить уведомления канала",
|
||||
"channels_deleteChannel": "Удалить канал",
|
||||
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
|
||||
"channels_channelDeleted": "Канал \"{name}\" удалён",
|
||||
@@ -472,8 +477,8 @@
|
||||
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cliSubtitle": "Отправка команд репитеру",
|
||||
"repeater_neighbours": "Соседи",
|
||||
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||
"repeater_neighbors": "Соседи",
|
||||
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||
"repeater_settings": "Настройки",
|
||||
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
||||
"repeater_statusTitle": "Статус репитера",
|
||||
@@ -666,7 +671,7 @@
|
||||
"neighbors_receivedData": "Полученные данные о соседях",
|
||||
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
||||
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
||||
"neighbors_repeatersNeighbours": "Соседи репитеров",
|
||||
"neighbors_repeatersNeighbors": "Соседи репитеров",
|
||||
"neighbors_noData": "Данные о соседях недоступны.",
|
||||
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
||||
"neighbors_heardA ago": "Слышали: {time} назад",
|
||||
@@ -799,6 +804,8 @@
|
||||
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
||||
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
||||
"appSettings_languageUk": "Українська",
|
||||
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
|
||||
"contacts_floodAdvert": "Рекламный поток",
|
||||
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
||||
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
||||
@@ -815,7 +822,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}",
|
||||
"notification_newTypeDiscovered": "Обнаружен новый {contactType}",
|
||||
"notification_receivedNewMessage": "Получено новое сообщение",
|
||||
"contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.",
|
||||
"settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.",
|
||||
"settings_gpxExportContacts": "Экспортировать спутников в GPX",
|
||||
@@ -831,6 +837,207 @@
|
||||
"settings_gpxExportNoContacts": "Нет контактов для экспорта.",
|
||||
"settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX",
|
||||
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!",
|
||||
"map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.",
|
||||
"map_removeLast": "Удалить последний",
|
||||
"map_pathTraceCancelled": "Отмена трассировки пути",
|
||||
"pathTrace_clearTooltip": "Очистить путь",
|
||||
"map_runTrace": "Запустить трассировку пути",
|
||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||
"snrIndicator_lastSeen": "Последний раз видели",
|
||||
"chat_ShowAllPaths": "Показать все пути",
|
||||
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
||||
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
||||
"settings_clientRepeat": "Повторение \"вне сети\"",
|
||||
"settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Единицы",
|
||||
"appSettings_unitsMetric": "Метрическая (м/км)",
|
||||
"appSettings_unitsImperial": "Имперская (ft / mi)",
|
||||
"map_lineOfSight": "Линия видимости",
|
||||
"map_losScreenTitle": "Линия видимости",
|
||||
"losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.",
|
||||
"losRunFailed": "Проверка прямой видимости не удалась: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Очистить все точки",
|
||||
"losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.",
|
||||
"losMenuTitle": "ЛОС Меню",
|
||||
"losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.",
|
||||
"losShowDisplayNodes": "Показать узлы отображения",
|
||||
"losCustomPoints": "Пользовательские точки",
|
||||
"losCustomPointLabel": "Пользовательский {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Точка А",
|
||||
"losPointB": "Точка Б",
|
||||
"losAntennaA": "Антенна А: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Антенна Б: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Запустить ЛОС",
|
||||
"losNoElevationData": "Нет данных о высоте",
|
||||
"losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "ЛОС: проверяю...",
|
||||
"losStatusNoData": "ЛОС: нет данных",
|
||||
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.",
|
||||
"losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.",
|
||||
"losRenameCustomPoint": "Переименовать пользовательскую точку",
|
||||
"losPointName": "Имя точки",
|
||||
"losShowPanelTooltip": "Показать панель LOS",
|
||||
"losHidePanelTooltip": "Скрыть панель LOS",
|
||||
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Радиогоризонт",
|
||||
"losLegendLosBeam": "Линия прямой видимости",
|
||||
"losLegendTerrain": "Рельеф",
|
||||
"losFrequencyLabel": "Частота",
|
||||
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
|
||||
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
|
||||
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_addToFavorites": "Добавить в избранное",
|
||||
"listFilter_favorites": "Избранное",
|
||||
"listFilter_removeFromFavorites": "Удалить из избранного",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...",
|
||||
"contacts_searchContactsNoNumber": "Поиск контактов...",
|
||||
"contacts_unread": "Непрочитанное",
|
||||
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sk",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakty",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Počet kontaktov",
|
||||
"settings_infoChannelCount": "Počet kanálov",
|
||||
"settings_presets": "Prednastavenia",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvencia (MHz)",
|
||||
"settings_frequencyHelper": "300,0 – 2500,0",
|
||||
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Výkon (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
|
||||
"settings_longRange": "Dlhý dosah",
|
||||
"settings_fastSpeed": "Rýchla rýchlosť",
|
||||
"settings_error": "Chyba: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Veľké verejne kanály",
|
||||
"channels_privateChannel": "Osobné kanál",
|
||||
"channels_editChannel": "Upraviť kanál",
|
||||
"channels_muteChannel": "Stlmiť kanál",
|
||||
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
|
||||
"channels_deleteChannel": "Odstrániť kanál",
|
||||
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
|
||||
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
|
||||
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
||||
"neighbors_receivedData": "Obdielo dáta suseda",
|
||||
"repeater_neighbours": "Súsezný",
|
||||
"repeater_neighbors": "Súsezný",
|
||||
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
||||
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
|
||||
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
|
||||
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||
@@ -1561,6 +1566,8 @@
|
||||
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
||||
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
||||
"appSettings_languageRu": "Ruština",
|
||||
"appSettings_enableMessageTracing": "Povoliť sledovanie správ",
|
||||
"appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ",
|
||||
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
||||
"contacts_contactImported": "Kontakt bol importovaný.",
|
||||
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
|
||||
"notification_newTypeDiscovered": "Nový {contactType} objavený",
|
||||
"notification_receivedNewMessage": "Prijatá nová správa",
|
||||
"contacts_ShareContact": "Kopírovať kontakt do schránky",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
|
||||
"settings_gpxExportContacts": "Export sprievodcov do GPX",
|
||||
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportChat": "Lokácie sprievodcov",
|
||||
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
|
||||
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
|
||||
"pathTrace_clearTooltip": "Zmazať cestu",
|
||||
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
|
||||
"map_removeLast": "Odstrániť posledný",
|
||||
"map_runTrace": "Spustiť trasovaním cesty",
|
||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||
"snrIndicator_lastSeen": "Naposledy videný",
|
||||
"snrIndicator_nearByRepeaters": "Miestne opakovače",
|
||||
"chat_ShowAllPaths": "Zobraziť všetky cesty",
|
||||
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
||||
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
|
||||
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Jednotky",
|
||||
"appSettings_unitsMetric": "Metrické (m / km)",
|
||||
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
|
||||
"map_lineOfSight": "Line of Sight",
|
||||
"map_losScreenTitle": "Line of Sight",
|
||||
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
|
||||
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Vymazať všetky body",
|
||||
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
|
||||
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
|
||||
"losCustomPoints": "Vlastné body",
|
||||
"losCustomPointLabel": "Vlastné {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Bod A",
|
||||
"losPointB": "Bod B",
|
||||
"losAntennaA": "Anténa A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Anténa B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Spustite LOS",
|
||||
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
|
||||
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: kontrolujem...",
|
||||
"losStatusNoData": "LOS: žiadne údaje",
|
||||
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
|
||||
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
|
||||
"losRenameCustomPoint": "Premenovať vlastný bod",
|
||||
"losPointName": "Názov bodu",
|
||||
"losShowPanelTooltip": "Zobraziť panel LOS",
|
||||
"losHidePanelTooltip": "Skryť panel LOS",
|
||||
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Rádiový horizont",
|
||||
"losLegendLosBeam": "Priama viditeľnosť",
|
||||
"losLegendTerrain": "Terén",
|
||||
"losFrequencyLabel": "Frekvencia",
|
||||
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
|
||||
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
|
||||
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Odstrániť z označení",
|
||||
"listFilter_addToFavorites": "Pridaj do obľúbených",
|
||||
"listFilter_favorites": "Obľúbené",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
|
||||
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
|
||||
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||
"contacts_unread": "Neprečítané"
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Stiki",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Število stikov",
|
||||
"settings_infoChannelCount": "Število kanalov",
|
||||
"settings_presets": "Prednastavitve",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvenca (MHz)",
|
||||
"settings_frequencyHelper": "300,00 - 2500,00",
|
||||
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Moč (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
|
||||
"settings_longRange": "DDolg doseg",
|
||||
"settings_fastSpeed": "Visoka hitrost",
|
||||
"settings_error": "Napaka: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Javni kanal",
|
||||
"channels_privateChannel": "Zasebni kanal",
|
||||
"channels_editChannel": "Uredi kanal",
|
||||
"channels_muteChannel": "Utišaj kanal",
|
||||
"channels_unmuteChannel": "Vklopi obvestila kanala",
|
||||
"channels_deleteChannel": "Pošlji kanal",
|
||||
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||
"repeater_neighbours": "Sosedi",
|
||||
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||
"repeater_neighbors": "Sosedi",
|
||||
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
||||
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
||||
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
||||
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
|
||||
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
|
||||
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||
@@ -1555,6 +1560,8 @@
|
||||
"contacts_pathTraceTo": "Trace route to {name}",
|
||||
"appSettings_languageRu": "Ruščina",
|
||||
"appSettings_languageUk": "Ukrajinsko",
|
||||
"appSettings_enableMessageTracing": "Omogoči sledenje sporočilom",
|
||||
"appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil",
|
||||
"contacts_contactImported": "Kontakt je bil uvožen.",
|
||||
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
||||
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
|
||||
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
|
||||
"notification_receivedNewMessage": "Prejeto novo sporočilo",
|
||||
"contacts_ShareContact": "Kopiraj stik v Odložišče",
|
||||
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
|
||||
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
|
||||
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
|
||||
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
|
||||
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
|
||||
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
|
||||
"map_removeLast": "Odstrani Zadnji",
|
||||
"map_runTrace": "Zaženi sledenje poti",
|
||||
"pathTrace_clearTooltip": "Počisti pot",
|
||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||
"snrIndicator_lastSeen": "Zadnjič videno",
|
||||
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
||||
"chat_ShowAllPaths": "Prikaži vse poti",
|
||||
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
||||
"settings_clientRepeat": "Neovadno ponavljanje",
|
||||
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Enote",
|
||||
"appSettings_unitsMetric": "Metrična (m/km)",
|
||||
"appSettings_unitsImperial": "Imperialno (ft / mi)",
|
||||
"map_lineOfSight": "Linija vida",
|
||||
"map_losScreenTitle": "Linija vida",
|
||||
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
|
||||
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Počisti vse točke",
|
||||
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
|
||||
"losMenuTitle": "LOS meni",
|
||||
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
|
||||
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
|
||||
"losCustomPoints": "Točke po meri",
|
||||
"losCustomPointLabel": "Po meri {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Točka A",
|
||||
"losPointB": "Točka B",
|
||||
"losAntennaA": "Antena A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antena B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Zaženi LOS",
|
||||
"losNoElevationData": "Ni podatkov o višini",
|
||||
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: preverjam ...",
|
||||
"losStatusNoData": "LOS: ni podatkov",
|
||||
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
|
||||
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
|
||||
"losRenameCustomPoint": "Preimenujte točko po meri",
|
||||
"losPointName": "Ime točke",
|
||||
"losShowPanelTooltip": "Pokaži ploščo LOS",
|
||||
"losHidePanelTooltip": "Skrij ploščo LOS",
|
||||
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Radijski horizont",
|
||||
"losLegendLosBeam": "Linija vidnosti",
|
||||
"losLegendTerrain": "Teren",
|
||||
"losFrequencyLabel": "Frekvenca",
|
||||
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
|
||||
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
|
||||
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_favorites": "Priljubljene",
|
||||
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
|
||||
"listFilter_addToFavorites": "Dodaj v priljubljene",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Neprebrano",
|
||||
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
|
||||
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sv",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakter",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Kontakterantal",
|
||||
"settings_infoChannelCount": "Kanalantal",
|
||||
"settings_presets": "Fördefinierade inställningar",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvens (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX-effekt (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
|
||||
"settings_longRange": "Lång räckvidd",
|
||||
"settings_fastSpeed": "Snabb hastighet",
|
||||
"settings_error": "Fel: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Allmänt kanal",
|
||||
"channels_privateChannel": "Privat kanal",
|
||||
"channels_editChannel": "Redigera kanal",
|
||||
"channels_muteChannel": "Tysta kanal",
|
||||
"channels_unmuteChannel": "Slå på ljud för kanal",
|
||||
"channels_deleteChannel": "Ta bort kanal",
|
||||
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1356,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Grannar",
|
||||
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
|
||||
"repeater_neighbors": "Grannar",
|
||||
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
|
||||
"neighbors_receivedData": "Mottagna grannars data",
|
||||
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
||||
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
||||
"neighbors_repeatersNeighbours": "Upprepar grannar",
|
||||
"neighbors_repeatersNeighbors": "Upprepar grannar",
|
||||
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||
@@ -1561,6 +1566,8 @@
|
||||
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
||||
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
||||
"appSettings_languageUk": "Ukrainska",
|
||||
"appSettings_enableMessageTracing": "Aktivera meddelandespårning",
|
||||
"appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden",
|
||||
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
||||
"contacts_contactImported": "Kontakt har importerats.",
|
||||
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
|
||||
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
|
||||
"notification_receivedNewMessage": "Nytt meddelande mottaget",
|
||||
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
|
||||
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
|
||||
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportAllContacts": "Alla kontakters platser",
|
||||
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
|
||||
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
|
||||
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
|
||||
"pathTrace_clearTooltip": "Rensa väg",
|
||||
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
|
||||
"map_runTrace": "Kör spårsökning",
|
||||
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
|
||||
"map_removeLast": "Ta bort sista",
|
||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||
"snrIndicator_lastSeen": "Senast sedd",
|
||||
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
||||
"chat_ShowAllPaths": "Visa alla vägar",
|
||||
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
||||
"settings_clientRepeat": "Upprepa utan elnät",
|
||||
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Enheter",
|
||||
"appSettings_unitsMetric": "Metriskt (m/km)",
|
||||
"appSettings_unitsImperial": "Imperialt (ft / mi)",
|
||||
"map_lineOfSight": "Synlinje",
|
||||
"map_losScreenTitle": "Synlinje",
|
||||
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
|
||||
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Rensa alla punkter",
|
||||
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
|
||||
"losMenuTitle": "LOS-menyn",
|
||||
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
|
||||
"losShowDisplayNodes": "Visa displaynoder",
|
||||
"losCustomPoints": "Anpassade poäng",
|
||||
"losCustomPointLabel": "Anpassad {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punkt A",
|
||||
"losPointB": "Punkt B",
|
||||
"losAntennaA": "Antenn A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenn B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Kör LOS",
|
||||
"losNoElevationData": "Inga höjddata",
|
||||
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: kollar...",
|
||||
"losStatusNoData": "LOS: inga data",
|
||||
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
|
||||
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
|
||||
"losRenameCustomPoint": "Byt namn på anpassad punkt",
|
||||
"losPointName": "Punktnamn",
|
||||
"losShowPanelTooltip": "Visa LOS-panelen",
|
||||
"losHidePanelTooltip": "Dölj LOS-panelen",
|
||||
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Radiohorisont",
|
||||
"losLegendLosBeam": "Siktlinje",
|
||||
"losLegendTerrain": "Terräng",
|
||||
"losFrequencyLabel": "Frekvens",
|
||||
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
|
||||
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
|
||||
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Ta bort från favoriter",
|
||||
"listFilter_addToFavorites": "Lägg till i favoriter",
|
||||
"listFilter_favorites": "Favoriter",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Oläst",
|
||||
"contacts_searchContactsNoNumber": "Sök kontakter...",
|
||||
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
|
||||
}
|
||||
|
||||
+218
-11
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "uk",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Контакти",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Кількість контактів",
|
||||
"settings_infoChannelCount": "Кількість каналів",
|
||||
"settings_presets": "Попередні налаштування",
|
||||
"settings_preset915Mhz": "915 МГц",
|
||||
"settings_preset868Mhz": "868 МГц",
|
||||
"settings_preset433Mhz": "433 МГц",
|
||||
"settings_frequency": "Частота (МГц)",
|
||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||
"settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "Потужність TX (дБм)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)",
|
||||
"settings_longRange": "Дальній діапазон",
|
||||
"settings_fastSpeed": "Висока швидкість",
|
||||
"settings_error": "Помилка: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -340,6 +343,8 @@
|
||||
"channels_publicChannel": "Публічний канал",
|
||||
"channels_privateChannel": "Приватний канал",
|
||||
"channels_editChannel": "Редагувати канал",
|
||||
"channels_muteChannel": "Вимкнути сповіщення каналу",
|
||||
"channels_unmuteChannel": "Увімкнути сповіщення каналу",
|
||||
"channels_deleteChannel": "Видалити канал",
|
||||
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -1357,12 +1362,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Сусіди",
|
||||
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
|
||||
"repeater_neighbors": "Сусіди",
|
||||
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||
"neighbors_receivedData": "Дані сусідів отримано",
|
||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
|
||||
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||
@@ -1562,6 +1567,8 @@
|
||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||
"appSettings_languageRu": "Російська",
|
||||
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
|
||||
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||
@@ -1575,7 +1582,6 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
|
||||
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
|
||||
"notification_receivedNewMessage": "Отримано нове повідомлення",
|
||||
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
|
||||
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
|
||||
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
|
||||
@@ -1591,6 +1597,207 @@
|
||||
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
|
||||
"settings_gpxExportAllContacts": "Усі місця контактів",
|
||||
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
|
||||
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!"
|
||||
|
||||
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
|
||||
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
|
||||
"map_runTrace": "Виконати трасування шляху",
|
||||
"pathTrace_clearTooltip": "Очистити шлях",
|
||||
"map_removeLast": "Видалити останній",
|
||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||
"chat_ShowAllPaths": "Показати всі шляхи",
|
||||
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||
"settings_clientRepeat": "Автономна система",
|
||||
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "одиниці",
|
||||
"appSettings_unitsMetric": "Метричний (м / км)",
|
||||
"appSettings_unitsImperial": "Імперська (ft / mi)",
|
||||
"map_lineOfSight": "Пряма видимість",
|
||||
"map_losScreenTitle": "Пряма видимість",
|
||||
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
|
||||
"losRunFailed": "Помилка перевірки прямої видимості: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Очистити всі пункти",
|
||||
"losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти",
|
||||
"losMenuTitle": "Меню LOS",
|
||||
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
|
||||
"losShowDisplayNodes": "Показати вузли відображення",
|
||||
"losCustomPoints": "Користувальницькі точки",
|
||||
"losCustomPointLabel": "Спеціальний {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Точка А",
|
||||
"losPointB": "Точка Б",
|
||||
"losAntennaA": "Антена A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Антена B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Запустіть LOS",
|
||||
"losNoElevationData": "Немає даних про висоту",
|
||||
"losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: перевірка...",
|
||||
"losStatusNoData": "LOS: немає даних",
|
||||
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
|
||||
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
|
||||
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
|
||||
"losPointName": "Назва точки",
|
||||
"losShowPanelTooltip": "Показати панель LOS",
|
||||
"losHidePanelTooltip": "Приховати панель LOS",
|
||||
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Радіогоризонт",
|
||||
"losLegendLosBeam": "Лінія прямої видимості",
|
||||
"losLegendTerrain": "Рельєф",
|
||||
"losFrequencyLabel": "Частота",
|
||||
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
|
||||
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
|
||||
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
|
||||
"listFilter_addToFavorites": "Додати до улюблених",
|
||||
"listFilter_favorites": "Улюблені",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...",
|
||||
"contacts_searchUsers": "Пошук {number}{str} користувачів...",
|
||||
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||
"contacts_unread": "Непрочитане"
|
||||
}
|
||||
|
||||
+696
-484
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -14,6 +15,7 @@ import 'services/ble_debug_log_service.dart';
|
||||
import 'services/app_debug_log_service.dart';
|
||||
import 'services/background_service.dart';
|
||||
import 'services/map_tile_cache_service.dart';
|
||||
import 'services/chat_text_scale_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
@@ -33,6 +35,7 @@ void main() async {
|
||||
final appDebugLogService = AppDebugLogService();
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
|
||||
// Load settings
|
||||
await appSettingsService.loadSettings();
|
||||
@@ -47,6 +50,9 @@ void main() async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await backgroundService.initialize();
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
|
||||
// Wire up connector with services
|
||||
connector.initialize(
|
||||
@@ -76,10 +82,32 @@ void main() async {
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _registerThirdPartyLicenses() {
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
<String>['Open-Meteo Elevation API Data'],
|
||||
'''
|
||||
Data used by LOS elevation lookups is provided by Open-Meteo.
|
||||
|
||||
Open-Meteo terms and attribution:
|
||||
https://open-meteo.com/en/terms
|
||||
|
||||
Elevation API:
|
||||
https://open-meteo.com/en/docs/elevation-api
|
||||
|
||||
Attribution license reference:
|
||||
Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
''',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreApp extends StatelessWidget {
|
||||
final MeshCoreConnector connector;
|
||||
final MessageRetryService retryService;
|
||||
@@ -89,6 +117,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final BleDebugLogService bleDebugLogService;
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -100,6 +129,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.bleDebugLogService,
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -112,6 +142,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: appSettingsService),
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
enum UnitSystem { metric, imperial }
|
||||
|
||||
extension UnitSystemValue on UnitSystem {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case UnitSystem.imperial:
|
||||
return 'imperial';
|
||||
case UnitSystem.metric:
|
||||
return 'metric';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppSettings {
|
||||
static const Object _unset = Object();
|
||||
|
||||
@@ -9,6 +22,7 @@ class AppSettings {
|
||||
final bool mapKeyPrefixEnabled;
|
||||
final String mapKeyPrefix;
|
||||
final bool mapShowMarkers;
|
||||
final bool enableMessageTracing;
|
||||
final Map<String, double>? mapCacheBounds;
|
||||
final int mapCacheMinZoom;
|
||||
final int mapCacheMaxZoom;
|
||||
@@ -21,6 +35,9 @@ class AppSettings {
|
||||
final String? languageOverride; // null = system default
|
||||
final bool appDebugLogEnabled;
|
||||
final Map<String, String> batteryChemistryByDeviceId;
|
||||
final Map<String, String> batteryChemistryByRepeaterId;
|
||||
final UnitSystem unitSystem;
|
||||
final Set<String> mutedChannels;
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
@@ -31,6 +48,7 @@ class AppSettings {
|
||||
this.mapKeyPrefixEnabled = false,
|
||||
this.mapKeyPrefix = '',
|
||||
this.mapShowMarkers = true,
|
||||
this.enableMessageTracing = false,
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
this.mapCacheMaxZoom = 15,
|
||||
@@ -43,7 +61,12 @@ class AppSettings {
|
||||
this.languageOverride,
|
||||
this.appDebugLogEnabled = false,
|
||||
Map<String, String>? batteryChemistryByDeviceId,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
this.unitSystem = UnitSystem.metric,
|
||||
Set<String>? mutedChannels,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
@@ -55,6 +78,7 @@ class AppSettings {
|
||||
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
||||
'map_key_prefix': mapKeyPrefix,
|
||||
'map_show_markers': mapShowMarkers,
|
||||
'enable_message_tracing': enableMessageTracing,
|
||||
'map_cache_bounds': mapCacheBounds,
|
||||
'map_cache_min_zoom': mapCacheMinZoom,
|
||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||
@@ -67,10 +91,20 @@ class AppSettings {
|
||||
'language_override': languageOverride,
|
||||
'app_debug_log_enabled': appDebugLogEnabled,
|
||||
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||
'unit_system': unitSystem.value,
|
||||
'muted_channels': mutedChannels.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
||||
UnitSystem parseUnitSystem(dynamic value) {
|
||||
if (value is String && value.toLowerCase() == 'imperial') {
|
||||
return UnitSystem.imperial;
|
||||
}
|
||||
return UnitSystem.metric;
|
||||
}
|
||||
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||
@@ -81,6 +115,7 @@ class AppSettings {
|
||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
||||
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
||||
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
|
||||
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||
),
|
||||
@@ -101,6 +136,17 @@ class AppSettings {
|
||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||
) ??
|
||||
{},
|
||||
batteryChemistryByRepeaterId:
|
||||
(json['battery_chemistry_by_repeater_id'] as Map?)?.map(
|
||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||
) ??
|
||||
{},
|
||||
unitSystem: parseUnitSystem(json['unit_system']),
|
||||
mutedChannels:
|
||||
((json['muted_channels'] as List?)
|
||||
?.map((e) => e.toString())
|
||||
.toSet()) ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,6 +159,7 @@ class AppSettings {
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
bool? mapShowMarkers,
|
||||
bool? enableMessageTracing,
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
int? mapCacheMaxZoom,
|
||||
@@ -125,6 +172,9 @@ class AppSettings {
|
||||
Object? languageOverride = _unset,
|
||||
bool? appDebugLogEnabled,
|
||||
Map<String, String>? batteryChemistryByDeviceId,
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
UnitSystem? unitSystem,
|
||||
Set<String>? mutedChannels,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
@@ -135,6 +185,7 @@ class AppSettings {
|
||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
||||
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
|
||||
mapCacheBounds: mapCacheBounds == _unset
|
||||
? this.mapCacheBounds
|
||||
: mapCacheBounds as Map<String, double>?,
|
||||
@@ -154,6 +205,10 @@ class AppSettings {
|
||||
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
||||
batteryChemistryByDeviceId:
|
||||
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||
batteryChemistryByRepeaterId:
|
||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||
unitSystem: unitSystem ?? this.unitSystem,
|
||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+45
-34
@@ -5,6 +5,7 @@ class Contact {
|
||||
final Uint8List publicKey;
|
||||
final String name;
|
||||
final int type;
|
||||
final int flags;
|
||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||
final Uint8List path; // Path bytes from device
|
||||
final int?
|
||||
@@ -19,6 +20,7 @@ class Contact {
|
||||
required this.publicKey,
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.flags = 0,
|
||||
required this.pathLength,
|
||||
required this.path,
|
||||
this.pathOverride,
|
||||
@@ -58,11 +60,13 @@ class Contact {
|
||||
}
|
||||
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||
|
||||
Contact copyWith({
|
||||
Uint8List? publicKey,
|
||||
String? name,
|
||||
int? type,
|
||||
int? flags,
|
||||
int? pathLength,
|
||||
Uint8List? path,
|
||||
int? pathOverride,
|
||||
@@ -77,6 +81,7 @@ class Contact {
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
flags: flags ?? this.flags,
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
path: path ?? this.path,
|
||||
pathOverride: clearPathOverride
|
||||
@@ -119,7 +124,7 @@ class Contact {
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
Uint8List? traceBytes;
|
||||
|
||||
if (pathLength <= 0) {
|
||||
if (pathBytes.isEmpty) {
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = publicKey[0];
|
||||
return traceBytes;
|
||||
@@ -160,43 +165,49 @@ class Contact {
|
||||
}
|
||||
|
||||
static Contact? fromFrame(Uint8List data) {
|
||||
if (data.length < contactFrameSize) return null;
|
||||
if (data.isEmpty) return null;
|
||||
if (data[0] != respCodeContact) return null;
|
||||
try {
|
||||
final pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
final flags = data[contactFlagsOffset];
|
||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = safePathLen > 0
|
||||
? Uint8List.fromList(
|
||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
||||
)
|
||||
: Uint8List(0);
|
||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
||||
|
||||
final pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = safePathLen > 0
|
||||
? Uint8List.fromList(
|
||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
||||
)
|
||||
: Uint8List(0);
|
||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
||||
double? lat, lon;
|
||||
final latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
|
||||
double? lat, lon;
|
||||
final latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
return Contact(
|
||||
publicKey: pubKey,
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
flags: flags,
|
||||
pathLength: pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
);
|
||||
} catch (e) {
|
||||
// If parsing fails, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact(
|
||||
publicKey: pubKey,
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
pathLength: pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+194
-40
@@ -59,46 +59,200 @@ class RadioSettings {
|
||||
required this.txPowerDbm,
|
||||
});
|
||||
|
||||
// Preset configurations
|
||||
static RadioSettings get preset915MHz => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get preset868MHz => RadioSettings(
|
||||
frequencyMHz: 868.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
);
|
||||
|
||||
static RadioSettings get preset433MHz => RadioSettings(
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get presetLongRange => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf12,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get presetFastSpeed => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw500,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
// Regional preset configurations
|
||||
static final List<(String, RadioSettings)> presets = [
|
||||
(
|
||||
'Australia',
|
||||
RadioSettings(
|
||||
frequencyMHz: 915.8,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 916.575,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia SA, WA, QLD',
|
||||
RadioSettings(
|
||||
frequencyMHz: 923.125,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Czech Republic',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.432,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU 433MHz',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.650,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Long Range)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.525,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Medium Range)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.525,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'New Zealand',
|
||||
RadioSettings(
|
||||
frequencyMHz: 917.375,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'New Zealand (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 917.375,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Portugal 433',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.375,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Portugal 869',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Switzerland',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'USA Arizona',
|
||||
RadioSettings(
|
||||
frequencyMHz: 908.205,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'USA/Canada',
|
||||
RadioSettings(
|
||||
frequencyMHz: 910.525,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Vietnam',
|
||||
RadioSettings(
|
||||
frequencyMHz: 920.250,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
// Off-grid repeat presets (valid client_repeat frequencies)
|
||||
(
|
||||
'Off-Grid 433',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Off-Grid 869',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Off-Grid 918',
|
||||
RadioSettings(
|
||||
frequencyMHz: 918.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
int get frequencyHz => (frequencyMHz * 1000).round();
|
||||
int get bandwidthHz => bandwidth.hz;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class AppDebugLogScreen extends StatelessWidget {
|
||||
const AppDebugLogScreen({super.key});
|
||||
@@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.debugLog_appTitle),
|
||||
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -55,7 +56,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
child: hasEntries
|
||||
? ListView.separated(
|
||||
itemCount: entries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return ListTile(
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
class AppSettingsScreen extends StatelessWidget {
|
||||
@@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.appSettings_title),
|
||||
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -80,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showLanguageDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.location_searching),
|
||||
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||
),
|
||||
value: settingsService.settings.enableMessageTracing,
|
||||
onChanged: (value) {
|
||||
settingsService.setEnableMessageTracing(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -360,6 +374,18 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
onTap: () => _showTimeFilterDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.straighten),
|
||||
title: Text(context.l10n.appSettings_unitsTitle),
|
||||
subtitle: Text(
|
||||
settingsService.settings.unitSystem == UnitSystem.imperial
|
||||
? context.l10n.appSettings_unitsImperial
|
||||
: context.l10n.appSettings_unitsMetric,
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showUnitsDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(context.l10n.appSettings_offlineMapCache),
|
||||
@@ -384,6 +410,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed rendering issues
|
||||
Widget _buildBatteryCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
@@ -399,6 +426,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
@@ -406,6 +434,8 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
// Main tile (icon + text only)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.battery_full),
|
||||
title: Text(context.l10n.appSettings_batteryChemistry),
|
||||
@@ -416,8 +446,19 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
)
|
||||
: context.l10n.appSettings_batteryChemistryConnectFirst,
|
||||
),
|
||||
trailing: DropdownButton<String>(
|
||||
value: selection,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
|
||||
// Dropdown (separate full-width row)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: selection,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: isConnected
|
||||
? (value) {
|
||||
if (value != null) {
|
||||
@@ -691,6 +732,46 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showUnitsDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_unitsTitle),
|
||||
content: RadioGroup<UnitSystem>(
|
||||
groupValue: settingsService.settings.unitSystem,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsService.setUnitSystem(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_unitsMetric),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_unitsImperial),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDebugCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/ble_debug_log_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
@@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
: rawEntries.isNotEmpty;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.debugLog_bleTitle),
|
||||
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.debugLog_copyLog,
|
||||
@@ -100,7 +101,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
itemCount: showingFrames
|
||||
? entries.length
|
||||
: rawEntries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (showingFrames) {
|
||||
final entry = entries[index];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@@ -16,11 +17,15 @@ import '../helpers/utf8_length_limiter.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
@@ -216,37 +221,50 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
ChatZoomWrapper(
|
||||
child: ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
final message = reversedMessages[messageIndex];
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final textScale = context
|
||||
.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildMessageBubble(
|
||||
message,
|
||||
textScale,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
final message = reversedMessages[messageIndex];
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: _buildMessageBubble(message),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
JumpToBottomButton(scrollController: _scrollController),
|
||||
],
|
||||
@@ -261,7 +279,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChannelMessage message) {
|
||||
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final gifId = _parseGifId(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
@@ -271,107 +291,184 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
? message.pathVariants.first
|
||||
: Uint8List(0));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: isOutgoing
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: isOutgoing
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
_buildAvatar(message.senderName),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showMessagePathInfo(message),
|
||||
onLongPress: () => _showMessageActions(message),
|
||||
child: Container(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.all(4)
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
const maxSwipeOffset = 64.0;
|
||||
const replySwipeThreshold = 64.0;
|
||||
const bodyFontSize = 14.0;
|
||||
final messageBody = Column(
|
||||
crossAxisAlignment: isOutgoing
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: isOutgoing
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
_buildAvatar(message.senderName),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showMessagePathInfo(message),
|
||||
onLongPress: () => _showMessageActions(message),
|
||||
child: Container(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.all(4)
|
||||
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
message.senderName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
message.senderName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (gifId == null) const SizedBox(height: 4),
|
||||
],
|
||||
if (message.replyToMessageId != null) ...[
|
||||
_buildReplyPreview(message, textScale),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (poi != null)
|
||||
_buildPoiMessage(
|
||||
context,
|
||||
poi,
|
||||
isOutgoing,
|
||||
textScale,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
else if (gifId != null)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: isOutgoing
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer
|
||||
.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (gifId == null) const SizedBox(height: 4),
|
||||
],
|
||||
if (message.replyToMessageId != null) ...[
|
||||
_buildReplyPreview(message),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (poi != null)
|
||||
_buildPoiMessage(context, poi, isOutgoing)
|
||||
else if (gifId != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: isOutgoing
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer
|
||||
.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
if (!enableTracing && isOutgoing)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: message.text,
|
||||
style: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
||||
context,
|
||||
link.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Linkify(
|
||||
text: message.text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
linkStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) =>
|
||||
LinkHandler.handleLinkTap(context, link.url),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (enableTracing) ...[
|
||||
if (displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
@@ -443,25 +540,81 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (message.reactions.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||
child: _buildReactionsDisplay(message),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (message.reactions.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||
child: _buildReactionsDisplay(message),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!isOutgoing) {
|
||||
return _SwipeReplyBubble(
|
||||
maxSwipeOffset: maxSwipeOffset,
|
||||
replySwipeThreshold: replySwipeThreshold,
|
||||
onReplyTriggered: () => _setReplyingTo(message),
|
||||
hintBuilder: ({required isStart}) =>
|
||||
_buildReplySwipeHint(isStart: isStart),
|
||||
child: messageBody,
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: messageBody,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildReplySwipeHint({required bool isStart}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final content = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.reply, color: colorScheme.primary),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
context.l10n.chat_reply,
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Container(
|
||||
alignment: isStart ? Alignment.centerLeft : Alignment.centerRight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: colorScheme.primary.withValues(alpha: 0.08),
|
||||
child: isStart
|
||||
? content
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_reply,
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Icon(Icons.reply, color: colorScheme.primary),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyPreview(ChannelMessage message) {
|
||||
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final isOwnNode = message.replyToSenderName == connector.selfName;
|
||||
final replyText = message.replyToText ?? '';
|
||||
@@ -489,7 +642,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.l10n.chat_location,
|
||||
style: TextStyle(fontSize: 12, color: previewTextColor),
|
||||
style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -499,7 +652,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 12 * textScale,
|
||||
color: previewTextColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
@@ -523,7 +676,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: 11 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOwnNode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
@@ -599,7 +752,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
bool isOutgoing,
|
||||
double textScale, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textColor = isOutgoing
|
||||
? colorScheme.onPrimaryContainer
|
||||
@@ -635,16 +794,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_poiShared,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14 * textScale,
|
||||
),
|
||||
),
|
||||
if (poi.label.isNotEmpty)
|
||||
Text(
|
||||
poi.label,
|
||||
style: TextStyle(color: metaColor, fontSize: 12),
|
||||
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[const SizedBox(width: 4), trailing],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -709,7 +873,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return colors[hash.abs() % colors.length];
|
||||
}
|
||||
|
||||
Widget _buildReplyBanner() {
|
||||
Widget _buildReplyBanner(double textScale) {
|
||||
final message = _replyingToMessage!;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -735,7 +899,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_replyingTo(message.senderName),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 12 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
@@ -745,7 +909,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
||||
@@ -772,7 +936,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_replyingToMessage != null) _buildReplyBanner(),
|
||||
if (_replyingToMessage != null)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final textScale = context.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildReplyBanner(textScale);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
@@ -798,30 +970,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
builder: (context, value, child) {
|
||||
final gifId = _parseGifId(value.text);
|
||||
if (gifId != null) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
(event.logicalKey == LogicalKeyboardKey.enter ||
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.numpadEnter)) {
|
||||
_sendMessage();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => _textController.clear(),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -884,6 +1073,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
connector.sendChannelMessage(widget.channel, messageText);
|
||||
_textController.clear();
|
||||
_cancelReply();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
@@ -901,7 +1091,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelMessagePathScreen(message: message),
|
||||
builder: (context) =>
|
||||
ChannelMessagePathScreen(message: message, channelMessage: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1006,6 +1197,157 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SwipeReplyBubble extends StatefulWidget {
|
||||
final double maxSwipeOffset;
|
||||
final double replySwipeThreshold;
|
||||
final VoidCallback onReplyTriggered;
|
||||
final Widget Function({required bool isStart}) hintBuilder;
|
||||
final Widget child;
|
||||
|
||||
const _SwipeReplyBubble({
|
||||
required this.maxSwipeOffset,
|
||||
required this.replySwipeThreshold,
|
||||
required this.onReplyTriggered,
|
||||
required this.hintBuilder,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState();
|
||||
}
|
||||
|
||||
class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
Offset? _swipeStartPosition;
|
||||
double _swipeOffset = 0;
|
||||
double _maxSwipeDistance = 0;
|
||||
int? _swipePointerId;
|
||||
bool _swipeLockedToHorizontal = false;
|
||||
|
||||
void _handleSwipeStart(Offset position) {
|
||||
_swipeStartPosition = position;
|
||||
_maxSwipeDistance = 0;
|
||||
if (_swipeOffset != 0) {
|
||||
setState(() => _swipeOffset = 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipePointerDown(PointerDownEvent event) {
|
||||
_swipePointerId = event.pointer;
|
||||
_swipeLockedToHorizontal = false;
|
||||
_handleSwipeStart(event.position);
|
||||
}
|
||||
|
||||
void _handleSwipePointerMove(PointerMoveEvent event) {
|
||||
if (_swipePointerId != event.pointer || _swipeStartPosition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dx = event.position.dx - _swipeStartPosition!.dx;
|
||||
|
||||
const axisLockThreshold = 12.0;
|
||||
if (!_swipeLockedToHorizontal) {
|
||||
if (-dx < axisLockThreshold) {
|
||||
return;
|
||||
}
|
||||
_swipeLockedToHorizontal = true;
|
||||
}
|
||||
|
||||
_handleSwipeUpdate(event.position);
|
||||
}
|
||||
|
||||
void _handleSwipeUpdate(Offset position) {
|
||||
if (_swipeStartPosition == null) return;
|
||||
|
||||
final dx = position.dx - _swipeStartPosition!.dx;
|
||||
if (dx >= 0) return;
|
||||
|
||||
if (-dx < 6) return;
|
||||
|
||||
if (-dx > _maxSwipeDistance) {
|
||||
_maxSwipeDistance = -dx;
|
||||
}
|
||||
|
||||
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
|
||||
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
|
||||
if (adjusted != _swipeOffset) {
|
||||
setState(() => _swipeOffset = adjusted);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipePointerUp(Offset position) {
|
||||
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
|
||||
final dx = position.dx - _swipeStartPosition!.dx;
|
||||
final peak = math.max(
|
||||
_maxSwipeDistance,
|
||||
(-dx).clamp(0.0, double.infinity),
|
||||
);
|
||||
if (peak >= widget.replySwipeThreshold) {
|
||||
widget.onReplyTriggered();
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
}
|
||||
_resetSwipe();
|
||||
}
|
||||
|
||||
void _resetSwipe() {
|
||||
if (_swipeOffset != 0) {
|
||||
setState(() => _swipeOffset = 0);
|
||||
}
|
||||
_swipeStartPosition = null;
|
||||
_maxSwipeDistance = 0;
|
||||
_swipePointerId = null;
|
||||
_swipeLockedToHorizontal = false;
|
||||
}
|
||||
|
||||
double _applySwipeResistance(double rawOffset, double maxOffset) {
|
||||
final abs = rawOffset.abs();
|
||||
if (abs <= 0) return 0;
|
||||
final norm = (abs / maxOffset).clamp(0.0, 1.0);
|
||||
const deadZone = 0.18;
|
||||
if (norm <= deadZone) {
|
||||
return rawOffset.sign * maxOffset * (norm * 0.08);
|
||||
}
|
||||
final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0);
|
||||
final curved = t < 0.5
|
||||
? 16 * math.pow(t, 5)
|
||||
: 1 - math.pow(-2 * t + 2, 5) / 2;
|
||||
const deadZoneEnd = 0.0144;
|
||||
return rawOffset.sign *
|
||||
maxOffset *
|
||||
(deadZoneEnd + curved * (1 - deadZoneEnd));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerDown: _handleSwipePointerDown,
|
||||
onPointerMove: _handleSwipePointerMove,
|
||||
onPointerUp: (event) => _handleSwipePointerUp(event.position),
|
||||
onPointerCancel: (_) => _resetSwipe(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Opacity(
|
||||
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
|
||||
child: widget.hintBuilder(isStart: false),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
|
||||
curve: Curves.easeOut,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PoiInfo {
|
||||
final double lat;
|
||||
final double lon;
|
||||
|
||||
@@ -9,26 +9,38 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final ChannelMessage message;
|
||||
|
||||
const ChannelMessagePathScreen({super.key, required this.message});
|
||||
final bool channelMessage;
|
||||
const ChannelMessagePathScreen({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.channelMessage = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final l10n = context.l10n;
|
||||
final primaryPath = _selectPrimaryPath(
|
||||
final primaryPathTmp = _selectPrimaryPath(
|
||||
message.pathBytes,
|
||||
message.pathVariants,
|
||||
);
|
||||
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
|
||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
@@ -37,10 +49,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
l10n,
|
||||
);
|
||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.channelPath_title),
|
||||
title: AdaptiveAppBarTitle(l10n.channelPath_title),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.radar_outlined),
|
||||
@@ -50,9 +61,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: Uint8List.fromList(primaryPath),
|
||||
path: primaryPath,
|
||||
flipPathRound: true,
|
||||
reversePathRound: true,
|
||||
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -62,7 +73,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
tooltip: l10n.channelPath_viewMap,
|
||||
onPressed: hasHopDetails
|
||||
? () {
|
||||
_openPathMap(context);
|
||||
_openPathMap(context, channelMessage: channelMessage);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -157,7 +168,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
subtitle: Text(_formatPathPrefixes(variants[i])),
|
||||
trailing: const Icon(Icons.map_outlined, size: 20),
|
||||
onTap: () => _openPathMap(context, initialPath: variants[i]),
|
||||
onTap: () => _openPathMap(
|
||||
context,
|
||||
initialPath: variants[i],
|
||||
channelMessage: channelMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -248,13 +263,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
|
||||
void _openPathMap(
|
||||
BuildContext context, {
|
||||
Uint8List? initialPath,
|
||||
bool channelMessage = false,
|
||||
}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelMessagePathMapScreen(
|
||||
message: message,
|
||||
initialPath: initialPath,
|
||||
channelMessage: channelMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -264,11 +284,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
final ChannelMessage message;
|
||||
final Uint8List? initialPath;
|
||||
final bool channelMessage;
|
||||
|
||||
const ChannelMessagePathMapScreen({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.initialPath,
|
||||
this.channelMessage = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -278,8 +300,12 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
|
||||
class _ChannelMessagePathMapScreenState
|
||||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
Uint8List? _selectedPath;
|
||||
double _pathDistance = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
bool _didReceivePositionUpdate = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -314,6 +340,8 @@ class _ChannelMessagePathMapScreenState
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final primaryPath = _selectPrimaryPath(
|
||||
widget.message.pathBytes,
|
||||
@@ -323,11 +351,18 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final selectedPath = _resolveSelectedPath(
|
||||
final selectedPathTmp = _resolveSelectedPath(
|
||||
_selectedPath,
|
||||
observedPaths,
|
||||
primaryPath,
|
||||
);
|
||||
|
||||
final selectedPath =
|
||||
((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(widget.message.isOutgoing && widget.channelMessage))
|
||||
? Uint8List.fromList(selectedPathTmp.reversed.toList())
|
||||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final hops = _buildPathHops(
|
||||
selectedPath,
|
||||
@@ -336,12 +371,22 @@ class _ChannelMessagePathMapScreenState
|
||||
);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
if ((widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(widget.message.isOutgoing && widget.channelMessage)) {
|
||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
}
|
||||
|
||||
for (final hop in hops) {
|
||||
if (hop.hasLocation) {
|
||||
points.add(hop.position!);
|
||||
}
|
||||
}
|
||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
|
||||
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(!widget.message.isOutgoing && widget.channelMessage)) {
|
||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
}
|
||||
|
||||
final polylines = points.length > 1
|
||||
? [
|
||||
@@ -357,6 +402,9 @@ class _ChannelMessagePathMapScreenState
|
||||
? points.first
|
||||
: const LatLng(0, 0);
|
||||
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
||||
if (!_didReceivePositionUpdate) {
|
||||
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||
}
|
||||
final bounds = points.length > 1
|
||||
? LatLngBounds.fromPoints(points)
|
||||
: null;
|
||||
@@ -366,7 +414,9 @@ class _ChannelMessagePathMapScreenState
|
||||
_pathDistance = _getPathDistance(points);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
@@ -388,6 +438,17 @@ class _ChannelMessagePathMapScreenState
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (!_didReceivePositionUpdate ||
|
||||
shouldShow != _showNodeLabels) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_didReceivePositionUpdate = true;
|
||||
_showNodeLabels = shouldShow;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -399,7 +460,12 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
if (polylines.isNotEmpty)
|
||||
PolylineLayer(polylines: polylines),
|
||||
MarkerLayer(markers: _buildHopMarkers(hops)),
|
||||
MarkerLayer(
|
||||
markers: _buildHopMarkers(
|
||||
hops,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
@@ -422,7 +488,7 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildLegendCard(context, hops),
|
||||
_buildLegendCard(context, hops, isImperial),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -494,45 +560,61 @@ class _ChannelMessagePathMapScreenState
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
|
||||
return [
|
||||
for (final hop in hops)
|
||||
if (hop.hasLocation)
|
||||
Marker(
|
||||
point: hop.position!,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<_PathHop> hops, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
for (final hop in hops) {
|
||||
if (!hop.hasLocation) continue;
|
||||
final point = hop.position!;
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
||||
Marker(
|
||||
point: LatLng(
|
||||
context.read<MeshCoreConnector>().selfLatitude!,
|
||||
context.read<MeshCoreConnector>().selfLongitude!,
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: hop.contact?.name ?? _formatPrefix(hop.prefix),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||
if (selfLat != null && selfLon != null) {
|
||||
final selfPoint = LatLng(selfLat, selfLon);
|
||||
markers.add(
|
||||
Marker(
|
||||
point: selfPoint,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
@@ -559,10 +641,60 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: selfPoint,
|
||||
label: context.l10n.pathTrace_you,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
return Marker(
|
||||
point: point,
|
||||
width: 120,
|
||||
height: 24,
|
||||
alignment: Alignment.topCenter,
|
||||
child: IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -20),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
List<_PathHop> hops,
|
||||
bool isImperial,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
||||
@@ -581,7 +713,7 @@ class _ChannelMessagePathMapScreenState
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
@@ -594,7 +726,7 @@ class _ChannelMessagePathMapScreenState
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: hops.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final hop = hops[index];
|
||||
return ListTile(
|
||||
|
||||
@@ -3,18 +3,20 @@ import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../utils/dialog_utils.dart';
|
||||
import '../utils/disconnect_navigation_mixin.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/battery_indicator.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/qr_code_display.dart';
|
||||
@@ -104,6 +106,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final channelMessageStore = ChannelMessageStore();
|
||||
|
||||
// Auto-navigate back to scanner if disconnected
|
||||
if (!checkConnectionAndNavigate(connector)) {
|
||||
@@ -116,8 +119,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BatteryIndicator(connector: connector),
|
||||
title: Text(context.l10n.channels_title),
|
||||
title: AppBarTitle(context.l10n.channels_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
@@ -304,6 +306,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return _buildChannelTile(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
showDragHandle: true,
|
||||
dragIndex: index,
|
||||
@@ -323,6 +326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return _buildChannelTile(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
);
|
||||
},
|
||||
@@ -352,6 +356,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Widget _buildChannelTile(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel, {
|
||||
bool showDragHandle = false,
|
||||
int? dragIndex,
|
||||
@@ -468,7 +473,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChannelActions(context, connector, channel),
|
||||
onLongPress: () => _showChannelActions(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -476,11 +486,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _showChannelActions(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel,
|
||||
) {
|
||||
final parentContext = context;
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final isMuted = settingsService.isChannelMuted(channel.name);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
context: parentContext,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -488,10 +503,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
leading: const Icon(Icons.edit_outlined),
|
||||
title: Text(context.l10n.channels_editChannel),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(sheetContext);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (context.mounted) {
|
||||
_showEditChannelDialog(context, connector, channel);
|
||||
if (parentContext.mounted) {
|
||||
_showEditChannelDialog(parentContext, connector, channel);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isMuted
|
||||
? Icons.notifications_outlined
|
||||
: Icons.notifications_off_outlined,
|
||||
),
|
||||
title: Text(
|
||||
isMuted
|
||||
? context.l10n.channels_unmuteChannel
|
||||
: context.l10n.channels_muteChannel,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
if (isMuted) {
|
||||
await settingsService.unmuteChannel(channel.name);
|
||||
} else {
|
||||
await settingsService.muteChannel(channel.name);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -502,10 +537,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(sheetContext);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (context.mounted) {
|
||||
_confirmDeleteChannel(context, connector, channel);
|
||||
if (parentContext.mounted) {
|
||||
_confirmDeleteChannel(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1415,7 +1455,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
child: Text(dialogContext.l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
final pskHex = pskController.text.trim();
|
||||
|
||||
@@ -1432,13 +1472,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
}
|
||||
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(channel.index, name, psk);
|
||||
connector.setChannelSmazEnabled(channel.index, smazEnabled);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await connector.setChannel(channel.index, name, psk);
|
||||
await connector.setChannelSmazEnabled(
|
||||
channel.index,
|
||||
smazEnabled,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint(st.toString());
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update channel: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_save),
|
||||
),
|
||||
@@ -1451,6 +1503,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _confirmDeleteChannel(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel,
|
||||
) {
|
||||
showDialog(
|
||||
@@ -1466,16 +1519,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
child: Text(dialogContext.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
connector.deleteChannel(channel.index);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
try {
|
||||
await connector.deleteChannel(channel.index);
|
||||
|
||||
channelMessageStore.clearChannelMessages(channel.index);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
} catch (e, st) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Preserve existing logging (if it was there)
|
||||
debugPrint('Failed to delete channel: $e\n$st');
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
dialogContext.l10n.common_delete,
|
||||
|
||||
+570
-372
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/qr_scanner_widget.dart';
|
||||
|
||||
/// Screen for scanning community QR codes to join communities.
|
||||
@@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.community_scanQr),
|
||||
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: _isProcessing
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart';
|
||||
import '../utils/disconnect_navigation_mixin.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/battery_indicator.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
@@ -90,79 +91,90 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final frameBuffer = BufferReader(frame);
|
||||
final code = frameBuffer.readUInt8();
|
||||
try {
|
||||
final code = frameBuffer.readUInt8();
|
||||
|
||||
if (code == respCodeExportContact) {
|
||||
final advertPacket = frameBuffer.readRemainingBytes();
|
||||
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||
if (advertPacket.length < 98) {
|
||||
if (mounted) {
|
||||
if (code == respCodeExportContact) {
|
||||
final advertPacket = frameBuffer.readRemainingBytes();
|
||||
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||
if (advertPacket.length < 98) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
),
|
||||
);
|
||||
}
|
||||
_pendingOperations.remove(ContactOperationType.export);
|
||||
return;
|
||||
}
|
||||
final hexString = pubKeyToHex(advertPacket);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
}
|
||||
|
||||
if (code == respCodeOk) {
|
||||
// Show a snackbar indicating success
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
),
|
||||
);
|
||||
}
|
||||
_pendingOperations.remove(ContactOperationType.export);
|
||||
return;
|
||||
}
|
||||
final hexString = pubKeyToHex(advertPacket);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
}
|
||||
|
||||
if (code == respCodeOk) {
|
||||
// Show a snackbar indicating success
|
||||
if (!mounted) return;
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||
);
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
),
|
||||
);
|
||||
if (code == respCodeErr) {
|
||||
// Show a snackbar indicating failure
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
|
||||
if (code == respCodeErr) {
|
||||
// Show a snackbar indicating failure
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error processing received frame: $e',
|
||||
tag: 'ContactsScreen',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -171,14 +183,17 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.export);
|
||||
await connector.sendFrame(exportContactFrame);
|
||||
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
|
||||
}
|
||||
|
||||
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
||||
await connector.sendFrame(exportContactZeroHopFrame);
|
||||
await connector.sendFrame(
|
||||
exportContactZeroHopFrame,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _contactImport() async {
|
||||
@@ -205,7 +220,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
try {
|
||||
final importContactFrame = buildImportContactFrame(hexString);
|
||||
_pendingOperations.add(ContactOperationType.import);
|
||||
await connector.sendFrame(importContactFrame);
|
||||
await connector.sendFrame(importContactFrame, expectsGenericAck: true);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -229,9 +244,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BatteryIndicator(connector: connector),
|
||||
title: Text(context.l10n.contacts_title),
|
||||
centerTitle: true,
|
||||
title: AppBarTitle(context.l10n.contacts_title),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
@@ -389,6 +402,41 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
? const <ContactGroup>[]
|
||||
: _filterAndSortGroups(_groups, contacts);
|
||||
|
||||
String hintText = "";
|
||||
|
||||
switch (_typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
hintText = context.l10n.contacts_searchContacts(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.users:
|
||||
hintText = context.l10n.contacts_searchUsers(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.repeaters:
|
||||
hintText = context.l10n.contacts_searchRepeaters(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.rooms:
|
||||
hintText = context.l10n.contacts_searchRoomServers(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.favorites:
|
||||
hintText = context.l10n.contacts_searchFavorites(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -396,7 +444,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.contacts_searchContacts,
|
||||
hintText: hintText,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -468,6 +516,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
contact: contact,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: contact.isFavorite,
|
||||
onTap: () => _openChat(context, contact),
|
||||
onLongPress: () =>
|
||||
_showContactOptions(context, connector, contact),
|
||||
@@ -504,6 +553,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
})
|
||||
.where((group) {
|
||||
if (_typeFilter == ContactTypeFilter.all) return true;
|
||||
// Groups don't have a favorite flag, so hide them under favorites filter
|
||||
if (_typeFilter == ContactTypeFilter.favorites) return false;
|
||||
for (final key in group.memberKeys) {
|
||||
final contact = contactsByKey[key];
|
||||
if (contact != null && _matchesTypeFilter(contact)) return true;
|
||||
@@ -578,6 +629,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
switch (_typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
return true;
|
||||
case ContactTypeFilter.favorites:
|
||||
return contact.isFavorite;
|
||||
case ContactTypeFilter.users:
|
||||
return contact.type == advTypeChat;
|
||||
case ContactTypeFilter.repeaters:
|
||||
@@ -968,6 +1021,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
) {
|
||||
final isRepeater = contact.type == advTypeRepeater;
|
||||
final isRoom = contact.type == advTypeRoom;
|
||||
final isFavorite = contact.isFavorite;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -1074,6 +1128,21 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
},
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isFavorite ? Icons.star : Icons.star_border,
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
title: Text(
|
||||
isFavorite
|
||||
? context.l10n.listFilter_removeFromFavorites
|
||||
: context.l10n.listFilter_addToFavorites,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setContactFavorite(contact, !isFavorite);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(context.l10n.contacts_ShareContact),
|
||||
@@ -1142,6 +1211,7 @@ class _ContactTile extends StatelessWidget {
|
||||
final Contact contact;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
|
||||
@@ -1149,6 +1219,7 @@ class _ContactTile extends StatelessWidget {
|
||||
required this.contact,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
@@ -1160,12 +1231,17 @@ class _ContactTile extends StatelessWidget {
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
child: _buildContactAvatar(contact),
|
||||
),
|
||||
title: Text(contact.name),
|
||||
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(contact.pathLabel),
|
||||
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
|
||||
Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
Text(
|
||||
contact.shortPubKeyHex,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Clamp text scaling in trailing section to prevent overflow while
|
||||
@@ -1176,26 +1252,36 @@ class _ContactTile extends StatelessWidget {
|
||||
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Text(
|
||||
_formatLastSeen(context, lastSeen),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact.hasLocation)
|
||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
],
|
||||
Text(
|
||||
_formatLastSeen(context, lastSeen),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isFavorite)
|
||||
Icon(Icons.star, size: 14, color: Colors.amber[700]),
|
||||
if (isFavorite && contact.hasLocation)
|
||||
const SizedBox(width: 2),
|
||||
if (contact.hasLocation)
|
||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -224,7 +225,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(l10n.mapCache_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
+405
-75
@@ -1,13 +1,17 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
@@ -15,8 +19,8 @@ import '../services/map_marker_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/battery_indicator.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
@@ -24,6 +28,7 @@ import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import 'repeater_hub_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'line_of_sight_map_screen.dart';
|
||||
|
||||
class MapScreen extends StatefulWidget {
|
||||
final LatLng? highlightPosition;
|
||||
@@ -44,13 +49,21 @@ class MapScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
final Set<String> _hiddenMarkerIds = {};
|
||||
Set<String> _removedMarkerIds = {};
|
||||
bool _isBuildingPathTrace = false;
|
||||
bool _isSelectingPoi = false;
|
||||
bool _hasInitializedMap = false;
|
||||
bool _removedMarkersLoaded = false;
|
||||
final List<int> _pathTrace = [];
|
||||
final List<LatLng> _points = [];
|
||||
final List<Polyline> _polylines = [];
|
||||
bool _legendExpanded = false;
|
||||
bool _showNodeLabels = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -98,7 +111,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
|
||||
final maxSpread = max(latStdDev, lonStdDev);
|
||||
if (maxSpread <= 0) return 13.0;
|
||||
// Approzimate: each zoom level halves the visible area
|
||||
// Approximate: each zoom level halves the visible area
|
||||
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
|
||||
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
|
||||
return zoom.clamp(4.0, 15.0);
|
||||
@@ -147,6 +160,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
.where((c) => c.hasLocation)
|
||||
.toList();
|
||||
|
||||
_polylines.clear();
|
||||
_polylines.addAll(
|
||||
_points.length > 1
|
||||
? [
|
||||
Polyline(
|
||||
points: _points,
|
||||
strokeWidth: 4,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
]
|
||||
: <Polyline>[],
|
||||
);
|
||||
|
||||
// Calculate center and zoom of all nodes, or default to (0, 0)
|
||||
LatLng center = const LatLng(0, 0);
|
||||
double initialZoom = 10.0;
|
||||
@@ -227,6 +253,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
// Re center map after removed markers have loaded
|
||||
if (!_hasInitializedMap && _removedMarkersLoaded) {
|
||||
_hasInitializedMap = true;
|
||||
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||
if (hasMapContent) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@@ -242,11 +269,57 @@ class _MapScreenState extends State<MapScreen> {
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BatteryIndicator(connector: connector),
|
||||
title: Text(context.l10n.map_title),
|
||||
title: AppBarTitle(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.radar),
|
||||
onPressed: () => _startPath(),
|
||||
tooltip: context.l10n.contacts_pathTrace,
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
icon: const LosIcon(),
|
||||
onPressed: () {
|
||||
final candidates = <LineOfSightEndpoint>[];
|
||||
if (connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: context.l10n.pathTrace_you,
|
||||
point: LatLng(
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
color: Colors.teal,
|
||||
icon: Icons.person_pin_circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final c in contactsWithLocation) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: c.name,
|
||||
point: LatLng(c.latitude!, c.longitude!),
|
||||
color: _getNodeColor(c.type),
|
||||
icon: _getNodeIcon(c.type),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LineOfSightMapScreen(
|
||||
title: context.l10n.map_losScreenTitle,
|
||||
candidates: candidates,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: context.l10n.map_lineOfSight,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
@@ -325,6 +398,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
position: latLng,
|
||||
);
|
||||
},
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
setState(() {
|
||||
_showNodeLabels = shouldShow;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -334,6 +415,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
MapTileCacheService.userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
),
|
||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
if (highlightPosition != null)
|
||||
@@ -347,7 +430,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
size: 34,
|
||||
),
|
||||
),
|
||||
..._buildMarkers(contactsWithLocation, settings),
|
||||
..._buildMarkers(
|
||||
contactsWithLocation,
|
||||
settings,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
...sharedMarkers.map(_buildSharedMarker),
|
||||
if (connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null)
|
||||
@@ -356,8 +443,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -376,21 +463,34 @@ class _MapScreenState extends State<MapScreen> {
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_pin_circle,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showNodeLabels &&
|
||||
connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null)
|
||||
_buildNodeLabelMarker(
|
||||
point: LatLng(
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
label: context.l10n.pathTrace_you,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildLegend(contactsWithLocation.length, sharedMarkers.length),
|
||||
if (!_isBuildingPathTrace)
|
||||
_buildLegend(
|
||||
contactsWithLocation,
|
||||
settings,
|
||||
sharedMarkers.length,
|
||||
),
|
||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
@@ -412,20 +512,28 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
||||
List<Marker> _buildMarkers(
|
||||
List<Contact> contacts,
|
||||
settings, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
|
||||
for (final contact in contacts) {
|
||||
if (!contact.hasLocation) continue;
|
||||
|
||||
// Apply node type filters
|
||||
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
||||
if (contact.type == advTypeRepeater &&
|
||||
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type == advTypeChat &&
|
||||
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
!settings.mapShowOtherNodes) {
|
||||
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -434,7 +542,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showNodeInfo(context, contact),
|
||||
onLongPress: () =>
|
||||
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, contact)
|
||||
: _showNodeInfo(context, contact),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -463,11 +575,54 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
|
||||
markers.add(marker);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: LatLng(contact.latitude!, contact.longitude!),
|
||||
label: contact.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
return Marker(
|
||||
point: point,
|
||||
width: 120,
|
||||
height: 24,
|
||||
alignment: Alignment.topCenter,
|
||||
child: IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -20),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getNodeColor(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
@@ -498,65 +653,126 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLegend(int nodeCount, int markerCount) {
|
||||
Widget _buildLegend(
|
||||
List<Contact> contactsWithLocation,
|
||||
settings,
|
||||
int markerCount,
|
||||
) {
|
||||
int nodeCount = 0;
|
||||
for (final contact in contactsWithLocation) {
|
||||
// Apply node type filters
|
||||
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
!settings.mapShowOtherNodes) {
|
||||
continue;
|
||||
}
|
||||
nodeCount++;
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.map_nodesCount(nodeCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_legendExpanded = !_legendExpanded;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.map_nodesCount(nodeCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.l10n.map_pinsCount(markerCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
AnimatedRotation(
|
||||
turns: _legendExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Icon(Icons.expand_more, size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.l10n.map_pinsCount(markerCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
_buildLegendItem(
|
||||
Icons.person,
|
||||
context.l10n.map_chat,
|
||||
Colors.blue,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.router,
|
||||
context.l10n.map_repeater,
|
||||
Colors.green,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.meeting_room,
|
||||
context.l10n.map_room,
|
||||
Colors.purple,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.sensors,
|
||||
context.l10n.map_sensor,
|
||||
Colors.orange,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.flag,
|
||||
context.l10n.map_pinDm,
|
||||
Colors.blue,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.flag,
|
||||
context.l10n.map_pinPrivate,
|
||||
Colors.purple,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.flag,
|
||||
context.l10n.map_pinPublic,
|
||||
Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildLegendItem(
|
||||
Icons.person,
|
||||
context.l10n.map_chat,
|
||||
Colors.blue,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.router,
|
||||
context.l10n.map_repeater,
|
||||
Colors.green,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.meeting_room,
|
||||
context.l10n.map_room,
|
||||
Colors.purple,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.sensors,
|
||||
context.l10n.map_sensor,
|
||||
Colors.orange,
|
||||
),
|
||||
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
|
||||
_buildLegendItem(
|
||||
Icons.flag,
|
||||
context.l10n.map_pinPrivate,
|
||||
Colors.purple,
|
||||
),
|
||||
_buildLegendItem(
|
||||
Icons.flag,
|
||||
context.l10n.map_pinPublic,
|
||||
Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
crossFadeState: _legendExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -564,7 +780,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
Widget _buildLegendItem(IconData icon, String label, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 1.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -747,7 +963,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
color: _getNodeColor(contact.type),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(contact.name)),
|
||||
Expanded(child: SelectableText(contact.name)),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
@@ -918,7 +1134,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: const TextStyle(fontSize: 14)),
|
||||
SelectableText(value, style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1085,7 +1301,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.contacts_searchContacts,
|
||||
hintText:
|
||||
context.l10n.contacts_searchContactsNoNumber,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -1412,6 +1629,119 @@ class _MapScreenState extends State<MapScreen> {
|
||||
return context.l10n.time_allTime;
|
||||
}
|
||||
}
|
||||
|
||||
void _addToPath(BuildContext context, Contact contact) {
|
||||
setState(() {
|
||||
_pathTrace.add(
|
||||
contact.publicKey[0],
|
||||
); // Add first 16 bytes of public key to path trace
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
});
|
||||
}
|
||||
|
||||
void _startPath() {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = true;
|
||||
_pathTrace.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _removePath() {
|
||||
setState(() {
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
_points.removeLast(); // Remove last point from points list
|
||||
_polylines.clear(); // Clear polylines
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPathTraceOverlay() {
|
||||
final l10n = context.l10n;
|
||||
final isImperial =
|
||||
context.read<AppSettingsService>().settings.unitSystem ==
|
||||
UnitSystem.imperial;
|
||||
return Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.contacts_pathTrace,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (_pathTrace.isEmpty) const SizedBox(height: 8),
|
||||
if (_pathTrace.isEmpty)
|
||||
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
Text(
|
||||
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
),
|
||||
SelectableText(
|
||||
_pathTrace
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(','),
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (_pathTrace.isNotEmpty)
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
});
|
||||
},
|
||||
child: Text(l10n.map_runTrace),
|
||||
),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
ElevatedButton(
|
||||
onPressed: _removePath,
|
||||
child: Text(l10n.map_removeLast),
|
||||
),
|
||||
if (_pathTrace.isEmpty)
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
_pathTrace.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MarkerPayload {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/snr_indicator.dart';
|
||||
|
||||
class NeighboursScreen extends StatefulWidget {
|
||||
class NeighborsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
|
||||
const NeighboursScreen({
|
||||
const NeighborsScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NeighboursScreen> createState() => _NeighboursScreenState();
|
||||
State<NeighborsScreen> createState() => _NeighborsScreenState();
|
||||
}
|
||||
|
||||
class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
static const int _reqNeighboursKeyLen = 4;
|
||||
class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
static const int _reqNeighborsKeyLen = 4;
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
int _neighbourCount = 0;
|
||||
int _neighborCount = 0;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
@@ -41,7 +42,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
RepeaterCommandService? _commandService;
|
||||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedNeighbours;
|
||||
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -49,7 +50,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
_commandService = RepeaterCommandService(connector);
|
||||
_setupMessageListener();
|
||||
_loadNeighbours();
|
||||
_loadNeighbors();
|
||||
_hasData = false;
|
||||
}
|
||||
|
||||
@@ -62,13 +63,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
|
||||
if (frame[0] == respCodeSent) {
|
||||
_tagData = frame.sublist(2, 6);
|
||||
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame[0] == pushCodeBinaryResponse &&
|
||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
_handleNeighboursResponse(connector, frame.sublist(6));
|
||||
_handleNeighborsResponse(connector, frame.sublist(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -91,65 +91,77 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
return '${h}h ${m2}m';
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> parseNeighboursData(
|
||||
static List<Map<String, dynamic>> parseNeighborsData(
|
||||
BufferReader buffer,
|
||||
int resultsCount,
|
||||
) {
|
||||
final Map<int, Map<String, dynamic>> neighbours = {};
|
||||
for (var i = 0; i < resultsCount; i++) {
|
||||
final neighbourData = neighbours.putIfAbsent(
|
||||
i,
|
||||
() => {
|
||||
'contact': null,
|
||||
'publicKey': <Uint8List>{},
|
||||
'lastHeard': <int>{},
|
||||
'snr': <double>{},
|
||||
},
|
||||
);
|
||||
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
|
||||
neighbourData['lastHeard'] = buffer.readUInt32LE();
|
||||
neighbourData['snr'] = buffer.readInt8() / 4.0;
|
||||
}
|
||||
final Map<int, Map<String, dynamic>> neighbors = {};
|
||||
try {
|
||||
for (var i = 0; i < resultsCount; i++) {
|
||||
final neighborData = neighbors.putIfAbsent(
|
||||
i,
|
||||
() => {
|
||||
'contact': null,
|
||||
'publicKey': <Uint8List>{},
|
||||
'lastHeard': <int>{},
|
||||
'snr': <double>{},
|
||||
},
|
||||
);
|
||||
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
|
||||
neighborData['lastHeard'] = buffer.readUInt32LE();
|
||||
neighborData['snr'] = buffer.readInt8() / 4.0;
|
||||
}
|
||||
|
||||
return neighbours.values.toList();
|
||||
return neighbors.values.toList();
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error parsing neighbors data: $e',
|
||||
tag: 'NeighborsScreen',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final neighbourCount = buffer.readUInt16LE();
|
||||
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
|
||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var neighbourData in parsedNeighbours) {
|
||||
final publicKey = neighbourData['publicKey'];
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
|
||||
publicKey,
|
||||
)) {
|
||||
neighbourData['contact'] = repeater;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var neighborData in parsedNeighbors) {
|
||||
final publicKey = neighborData['publicKey'];
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
|
||||
publicKey,
|
||||
)) {
|
||||
neighborData['contact'] = repeater;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_parsedNeighbours = parsedNeighbours;
|
||||
_neighbourCount = neighbourCount;
|
||||
});
|
||||
setState(() {
|
||||
_parsedNeighbors = parsedNeighbors;
|
||||
_neighborCount = neighborCount;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = true;
|
||||
_hasData = true;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = true;
|
||||
_hasData = true;
|
||||
});
|
||||
} catch (e) {
|
||||
appLogger.error('Error handling neighbors response: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
@@ -159,7 +171,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadNeighbours() async {
|
||||
Future<void> _loadNeighbors() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
setState(() {
|
||||
@@ -172,17 +184,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
_pendingStatusSelection = selection;
|
||||
|
||||
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
|
||||
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
|
||||
final frame = buildSendBinaryReq(
|
||||
repeater.publicKey,
|
||||
payload: Uint8List.fromList([
|
||||
reqTypeGetNeighbours,
|
||||
reqTypeGetNeighbors,
|
||||
0x00,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
_reqNeighboursKeyLen,
|
||||
_reqNeighborsKeyLen,
|
||||
]),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
@@ -258,7 +270,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.neighbors_repeatersNeighbours,
|
||||
l10n.neighbors_repeatersNeighbors,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
@@ -345,7 +357,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isLoading ? null : _loadNeighbours,
|
||||
onPressed: _isLoading ? null : _loadNeighbors,
|
||||
tooltip: l10n.repeater_refresh,
|
||||
),
|
||||
],
|
||||
@@ -353,13 +365,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _loadNeighbours,
|
||||
onRefresh: _loadNeighbors,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (!_isLoaded &&
|
||||
!_hasData &&
|
||||
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
|
||||
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.neighbors_noData,
|
||||
@@ -368,10 +380,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
),
|
||||
if (_isLoaded ||
|
||||
_hasData &&
|
||||
!(_parsedNeighbours == null ||
|
||||
_parsedNeighbours!.isEmpty))
|
||||
_buildNeighboursInfoCard(
|
||||
"${l10n.repeater_neighbours} - $_neighbourCount",
|
||||
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||
_buildNeighborsInfoCard(
|
||||
"${l10n.repeater_neighbors} - $_neighborCount",
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -380,7 +391,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNeighboursInfoCard(String title) {
|
||||
Widget _buildNeighborsInfoCard(String title) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -405,7 +416,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
for (final entry in _parsedNeighbours!.asMap().entries)
|
||||
for (final entry in _parsedNeighbors!.asMap().entries)
|
||||
_buildInfoRow(
|
||||
entry.value['contact'] != null
|
||||
? entry.value['contact'].name
|
||||
@@ -430,6 +441,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
double snr,
|
||||
int spreadingFactor,
|
||||
) {
|
||||
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
@@ -443,9 +455,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(value),
|
||||
trailing: SNRIcon(
|
||||
snr: snr,
|
||||
snrLevels: getSNRfromSF(spreadingFactor),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
|
||||
Text(
|
||||
snrUi.text,
|
||||
style: TextStyle(fontSize: 10, color: snrUi.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
+338
-197
@@ -8,14 +8,37 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/models/app_settings.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
import 'package:meshcore_open/services/app_settings_service.dart';
|
||||
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
double getPathDistanceMeters(List<LatLng> points) {
|
||||
if (points.length <= 1) return 0.0;
|
||||
|
||||
double distanceMeters = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
|
||||
for (int i = 0; i < points.length - 1; i++) {
|
||||
distanceMeters += distanceCalculator(points[i], points[i + 1]);
|
||||
}
|
||||
|
||||
return distanceMeters;
|
||||
}
|
||||
|
||||
String formatDistance(double distanceMeters, {required bool isImperial}) {
|
||||
if (isImperial) {
|
||||
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||
}
|
||||
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
|
||||
}
|
||||
|
||||
class PathTraceData {
|
||||
final Uint8List pathData;
|
||||
final Uint8List snrData;
|
||||
final List<double> snrData;
|
||||
final Map<int, Contact> pathContacts;
|
||||
|
||||
PathTraceData({
|
||||
@@ -28,6 +51,7 @@ class PathTraceData {
|
||||
class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
final int? repeaterId;
|
||||
final bool flipPathRound;
|
||||
final bool reversePathRound;
|
||||
|
||||
@@ -35,6 +59,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.path,
|
||||
this.repeaterId,
|
||||
this.flipPathRound = false,
|
||||
this.reversePathRound = false,
|
||||
});
|
||||
@@ -44,13 +69,14 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _failed2Loaded = false;
|
||||
bool _hasData = false;
|
||||
bool _noLocationErr = false;
|
||||
PathTraceData? _traceData;
|
||||
List<LatLng> _points = <LatLng>[];
|
||||
List<Polyline> _polylines = [];
|
||||
@@ -58,7 +84,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
double _initialZoom = 2.0;
|
||||
LatLngBounds? _bounds;
|
||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||
double _pathDistance = 0.0;
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
@@ -80,7 +107,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Uint8List addReturnpath(Uint8List pathBytes) {
|
||||
Uint8List addReturnPath(Uint8List pathBytes) {
|
||||
Uint8List? traceBytes;
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
@@ -93,23 +120,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
double getPathDistance() {
|
||||
double totalDistance = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
|
||||
for (int i = 0; i < _points.length - 1; i++) {
|
||||
totalDistance += distanceCalculator(_points[i], _points[i + 1]);
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
Future<void> _doPathTrace() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_failed2Loaded = false;
|
||||
_noLocationErr = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +135,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
: widget.path;
|
||||
|
||||
if (widget.flipPathRound) {
|
||||
path = addReturnpath(pathTmp);
|
||||
path = addReturnPath(pathTmp);
|
||||
} else {
|
||||
path = pathTmp;
|
||||
}
|
||||
@@ -142,34 +157,57 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final frameBuffer = BufferReader(frame);
|
||||
final code = frameBuffer.readUInt8();
|
||||
try {
|
||||
final code = frameBuffer.readUInt8();
|
||||
|
||||
if (code == respCodeSent) {
|
||||
frameBuffer.skipBytes(1); //reserved
|
||||
tagData = frameBuffer.readBytes(4);
|
||||
final timeoutSeconds = frameBuffer.readUInt32LE();
|
||||
if (code == respCodeSent) {
|
||||
frameBuffer.skipBytes(1); //reserved
|
||||
tagData = frameBuffer.readBytes(4);
|
||||
final timeoutMilliseconds = frameBuffer.readUInt32LE();
|
||||
|
||||
// Start timeout timer for trace response
|
||||
_timeoutTimer?.cancel();
|
||||
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
|
||||
// Start timeout timer for trace response
|
||||
_timeoutTimer?.cancel();
|
||||
_timeoutTimer = Timer(
|
||||
Duration(milliseconds: timeoutMilliseconds),
|
||||
() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (code == respCodeErr) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame.length > 8 &&
|
||||
code == pushCodeTraceData &&
|
||||
listEquals(frame.sublist(4, 8), tagData)) {
|
||||
// Check if it's a binary response
|
||||
if (frame.length > 8 &&
|
||||
code == pushCodeTraceData &&
|
||||
listEquals(frame.sublist(4, 8), tagData)) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||
_handleTraceResponse(frame);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||
_handleTraceResponse(frame);
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
// Handle any parsing errors gracefully
|
||||
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -178,71 +216,91 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
final buffer = BufferReader(frame);
|
||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||
int pathLength = buffer.readUInt8();
|
||||
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||
buffer.skipBytes(4); // Skip auth code
|
||||
Uint8List pathData = buffer.readBytes(pathLength);
|
||||
Uint8List snrData = buffer.readRemainingBytes();
|
||||
try {
|
||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||
int pathLength = buffer.readUInt8();
|
||||
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||
buffer.skipBytes(4); // Skip auth code
|
||||
Uint8List pathData = buffer.readBytes(pathLength);
|
||||
List<double> snrData = buffer
|
||||
.readRemainingBytes()
|
||||
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
Map<int, Contact> pathContacts = {};
|
||||
|
||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = true;
|
||||
_traceData = PathTraceData(
|
||||
pathData: pathData,
|
||||
snrData: snrData,
|
||||
pathContacts: pathContacts,
|
||||
);
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
for (final hop in _traceData!.pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact != null &&
|
||||
contact.hasLocation &&
|
||||
contact.latitude != null &&
|
||||
contact.longitude != null) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
} else {
|
||||
_noLocationErr = true;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = true;
|
||||
_traceData = PathTraceData(
|
||||
pathData: pathData,
|
||||
snrData: snrData,
|
||||
pathContacts: pathContacts,
|
||||
);
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
for (final hop in _traceData!.pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact != null &&
|
||||
contact.hasLocation &&
|
||||
contact.latitude != null &&
|
||||
contact.longitude != null) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
}
|
||||
}
|
||||
}
|
||||
_polylines = _points.length > 1
|
||||
? [
|
||||
Polyline(
|
||||
points: _points,
|
||||
strokeWidth: 4,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
]
|
||||
: <Polyline>[];
|
||||
_polylines = _points.length > 1
|
||||
? [
|
||||
Polyline(
|
||||
points: _points,
|
||||
strokeWidth: 4,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
]
|
||||
: <Polyline>[];
|
||||
|
||||
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
|
||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||
_mapKey = ValueKey(
|
||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||
_initialCenter = _points.isNotEmpty
|
||||
? _points.first
|
||||
: const LatLng(0, 0);
|
||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||
_mapKey = ValueKey(
|
||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||
);
|
||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||
});
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error handling trace response: $e',
|
||||
tag: 'PathTraceMapScreen',
|
||||
);
|
||||
_pathDistance = getPathDistance();
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
|
||||
return Scaffold(
|
||||
@@ -279,20 +337,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_noLocationErr)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.red,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
context.l10n.pathTrace_someHopsNoLocation,
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_hasData && !_noLocationErr)
|
||||
if (!_hasData)
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -304,43 +349,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasData && !_noLocationErr)
|
||||
FlutterMap(
|
||||
key: _mapKey,
|
||||
options: MapOptions(
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
? null
|
||||
: CameraFit.bounds(
|
||||
bounds: _bounds!,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: kMapTileUrlTemplate,
|
||||
tileProvider: tileCache.tileProvider,
|
||||
userAgentPackageName:
|
||||
MapTileCacheService.userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
),
|
||||
if (_polylines.isNotEmpty)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
if (_traceData!.pathData.isNotEmpty)
|
||||
MarkerLayer(
|
||||
markers: _buildHopMarkers(_traceData!.pathData),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
!_failed2Loaded &&
|
||||
!_noLocationErr)
|
||||
!_failed2Loaded)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
@@ -352,8 +365,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_hasData && !_noLocationErr)
|
||||
_buildLegendCard(context, _traceData!),
|
||||
if (_hasData)
|
||||
_buildLegendCard(context, _traceData!, isImperial),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -362,54 +375,61 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(List<int> pathData) {
|
||||
return [
|
||||
for (final hop in pathData)
|
||||
if (_traceData!.pathContacts[hop]!.hasLocation)
|
||||
Marker(
|
||||
point: LatLng(
|
||||
_traceData!.pathContacts[hop]!.latitude!,
|
||||
_traceData!.pathContacts[hop]!.longitude!,
|
||||
),
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_traceData!.pathContacts[hop]!.publicKey
|
||||
.sublist(0, 1)
|
||||
.map(
|
||||
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
|
||||
)
|
||||
.join(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
for (final hop in pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact == null || !contact.hasLocation) continue;
|
||||
final point = LatLng(contact.latitude!, contact.longitude!);
|
||||
markers.add(
|
||||
Marker(
|
||||
point: LatLng(
|
||||
context.read<MeshCoreConnector>().selfLatitude!,
|
||||
context.read<MeshCoreConnector>().selfLongitude!,
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
contact.publicKey
|
||||
.sublist(0, 1)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
|
||||
}
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||
if (selfLat != null && selfLon != null) {
|
||||
final selfPoint = LatLng(selfLat, selfLon);
|
||||
markers.add(
|
||||
Marker(
|
||||
point: selfPoint,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
@@ -437,7 +457,53 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: selfPoint,
|
||||
label: context.l10n.pathTrace_you,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
return Marker(
|
||||
point: point,
|
||||
width: 120,
|
||||
height: 24,
|
||||
alignment: Alignment.topCenter,
|
||||
child: IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -20),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||
@@ -453,7 +519,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null ? "$hex: $contactName" : hex;
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
} else {
|
||||
final contactName =
|
||||
@@ -462,7 +530,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null ? "$hex: $contactName" : hex;
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +545,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null ? "$hex: $contactName" : hex;
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
} else {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
@@ -486,11 +558,64 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null ? "$hex: $contactName" : hex;
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
|
||||
Widget _buildMapPathTrace(
|
||||
BuildContext context,
|
||||
MapTileCacheService tileCache,
|
||||
) {
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
options: MapOptions(
|
||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
? null
|
||||
: CameraFit.bounds(
|
||||
bounds: _bounds!,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
setState(() {
|
||||
_showNodeLabels = shouldShow;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: kMapTileUrlTemplate,
|
||||
tileProvider: tileCache.tileProvider,
|
||||
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
),
|
||||
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
||||
if (_traceData!.pathData.isNotEmpty)
|
||||
MarkerLayer(
|
||||
markers: _buildHopMarkers(
|
||||
_traceData!.pathData,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
PathTraceData pathTraceData,
|
||||
bool isImperial,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
||||
@@ -509,7 +634,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
@@ -523,8 +648,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: pathTraceData.pathData.length + 1,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final snrUi = snrUiFromSNR(
|
||||
index < pathTraceData.snrData.length
|
||||
? pathTraceData.snrData[index]
|
||||
: null,
|
||||
context.read<MeshCoreConnector>().currentSf,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@@ -543,12 +674,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
trailing: SNRIcon(
|
||||
snr:
|
||||
pathTraceData.snrData[index].toSigned(
|
||||
8,
|
||||
) /
|
||||
4.0,
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
snrUi.icon,
|
||||
color: snrUi.color,
|
||||
size: 18.0,
|
||||
),
|
||||
Text(
|
||||
snrUi.text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: snrUi.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// Handle item tap
|
||||
|
||||
@@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
|
||||
_commandController.clear();
|
||||
_historyIndex = -1;
|
||||
_commandFocusNode.requestFocus();
|
||||
|
||||
// Auto-scroll to bottom
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import 'repeater_status_screen.dart';
|
||||
import 'repeater_cli_screen.dart';
|
||||
import 'repeater_settings_screen.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
import 'neighbours_screen.dart';
|
||||
import 'neighbors_screen.dart';
|
||||
|
||||
class RepeaterHubScreen extends StatelessWidget {
|
||||
final Contact repeater;
|
||||
@@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final chemistry = settingsService.batteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
@@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.repeater_managementTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
@@ -174,17 +236,15 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.group,
|
||||
title: l10n.repeater_neighbours,
|
||||
subtitle: l10n.repeater_neighboursSubtitle,
|
||||
title: l10n.repeater_neighbors,
|
||||
subtitle: l10n.repeater_neighborsSubtitle,
|
||||
color: Colors.orange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NeighboursScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
builder: (context) =>
|
||||
NeighborsScreen(repeater: repeater, password: password),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../models/contact.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
|
||||
class RepeaterStatusScreen extends StatefulWidget {
|
||||
@@ -179,6 +181,12 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_dupDirect = directDups;
|
||||
_dupFlood = floodDups;
|
||||
});
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'status_binary',
|
||||
);
|
||||
_recordStatusResult(true);
|
||||
}
|
||||
|
||||
@@ -201,6 +209,18 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_uptimeSecs = _asInt(data['uptime_secs']);
|
||||
_queueLen = _asInt(data['queue_len']);
|
||||
_debugFlags = _asInt(data['errors']);
|
||||
final batteryMv = _batteryMv;
|
||||
if (batteryMv != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'status_text',
|
||||
);
|
||||
}
|
||||
} else if (data.containsKey('noise_floor')) {
|
||||
_noiseFloor = _asInt(data['noise_floor']);
|
||||
_lastRssi = _asInt(data['last_rssi']);
|
||||
@@ -590,18 +610,24 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
}
|
||||
|
||||
String _batteryText() {
|
||||
if (_batteryMv == null) return '—';
|
||||
final percent = _batteryPercentFromMv(_batteryMv!);
|
||||
final volts = (_batteryMv! / 1000.0).toStringAsFixed(2);
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
_batteryMv;
|
||||
if (batteryMv == null) return '—';
|
||||
final percent = estimateBatteryPercentFromMillivolts(
|
||||
batteryMv,
|
||||
_batteryChemistry(),
|
||||
);
|
||||
final volts = (batteryMv / 1000.0).toStringAsFixed(2);
|
||||
return '$percent% / ${volts}V';
|
||||
}
|
||||
|
||||
int _batteryPercentFromMv(int millivolts) {
|
||||
const minMv = 3000;
|
||||
const maxMv = 4200;
|
||||
if (millivolts <= minMv) return 0;
|
||||
if (millivolts >= maxMv) return 100;
|
||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
||||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
String _clockText() {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import 'contacts_screen.dart';
|
||||
|
||||
@@ -18,6 +22,8 @@ class ScannerScreen extends StatefulWidget {
|
||||
class _ScannerScreenState extends State<ScannerScreen> {
|
||||
bool _changedNavigation = false;
|
||||
late final VoidCallback _connectionListener;
|
||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -39,12 +45,25 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
};
|
||||
|
||||
connector.addListener(_connectionListener);
|
||||
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(connector.stopScan());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.removeListener(_connectionListener);
|
||||
unawaited(_bluetoothStateSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -52,7 +71,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.scanner_title),
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
@@ -62,6 +81,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
builder: (context, connector, child) {
|
||||
return Column(
|
||||
children: [
|
||||
// Bluetooth off warning
|
||||
if (_bluetoothState == BluetoothAdapterState.off)
|
||||
_bluetoothOffWarning(context),
|
||||
|
||||
// Status bar
|
||||
_buildStatusBar(context, connector),
|
||||
|
||||
@@ -76,15 +99,18 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
builder: (context, connector, child) {
|
||||
final isScanning =
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
connector.startScan();
|
||||
}
|
||||
},
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
connector.startScan();
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
@@ -205,4 +231,47 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _bluetoothOffWarning(BuildContext context) {
|
||||
final errorColor = Theme.of(context).colorScheme.error;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
color: errorColor.withValues(alpha: 0.15),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.scanner_bluetoothOff,
|
||||
style: TextStyle(
|
||||
color: errorColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.scanner_bluetoothOffMessage,
|
||||
style: TextStyle(
|
||||
color: errorColor.withValues(alpha: 0.85),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
TextButton(
|
||||
onPressed: () => FlutterBluePlus.turnOn(),
|
||||
child: Text(context.l10n.scanner_enableBluetooth),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
@@ -21,6 +22,7 @@ class SettingsScreen extends StatefulWidget {
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool _showBatteryVoltage = false;
|
||||
bool _deviceInfoExpanded = false;
|
||||
String _appVersion = '';
|
||||
|
||||
@override
|
||||
@@ -40,7 +42,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(l10n.settings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
@@ -74,43 +79,84 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settings_deviceInfo,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
|
||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoStatus,
|
||||
connector.isConnected
|
||||
? l10n.common_connected
|
||||
: l10n.common_disconnected,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_deviceInfoExpanded = !_deviceInfoExpanded;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.settings_deviceInfo,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: _deviceInfoExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Icon(Icons.expand_more),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoContactsCount,
|
||||
'${connector.contacts.length}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoChannelCount,
|
||||
'${connector.channels.length}',
|
||||
),
|
||||
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoName,
|
||||
connector.deviceDisplayName,
|
||||
),
|
||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoStatus,
|
||||
connector.isConnected
|
||||
? l10n.common_connected
|
||||
: l10n.common_disconnected,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoContactsCount,
|
||||
'${connector.contacts.length}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoChannelCount,
|
||||
'${connector.channels.length}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
crossFadeState: _deviceInfoExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -355,22 +401,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color? valueColor,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final row = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (leading != null) ...[leading, const SizedBox(width: 8)],
|
||||
Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: valueColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -379,11 +436,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: row,
|
||||
);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -688,7 +746,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
_gpxExport(
|
||||
Future<void> _gpxExport(
|
||||
GpxExport exporter,
|
||||
String name,
|
||||
String description,
|
||||
@@ -728,7 +786,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
_buildExportCard(MeshCoreConnector connector) {
|
||||
Widget _buildExportCard(MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
child: Column(
|
||||
@@ -808,6 +866,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
|
||||
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
|
||||
final _txPowerController = TextEditingController(text: '20');
|
||||
bool _clientRepeat = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -857,6 +916,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
if (widget.connector.currentTxPower != null) {
|
||||
_txPowerController.text = widget.connector.currentTxPower.toString();
|
||||
}
|
||||
|
||||
_clientRepeat = widget.connector.clientRepeat ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -906,9 +967,29 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
widget.connector.currentCr,
|
||||
);
|
||||
|
||||
// if the client repeat isnt null then we know its supported
|
||||
//otherwise we leave it out of the frame to avoid accidentally enabling
|
||||
final knownRepeat = widget.connector.clientRepeat != null;
|
||||
|
||||
if (knownRepeat) {
|
||||
const validRepeatFreqsKHz = {433000, 869000, 918000};
|
||||
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await widget.connector.sendFrame(
|
||||
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
|
||||
buildSetRadioParamsFrame(
|
||||
freqHz,
|
||||
bwHz,
|
||||
sf,
|
||||
cr,
|
||||
clientRepeat: knownRepeat ? _clientRepeat : null,
|
||||
),
|
||||
);
|
||||
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
||||
await widget.connector.refreshDeviceInfo();
|
||||
@@ -947,37 +1028,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settings_presets,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset915Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset915MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset868Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset868MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset433Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset433MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_longRange,
|
||||
onTap: () => _applyPreset(RadioSettings.presetLongRange),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_fastSpeed,
|
||||
onTap: () => _applyPreset(RadioSettings.presetFastSpeed),
|
||||
),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_presets,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (var i = 0; i < RadioSettings.presets.length; i++)
|
||||
DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(RadioSettings.presets[i].$1),
|
||||
),
|
||||
],
|
||||
onChanged: (index) {
|
||||
if (index != null) {
|
||||
_applyPreset(RadioSettings.presets[index].$2);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _frequencyController,
|
||||
decoration: InputDecoration(
|
||||
@@ -1049,6 +1118,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
if (widget.connector.clientRepeat != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.settings_clientRepeat),
|
||||
subtitle: Text(l10n.settings_clientRepeatSubtitle),
|
||||
value: _clientRepeat,
|
||||
onChanged: (value) => setState(() => _clientRepeat = value),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1062,15 +1141,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PresetChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PresetChip({required this.label, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(label: Text(label), onPressed: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -72,9 +75,19 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
}
|
||||
|
||||
void _handleStatusResponse(Uint8List frame) {
|
||||
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
||||
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
||||
if (batteryMv != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'telemetry',
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
||||
_parsedTelemetry = parsedTelemetry;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -181,6 +194,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final isFloodMode = repeater.pathOverride == -1;
|
||||
|
||||
@@ -307,6 +322,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
entry['values'],
|
||||
l10n.telemetry_channelTitle(entry['channel']),
|
||||
entry['channel'],
|
||||
isImperialUnits,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -319,6 +335,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
Map<String, dynamic> channelData,
|
||||
String title,
|
||||
int channel,
|
||||
bool isImperialUnits,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
@@ -358,12 +375,12 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
else if (entry.key == 'temperature' && channel == 1)
|
||||
_buildInfoRow(
|
||||
l10n.telemetry_mcuTemperatureLabel,
|
||||
_temperatureText(entry.value),
|
||||
_temperatureText(entry.value, isImperialUnits),
|
||||
)
|
||||
else if (entry.key == 'temperature')
|
||||
_buildInfoRow(
|
||||
l10n.telemetry_temperatureLabel,
|
||||
_temperatureText(entry.value),
|
||||
_temperatureText(entry.value, isImperialUnits),
|
||||
)
|
||||
else if (entry.key == 'current' && channel == 1)
|
||||
_buildInfoRow(
|
||||
@@ -405,29 +422,44 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _batteryText(double? batteryMv) {
|
||||
int? _extractTelemetryBatteryMillivolts(List<Map<String, dynamic>> entries) {
|
||||
for (final entry in entries) {
|
||||
if (entry['channel'] != 1) continue;
|
||||
final values = entry['values'];
|
||||
if (values is! Map<String, dynamic>) continue;
|
||||
final voltage = values['voltage'];
|
||||
if (voltage is num) return (voltage.toDouble() * 1000).round();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _batteryText(double? telemetryVolts) {
|
||||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||
if (batteryMv == null) return l10n.common_notAvailable;
|
||||
final percent = _batteryPercentFromMv(batteryMv);
|
||||
final volts = batteryMv.toStringAsFixed(2);
|
||||
final chemistry = _batteryChemistry();
|
||||
final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry);
|
||||
final volts = (batteryMv / 1000).toStringAsFixed(2);
|
||||
return l10n.telemetry_batteryValue(percent, volts);
|
||||
}
|
||||
|
||||
int _batteryPercentFromMv(double millivolts) {
|
||||
const minMv = 2.800;
|
||||
const maxMv = 4.200;
|
||||
if (millivolts <= minMv) return 0;
|
||||
if (millivolts >= maxMv) return 100;
|
||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
||||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
String _temperatureText(double? tempC) {
|
||||
String _temperatureText(double? tempC, bool isImperialUnits) {
|
||||
final l10n = context.l10n;
|
||||
if (tempC == null) return l10n.common_notAvailable;
|
||||
final tempF = (tempC * 9 / 5) + 32;
|
||||
return l10n.telemetry_temperatureValue(
|
||||
tempC.toStringAsFixed(1),
|
||||
tempF.toStringAsFixed(1),
|
||||
);
|
||||
if (isImperialUnits) {
|
||||
return '${tempF.toStringAsFixed(1)}°F';
|
||||
}
|
||||
return '${tempC.toStringAsFixed(1)}°C';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier {
|
||||
return stored ?? 'nmc';
|
||||
}
|
||||
|
||||
String batteryChemistryForRepeater(String repeaterPubKeyHex) {
|
||||
final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex];
|
||||
if (stored == 'liion') return 'nmc';
|
||||
return stored ?? 'nmc';
|
||||
}
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_settingsKey);
|
||||
@@ -74,6 +80,10 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(mapShowMarkers: value));
|
||||
}
|
||||
|
||||
Future<void> setEnableMessageTracing(bool value) async {
|
||||
await updateSettings(_settings.copyWith(enableMessageTracing: value));
|
||||
}
|
||||
|
||||
Future<void> setMapCacheBounds(Map<String, double>? value) async {
|
||||
await updateSettings(_settings.copyWith(mapCacheBounds: value));
|
||||
}
|
||||
@@ -132,4 +142,36 @@ class AppSettingsService extends ChangeNotifier {
|
||||
_settings.copyWith(batteryChemistryByDeviceId: updated),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setBatteryChemistryForRepeater(
|
||||
String repeaterPubKeyHex,
|
||||
String chemistry,
|
||||
) async {
|
||||
final updated = Map<String, String>.from(
|
||||
_settings.batteryChemistryByRepeaterId,
|
||||
);
|
||||
updated[repeaterPubKeyHex] = chemistry;
|
||||
await updateSettings(
|
||||
_settings.copyWith(batteryChemistryByRepeaterId: updated),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setUnitSystem(UnitSystem value) async {
|
||||
await updateSettings(_settings.copyWith(unitSystem: value));
|
||||
}
|
||||
|
||||
bool isChannelMuted(String channelName) {
|
||||
return _settings.mutedChannels.contains(channelName);
|
||||
}
|
||||
|
||||
Future<void> muteChannel(String channelName) async {
|
||||
final updated = Set<String>.from(_settings.mutedChannels)..add(channelName);
|
||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||
}
|
||||
|
||||
Future<void> unmuteChannel(String channelName) async {
|
||||
final updated = Set<String>.from(_settings.mutedChannels)
|
||||
..remove(channelName);
|
||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
@@ -15,20 +14,14 @@ class BackgroundService {
|
||||
channelDescription: 'Keeps MeshCore running in the background.',
|
||||
channelImportance: NotificationChannelImportance.LOW,
|
||||
priority: NotificationPriority.LOW,
|
||||
iconData: const NotificationIconData(
|
||||
resType: ResourceType.mipmap,
|
||||
resPrefix: ResourcePrefix.ic,
|
||||
name: 'launcher',
|
||||
),
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: false,
|
||||
playSound: false,
|
||||
),
|
||||
foregroundTaskOptions: const ForegroundTaskOptions(
|
||||
interval: 5000,
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
eventAction: ForegroundTaskEventAction.repeat(5000),
|
||||
autoRunOnBoot: false,
|
||||
allowWakeLock: true,
|
||||
allowWifiLock: false,
|
||||
),
|
||||
);
|
||||
@@ -64,13 +57,13 @@ void startCallback() {
|
||||
|
||||
class _MeshCoreTaskHandler extends TaskHandler {
|
||||
@override
|
||||
void onStart(DateTime timestamp, SendPort? sendPort) {}
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
|
||||
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {}
|
||||
void onRepeatEvent(DateTime timestamp) {}
|
||||
|
||||
@override
|
||||
void onDestroy(DateTime timestamp, SendPort? sendPort) {}
|
||||
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
|
||||
|
||||
@override
|
||||
void onNotificationButtonPressed(String id) {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class BleDebugLogEntry {
|
||||
@@ -44,6 +45,7 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
static const int maxEntries = 500;
|
||||
final List<BleDebugLogEntry> _entries = [];
|
||||
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
|
||||
bool _notifyScheduled = false;
|
||||
|
||||
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
|
||||
List<BleRawLogRxEntry> get rawLogRxEntries =>
|
||||
@@ -78,13 +80,31 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
_notifyListenersSafely();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_entries.clear();
|
||||
_rawLogRxEntries.clear();
|
||||
notifyListeners();
|
||||
_notifyListenersSafely();
|
||||
}
|
||||
|
||||
void _notifyListenersSafely() {
|
||||
final phase = SchedulerBinding.instance.schedulerPhase;
|
||||
final canNotifyNow =
|
||||
phase == SchedulerPhase.idle ||
|
||||
phase == SchedulerPhase.postFrameCallbacks;
|
||||
if (canNotifyNow) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_notifyScheduled) return;
|
||||
_notifyScheduled = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_notifyScheduled = false;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
String _describeFrame(
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../storage/prefs_manager.dart';
|
||||
|
||||
/// Client-side accessibility/UI service that exposes a persistent shared text scale
|
||||
/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the
|
||||
/// value is saved locally via SharedPreferences so it can be reused in Markdown
|
||||
/// viewers, log panels, or other text-heavy widgets without redundant network
|
||||
/// dependencies.
|
||||
///
|
||||
/// Widgets should scope rebuilds using the snippet below so only the scaled text
|
||||
/// is rebuilt instead of the entire chat list:
|
||||
/// ```dart
|
||||
/// context.select<ChatTextScaleService, double>(
|
||||
/// (service) => service.scale,
|
||||
/// )
|
||||
/// ```
|
||||
class ChatTextScaleService extends ChangeNotifier {
|
||||
static const _prefKey = 'chat_text_scale';
|
||||
static const double _minScale = 0.8;
|
||||
static const double _maxScale = 1.8;
|
||||
|
||||
double _scale = 1.0;
|
||||
Timer? _saveTimer;
|
||||
|
||||
double get scale => _scale;
|
||||
|
||||
Future<void> initialize() async {
|
||||
final stored = PrefsManager.instance.getDouble(_prefKey);
|
||||
if (stored != null) {
|
||||
_scale = _clamp(stored);
|
||||
}
|
||||
}
|
||||
|
||||
void setScale(double value, {bool persistImmediately = false}) {
|
||||
final next = _clamp(value);
|
||||
if (next == _scale) return;
|
||||
_scale = next;
|
||||
notifyListeners();
|
||||
if (persistImmediately) {
|
||||
_commitScale();
|
||||
} else {
|
||||
_scheduleSave();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
setScale(1.0, persistImmediately: true);
|
||||
}
|
||||
|
||||
void persist() => _commitScale();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scheduleSave() {
|
||||
_saveTimer?.cancel();
|
||||
_saveTimer = Timer(const Duration(milliseconds: 250), _commitScale);
|
||||
}
|
||||
|
||||
void _commitScale() {
|
||||
_saveTimer?.cancel();
|
||||
PrefsManager.instance.setDouble(_prefKey, _scale);
|
||||
}
|
||||
|
||||
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
typedef ElevationDataSource =
|
||||
Future<List<double?>> Function(List<LatLng> points);
|
||||
|
||||
class LineOfSightSample {
|
||||
final double distanceMeters;
|
||||
final double terrainMeters;
|
||||
final double lineHeightMeters;
|
||||
final double refractedHeightMeters;
|
||||
final double clearanceMeters;
|
||||
|
||||
const LineOfSightSample({
|
||||
required this.distanceMeters,
|
||||
required this.terrainMeters,
|
||||
required this.lineHeightMeters,
|
||||
required this.refractedHeightMeters,
|
||||
required this.clearanceMeters,
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightResult {
|
||||
final bool hasData;
|
||||
final bool isClear;
|
||||
final double totalDistanceMeters;
|
||||
final double maxObstructionMeters;
|
||||
final double? firstObstructionDistanceMeters;
|
||||
final List<LineOfSightSample> samples;
|
||||
final String? errorMessage;
|
||||
final double usedKFactor;
|
||||
final double? frequencyMHz;
|
||||
|
||||
const LineOfSightResult({
|
||||
required this.hasData,
|
||||
required this.isClear,
|
||||
required this.totalDistanceMeters,
|
||||
required this.maxObstructionMeters,
|
||||
required this.firstObstructionDistanceMeters,
|
||||
required this.samples,
|
||||
required this.usedKFactor,
|
||||
this.frequencyMHz,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
const LineOfSightResult.error({
|
||||
required this.totalDistanceMeters,
|
||||
required this.errorMessage,
|
||||
this.usedKFactor = 4.0 / 3.0,
|
||||
this.frequencyMHz,
|
||||
}) : hasData = false,
|
||||
isClear = false,
|
||||
maxObstructionMeters = 0,
|
||||
firstObstructionDistanceMeters = null,
|
||||
samples = const [];
|
||||
}
|
||||
|
||||
class LineOfSightPathSegment {
|
||||
final int index;
|
||||
final LatLng start;
|
||||
final LatLng end;
|
||||
final LineOfSightResult result;
|
||||
|
||||
const LineOfSightPathSegment({
|
||||
required this.index,
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.result,
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightPathResult {
|
||||
final List<LineOfSightPathSegment> segments;
|
||||
final int clearSegments;
|
||||
final int blockedSegments;
|
||||
final int unknownSegments;
|
||||
|
||||
const LineOfSightPathResult({
|
||||
required this.segments,
|
||||
required this.clearSegments,
|
||||
required this.blockedSegments,
|
||||
required this.unknownSegments,
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightService {
|
||||
static const String errorElevationUnavailable =
|
||||
'los_error_elevation_unavailable';
|
||||
static const String errorInvalidInput = 'los_error_invalid_input';
|
||||
|
||||
static const double _earthRadiusMeters = 6371000.0;
|
||||
static const Distance _distance = Distance();
|
||||
static const Duration _cacheTtl = Duration(hours: 24);
|
||||
static const int _maxFetchAttempts = 4; // initial try + 3 retries
|
||||
static const Duration _initialBackoff = Duration(milliseconds: 300);
|
||||
static const double _baselineFrequencyMHz = 915.0;
|
||||
static const double _baselineKFactor = 4.0 / 3.0;
|
||||
|
||||
static double get baselineFrequencyMHz => _baselineFrequencyMHz;
|
||||
static double get baselineKFactor => _baselineKFactor;
|
||||
|
||||
final http.Client _httpClient;
|
||||
final bool _ownsHttpClient;
|
||||
final ElevationDataSource? _elevationDataSource;
|
||||
final Map<String, _CachedElevation> _elevationCache = {};
|
||||
|
||||
LineOfSightService({
|
||||
http.Client? httpClient,
|
||||
ElevationDataSource? elevationDataSource,
|
||||
}) : _httpClient = httpClient ?? http.Client(),
|
||||
_ownsHttpClient = httpClient == null,
|
||||
_elevationDataSource = elevationDataSource;
|
||||
|
||||
Future<LineOfSightPathResult> analyzePath(
|
||||
List<LatLng> points, {
|
||||
double startAntennaHeightMeters = 1.5,
|
||||
double endAntennaHeightMeters = 1.5,
|
||||
double? frequencyMHz,
|
||||
double obstructionToleranceMeters = 0.0,
|
||||
}) async {
|
||||
if (points.length < 2) {
|
||||
return const LineOfSightPathResult(
|
||||
segments: [],
|
||||
clearSegments: 0,
|
||||
blockedSegments: 0,
|
||||
unknownSegments: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final segments = <LineOfSightPathSegment>[];
|
||||
var clearSegments = 0;
|
||||
var blockedSegments = 0;
|
||||
var unknownSegments = 0;
|
||||
|
||||
final kFactor = _kFactorForFrequency(frequencyMHz);
|
||||
for (int i = 0; i < points.length - 1; i++) {
|
||||
final result = await analyzeLink(
|
||||
points[i],
|
||||
points[i + 1],
|
||||
startAntennaHeightMeters: startAntennaHeightMeters,
|
||||
endAntennaHeightMeters: endAntennaHeightMeters,
|
||||
kFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
obstructionToleranceMeters: obstructionToleranceMeters,
|
||||
);
|
||||
segments.add(
|
||||
LineOfSightPathSegment(
|
||||
index: i,
|
||||
start: points[i],
|
||||
end: points[i + 1],
|
||||
result: result,
|
||||
),
|
||||
);
|
||||
|
||||
if (!result.hasData) {
|
||||
unknownSegments++;
|
||||
} else if (result.isClear) {
|
||||
clearSegments++;
|
||||
} else {
|
||||
blockedSegments++;
|
||||
}
|
||||
}
|
||||
|
||||
return LineOfSightPathResult(
|
||||
segments: segments,
|
||||
clearSegments: clearSegments,
|
||||
blockedSegments: blockedSegments,
|
||||
unknownSegments: unknownSegments,
|
||||
);
|
||||
}
|
||||
|
||||
Future<LineOfSightResult> analyzeLink(
|
||||
LatLng start,
|
||||
LatLng end, {
|
||||
double startAntennaHeightMeters = 1.5,
|
||||
double endAntennaHeightMeters = 1.5,
|
||||
required double kFactor,
|
||||
double? frequencyMHz,
|
||||
double obstructionToleranceMeters = 0.0,
|
||||
}) async {
|
||||
final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end);
|
||||
if (totalDistanceMeters <= 1) {
|
||||
return LineOfSightResult(
|
||||
hasData: true,
|
||||
isClear: true,
|
||||
totalDistanceMeters: totalDistanceMeters,
|
||||
maxObstructionMeters: 0,
|
||||
firstObstructionDistanceMeters: null,
|
||||
samples: const [],
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
}
|
||||
|
||||
final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters);
|
||||
final elevations = await _getElevations(samplePoints);
|
||||
|
||||
if (elevations.any((e) => e == null)) {
|
||||
return LineOfSightResult.error(
|
||||
totalDistanceMeters: totalDistanceMeters,
|
||||
errorMessage: errorElevationUnavailable,
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
}
|
||||
|
||||
return computeFromElevations(
|
||||
points: samplePoints,
|
||||
elevations: elevations.cast<double>(),
|
||||
startAntennaHeightMeters: startAntennaHeightMeters,
|
||||
endAntennaHeightMeters: endAntennaHeightMeters,
|
||||
kFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
obstructionToleranceMeters: obstructionToleranceMeters,
|
||||
);
|
||||
}
|
||||
|
||||
static LineOfSightResult computeFromElevations({
|
||||
required List<LatLng> points,
|
||||
required List<double> elevations,
|
||||
double startAntennaHeightMeters = 1.5,
|
||||
double endAntennaHeightMeters = 1.5,
|
||||
required double kFactor,
|
||||
double? frequencyMHz,
|
||||
double obstructionToleranceMeters = 0.0,
|
||||
}) {
|
||||
if (points.length < 2 || elevations.length != points.length) {
|
||||
return LineOfSightResult.error(
|
||||
totalDistanceMeters: 0,
|
||||
errorMessage: errorInvalidInput,
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
}
|
||||
|
||||
final totalDistanceMeters = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
points.first,
|
||||
points.last,
|
||||
);
|
||||
final effectiveEarthRadius = _earthRadiusMeters * kFactor;
|
||||
final startLineHeight = elevations.first + startAntennaHeightMeters;
|
||||
final endLineHeight = elevations.last + endAntennaHeightMeters;
|
||||
|
||||
var maxObstructionMeters = 0.0;
|
||||
double? firstObstructionDistanceMeters;
|
||||
final samples = <LineOfSightSample>[];
|
||||
var isClear = true;
|
||||
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
||||
final distanceFromStart = totalDistanceMeters * fraction;
|
||||
final lineHeight =
|
||||
startLineHeight + (endLineHeight - startLineHeight) * fraction;
|
||||
|
||||
final earthBulge =
|
||||
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
|
||||
(2 * effectiveEarthRadius);
|
||||
final terrainHeight = elevations[i] + earthBulge;
|
||||
final clearance = lineHeight - terrainHeight;
|
||||
final unrefBulge =
|
||||
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
|
||||
(2 * _earthRadiusMeters);
|
||||
final refractedHeight = lineHeight + (unrefBulge - earthBulge);
|
||||
|
||||
if (clearance < -obstructionToleranceMeters) {
|
||||
isClear = false;
|
||||
final obstruction = -clearance;
|
||||
if (obstruction > maxObstructionMeters) {
|
||||
maxObstructionMeters = obstruction;
|
||||
}
|
||||
firstObstructionDistanceMeters ??= distanceFromStart;
|
||||
}
|
||||
|
||||
samples.add(
|
||||
LineOfSightSample(
|
||||
distanceMeters: distanceFromStart,
|
||||
terrainMeters: terrainHeight,
|
||||
lineHeightMeters: lineHeight,
|
||||
refractedHeightMeters: refractedHeight,
|
||||
clearanceMeters: clearance,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return LineOfSightResult(
|
||||
hasData: true,
|
||||
isClear: isClear,
|
||||
totalDistanceMeters: totalDistanceMeters,
|
||||
maxObstructionMeters: maxObstructionMeters,
|
||||
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
||||
samples: samples,
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
}
|
||||
|
||||
static double _kFactorForFrequency(double? frequencyMHz) {
|
||||
if (frequencyMHz == null) return _baselineKFactor;
|
||||
final delta =
|
||||
(frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz;
|
||||
final adjustment = delta * 0.15;
|
||||
final scaled = _baselineKFactor * (1 + adjustment);
|
||||
return scaled.clamp(1.1, 1.6).toDouble();
|
||||
}
|
||||
|
||||
List<LatLng> _buildSamplePoints(
|
||||
LatLng start,
|
||||
LatLng end,
|
||||
double distanceMeters,
|
||||
) {
|
||||
final sampleCount = distanceMeters < 2000
|
||||
? 21
|
||||
: distanceMeters < 10000
|
||||
? 41
|
||||
: 81;
|
||||
|
||||
final points = <LatLng>[];
|
||||
for (int i = 0; i < sampleCount; i++) {
|
||||
final t = i / (sampleCount - 1);
|
||||
points.add(
|
||||
LatLng(
|
||||
start.latitude + (end.latitude - start.latitude) * t,
|
||||
start.longitude + (end.longitude - start.longitude) * t,
|
||||
),
|
||||
);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
Future<List<double?>> _getElevations(List<LatLng> points) async {
|
||||
final dataSource = _elevationDataSource;
|
||||
if (dataSource != null) {
|
||||
return dataSource(points);
|
||||
}
|
||||
|
||||
final uncached = <int, LatLng>{};
|
||||
final values = List<double?>.filled(points.length, null);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final key = _cacheKey(points[i]);
|
||||
final cached = _readCachedValue(key);
|
||||
if (cached != null) {
|
||||
values[i] = cached;
|
||||
} else {
|
||||
uncached[i] = points[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (uncached.isEmpty) return values;
|
||||
|
||||
final latCsv = uncached.values
|
||||
.map((p) => p.latitude.toStringAsFixed(6))
|
||||
.join(',');
|
||||
final lonCsv = uncached.values
|
||||
.map((p) => p.longitude.toStringAsFixed(6))
|
||||
.join(',');
|
||||
|
||||
final uri = Uri.parse(
|
||||
'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv',
|
||||
);
|
||||
|
||||
final response = await _getWithBackoff(uri);
|
||||
if (response.statusCode != 200) {
|
||||
return values;
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return values;
|
||||
}
|
||||
final elevations = decoded['elevation'];
|
||||
if (elevations is! List) {
|
||||
return values;
|
||||
}
|
||||
|
||||
final indices = uncached.keys.toList();
|
||||
for (int i = 0; i < min(indices.length, elevations.length); i++) {
|
||||
final value = elevations[i];
|
||||
if (value is! num) continue;
|
||||
final index = indices[i];
|
||||
final elevation = value.toDouble();
|
||||
values[index] = elevation;
|
||||
_elevationCache[_cacheKey(points[index])] = _CachedElevation(
|
||||
value: elevation,
|
||||
expiresAt: DateTime.now().add(_cacheTtl),
|
||||
);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
Future<http.Response> _getWithBackoff(Uri uri) async {
|
||||
var attempt = 0;
|
||||
Duration backoff = _initialBackoff;
|
||||
|
||||
while (true) {
|
||||
attempt++;
|
||||
try {
|
||||
final response = await _httpClient.get(uri);
|
||||
if (!_shouldRetryStatus(response.statusCode) ||
|
||||
attempt >= _maxFetchAttempts) {
|
||||
return response;
|
||||
}
|
||||
} catch (_) {
|
||||
if (attempt >= _maxFetchAttempts) rethrow;
|
||||
}
|
||||
|
||||
await Future.delayed(backoff);
|
||||
backoff *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldRetryStatus(int statusCode) {
|
||||
return statusCode == 429 || statusCode >= 500;
|
||||
}
|
||||
|
||||
double? _readCachedValue(String key) {
|
||||
final cached = _elevationCache[key];
|
||||
if (cached == null) return null;
|
||||
if (DateTime.now().isAfter(cached.expiresAt)) {
|
||||
_elevationCache.remove(key);
|
||||
return null;
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
String _cacheKey(LatLng point) {
|
||||
return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}';
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_ownsHttpClient) {
|
||||
_httpClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CachedElevation {
|
||||
final double value;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const _CachedElevation({required this.value, required this.expiresAt});
|
||||
}
|
||||
@@ -234,7 +234,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||
bool updateMessageFromSent(
|
||||
Uint8List ackHash,
|
||||
int timeoutMs, {
|
||||
bool allowQueueFallback = true,
|
||||
}) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
@@ -277,7 +281,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
||||
if (messageId == null) {
|
||||
if (messageId == null && allowQueueFallback) {
|
||||
_debugLogService?.warn(
|
||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
||||
tag: 'AckHash',
|
||||
@@ -320,7 +324,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
if (messageId == null || contact == null) {
|
||||
debugPrint('No pending message found for ACK hash: $ackHashHex');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the mapping for future lookups (e.g., when ACK arrives)
|
||||
@@ -339,7 +343,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
'Message $messageId no longer pending for ACK hash: $ackHashHex',
|
||||
);
|
||||
_ackHashToMessageId.remove(ackHashHex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||
@@ -389,8 +393,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
_startTimeoutTimer(messageId, actualTimeout);
|
||||
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
|
||||
|
||||
void _startTimeoutTimer(String messageId, int timeoutMs) {
|
||||
_timeoutTimers[messageId]?.cancel();
|
||||
_timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () {
|
||||
|
||||
@@ -58,16 +58,22 @@ class NotificationService {
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
const windowsSettings = WindowsInitializationSettings(
|
||||
appName: 'MeshCore Open',
|
||||
appUserModelId: 'org.meshcore.open.app',
|
||||
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
|
||||
);
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
macOS: macSettings,
|
||||
windows: windowsSettings,
|
||||
);
|
||||
|
||||
try {
|
||||
await _notifications.initialize(
|
||||
initSettings,
|
||||
settings: initSettings,
|
||||
onDidReceiveNotificationResponse: _onNotificationTapped,
|
||||
);
|
||||
_isInitialized = true;
|
||||
@@ -76,6 +82,13 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _ensureInitialized() async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
return _isInitialized;
|
||||
}
|
||||
|
||||
Future<bool> requestPermissions() async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
@@ -114,9 +127,7 @@ class NotificationService {
|
||||
String? contactId,
|
||||
int? badgeCount,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
if (!await _ensureInitialized()) return;
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'messages',
|
||||
@@ -148,13 +159,17 @@ class NotificationService {
|
||||
macOS: macDetails,
|
||||
);
|
||||
|
||||
await _notifications.show(
|
||||
contactId?.hashCode ?? 0,
|
||||
contactName,
|
||||
message,
|
||||
notificationDetails,
|
||||
payload: 'message:$contactId',
|
||||
);
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: contactId?.hashCode ?? 0,
|
||||
title: contactName,
|
||||
body: message,
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'message:$contactId',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show message notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showAdvertNotificationImpl({
|
||||
@@ -162,9 +177,7 @@ class NotificationService {
|
||||
required String contactType,
|
||||
String? contactId,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
if (!await _ensureInitialized()) return;
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'adverts',
|
||||
@@ -193,13 +206,17 @@ class NotificationService {
|
||||
macOS: macDetails,
|
||||
);
|
||||
|
||||
await _notifications.show(
|
||||
contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
_l10n.notification_newTypeDiscovered(contactType),
|
||||
contactName,
|
||||
notificationDetails,
|
||||
payload: 'advert:$contactId',
|
||||
);
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||
body: contactName,
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'advert:$contactId',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show advert notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showChannelMessageNotificationImpl({
|
||||
@@ -208,9 +225,7 @@ class NotificationService {
|
||||
int? channelIndex,
|
||||
int? badgeCount,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
if (!await _ensureInitialized()) return;
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'channel_messages',
|
||||
@@ -247,13 +262,17 @@ class NotificationService {
|
||||
? _l10n.notification_receivedNewMessage
|
||||
: preview;
|
||||
|
||||
await _notifications.show(
|
||||
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
channelName,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: 'channel:$channelIndex',
|
||||
);
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
title: channelName,
|
||||
body: body,
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'channel:$channelIndex',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show channel notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a privacy-safe identifier for debug logging.
|
||||
@@ -285,7 +304,7 @@ class NotificationService {
|
||||
}
|
||||
|
||||
Future<void> cancel(int id) async {
|
||||
await _notifications.cancel(id);
|
||||
await _notifications.cancel(id: id);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -396,35 +415,39 @@ class NotificationService {
|
||||
Future<void> _showNotificationImmediately(
|
||||
_PendingNotification notification,
|
||||
) async {
|
||||
switch (notification.type) {
|
||||
case _NotificationType.message:
|
||||
await _showMessageNotificationImpl(
|
||||
contactName: notification.title,
|
||||
message: notification.body,
|
||||
contactId: notification.id,
|
||||
badgeCount: notification.badgeCount,
|
||||
);
|
||||
break;
|
||||
case _NotificationType.advert:
|
||||
await _showAdvertNotificationImpl(
|
||||
contactName: notification.body,
|
||||
contactType: notification.title,
|
||||
contactId: notification.id,
|
||||
);
|
||||
break;
|
||||
case _NotificationType.channelMessage:
|
||||
await _showChannelMessageNotificationImpl(
|
||||
channelName: notification.title,
|
||||
message: notification.body,
|
||||
channelIndex: int.tryParse(notification.id ?? ''),
|
||||
badgeCount: notification.badgeCount,
|
||||
);
|
||||
break;
|
||||
try {
|
||||
switch (notification.type) {
|
||||
case _NotificationType.message:
|
||||
await _showMessageNotificationImpl(
|
||||
contactName: notification.title,
|
||||
message: notification.body,
|
||||
contactId: notification.id,
|
||||
badgeCount: notification.badgeCount,
|
||||
);
|
||||
break;
|
||||
case _NotificationType.advert:
|
||||
await _showAdvertNotificationImpl(
|
||||
contactName: notification.body,
|
||||
contactType: notification.title,
|
||||
contactId: notification.id,
|
||||
);
|
||||
break;
|
||||
case _NotificationType.channelMessage:
|
||||
await _showChannelMessageNotificationImpl(
|
||||
channelName: notification.title,
|
||||
message: notification.body,
|
||||
channelIndex: int.tryParse(notification.id ?? ''),
|
||||
badgeCount: notification.badgeCount,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show immediate notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!await _ensureInitialized()) return;
|
||||
|
||||
// Group by type
|
||||
final messages = batch
|
||||
@@ -468,13 +491,17 @@ class NotificationService {
|
||||
|
||||
const notificationDetails = NotificationDetails(android: androidDetails);
|
||||
|
||||
await _notifications.show(
|
||||
'batch_summary'.hashCode,
|
||||
_l10n.notification_activityTitle,
|
||||
parts.join(', '),
|
||||
notificationDetails,
|
||||
payload: 'batch',
|
||||
);
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: 'batch_summary'.hashCode,
|
||||
title: _l10n.notification_activityTitle,
|
||||
body: parts.join(', '),
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'batch',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show batch summary notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class ContactStore {
|
||||
'publicKey': base64Encode(contact.publicKey),
|
||||
'name': contact.name,
|
||||
'type': contact.type,
|
||||
'flags': contact.flags,
|
||||
'pathLength': contact.pathLength,
|
||||
'path': base64Encode(contact.path),
|
||||
'pathOverride': contact.pathOverride,
|
||||
@@ -53,6 +54,7 @@ class ContactStore {
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
type: json['type'] as int? ?? 0,
|
||||
flags: json['flags'] as int? ?? 0,
|
||||
pathLength: json['pathLength'] as int? ?? -1,
|
||||
path: json['path'] != null
|
||||
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
typedef BatteryVoltageRange = ({int minMv, int maxMv});
|
||||
|
||||
BatteryVoltageRange batteryVoltageRange(String chemistry) {
|
||||
switch (chemistry) {
|
||||
case 'lifepo4':
|
||||
return (minMv: 2600, maxMv: 3650);
|
||||
case 'lipo':
|
||||
return (minMv: 3000, maxMv: 4200);
|
||||
case 'nmc':
|
||||
default:
|
||||
return (minMv: 3000, maxMv: 4200);
|
||||
}
|
||||
}
|
||||
|
||||
int estimateBatteryPercentFromMillivolts(int millivolts, String chemistry) {
|
||||
final range = batteryVoltageRange(chemistry);
|
||||
if (millivolts <= range.minMv) return 0;
|
||||
if (millivolts >= range.maxMv) return 100;
|
||||
return (((millivolts - range.minMv) * 100) / (range.maxMv - range.minMv))
|
||||
.round();
|
||||
}
|
||||
|
||||
int estimateBatteryPercentFromVolts(double volts, String chemistry) {
|
||||
final millivolts = (volts * 1000).round();
|
||||
return estimateBatteryPercentFromMillivolts(millivolts, chemistry);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AdaptiveAppBarTitle extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const AdaptiveAppBarTitle(this.text, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
child: FittedBox(fit: BoxFit.scaleDown, child: Text(text, maxLines: 1)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/widgets/battery_indicator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'snr_indicator.dart';
|
||||
|
||||
class AppBarTitle extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
const AppBarTitle(this.title, {this.leading, this.trailing, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final selfName = connector.selfName;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.hasBoundedWidth
|
||||
? constraints.maxWidth
|
||||
: MediaQuery.sizeOf(context).width;
|
||||
final compact = availableWidth < 240;
|
||||
final showSubtitle =
|
||||
!compact && connector.isConnected && selfName != null;
|
||||
final showBattery = availableWidth >= 60;
|
||||
final showSnr = availableWidth >= 110;
|
||||
final showIndicators = showBattery || showSnr;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
leading ?? const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
if (showSubtitle)
|
||||
Text(
|
||||
'($selfName)',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showIndicators) const SizedBox(width: 6),
|
||||
if (showIndicators)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showBattery) BatteryIndicator(connector: connector),
|
||||
if (showSnr) SNRIndicator(connector: connector),
|
||||
],
|
||||
),
|
||||
trailing ?? const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,20 +68,24 @@ class _BatteryIndicatorState extends State<BatteryIndicator> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(batteryUi.icon, size: 18, color: batteryUi.color),
|
||||
const SizedBox(width: 2),
|
||||
Flexible(
|
||||
child: Text(
|
||||
displayText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: batteryUi.color,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(batteryUi.icon, size: 18, color: batteryUi.color),
|
||||
const SizedBox(height: 2),
|
||||
Flexible(
|
||||
child: Text(
|
||||
displayText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: batteryUi.color,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
overflow: TextOverflow.visible,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
|
||||
/// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables.
|
||||
/// Double-tap resets the scale. Only the wrapper itself listens to gestures;
|
||||
/// child scrollables keep their normal touch handling.
|
||||
class ChatZoomWrapper extends StatefulWidget {
|
||||
const ChatZoomWrapper({super.key, required this.child, this.onDoubleTap});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onDoubleTap;
|
||||
|
||||
@override
|
||||
State<ChatZoomWrapper> createState() => _ChatZoomWrapperState();
|
||||
}
|
||||
|
||||
class _ChatZoomWrapperState extends State<ChatZoomWrapper> {
|
||||
double? _startScale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final service = context.read<ChatTextScaleService>();
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onDoubleTap: () {
|
||||
service.reset();
|
||||
service.persist();
|
||||
widget.onDoubleTap?.call();
|
||||
},
|
||||
onScaleStart: (details) {
|
||||
if (details.pointerCount != 2) return;
|
||||
_startScale = service.scale;
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
if (details.pointerCount != 2) return;
|
||||
final baseScale = _startScale ?? service.scale;
|
||||
service.setScale(baseScale * details.scale);
|
||||
},
|
||||
onScaleEnd: (_) {
|
||||
_startScale = null;
|
||||
service.persist();
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import '../l10n/l10n.dart';
|
||||
|
||||
enum ContactSortOption { lastSeen, recentMessages, name }
|
||||
|
||||
enum ContactTypeFilter { all, users, repeaters, rooms }
|
||||
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
|
||||
|
||||
class SortFilterMenuOption {
|
||||
final int value;
|
||||
@@ -94,11 +94,12 @@ const int _actionSortRecentMessages = 1;
|
||||
const int _actionSortName = 2;
|
||||
const int _actionSortLastSeen = 3;
|
||||
const int _actionFilterAll = 4;
|
||||
const int _actionFilterUsers = 5;
|
||||
const int _actionFilterRepeaters = 6;
|
||||
const int _actionFilterRooms = 7;
|
||||
const int _actionToggleUnreadOnly = 8;
|
||||
const int _actionNewGroup = 9;
|
||||
const int _actionFilterFavorites = 5;
|
||||
const int _actionFilterUsers = 6;
|
||||
const int _actionFilterRepeaters = 7;
|
||||
const int _actionFilterRooms = 8;
|
||||
const int _actionToggleUnreadOnly = 9;
|
||||
const int _actionNewGroup = 10;
|
||||
|
||||
class ContactsFilterMenu extends StatelessWidget {
|
||||
final ContactSortOption sortOption;
|
||||
@@ -154,6 +155,11 @@ class ContactsFilterMenu extends StatelessWidget {
|
||||
label: l10n.listFilter_all,
|
||||
checked: typeFilter == ContactTypeFilter.all,
|
||||
),
|
||||
SortFilterMenuOption(
|
||||
value: _actionFilterFavorites,
|
||||
label: l10n.listFilter_favorites,
|
||||
checked: typeFilter == ContactTypeFilter.favorites,
|
||||
),
|
||||
SortFilterMenuOption(
|
||||
value: _actionFilterUsers,
|
||||
label: l10n.listFilter_users,
|
||||
@@ -198,6 +204,9 @@ class ContactsFilterMenu extends StatelessWidget {
|
||||
case _actionFilterUsers:
|
||||
onTypeFilterChanged(ContactTypeFilter.users);
|
||||
break;
|
||||
case _actionFilterFavorites:
|
||||
onTypeFilterChanged(ContactTypeFilter.favorites);
|
||||
break;
|
||||
case _actionFilterRepeaters:
|
||||
onTypeFilterChanged(ContactTypeFilter.repeaters);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class MessageStatusIcon extends StatelessWidget {
|
||||
final bool isAcked;
|
||||
final bool isFailed;
|
||||
final double size;
|
||||
|
||||
const MessageStatusIcon({
|
||||
super.key,
|
||||
required this.isAcked,
|
||||
this.isFailed = false,
|
||||
this.size = 14,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFailed) {
|
||||
return Icon(Icons.cancel, size: size, color: Colors.red);
|
||||
}
|
||||
|
||||
final Color color;
|
||||
if (isAcked) {
|
||||
color = Colors.green;
|
||||
} else {
|
||||
color = Colors.grey;
|
||||
}
|
||||
|
||||
return SvgPicture.asset(
|
||||
'assets/icons/done_all.svg',
|
||||
width: size,
|
||||
height: size,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/models/path_history.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -19,15 +21,22 @@ class PathManagementDialog {
|
||||
}
|
||||
}
|
||||
|
||||
class _PathManagementDialog extends StatelessWidget {
|
||||
class _PathManagementDialog extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
||||
const _PathManagementDialog({required this.contact});
|
||||
|
||||
@override
|
||||
State<_PathManagementDialog> createState() => _PathManagementDialogState();
|
||||
}
|
||||
|
||||
class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
bool _showAllPaths = false;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
orElse: () => contact,
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,6 +143,59 @@ class _PathManagementDialog extends StatelessWidget {
|
||||
final currentContact = _resolveContact(connector);
|
||||
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
|
||||
|
||||
final repeatersList = List.of(connector.directRepeaters)
|
||||
..sort((a, b) => b.ranking.compareTo(a.ranking));
|
||||
|
||||
if (repeatersList.isEmpty) {
|
||||
_showAllPaths = true;
|
||||
}
|
||||
|
||||
final directRepeater = repeatersList.isEmpty
|
||||
? null
|
||||
: repeatersList.first;
|
||||
final secondDirectRepeater = repeatersList.length < 2
|
||||
? null
|
||||
: repeatersList.elementAt(1);
|
||||
final thirdDirectRepeater = repeatersList.length < 3
|
||||
? null
|
||||
: repeatersList.elementAt(2);
|
||||
|
||||
List<MapEntry<int, MapEntry<Color, PathRecord>>> pathsWithRepeaters =
|
||||
paths.map((path) {
|
||||
final isDirectRepeater =
|
||||
directRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
directRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
final isSecondDirectRepeater =
|
||||
secondDirectRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
final isThirdDirectRepeater =
|
||||
thirdDirectRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
|
||||
int ranking = -1;
|
||||
Color color = Colors.grey;
|
||||
if (isDirectRepeater) {
|
||||
color = Colors.green;
|
||||
ranking = 3;
|
||||
} else if (isSecondDirectRepeater) {
|
||||
color = Colors.yellow;
|
||||
ranking = 2;
|
||||
} else if (isThirdDirectRepeater) {
|
||||
color = Colors.red;
|
||||
ranking = 1;
|
||||
} else if (path.wasFloodDiscovery) {
|
||||
color = Colors.blue;
|
||||
ranking = 0;
|
||||
}
|
||||
|
||||
return MapEntry(ranking, MapEntry(color, path));
|
||||
}).toList();
|
||||
|
||||
pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key));
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(l10n.chat_pathManagement),
|
||||
content: SingleChildScrollView(
|
||||
@@ -147,6 +209,17 @@ class _PathManagementDialog extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (paths.isNotEmpty) ...[
|
||||
if (repeatersList.isNotEmpty)
|
||||
FeatureToggleRow(
|
||||
title: l10n.chat_ShowAllPaths,
|
||||
subtitle: "",
|
||||
value: _showAllPaths,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_showAllPaths = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
l10n.chat_recentAckPaths,
|
||||
style: const TextStyle(
|
||||
@@ -154,7 +227,7 @@ class _PathManagementDialog extends StatelessWidget {
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (paths.length >= 100) ...[
|
||||
if (pathsWithRepeaters.length >= 100) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -173,92 +246,99 @@ class _PathManagementDialog extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
...paths.map((path) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: path.wasFloodDiscovery
|
||||
? Colors.blue
|
||||
: Colors.green,
|
||||
child: Text(
|
||||
'${path.hopCount}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
l10n.chat_hopsCount(path.hopCount),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
tooltip: l10n.chat_removePath,
|
||||
onPressed: () async {
|
||||
await pathService.removePathRecord(
|
||||
currentContact.publicKeyHex,
|
||||
path.pathBytes,
|
||||
);
|
||||
},
|
||||
...pathsWithRepeaters.map((entry) {
|
||||
final path = entry.value.value;
|
||||
final color = entry.value.key;
|
||||
|
||||
if (!_showAllPaths && entry.key < 1) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: color,
|
||||
child: Text(
|
||||
'${path.hopCount}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
path.wasFloodDiscovery
|
||||
? const Icon(
|
||||
Icons.waves,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.route,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
l10n.chat_hopsCount(path.hopCount),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
tooltip: l10n.chat_removePath,
|
||||
onPressed: () async {
|
||||
await pathService.removePathRecord(
|
||||
currentContact.publicKeyHex,
|
||||
path.pathBytes,
|
||||
);
|
||||
},
|
||||
),
|
||||
path.wasFloodDiscovery
|
||||
? const Icon(
|
||||
Icons.waves,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.route,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
onLongPress: () =>
|
||||
_showFullPathDialog(context, path.pathBytes),
|
||||
onTap: () async {
|
||||
if (path.pathBytes.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.chat_pathDetailsNotAvailable,
|
||||
),
|
||||
],
|
||||
),
|
||||
onLongPress: () =>
|
||||
_showFullPathDialog(context, path.pathBytes),
|
||||
onTap: () async {
|
||||
if (path.pathBytes.isEmpty) {
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final pathBytes = Uint8List.fromList(
|
||||
path.pathBytes,
|
||||
);
|
||||
final pathLength = path.pathBytes.length;
|
||||
|
||||
await connector.setPathOverride(
|
||||
currentContact,
|
||||
pathLen: pathLength,
|
||||
pathBytes: pathBytes,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.chat_pathDetailsNotAvailable,
|
||||
l10n.path_usingHopsPath(path.hopCount),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final pathBytes = Uint8List.fromList(path.pathBytes);
|
||||
final pathLength = path.pathBytes.length;
|
||||
|
||||
await connector.setPathOverride(
|
||||
currentContact,
|
||||
pathLen: pathLength,
|
||||
pathBytes: pathBytes,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.path_usingHopsPath(path.hopCount),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const Divider(),
|
||||
] else ...[
|
||||
|
||||
@@ -156,7 +156,7 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
|
||||
MobileScanner(
|
||||
controller: _controller,
|
||||
onDetect: _handleDetection,
|
||||
errorBuilder: (context, error, child) {
|
||||
errorBuilder: (context, error) {
|
||||
return _buildErrorWidget(context, error);
|
||||
},
|
||||
),
|
||||
|
||||
+174
-31
@@ -1,4 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class SNRUi {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String text;
|
||||
const SNRUi(this.icon, this.color, this.text);
|
||||
}
|
||||
|
||||
List<double> getSNRfromSF(int spreadingFactor) {
|
||||
switch (spreadingFactor) {
|
||||
@@ -19,44 +28,178 @@ List<double> getSNRfromSF(int spreadingFactor) {
|
||||
}
|
||||
}
|
||||
|
||||
class SNRIcon extends StatelessWidget {
|
||||
final double snr;
|
||||
final List<double> snrLevels;
|
||||
SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) {
|
||||
if (snr == null ||
|
||||
spreadingFactor == null ||
|
||||
spreadingFactor < 7 ||
|
||||
spreadingFactor > 12) {
|
||||
return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—');
|
||||
}
|
||||
|
||||
const SNRIcon({
|
||||
super.key,
|
||||
required this.snr,
|
||||
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
|
||||
});
|
||||
final snrLevels = getSNRfromSF(spreadingFactor);
|
||||
|
||||
IconData icon;
|
||||
Color color;
|
||||
String text = '${snr.toStringAsFixed(1)} dB';
|
||||
|
||||
if (snr >= snrLevels[0]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.green;
|
||||
} else if (snr >= snrLevels[1]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.lightGreen;
|
||||
} else if (snr >= snrLevels[2]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.yellow;
|
||||
} else if (snr >= snrLevels[3]) {
|
||||
icon = Icons.signal_cellular_alt_2_bar;
|
||||
color = Colors.orange;
|
||||
} else {
|
||||
icon = Icons.signal_cellular_alt_1_bar;
|
||||
color = Colors.red;
|
||||
}
|
||||
|
||||
return SNRUi(icon, color, text);
|
||||
}
|
||||
|
||||
class SNRIndicator extends StatefulWidget {
|
||||
final MeshCoreConnector connector;
|
||||
|
||||
const SNRIndicator({super.key, required this.connector});
|
||||
|
||||
@override
|
||||
State<SNRIndicator> createState() => _SNRIndicatorState();
|
||||
}
|
||||
|
||||
class _SNRIndicatorState extends State<SNRIndicator> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
final directRepeaters = widget.connector.directRepeaters;
|
||||
final directBestRepeaters = List.of(directRepeaters)
|
||||
..sort((a, b) => (b.ranking).compareTo(a.ranking));
|
||||
final directRepeater = directBestRepeaters.isEmpty
|
||||
? null
|
||||
: directBestRepeaters.first;
|
||||
|
||||
if (snr >= snrLevels[0]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.green;
|
||||
} else if (snr >= snrLevels[1]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.lightGreen;
|
||||
} else if (snr >= snrLevels[2]) {
|
||||
icon = Icons.signal_cellular_alt;
|
||||
color = Colors.yellow;
|
||||
} else if (snr >= snrLevels[3]) {
|
||||
icon = Icons.signal_cellular_alt_2_bar;
|
||||
color = Colors.orange;
|
||||
} else {
|
||||
icon = Icons.signal_cellular_alt_1_bar;
|
||||
color = Colors.red;
|
||||
final snrUi = snrUiFromSNR(
|
||||
directBestRepeaters.isNotEmpty ? directRepeater!.snr : null,
|
||||
widget.connector.currentSf,
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (directRepeater != null) {
|
||||
_showFullPathDialog(context, directBestRepeaters);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(snrUi.icon, size: 18, color: snrUi.color),
|
||||
Text(
|
||||
snrUi.text,
|
||||
style: TextStyle(fontSize: 12, color: snrUi.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (directRepeater != null)
|
||||
Text(
|
||||
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLastUpdated(DateTime lastSeen) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSeen);
|
||||
if (diff.isNegative) {
|
||||
return "0s";
|
||||
}
|
||||
if (diff.inMinutes < 1) {
|
||||
return "${diff.inSeconds}s";
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return "${diff.inMinutes}m";
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
final hours = diff.inHours;
|
||||
return "${hours}h";
|
||||
}
|
||||
final days = diff.inDays;
|
||||
return "${days}d";
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color),
|
||||
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
|
||||
],
|
||||
void _showFullPathDialog(
|
||||
BuildContext context,
|
||||
List<DirectRepeater> directBestRepeaters,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.snrIndicator_nearByRepeaters),
|
||||
content: SizedBox(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: directBestRepeaters.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final repeater = directBestRepeaters[index];
|
||||
final snrUi = snrUiFromSNR(
|
||||
repeater.snr,
|
||||
widget.connector.currentSf,
|
||||
);
|
||||
|
||||
final name = widget.connector.contacts
|
||||
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
|
||||
.map((c) => c.name)
|
||||
.firstOrNull;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(snrUi.icon, color: snrUi.color),
|
||||
title: Text(
|
||||
name ??
|
||||
repeater.pubkeyFirstByte
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0'),
|
||||
),
|
||||
subtitle: Text(
|
||||
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -5,10 +5,13 @@ PODS:
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- mobile_scanner (6.0.2):
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -24,8 +27,9 @@ DEPENDENCIES:
|
||||
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
|
||||
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
@@ -39,9 +43,11 @@ EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
mobile_scanner:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
share_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
@@ -53,10 +59,11 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
|
||||
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
|
||||
-1095
File diff suppressed because it is too large
Load Diff
+14
-10
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 5.0.0+6
|
||||
version: 6.0.0+7
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
@@ -41,25 +41,28 @@ dependencies:
|
||||
provider: ^6.1.5+1
|
||||
shared_preferences: ^2.2.2
|
||||
uuid: ^4.3.3
|
||||
flutter_map: ^7.0.2
|
||||
flutter_map: ^8.2.2
|
||||
latlong2: ^0.9.1
|
||||
flutter_local_notifications: ^18.0.1
|
||||
flutter_local_notifications: ^20.1.0
|
||||
crypto: ^3.0.3
|
||||
pointycastle: ^3.7.4
|
||||
pointycastle: ^4.0.0
|
||||
http: ^1.2.0
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_foreground_task: ^6.1.2
|
||||
wakelock_plus: ^1.2.8
|
||||
flutter_foreground_task: ^9.2.0
|
||||
wakelock_plus: ^1.4.0
|
||||
characters: ^1.4.0
|
||||
package_info_plus: ^8.0.0
|
||||
mobile_scanner: ^6.0.0 # QR/barcode scanning
|
||||
package_info_plus: ^9.0.0
|
||||
mobile_scanner: ^7.1.4 # QR/barcode scanning
|
||||
qr_flutter: ^4.1.0 # QR code generation
|
||||
url_launcher: ^6.3.0 # Launch URLs in system browser
|
||||
flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text
|
||||
gpx: ^2.3.0
|
||||
path_provider: ^2.1.5
|
||||
share_plus: ^12.0.1
|
||||
material_symbols_icons: ^4.2906.0
|
||||
web: ^1.1.1
|
||||
flutter_svg: ^2.0.10+1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -70,8 +73,8 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@@ -87,6 +90,7 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/icons/
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/services/line_of_sight_service.dart';
|
||||
|
||||
void main() {
|
||||
List<LatLng> makePoints(int count) {
|
||||
return List<LatLng>.generate(count, (i) => LatLng(0, i * 0.00001));
|
||||
}
|
||||
|
||||
test('computeFromElevations reports clear LOS on flat terrain', () {
|
||||
final points = makePoints(21);
|
||||
final elevations = List<double>.filled(points.length, 100);
|
||||
|
||||
final result = LineOfSightService.computeFromElevations(
|
||||
points: points,
|
||||
elevations: elevations,
|
||||
startAntennaHeightMeters: 2,
|
||||
endAntennaHeightMeters: 2,
|
||||
kFactor: 4.0 / 3.0,
|
||||
);
|
||||
|
||||
expect(result.hasData, isTrue);
|
||||
expect(result.isClear, isTrue);
|
||||
expect(result.maxObstructionMeters, equals(0));
|
||||
expect(result.firstObstructionDistanceMeters, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'computeFromElevations reports blocked LOS with central obstruction',
|
||||
() {
|
||||
final points = makePoints(21);
|
||||
final elevations = List<double>.filled(points.length, 100);
|
||||
elevations[10] = 300;
|
||||
|
||||
final result = LineOfSightService.computeFromElevations(
|
||||
points: points,
|
||||
elevations: elevations,
|
||||
startAntennaHeightMeters: 1.5,
|
||||
endAntennaHeightMeters: 1.5,
|
||||
kFactor: 4.0 / 3.0,
|
||||
);
|
||||
|
||||
expect(result.hasData, isTrue);
|
||||
expect(result.isClear, isFalse);
|
||||
expect(result.maxObstructionMeters, greaterThan(0));
|
||||
expect(result.firstObstructionDistanceMeters, isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('analyzePath summarizes clear and blocked segments', () async {
|
||||
final service = LineOfSightService(
|
||||
elevationDataSource: (points) async {
|
||||
final elevations = List<double?>.filled(points.length, 100);
|
||||
if (points.first.longitude > 0.00005) {
|
||||
elevations[elevations.length ~/ 2] = 300;
|
||||
}
|
||||
return elevations;
|
||||
},
|
||||
);
|
||||
|
||||
final path = [
|
||||
const LatLng(0, 0),
|
||||
const LatLng(0, 0.0001),
|
||||
const LatLng(0, 0.0002),
|
||||
];
|
||||
|
||||
final result = await service.analyzePath(path);
|
||||
|
||||
expect(result.segments.length, 2);
|
||||
expect(result.clearSegments, 1);
|
||||
expect(result.blockedSegments, 1);
|
||||
expect(result.unknownSegments, 0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/utils/battery_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('battery utils', () {
|
||||
test('nmc range maps 3.0V to 0% and 4.2V to 100%', () {
|
||||
expect(estimateBatteryPercentFromVolts(3.0, 'nmc'), 0);
|
||||
expect(estimateBatteryPercentFromVolts(4.2, 'nmc'), 100);
|
||||
});
|
||||
|
||||
test('lifepo4 range maps 2.6V to 0% and 3.65V to 100%', () {
|
||||
expect(estimateBatteryPercentFromVolts(2.6, 'lifepo4'), 0);
|
||||
expect(estimateBatteryPercentFromVolts(3.65, 'lifepo4'), 100);
|
||||
});
|
||||
|
||||
test('unknown chemistry falls back to nmc mapping', () {
|
||||
expect(
|
||||
estimateBatteryPercentFromMillivolts(3600, 'unknown'),
|
||||
estimateBatteryPercentFromMillivolts(3600, 'nmc'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-127
@@ -1,127 +1 @@
|
||||
{
|
||||
"bg": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"sk": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"sv": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"notification_activityTitle",
|
||||
"notification_messagesCount",
|
||||
"notification_channelMessagesCount",
|
||||
"notification_newNodesCount",
|
||||
"notification_newTypeDiscovered",
|
||||
"notification_receivedNewMessage"
|
||||
]
|
||||
}
|
||||
{}
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flutter_local_notifications_windows
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
Reference in New Issue
Block a user