Merge branch 'main' into community-#-names

This commit is contained in:
Ded
2026-01-29 08:07:05 -08:00
committed by GitHub
78 changed files with 13368 additions and 1290 deletions
+9
View File
@@ -67,5 +67,14 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- URL launcher intents for opening links -->
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
</manifest>
File diff suppressed because one or more lines are too long
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+1 -1
View File
@@ -1,4 +1,4 @@
platform :ios, '12.0'
platform :ios, '15.5'
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+152
View File
@@ -0,0 +1,152 @@
PODS:
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_foreground_task (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- MLImage (1.0.0-beta6)
- MLKitBarcodeScanning (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitCommon (12.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta6)
- MLKitCommon (~> 12.0)
- mobile_scanner (6.0.2):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
COCOAPODS: 1.16.2
+74 -6
View File
@@ -14,6 +14,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4268181FCF3E12817B700E9C /* libPods-Runner.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -42,9 +43,13 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
4268181FCF3E12817B700E9C /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -62,6 +67,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -94,6 +100,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
DEE6F094D3B70E76087722E1 /* Pods */,
DAE613E34DF694C2E33B64C7 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -121,6 +129,25 @@
path = Runner;
sourceTree = "<group>";
};
DAE613E34DF694C2E33B64C7 /* Frameworks */ = {
isa = PBXGroup;
children = (
4268181FCF3E12817B700E9C /* libPods-Runner.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
DEE6F094D3B70E76087722E1 /* Pods */ = {
isa = PBXGroup;
children = (
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */,
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */,
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -145,12 +172,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -253,6 +282,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -368,7 +436,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -384,7 +452,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -401,7 +469,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -416,7 +484,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -547,7 +615,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -569,7 +637,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
+3
View File
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+5
View File
@@ -55,5 +55,10 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
</dict>
</plist>
+104 -46
View File
@@ -67,9 +67,9 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<int, List<ChannelMessage>> _channelMessages = {};
final Set<String> _loadedConversationKeys = {};
final Map<int, Set<String>> _processedChannelReactions =
{}; // channelIndex -> Set of "reactionKey_emoji"
{}; // channelIndex -> Set of "targetHash_emoji"
final Map<String, Set<String>> _processedContactReactions =
{}; // contactPubKeyHex -> Set of "reactionKey_emoji"
{}; // contactPubKeyHex -> Set of "targetHash_emoji"
StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
@@ -146,6 +146,7 @@ class MeshCoreConnector extends ChangeNotifier {
final Set<String> _knownContactKeys = {};
final Map<String, int> _contactLastReadMs = {};
final Map<int, int> _channelLastReadMs = {};
bool _unreadStateLoaded = false;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
int? _activeChannelIndex;
@@ -317,6 +318,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getUnreadCountForContactKey(String contactKeyHex) {
if (!_unreadStateLoaded) return 0;
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0;
final messages = _conversations[contactKeyHex];
if (messages == null || messages.isEmpty) return 0;
@@ -336,6 +338,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getUnreadCountForChannelIndex(int channelIndex) {
if (!_unreadStateLoaded) return 0;
final messages = _channelMessages[channelIndex];
if (messages == null || messages.isEmpty) return 0;
final lastReadMs = _channelLastReadMs[channelIndex] ?? 0;
@@ -350,6 +353,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0;
var total = 0;
// Count unread contact messages
for (final contact in _contacts) {
@@ -381,6 +385,7 @@ class MeshCoreConnector extends ChangeNotifier {
_channelLastReadMs
..clear()
..addAll(await _unreadStore.loadChannelLastRead());
_unreadStateLoaded = true;
notifyListeners();
}
@@ -620,6 +625,32 @@ class MeshCoreConnector extends ChangeNotifier {
_scanResults.clear();
_setState(MeshCoreConnectionState.scanning);
// Ensure any previous scan is fully stopped
await FlutterBluePlus.stopScan();
await _scanSubscription?.cancel();
// On iOS/macOS, wait for Bluetooth to be powered on before scanning
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
// Wait for adapter state to be powered on
final adapterState = await FlutterBluePlus.adapterState.first;
if (adapterState != BluetoothAdapterState.on) {
// Wait for the adapter to turn on, with timeout
await FlutterBluePlus.adapterState
.firstWhere((state) => state == BluetoothAdapterState.on)
.timeout(
const Duration(seconds: 5),
onTimeout: () {
_setState(MeshCoreConnectionState.disconnected);
throw Exception('Bluetooth adapter not available');
},
);
}
// Add a small delay to allow BLE stack to fully initialize
await Future.delayed(const Duration(milliseconds: 300));
}
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults.clear();
for (var result in results) {
@@ -928,7 +959,12 @@ class MeshCoreConnector extends ChangeNotifier {
if (!isConnected) return;
if (_batteryRequested && !force) return;
_batteryRequested = true;
await sendFrame(buildGetBattAndStorageFrame());
try {
await sendFrame(buildGetBattAndStorageFrame());
} catch (e) {
// Connection likely lost - trigger disconnection handling
_handleDisconnection();
}
}
void _startBatteryPolling() {
@@ -1252,15 +1288,9 @@ class MeshCoreConnector extends ChangeNotifier {
if (reactionInfo != null) {
// Check if we've already processed this reaction
_processedChannelReactions.putIfAbsent(channel.index, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}';
if (reactionIdentifier != null &&
_processedChannelReactions[channel.index]!.contains(
reactionIdentifier,
)) {
if (_processedChannelReactions[channel.index]!.contains(reactionIdentifier)) {
// Already processed, don't process again
return;
}
@@ -1274,9 +1304,7 @@ class MeshCoreConnector extends ChangeNotifier {
await _channelMessageStore.saveChannelMessages(channel.index, messages);
// Mark this reaction as processed
if (reactionIdentifier != null) {
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
}
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
notifyListeners();
@@ -2652,26 +2680,20 @@ class MeshCoreConnector extends ChangeNotifier {
// Parse reaction info
final reactionInfo = Message.parseReaction(message.text);
if (reactionInfo != null) {
// Check if we've already processed this exact reaction using lightweight key
// Check if we've already processed this exact reaction
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}';
final isDuplicate =
reactionIdentifier != null &&
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
if (!isDuplicate) {
// New reaction - process it
_processContactReaction(messages, reactionInfo);
_processContactReaction(messages, reactionInfo, pubKeyHex);
_messageStore.saveMessages(pubKeyHex, messages);
// Mark as processed
if (reactionIdentifier != null) {
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
}
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
notifyListeners();
}
@@ -2686,15 +2708,51 @@ class MeshCoreConnector extends ChangeNotifier {
void _processContactReaction(
List<Message> messages,
ReactionInfo reactionInfo,
String contactPubKeyHex,
) {
// Find target message by messageId
for (int i = 0; i < messages.length; i++) {
if (messages[i].messageId == reactionInfo.targetMessageId) {
final currentReactions = Map<String, int>.from(messages[i].reactions);
// Find target message by computing hash and comparing
final targetHash = reactionInfo.targetHash;
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c?.publicKeyHex == contactPubKeyHex,
orElse: () => null,
);
final isRoomServer = contact?.type == advTypeRoom;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
// For 1:1 chats: contact reacts to my outgoing messages only
// For room servers: any message can be reacted to (multi-user)
if (!isRoomServer && !msg.isOutgoing) continue;
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
// For room servers, include sender name (resolve from fourByteRoomContactKey)
// For 1:1 chats, sender is implicit (null)
String? senderName;
if (isRoomServer && !msg.isOutgoing) {
// Resolve sender from the message's fourByteRoomContactKey
final senderContact = _contacts.cast<Contact?>().firstWhere(
(c) => c != null && _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
orElse: () => null,
);
senderName = senderContact?.name;
} else if (isRoomServer && msg.isOutgoing) {
senderName = selfName;
}
// For 1:1, senderName stays null
final msgHash = ReactionHelper.computeReactionHash(
timestampSecs,
senderName,
msg.text,
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(msg.reactions);
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
messages[i] = messages[i].copyWith(reactions: currentReactions);
messages[i] = msg.copyWith(reactions: currentReactions);
break;
}
}
@@ -2845,18 +2903,12 @@ class MeshCoreConnector extends ChangeNotifier {
// Parse reaction info
final reactionInfo = ChannelMessage.parseReaction(message.text);
if (reactionInfo != null) {
// Check if we've already processed this exact reaction using lightweight key
// Check if we've already processed this exact reaction
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}';
final isDuplicate =
reactionIdentifier != null &&
_processedChannelReactions[channelIndex]!.contains(
reactionIdentifier,
);
_processedChannelReactions[channelIndex]!.contains(reactionIdentifier);
if (!isDuplicate) {
// New reaction - process it
@@ -2865,9 +2917,7 @@ class MeshCoreConnector extends ChangeNotifier {
_channelMessageStore.saveChannelMessages(channelIndex, messages);
// Mark as processed
if (reactionIdentifier != null) {
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
}
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
}
return false; // Don't add reaction as a visible message
}
@@ -2963,14 +3013,22 @@ class MeshCoreConnector extends ChangeNotifier {
List<ChannelMessage> messages,
ReactionInfo reactionInfo,
) {
// Find target message by messageId
for (int i = 0; i < messages.length; i++) {
if (messages[i].messageId == reactionInfo.targetMessageId) {
final currentReactions = Map<String, int>.from(messages[i].reactions);
// Find target message by computing hash and comparing
final targetHash = reactionInfo.targetHash;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
final msgHash = ReactionHelper.computeReactionHash(
timestampSecs,
msg.senderName,
msg.text,
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(msg.reactions);
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
messages[i] = messages[i].copyWith(reactions: currentReactions);
messages[i] = msg.copyWith(reactions: currentReactions);
notifyListeners();
break;
}
+23 -2
View File
@@ -18,6 +18,10 @@ class BufferReader {
return data;
}
void skipBytes(int count) {
_pointer += count;
}
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
@@ -127,6 +131,7 @@ const int cmdSendStatusReq = 27;
const int cmdGetContactByKey = 30;
const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
@@ -176,6 +181,7 @@ const int pushCodeLoginSuccess = 0x85;
const int pushCodeLoginFail = 0x86;
const int pushCodeStatusResponse = 0x87;
const int pushCodeLogRxData = 0x88;
const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
@@ -195,8 +201,8 @@ const int maxFrameSize = 172;
const int appProtocolVersion = 3;
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
const int maxTextPayloadBytes = 160;
const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1;
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1;
const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
int maxContactMessageBytes() {
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
@@ -708,3 +714,18 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
}
return writer.toBytes();
}
//Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload})
{
final writer = BufferWriter();
writer.writeByte(cmdSendTracePath);
writer.writeUInt32LE(tag);
writer.writeUInt32LE(auth);
writer.writeByte(flag);
if (payload != null && payload.isNotEmpty) {
writer.writeBytes(payload);
}
return writer.toBytes();
}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class ChatScrollController extends ScrollController {
final ValueNotifier<bool> showJumpToBottom = ValueNotifier(false);
VoidCallback? onScrollNearTop;
static const _bottomThreshold = 100.0;
static const _topThreshold = 50.0;
ChatScrollController() {
addListener(_handleScroll);
}
void _handleScroll() {
if (!hasClients) return;
final pos = position;
// With reverse: true, position 0 is bottom, maxScrollExtent is top
// Show jump button when scrolled away from bottom (position > threshold)
final isAtBottom = pos.pixels <= _bottomThreshold;
if (showJumpToBottom.value == isAtBottom) {
showJumpToBottom.value = !isAtBottom;
}
// Pagination trigger when scrolled near top (maxScrollExtent)
if (pos.pixels >= pos.maxScrollExtent - _topThreshold) {
onScrollNearTop?.call();
}
}
void jumpToBottom() {
if (hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void handleKeyboardOpen() {
// Simple: just scroll to bottom when keyboard opens
if (hasClients) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
void scrollToBottomIfAtBottom() {
// Only scroll if jump button is NOT showing (i.e., already at bottom)
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
showJumpToBottom.dispose();
super.dispose();
}
}
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart';
class LinkHandler {
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_openLink),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.chat_openLinkConfirmation,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
url,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.chat_open),
),
],
),
);
if (shouldOpen != true) return;
// Launch URL
try {
final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red,
),
);
}
}
}
}
+51 -34
View File
@@ -1,53 +1,70 @@
import '../widgets/emoji_picker.dart';
class ReactionInfo {
final String targetMessageId;
final String targetHash;
final String emoji;
final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix
ReactionInfo({
required this.targetMessageId,
required this.targetHash,
required this.emoji,
this.reactionKey,
});
}
class ReactionHelper {
/// Parse reaction format: r:[messageId]:[emoji]
/// Supports both old format (full messageId) and new format (timestamp_senderPrefix)
static List<String>? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order.
/// Order must stay stable for index compatibility.
static List<String> get reactionEmojis {
return _cachedEmojis ??= [
...EmojiPicker.quickEmojis,
...EmojiPicker.smileys,
...EmojiPicker.gestures,
...EmojiPicker.hearts,
...EmojiPicker.objects,
];
}
/// Convert emoji to 2-char hex index. Returns null if emoji not in list.
static String? emojiToIndex(String emoji) {
final idx = reactionEmojis.indexOf(emoji);
if (idx < 0) return null;
return idx.toRadixString(16).padLeft(2, '0');
}
/// Convert 2-char hex index to emoji. Returns null if invalid index.
static String? indexToEmoji(String hexIndex) {
final idx = int.tryParse(hexIndex, radix: 16);
if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null;
return reactionEmojis[idx];
}
/// Compute a 4-char hex hash for a message reaction.
/// Hash input: timestampSeconds + [senderName] + first 5 chars of text
/// For 1:1 chats, senderName can be null (sender is implicit).
static String computeReactionHash(int timestampSeconds, String? senderName, String text) {
final first5 = text.length >= 5 ? text.substring(0, 5) : text;
final input = senderName != null
? '$timestampSeconds$senderName$first5'
: '$timestampSeconds$first5';
// Use hashCode and take lower 16 bits, format as 4 hex chars
final hash = input.hashCode & 0xFFFF;
return hash.toRadixString(16).padLeft(4, '0');
}
/// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index)
/// Returns null if text is not a valid reaction format
static ReactionInfo? parseReaction(String text) {
final regex = RegExp(r'^r:([^:]+):(.+)$');
final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$');
final match = regex.firstMatch(text);
if (match == null) return null;
final targetId = match.group(1)!;
final emoji = match.group(2)!;
// Extract reaction key for deduplication
// If targetId is in new format (timestamp_senderPrefix), use it directly
// Otherwise, extract timestamp from old format (timestamp_nameHash_textHash)
String? reactionKey;
if (targetId.contains('_')) {
final parts = targetId.split('_');
if (parts.length >= 2) {
// New format: timestamp_senderPrefix, or old format with at least timestamp
reactionKey = '${parts[0]}_${parts[1]}';
}
}
final emoji = indexToEmoji(match.group(2)!);
if (emoji == null) return null;
return ReactionInfo(
targetMessageId: targetId,
targetHash: match.group(1)!,
emoji: emoji,
reactionKey: reactionKey,
);
}
/// Generate a lightweight reaction key for a message
/// Format: r:[timestamp]_[senderPrefix]:[emoji]
static String buildReactionText(String timestamp, String senderPrefix, String emoji) {
return 'r:${timestamp}_$senderPrefix:$emoji';
}
/// Extract sender prefix from public key hex (first 8 chars)
static String getSenderPrefix(String senderKeyHex) {
return senderKeyHex.substring(0, 8);
}
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Отваряне на връзката?",
"chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?",
"chat_open": "Отвори",
"chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Невалиден формат на връзката",
"map_title": "Карта на възлите",
"map_noNodesWithLocation": "Няма възли с данни за местоположение.",
"map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Остави общността \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"community_forCommunity": "За {name}"
"community_forCommunity": "За {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.",
"community_secretRegenerated": "Секретно презареждане за \"{name}\"",
"community_regenerateSecret": "Регенерейрай секрет",
"community_regenerate": "Регенерация",
"community_updateSecret": "Актуализирай тайна",
"community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
"community_secretUpdated": "Секретно обновено за \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Вие",
"pathTrace_notAvailable": "Пътека за проследяване не е достъпна.",
"contacts_pathTrace": "Пътен проследяване",
"pathTrace_refreshTooltip": "Обнови Path Trace.",
"pathTrace_failed": "Пътят за проследяване не успя.",
"contacts_repeaterPing": "Пингване на повторителя",
"contacts_repeaterPathTrace": "Трасировка до повторител",
"contacts_ping": "Пинг",
"contacts_chatTraceRoute": "Трасиране на път",
"contacts_roomPathTrace": "Трасиране на път до съ",
"contacts_roomPing": "Ping на сървъра на стаята",
"contacts_pathTraceTo": "Проследи маршрут към {name}"
}
+77 -9
View File
@@ -74,7 +74,7 @@
"settings_title": "Einstellungen",
"settings_deviceInfo": "Geräteinformationen",
"settings_appSettings": "App-Einstellungen",
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmungen",
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung",
"settings_nodeSettings": "Knoten-Einstellungen",
"settings_nodeName": "Knotenname",
"settings_nodeNameNotSet": "Nicht festgelegt",
@@ -266,7 +266,7 @@
}
}
},
"contacts_manageRepeater": "Wiederholungen verwalten",
"contacts_manageRepeater": "Repeater verwalten",
"contacts_roomLogin": "Raum-Login",
"contacts_openChat": "Öffne Chat",
"contacts_editGroup": "Gruppe bearbeiten",
@@ -360,7 +360,7 @@
"channels_channelIndexLabel": "Kanalindex",
"channels_channelName": "Kanalname",
"channels_usePublicChannel": "Verwende öffentlichen Kanal",
"channels_standardPublicPsk": "Standard-Öffentliche PSK",
"channels_standardPublicPsk": "Öffentliche Standard PSK",
"channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Zufällige PSK generieren",
"channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.",
@@ -489,8 +489,8 @@
}
}
},
"debugFrame_textMessageHeader": "Textnachricht-Frame:",
"debugFrame_destinationPubKey": "- Ziel-Pub-Schlüssel: {pubKey}",
"debugFrame_textMessageHeader": "Textnachrichten Frame:",
"debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}",
"@debugFrame_destinationPubKey": {
"placeholders": {
"pubKey": {
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Link öffnen?",
"chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?",
"chat_open": "Öffnen",
"chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ungültiges Link-Format",
"map_title": "Karte",
"map_noNodesWithLocation": "Keine Knoten mit Standortdaten",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
@@ -1014,7 +1026,7 @@
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
"repeater_dangerZone": "Gefahrenzone",
"repeater_rebootRepeater": "Neustart Repeater",
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
"repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.",
"repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?",
"repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung",
"repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren",
@@ -1349,7 +1361,7 @@
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
"neighbors_repeatersNeighbours": "Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlassen",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
"community_communityHashtag": "Community Hashtag"
"community_communityHashtag": "Community Hashtag",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerate": "Neu generieren",
"community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich",
"community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
"community_regenerateSecret": "Neugenerierung des Schlüssels",
"community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert",
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
"community_updateSecret": "Aktualisieren Sie den Schlüssel",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_refreshTooltip": "Path Trace aktualisieren.",
"pathTrace_you": "Du",
"pathTrace_failed": "Pfadverfolgung fehlgeschlagen.",
"pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.",
"contacts_pathTrace": "Pfadverfolgung",
"contacts_ping": "Pingen",
"contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater",
"contacts_repeaterPing": "Repeater pingen",
"contacts_roomPathTrace": "Pfadverfolgung zum Raumserver",
"contacts_roomPing": "Raumserver anpingen",
"contacts_pathTraceTo": "Route nach {name} verfolgen",
"contacts_chatTraceRoute": "Pfadverfolgungsroute"
}
+32 -1
View File
@@ -174,6 +174,8 @@
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська",
"appSettings_notifications": "Notifications",
"appSettings_enableNotifications": "Enable Notifications",
"appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts",
@@ -550,6 +552,16 @@
"count": {"type": "int"}
}
},
"chat_openLink": "Open Link?",
"chat_openLinkConfirmation": "Do you want to open this link in your browser?",
"chat_open": "Open",
"chat_couldNotOpenLink": "Could not open link: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {"type": "String"}
}
},
"chat_invalidLink": "Invalid link format",
"map_title": "Node Map",
"map_noNodesWithLocation": "No nodes with location data",
@@ -1298,5 +1310,24 @@
"listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Room servers",
"listFilter_unreadOnly": "Unread only",
"listFilter_newGroup": "New group"
"listFilter_newGroup": "New group",
"pathTrace_you": "You",
"pathTrace_failed": "Path trace failed.",
"pathTrace_notAvailable": "Path trace not available.",
"pathTrace_refreshTooltip": "Refresh Path Trace.",
"contacts_pathTrace": "Path Trace",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Path trace to repeater",
"contacts_repeaterPing": "Ping repeater",
"contacts_roomPathTrace": "Path trace to room server",
"contacts_roomPing": "Ping room server",
"contacts_chatTraceRoute": "Path trace route",
"contacts_pathTraceTo": "Trace route to {name}",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {"type": "String"}
}
}
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "¿Abrir enlace?",
"chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?",
"chat_open": "Abrir",
"chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato de enlace no válido",
"map_title": "Mapa de Nodos",
"map_noNodesWithLocation": "No hay nodos con datos de ubicación",
"map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
"community_communityHashtag": "Hashtag de la Comunidad",
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
"community_forCommunity": "Para {name}"
"community_forCommunity": "Para {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecret": "Regenerar Contraseña Secreta",
"community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.",
"community_secretRegenerated": "Código secreto regenerado para \"{name}\"",
"community_regenerate": "Regenerar",
"community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
"community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
"community_updateSecret": "Actualizar Contraseña",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Tú",
"pathTrace_failed": "El trazado de ruta falló.",
"pathTrace_refreshTooltip": "Actualizar Path Trace",
"contacts_pathTrace": "Rastreo de caminos",
"contacts_repeaterPathTrace": "Rastrear ruta al repetidor",
"contacts_repeaterPing": "Pingar repetidor",
"contacts_ping": "Ping",
"pathTrace_notAvailable": "El trazado de ruta no está disponible.",
"contacts_roomPing": "Pingar servidor de sala",
"contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación",
"contacts_pathTraceTo": "Rastrear ruta a {name}",
"contacts_chatTraceRoute": "Ruta de trazado"
}
+96 -28
View File
@@ -104,7 +104,7 @@
"settings_timeSynchronized": "Synchronisation temporelle",
"settings_refreshContacts": "Rafraîchir les Contacts",
"settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil",
"settings_rebootDevice": "Réinitialiser l'appareil",
"settings_rebootDevice": "Redémarrer l'appareil",
"settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore",
"settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.",
"settings_debug": "Déboguer",
@@ -279,7 +279,7 @@
}
}
},
"contacts_newGroup": "Nouvelle Groupe",
"contacts_newGroup": "Nouveau Groupe",
"contacts_groupName": "Nom du groupe",
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
@@ -293,8 +293,8 @@
"contacts_filterContacts": "Filtrer les contacts...",
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Dernière fois vu maintenant",
"contacts_lastSeenMinsAgo": "Dernière fois vu il y a {minutes} minutes.",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -302,8 +302,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Dernière fois vu il y a 1 heure.",
"contacts_lastSeenHoursAgo": "Dernière fois vu il y a {hours} heures.",
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -311,8 +311,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Dernière fois vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Dernière activité il y a {days} jours",
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -394,7 +394,7 @@
"channels_sortBy": "Trier par",
"channels_sortManual": "Manuel",
"channels_sortAZ": "A à Z",
"channels_sortLatestMessages": "Dernières messages",
"channels_sortLatestMessages": "Derniers messages",
"channels_sortUnread": "Non lu",
"chat_noMessages": "Aucun message pour le moment.",
"chat_sendMessageToStart": "Envoyer un message pour commencer",
@@ -436,7 +436,7 @@
"chat_messageCopied": "Message copié",
"chat_messageDeleted": "Message supprimé",
"chat_retryingMessage": "Tentative de récupération.",
"chat_retryCount": "Réessayer {current}/{max}",
"chat_retryCount": "Essai {current}/{max}",
"@chat_retryCount": {
"placeholders": {
"current": {
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Ouvrir le lien ?",
"chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?",
"chat_open": "Ouvrir",
"chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Format de lien invalide",
"map_title": "Carte des nœuds",
"map_noNodesWithLocation": "Aucun nœud avec des données de localisation",
"map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.",
@@ -687,7 +699,7 @@
}
}
},
"mapCache_cachedTilesWithFailed": "Tiles mis en cache ({downloaded}) ({failed} ratés)",
"mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)",
"@mapCache_cachedTilesWithFailed": {
"placeholders": {
"downloaded": {
@@ -734,7 +746,7 @@
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
"@mapCache_boundsLabel": {
"placeholders": {
"north": {
@@ -751,7 +763,7 @@
}
}
},
"time_justNow": "Il y a tout juste maintenant",
"time_justNow": "Maintenant",
"time_minutesAgo": "{minutes} minutes auparavant",
"@time_minutesAgo": {
"placeholders": {
@@ -899,7 +911,7 @@
"repeater_packetStatistics": "Statistiques des paquets",
"repeater_sent": "Envoyé",
"repeater_received": "Reçu",
"repeater_duplicates": "Dupliques",
"repeater_duplicates": "Doublons",
"repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s",
"@repeater_daysHoursMinsSecs": {
"placeholders": {
@@ -1012,7 +1024,7 @@
}
},
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone d'alerte",
"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 ?",
@@ -1108,7 +1120,7 @@
"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_cliHelpSetAllowReadOnly": "(Serveur de pièce) 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_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.",
@@ -1327,16 +1339,16 @@
"channelPath_unknownRepeater": "Répéteur Inconnu",
"listFilter_tooltip": "Filtrer et trier",
"listFilter_sortBy": "Trier par",
"listFilter_latestMessages": "Dernières messages",
"listFilter_latestMessages": "Derniers messages",
"listFilter_heardRecently": "Écoute récemment",
"listFilter_az": "A à Z",
"listFilter_filters": "Filtres",
"listFilter_all": "Tout",
"listFilter_users": "Utilisateurs",
"listFilter_repeaters": "Répéteurs",
"listFilter_roomServers": "Serveurs de pièce",
"listFilter_roomServers": "Room servers",
"listFilter_unreadOnly": "Messages non lus seulement",
"listFilter_newGroup": "Nouvelle groupe",
"listFilter_newGroup": "Nouveau groupe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
@@ -1362,7 +1374,7 @@
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
@@ -1379,11 +1391,11 @@
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"settings_locationGPSEnable": "Activer le GPS",
"settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS",
"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",
"@community_joinConfirmation": {
"placeholders": {
@@ -1473,16 +1485,72 @@
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté",
"community_selectCommunity": "Sélectionner Communauté",
"community_regularHashtag": "Hashtag régulier",
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
"community_communityHashtag": "Hashtag de la communauté",
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
"community_forCommunity": "Pour {name}"
"community_forCommunity": "Pour {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecret": "Régénérer le secret",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
"community_regenerate": "Régénérer",
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
"community_updateSecret": "Mettre à jour le secret",
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Vous",
"pathTrace_refreshTooltip": "Actualiser Path Trace",
"pathTrace_failed": "Traçage du chemin échoué.",
"pathTrace_notAvailable": "Tracé de chemin non disponible.",
"contacts_pathTrace": "Traçage de chemin",
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
"contacts_repeaterPing": "Pinguer le répéteur",
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
"contacts_chatTraceRoute": "Tracer le chemin",
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
"contacts_ping": "Ping",
"contacts_roomPing": "Pinguer le serveur de la salle"
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Aprire il link?",
"chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?",
"chat_open": "Apri",
"chat_couldNotOpenLink": "Impossibile aprire il link: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato di link non valido",
"map_title": "Mappa Nodi",
"map_noNodesWithLocation": "Nessun nodo con dati di posizione",
"map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}"
"community_forCommunity": "Per {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.",
"community_regenerateSecret": "Ri genera la chiave segreta",
"community_regenerate": "Rigenera",
"community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
"community_updateSecret": "Aggiorna Segreto",
"community_secretUpdated": "Segreto aggiornato per \"{name}\"",
"community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_failed": "Tracciamento del percorso fallito.",
"pathTrace_you": "Tu",
"pathTrace_notAvailable": "Tracciamento del percorso non disponibile.",
"pathTrace_refreshTooltip": "Aggiorna Path Trace.",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Traccia percorso al ripetitore",
"contacts_roomPathTrace": "Traccia del percorso al server della stanza",
"contacts_pathTrace": "Traccia Percorso",
"contacts_repeaterPing": "Ripetitore ping",
"contacts_pathTraceTo": "Traccia percorso verso {name}",
"contacts_roomPing": "Ping al server della stanza",
"contacts_chatTraceRoute": "Traccia percorso path"
}
+124
View File
@@ -14,9 +14,11 @@ import 'app_localizations_it.dart';
import 'app_localizations_nl.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_sk.dart';
import 'app_localizations_sl.dart';
import 'app_localizations_sv.dart';
import 'app_localizations_uk.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
@@ -114,9 +116,11 @@ abstract class AppLocalizations {
Locale('nl'),
Locale('pl'),
Locale('pt'),
Locale('ru'),
Locale('sk'),
Locale('sl'),
Locale('sv'),
Locale('uk'),
Locale('zh'),
];
@@ -942,6 +946,18 @@ abstract class AppLocalizations {
/// **'Български'**
String get appSettings_languageBg;
/// No description provided for @appSettings_languageRu.
///
/// In en, this message translates to:
/// **'Русский'**
String get appSettings_languageRu;
/// No description provided for @appSettings_languageUk.
///
/// In en, this message translates to:
/// **'Українська'**
String get appSettings_languageUk;
/// No description provided for @appSettings_notifications.
///
/// In en, this message translates to:
@@ -2226,6 +2242,36 @@ abstract class AppLocalizations {
/// **'Unread: {count}'**
String chat_unread(int count);
/// No description provided for @chat_openLink.
///
/// In en, this message translates to:
/// **'Open Link?'**
String get chat_openLink;
/// No description provided for @chat_openLinkConfirmation.
///
/// In en, this message translates to:
/// **'Do you want to open this link in your browser?'**
String get chat_openLinkConfirmation;
/// No description provided for @chat_open.
///
/// In en, this message translates to:
/// **'Open'**
String get chat_open;
/// No description provided for @chat_couldNotOpenLink.
///
/// In en, this message translates to:
/// **'Could not open link: {url}'**
String chat_couldNotOpenLink(String url);
/// No description provided for @chat_invalidLink.
///
/// In en, this message translates to:
/// **'Invalid link format'**
String get chat_invalidLink;
/// No description provided for @map_title.
///
/// In en, this message translates to:
@@ -4653,6 +4699,78 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'New group'**
String get listFilter_newGroup;
/// No description provided for @pathTrace_you.
///
/// In en, this message translates to:
/// **'You'**
String get pathTrace_you;
/// No description provided for @pathTrace_failed.
///
/// In en, this message translates to:
/// **'Path trace failed.'**
String get pathTrace_failed;
/// No description provided for @pathTrace_notAvailable.
///
/// In en, this message translates to:
/// **'Path trace not available.'**
String get pathTrace_notAvailable;
/// No description provided for @pathTrace_refreshTooltip.
///
/// In en, this message translates to:
/// **'Refresh Path Trace.'**
String get pathTrace_refreshTooltip;
/// No description provided for @contacts_pathTrace.
///
/// In en, this message translates to:
/// **'Path Trace'**
String get contacts_pathTrace;
/// No description provided for @contacts_ping.
///
/// In en, this message translates to:
/// **'Ping'**
String get contacts_ping;
/// No description provided for @contacts_repeaterPathTrace.
///
/// In en, this message translates to:
/// **'Path trace to repeater'**
String get contacts_repeaterPathTrace;
/// No description provided for @contacts_repeaterPing.
///
/// In en, this message translates to:
/// **'Ping repeater'**
String get contacts_repeaterPing;
/// No description provided for @contacts_roomPathTrace.
///
/// In en, this message translates to:
/// **'Path trace to room server'**
String get contacts_roomPathTrace;
/// No description provided for @contacts_roomPing.
///
/// In en, this message translates to:
/// **'Ping room server'**
String get contacts_roomPing;
/// No description provided for @contacts_chatTraceRoute.
///
/// In en, this message translates to:
/// **'Path trace route'**
String get contacts_chatTraceRoute;
/// No description provided for @contacts_pathTraceTo.
///
/// In en, this message translates to:
/// **'Trace route to {name}'**
String contacts_pathTraceTo(String name);
}
class _AppLocalizationsDelegate
@@ -4675,9 +4793,11 @@ class _AppLocalizationsDelegate
'nl',
'pl',
'pt',
'ru',
'sk',
'sl',
'sv',
'uk',
'zh',
].contains(locale.languageCode);
@@ -4706,12 +4826,16 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsPl();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'sk':
return AppLocalizationsSk();
case 'sl':
return AppLocalizationsSl();
case 'sv':
return AppLocalizationsSv();
case 'uk':
return AppLocalizationsUk();
case 'zh':
return AppLocalizationsZh();
}
+69 -7
View File
@@ -450,6 +450,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Уведомления';
@@ -1207,6 +1213,24 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Непрочетени: $count';
}
@override
String get chat_openLink => 'Отваряне на връзката?';
@override
String get chat_openLinkConfirmation =>
'Искате ли да отворите тази връзка в браузъра си?';
@override
String get chat_open => 'Отвори';
@override
String chat_couldNotOpenLink(String url) {
return 'Не можа да се отвори връзката: $url';
}
@override
String get chat_invalidLink => 'Невалиден формат на връзката';
@override
String get map_title => 'Карта на възлите';
@@ -2567,32 +2591,32 @@ class AppLocalizationsBg extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Регенерейрай секрет';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Регенерация';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Секретно презареждане за \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Актуализирай тайна';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Секретно обновено за \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
}
@override
@@ -2658,4 +2682,42 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_newGroup => 'Нова група';
@override
String get pathTrace_you => 'Вие';
@override
String get pathTrace_failed => 'Пътят за проследяване не успя.';
@override
String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.';
@override
String get pathTrace_refreshTooltip => 'Обнови Path Trace.';
@override
String get contacts_pathTrace => 'Пътен проследяване';
@override
String get contacts_ping => 'Пинг';
@override
String get contacts_repeaterPathTrace => 'Трасировка до повторител';
@override
String get contacts_repeaterPing => 'Пингване на повторителя';
@override
String get contacts_roomPathTrace => 'Трасиране на път до съ';
@override
String get contacts_roomPing => 'Ping на сървъра на стаята';
@override
String get contacts_chatTraceRoute => 'Трасиране на път';
@override
String contacts_pathTraceTo(String name) {
return 'Проследи маршрут към $name';
}
}
+76 -15
View File
@@ -160,7 +160,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_appSettingsSubtitle =>
'Benachrichtigungen, Messaging und Kartenwahrnehmungen';
'Benachrichtigungen, Messaging und Kartenwahrnehmung';
@override
String get settings_nodeSettings => 'Knoten-Einstellungen';
@@ -444,6 +444,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Benachrichtigungen';
@@ -662,7 +668,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Wiederholungen verwalten';
String get contacts_manageRepeater => 'Repeater verwalten';
@override
String get contacts_manageRoom => 'Raum-Server verwalten';
@@ -796,7 +802,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get channels_usePublicChannel => 'Verwende öffentlichen Kanal';
@override
String get channels_standardPublicPsk => 'Standard-Öffentliche PSK';
String get channels_standardPublicPsk => 'Öffentliche Standard PSK';
@override
String get channels_pskHex => 'PSK (Hex)';
@@ -1029,11 +1035,11 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get debugFrame_textMessageHeader => 'Textnachricht-Frame:';
String get debugFrame_textMessageHeader => 'Textnachrichten Frame:';
@override
String debugFrame_destinationPubKey(String pubKey) {
return '- Ziel-Pub-Schlüssel: $pubKey';
return '- Ziel-Public-Schlüssel: $pubKey';
}
@override
@@ -1206,6 +1212,24 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Ungelesen: $count';
}
@override
String get chat_openLink => 'Link öffnen?';
@override
String get chat_openLinkConfirmation =>
'Möchten Sie diesen Link in Ihrem Browser öffnen?';
@override
String get chat_open => 'Öffnen';
@override
String chat_couldNotOpenLink(String url) {
return 'Link konnte nicht geöffnet werden: $url';
}
@override
String get chat_invalidLink => 'Ungültiges Link-Format';
@override
String get map_title => 'Karte';
@@ -1870,8 +1894,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_rebootRepeater => 'Neustart Repeater';
@override
String get repeater_rebootRepeaterSubtitle =>
'Wiederholen Sie das Repeater-Gerät.';
String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.';
@override
String get repeater_rebootRepeaterConfirm =>
@@ -2339,7 +2362,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn';
String get neighbors_repeatersNeighbours => 'Nachbarn';
@override
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
@@ -2570,32 +2593,32 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Neugenerierung des Schlüssels';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Neu generieren';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aktualisieren Sie den Schlüssel';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Schlüssel für \"$name\" aktualisiert';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.';
}
@override
@@ -2663,4 +2686,42 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get listFilter_newGroup => 'Neue Gruppe';
@override
String get pathTrace_you => 'Du';
@override
String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.';
@override
String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.';
@override
String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.';
@override
String get contacts_pathTrace => 'Pfadverfolgung';
@override
String get contacts_ping => 'Pingen';
@override
String get contacts_repeaterPathTrace => 'Pfadverfolgung zum Repeater';
@override
String get contacts_repeaterPing => 'Repeater pingen';
@override
String get contacts_roomPathTrace => 'Pfadverfolgung zum Raumserver';
@override
String get contacts_roomPing => 'Raumserver anpingen';
@override
String get contacts_chatTraceRoute => 'Pfadverfolgungsroute';
@override
String contacts_pathTraceTo(String name) {
return 'Route nach $name verfolgen';
}
}
+62
View File
@@ -442,6 +442,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notifications';
@@ -1186,6 +1192,24 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Unread: $count';
}
@override
String get chat_openLink => 'Open Link?';
@override
String get chat_openLinkConfirmation =>
'Do you want to open this link in your browser?';
@override
String get chat_open => 'Open';
@override
String chat_couldNotOpenLink(String url) {
return 'Could not open link: $url';
}
@override
String get chat_invalidLink => 'Invalid link format';
@override
String get map_title => 'Node Map';
@@ -2618,4 +2642,42 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_newGroup => 'New group';
@override
String get pathTrace_you => 'You';
@override
String get pathTrace_failed => 'Path trace failed.';
@override
String get pathTrace_notAvailable => 'Path trace not available.';
@override
String get pathTrace_refreshTooltip => 'Refresh Path Trace.';
@override
String get contacts_pathTrace => 'Path Trace';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Path trace to repeater';
@override
String get contacts_repeaterPing => 'Ping repeater';
@override
String get contacts_roomPathTrace => 'Path trace to room server';
@override
String get contacts_roomPing => 'Ping room server';
@override
String get contacts_chatTraceRoute => 'Path trace route';
@override
String contacts_pathTraceTo(String name) {
return 'Trace route to $name';
}
}
+70 -7
View File
@@ -447,6 +447,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notificaciones';
@@ -1204,6 +1210,24 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Sin leer: $count';
}
@override
String get chat_openLink => '¿Abrir enlace?';
@override
String get chat_openLinkConfirmation =>
'¿Quiere abrir este enlace en su navegador?';
@override
String get chat_open => 'Abrir';
@override
String chat_couldNotOpenLink(String url) {
return 'No se pudo abrir el enlace: $url';
}
@override
String get chat_invalidLink => 'Formato de enlace no válido';
@override
String get map_title => 'Mapa de Nodos';
@@ -2565,32 +2589,32 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Código secreto regenerado para \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Actualizar Contraseña';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Confidencialidad actualizada para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
}
@override
@@ -2657,4 +2681,43 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nuevo grupo';
@override
String get pathTrace_you => '';
@override
String get pathTrace_failed => 'El trazado de ruta falló.';
@override
String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.';
@override
String get pathTrace_refreshTooltip => 'Actualizar Path Trace';
@override
String get contacts_pathTrace => 'Rastreo de caminos';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor';
@override
String get contacts_repeaterPing => 'Pingar repetidor';
@override
String get contacts_roomPathTrace =>
'Rastreo de ruta al servidor de la habitación';
@override
String get contacts_roomPing => 'Pingar servidor de sala';
@override
String get contacts_chatTraceRoute => 'Ruta de trazado';
@override
String contacts_pathTraceTo(String name) {
return 'Rastrear ruta a $name';
}
}
+97 -33
View File
@@ -204,18 +204,19 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_locationInvalid => 'Latitude ou longitude invalide.';
@override
String get settings_locationGPSEnable => 'Habilita GPS';
String get settings_locationGPSEnable => 'Activer le GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
'Activer la mise à jour automatique de la position via GPS';
@override
String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)';
String get settings_locationIntervalSec =>
'Intervalle de mise-à-jour du GPS (Secondes)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
'L\'intervalle doit être compris entre 60 et 86400 secondes.';
@override
String get settings_latitude => 'Latitude';
@@ -272,7 +273,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Recharger la liste des contacts depuis l\'appareil';
@override
String get settings_rebootDevice => 'Réinitialiser l\'appareil';
String get settings_rebootDevice => 'Redémarrer l\'appareil';
@override
String get settings_rebootDeviceSubtitle => 'Redémarrer l\'appareil MeshCore';
@@ -447,6 +448,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notifications';
@@ -667,7 +674,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_manageRepeater => 'Gérer le répétiteur';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
String get contacts_manageRoom => 'Gérer le Room Server';
@override
String get contacts_roomLogin => 'Connexion Salle';
@@ -687,7 +694,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get contacts_newGroup => 'Nouvelle Groupe';
String get contacts_newGroup => 'Nouveau Groupe';
@override
String get contacts_groupName => 'Nom du groupe';
@@ -711,27 +718,27 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_noMembers => 'Aucun membre';
@override
String get contacts_lastSeenNow => 'Dernière fois vu maintenant';
String get contacts_lastSeenNow => 'Vu maintenant';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Dernière fois vu il y a $minutes minutes.';
return 'Vu il y a $minutes minutes';
}
@override
String get contacts_lastSeenHourAgo => 'Dernière fois vu il y a 1 heure.';
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Dernière fois vu il y a $hours heures.';
return 'Vu il y a $hours heures';
}
@override
String get contacts_lastSeenDayAgo => 'Dernière fois vu il y a 1 jour';
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Dernière activité il y a $days jours';
return 'Vu il y a $days jours';
}
@override
@@ -845,7 +852,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channels_sortAZ => 'A à Z';
@override
String get channels_sortLatestMessages => 'Dernières messages';
String get channels_sortLatestMessages => 'Derniers messages';
@override
String get channels_sortUnread => 'Non lu';
@@ -888,7 +895,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channels_enterHashtag => 'Entrez le hashtag';
@override
String get channels_hashtagHint => 'ex. #équipe';
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@@ -936,7 +943,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String chat_retryCount(int current, int max) {
return 'Réessayer $current/$max';
return 'Essai $current/$max';
}
@override
@@ -1209,6 +1216,24 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Non lu : $count';
}
@override
String get chat_openLink => 'Ouvrir le lien ?';
@override
String get chat_openLinkConfirmation =>
'Voulez-vous ouvrir ce lien dans votre navigateur ?';
@override
String get chat_open => 'Ouvrir';
@override
String chat_couldNotOpenLink(String url) {
return 'Impossible d\'ouvrir le lien : $url';
}
@override
String get chat_invalidLink => 'Format de lien invalide';
@override
String get map_title => 'Carte des nœuds';
@@ -1371,7 +1396,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
return 'Tiles mis en cache ($downloaded) ($failed ratés)';
return 'Tuiles mis en cache ($downloaded) ($failed ratés)';
}
@override
@@ -1425,11 +1450,11 @@ class AppLocalizationsFr extends AppLocalizations {
String east,
String west,
) {
return 'N $north, S $south, E $east, W $west';
return 'N $north, S $south, E $east, O $west';
}
@override
String get time_justNow => 'Il y a tout juste maintenant';
String get time_justNow => 'Maintenant';
@override
String time_minutesAgo(int minutes) {
@@ -1725,7 +1750,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_received => 'Reçu';
@override
String get repeater_duplicates => 'Dupliques';
String get repeater_duplicates => 'Doublons';
@override
String repeater_daysHoursMinsSecs(
@@ -1873,7 +1898,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Intervalle d\'annonces cryptées';
@override
String get repeater_dangerZone => 'Zone d\'alerte';
String get repeater_dangerZone => 'Zone dangereuse';
@override
String get repeater_rebootRepeater => 'Redémarrer Répéteur';
@@ -2070,7 +2095,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetAllowReadOnly =>
'(Serveur de pièce) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)';
'(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)';
@override
String get repeater_cliHelpSetFloodMax =>
@@ -2581,32 +2606,32 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Régénérer le secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Régénérer';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Mot de passe secret régénéré pour \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Mettre à jour le secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Modification secrète mise à jour pour \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
}
@override
@@ -2614,7 +2639,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get community_addHashtagChannelDesc =>
'Ajouter un canal hachage pour cette communauté';
'Ajouter un canal hashtag pour cette communauté';
@override
String get community_selectCommunity => 'Sélectionner Communauté';
@@ -2645,7 +2670,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get listFilter_sortBy => 'Trier par';
@override
String get listFilter_latestMessages => 'Dernières messages';
String get listFilter_latestMessages => 'Derniers messages';
@override
String get listFilter_heardRecently => 'Écoute récemment';
@@ -2666,11 +2691,50 @@ class AppLocalizationsFr extends AppLocalizations {
String get listFilter_repeaters => 'Répéteurs';
@override
String get listFilter_roomServers => 'Serveurs de pièce';
String get listFilter_roomServers => 'Room servers';
@override
String get listFilter_unreadOnly => 'Messages non lus seulement';
@override
String get listFilter_newGroup => 'Nouvelle groupe';
String get listFilter_newGroup => 'Nouveau groupe';
@override
String get pathTrace_you => 'Vous';
@override
String get pathTrace_failed => 'Traçage du chemin échoué.';
@override
String get pathTrace_notAvailable => 'Tracé de chemin non disponible.';
@override
String get pathTrace_refreshTooltip => 'Actualiser Path Trace';
@override
String get contacts_pathTrace => 'Traçage de chemin';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur';
@override
String get contacts_repeaterPing => 'Pinguer le répéteur';
@override
String get contacts_roomPathTrace =>
'Traçage du chemin vers le serveur de la salle';
@override
String get contacts_roomPing => 'Pinguer le serveur de la salle';
@override
String get contacts_chatTraceRoute => 'Tracer le chemin';
@override
String contacts_pathTraceTo(String name) {
return 'Tracer l\'itinéraire vers $name';
}
}
+71 -7
View File
@@ -446,6 +446,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notifiche';
@@ -1203,6 +1209,24 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Non letti: $count';
}
@override
String get chat_openLink => 'Aprire il link?';
@override
String get chat_openLinkConfirmation =>
'Vuoi aprire questo link nel tuo browser?';
@override
String get chat_open => 'Apri';
@override
String chat_couldNotOpenLink(String url) {
return 'Impossibile aprire il link: $url';
}
@override
String get chat_invalidLink => 'Formato di link non valido';
@override
String get map_title => 'Mappa Nodi';
@@ -2565,32 +2589,32 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Ri genera la chiave segreta';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Rigenera';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Codice segreto rigenerato per \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aggiorna Segreto';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Segreto aggiornato per \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
}
@override
@@ -2657,4 +2681,44 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nuovo gruppo';
@override
String get pathTrace_you => 'Tu';
@override
String get pathTrace_failed => 'Tracciamento del percorso fallito.';
@override
String get pathTrace_notAvailable =>
'Tracciamento del percorso non disponibile.';
@override
String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.';
@override
String get contacts_pathTrace => 'Traccia Percorso';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Traccia percorso al ripetitore';
@override
String get contacts_repeaterPing => 'Ripetitore ping';
@override
String get contacts_roomPathTrace =>
'Traccia del percorso al server della stanza';
@override
String get contacts_roomPing => 'Ping al server della stanza';
@override
String get contacts_chatTraceRoute => 'Traccia percorso path';
@override
String contacts_pathTraceTo(String name) {
return 'Traccia percorso verso $name';
}
}
+69 -7
View File
@@ -444,6 +444,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notificaties';
@@ -1199,6 +1205,24 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Nieuw: $count';
}
@override
String get chat_openLink => 'Link openen?';
@override
String get chat_openLinkConfirmation =>
'Wilt u deze link in uw browser openen?';
@override
String get chat_open => 'Openen';
@override
String chat_couldNotOpenLink(String url) {
return 'Kan link niet openen: $url';
}
@override
String get chat_invalidLink => 'Ongeldig linkformaat';
@override
String get map_title => 'Node Map';
@@ -2556,32 +2580,32 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regeneer Geheimwoord';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Regeneer';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Geheim hersteld voor \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Bijwerken Geheime';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Geheim gewijzigd voor \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken';
}
@override
@@ -2648,4 +2672,42 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nieuwe groep';
@override
String get pathTrace_you => 'Jij';
@override
String get pathTrace_failed => 'Padtrace mislukt.';
@override
String get pathTrace_notAvailable => 'Padtrace niet beschikbaar.';
@override
String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.';
@override
String get contacts_pathTrace => 'Pad Traceren';
@override
String get contacts_ping => 'Pingen';
@override
String get contacts_repeaterPathTrace => 'Pad traceren naar repeater';
@override
String get contacts_repeaterPing => 'Ping repeater';
@override
String get contacts_roomPathTrace => 'Padtrace naar room server';
@override
String get contacts_roomPing => 'Ping kamer server';
@override
String get contacts_chatTraceRoute => 'Route traceren';
@override
String contacts_pathTraceTo(String name) {
return 'Trace route to $name';
}
}
+70 -7
View File
@@ -448,6 +448,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Powiadomienia';
@@ -1205,6 +1211,24 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Niezgłoszone: $count';
}
@override
String get chat_openLink => 'Otworzyć link?';
@override
String get chat_openLinkConfirmation =>
'Czy chcesz otworzyć ten link w przeglądarce?';
@override
String get chat_open => 'Otwórz';
@override
String chat_couldNotOpenLink(String url) {
return 'Nie można otworzyć linku: $url';
}
@override
String get chat_invalidLink => 'Nieprawidłowy format linku';
@override
String get map_title => 'Mapa węzłów';
@@ -2564,32 +2588,32 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Zregeneruj sekret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Zregeneruj';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Hasło ponownie wygenerowane dla \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Zaktualizuj tajny klucz';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Hasło zaktualizowane dla \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
}
@override
@@ -2656,4 +2680,43 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nowa grupa';
@override
String get pathTrace_you => 'Ty';
@override
String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.';
@override
String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.';
@override
String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.';
@override
String get contacts_pathTrace => 'Śledzenie Ścieżek';
@override
String get contacts_ping => 'Pingować';
@override
String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera';
@override
String get contacts_repeaterPing => 'Repeater pingowy';
@override
String get contacts_roomPathTrace =>
'Śledzenie ścieżki do serwera pokojowego';
@override
String get contacts_roomPing => 'Pinguj serwer pokoju';
@override
String get contacts_chatTraceRoute => 'Śledź trasę promienia';
@override
String contacts_pathTraceTo(String name) {
return 'Śledź trasę do $name';
}
}
+69 -7
View File
@@ -448,6 +448,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notificações';
@@ -1204,6 +1210,24 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Não lido: $count';
}
@override
String get chat_openLink => 'Abrir link?';
@override
String get chat_openLinkConfirmation =>
'Deseja abrir este link no seu navegador?';
@override
String get chat_open => 'Abrir';
@override
String chat_couldNotOpenLink(String url) {
return 'Não foi possível abrir o link: $url';
}
@override
String get chat_invalidLink => 'Formato de link inválido';
@override
String get map_title => 'Mapa de Nós';
@@ -2567,32 +2591,32 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerar Senha Segura';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Senha secreta regenerada para \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Atualizar Segredo';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Segredo atualizado para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
}
@override
@@ -2659,4 +2683,42 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_newGroup => 'Novo grupo';
@override
String get pathTrace_you => 'Você';
@override
String get pathTrace_failed => 'Falha no rastreamento de caminho.';
@override
String get pathTrace_notAvailable => 'Traçado de caminho não disponível.';
@override
String get pathTrace_refreshTooltip => 'Atualizar Path Trace.';
@override
String get contacts_pathTrace => 'Traçado de Caminho';
@override
String get contacts_ping => 'Pingar';
@override
String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor';
@override
String get contacts_repeaterPing => 'Pingar repetidor';
@override
String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala';
@override
String get contacts_roomPing => 'Pingar servidor da sala';
@override
String get contacts_chatTraceRoute => 'Rastrear rota do caminho';
@override
String contacts_pathTraceTo(String name) {
return 'Rastrear rota para $name';
}
}
File diff suppressed because it is too large Load Diff
+69 -7
View File
@@ -444,6 +444,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Upozornenia';
@@ -1200,6 +1206,24 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Nezriadené: $count';
}
@override
String get chat_openLink => 'Otvoriť odkaz?';
@override
String get chat_openLinkConfirmation =>
'Chcete otvoriť tento odkaz v prehliadači?';
@override
String get chat_open => 'Otvoriť';
@override
String chat_couldNotOpenLink(String url) {
return 'Nepodarilo sa otvoriť odkaz: $url';
}
@override
String get chat_invalidLink => 'Neplatný formát odkazu';
@override
String get map_title => 'Mapa uzlov';
@@ -2553,32 +2577,32 @@ class AppLocalizationsSk extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Zobraziť nový tajný kód';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Znovu vygenerovať';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Záznam pre \"$name\" bol regenerovaný tajne';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aktualizovať tajné heslo';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Zmena tajnej slova pre \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
}
@override
@@ -2644,4 +2668,42 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nová skupina';
@override
String get pathTrace_you => 'Vy';
@override
String get pathTrace_failed => 'Sledovanie cesty zlyhalo.';
@override
String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.';
@override
String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.';
@override
String get contacts_pathTrace => 'Sledovanie lúčov';
@override
String get contacts_ping => 'Pingovať';
@override
String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču';
@override
String get contacts_repeaterPing => 'Pingovať opakovač';
@override
String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti';
@override
String get contacts_roomPing => 'Ping server miestnosti';
@override
String get contacts_chatTraceRoute => 'Sledovať trasu lúča';
@override
String contacts_pathTraceTo(String name) {
return 'Sledovať trasu k $name';
}
}
+208 -148
View File
@@ -12,7 +12,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get appTitle => 'MeshCore Open';
@override
String get nav_contacts => 'Kontakti';
String get nav_contacts => 'Stiki';
@override
String get nav_channels => 'Kanali';
@@ -144,7 +144,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_scan => 'Skeniraj';
@override
String get device_quickSwitch => 'Hitro preklopiti';
String get device_quickSwitch => 'Hitro preklop';
@override
String get device_meshcore => 'MeshCore';
@@ -163,16 +163,16 @@ class AppLocalizationsSl extends AppLocalizations {
'Obveščanja, sporoščanje in zemljevidi.';
@override
String get settings_nodeSettings => 'Nastavitve časa';
String get settings_nodeSettings => 'Nastavitev časa';
@override
String get settings_nodeName => 'Ime omrežno mesto';
String get settings_nodeName => 'Ime node-a';
@override
String get settings_nodeNameNotSet => 'Nezavedeno';
String get settings_nodeNameNotSet => 'Ni nastavljeno';
@override
String get settings_nodeNameHint => 'Vnesite ime časa';
String get settings_nodeNameHint => 'Vnesite ime node-a';
@override
String get settings_nodeNameUpdated => 'Ime posodobljeno';
@@ -182,7 +182,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_radioSettingsSubtitle =>
'Frekvenca, moč, razširni faktor';
'Frekvenca, moč, razširitveni faktor';
@override
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene';
@@ -201,7 +201,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_locationInvalid =>
'Neveljna zemeljska širina ali dolžina.';
'Neveljavna zemeljska širina ali dolžina.';
@override
String get settings_locationGPSEnable => 'Omogoči GPS';
@@ -224,7 +224,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_longitude => 'Dolžina';
@override
String get settings_privacyMode => 'Mod podjetja';
String get settings_privacyMode => 'Zasebnost';
@override
String get settings_privacyModeSubtitle => 'Skrita imena/lokacije v oglasih';
@@ -234,10 +234,10 @@ class AppLocalizationsSl extends AppLocalizations {
'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.';
@override
String get settings_privacyModeEnabled => 'Privatni režim je omogočen.';
String get settings_privacyModeEnabled => 'Privatni način je omogočen.';
@override
String get settings_privacyModeDisabled => 'Privatni režim je onemogočen.';
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
@override
String get settings_actions => 'Akcije';
@@ -253,47 +253,46 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_advertisementSent => 'Oglas poslan';
@override
String get settings_syncTime => 'Ugasniti čas';
String get settings_syncTime => 'Nastavi uro';
@override
String get settings_syncTimeSubtitle => 'Nastavi uro naprave v čas telefona';
String get settings_syncTimeSubtitle => 'Nastavi uro naprave na čas telefona';
@override
String get settings_timeSynchronized => 'Sinhronizirano po času';
String get settings_timeSynchronized => 'Ura sinhronizirana';
@override
String get settings_refreshContacts => 'Ponovno obišči kontakte';
@override
String get settings_refreshContactsSubtitle =>
'Ponovno naloži seznam kontaktov iz naprave';
'Ponovno naloži seznam stikov v napravi';
@override
String get settings_rebootDevice => 'Restart Naprave';
String get settings_rebootDevice => 'Ponovni zagon naprave';
@override
String get settings_rebootDeviceSubtitle =>
'Ponovite zažetek naprave MeshCore';
String get settings_rebootDeviceSubtitle => 'Ponovno zaženi MeshCore napravo';
@override
String get settings_rebootDeviceConfirm =>
'Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.';
'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.';
@override
String get settings_debug => 'Napravi popravek';
String get settings_debug => 'Debug';
@override
String get settings_bleDebugLog => 'Logarjev zapis BLE';
String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)';
@override
String get settings_bleDebugLogSubtitle =>
'Navodila BLE, odgovori in surovo podatkovno';
'BLE ukazi, odgovori in surovi podatki';
@override
String get settings_appDebugLog => 'Log zapiske aplikacije';
String get settings_appDebugLog => 'Logi aplikacije';
@override
String get settings_appDebugLogSubtitle => 'Prijavni sporočila aplikacije';
String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije';
@override
String get settings_about => 'Oglejte si';
@@ -304,11 +303,11 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get settings_aboutLegalese => 'MeshCore Odprtokodni Projekt 2024';
String get settings_aboutLegalese => 'Odprtokodni projekt MeshCore 2024';
@override
String get settings_aboutDescription =>
'Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.';
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
@override
String get settings_infoName => 'Ime';
@@ -323,10 +322,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_infoBattery => 'Baterija';
@override
String get settings_infoPublicKey => 'Ključ javnega tipa';
String get settings_infoPublicKey => 'Javni ključ';
@override
String get settings_infoContactsCount => 'Število kontaktov';
String get settings_infoContactsCount => 'Število stikov';
@override
String get settings_infoChannelCount => 'Število kanalov';
@@ -350,7 +349,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_frequencyHelper => '300,00 - 2500,00';
@override
String get settings_frequencyInvalid => 'Neveljčna frekvenca (300-2500 MHz)';
String get settings_frequencyInvalid => 'Neveljavna frekvenca (300-2500 MHz)';
@override
String get settings_bandwidth => 'Pasovna širina';
@@ -368,13 +367,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_txPowerHelper => '0 - 22';
@override
String get settings_txPowerInvalid => 'Neveljaven TX moč (0-22 dBm)';
String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)';
@override
String get settings_longRange => 'Dolenje območje';
String get settings_longRange => 'DDolg doseg';
@override
String get settings_fastSpeed => 'Hitra hitrost';
String get settings_fastSpeed => 'Visoka hitrost';
@override
String settings_error(String message) {
@@ -391,10 +390,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_theme => 'Tema';
@override
String get appSettings_themeSystem => 'Predpomnilnik sistema';
String get appSettings_themeSystem => 'Sistemska tema';
@override
String get appSettings_themeLight => 'Luč';
String get appSettings_themeLight => 'Svetlo';
@override
String get appSettings_themeDark => 'Temno';
@@ -445,10 +444,16 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_languageBg => 'Български';
@override
String get appSettings_notifications => 'Obveščanja';
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_enableNotifications => 'Omogoči obveščanje';
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Obvestila';
@override
String get appSettings_enableNotifications => 'Omogoči obvestila';
@override
String get appSettings_enableNotificationsSubtitle =>
@@ -484,7 +489,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_advertisementNotificationsSubtitle =>
'Pokaži obvestilo, ko so novi vozlišči odkrivljeni.';
'Pokaži obvestilo, ko so najdene nove naprave.';
@override
String get appSettings_messaging => 'Komuniciranje';
@@ -499,18 +504,19 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_pathsWillBeCleared =>
'Potnice bodo očiščene po 5 neuspešnih poskusih.';
'Počisti pot po 5 neuspešnih poskusih.';
@override
String get appSettings_pathsWillNotBeCleared =>
'Potniški poti ne bodo samodejno čiščeni.';
'Poti ne bodo samodejno čiščene.';
@override
String get appSettings_autoRouteRotation => 'Avtomatsko Občutke in Rotacije';
String get appSettings_autoRouteRotation =>
'Avtomatsko rotacija prenosne poti';
@override
String get appSettings_autoRouteRotationSubtitle =>
'Med spreminjanjem med najboljšimi potmi in plovilnim načinom';
'Menjaj med boljšo potjo in flood načinom';
@override
String get appSettings_autoRouteRotationEnabled =>
@@ -524,16 +530,16 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_battery => 'Baterija';
@override
String get appSettings_batteryChemistry => 'Razem z možnostmi';
String get appSettings_batteryChemistry => 'Kemija baterije';
@override
String appSettings_batteryChemistryPerDevice(String deviceName) {
return 'Nastavitve za naprave ($deviceName)';
return 'Nastavitev za napravo ($deviceName)';
}
@override
String get appSettings_batteryChemistryConnectFirst =>
'Povežite se z napravo za izbiro';
'Za izbiro se poveži z napravo';
@override
String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)';
@@ -545,52 +551,51 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_mapDisplay => 'Prikaz zemljevide';
String get appSettings_mapDisplay => 'Prikaz zemljevida';
@override
String get appSettings_showRepeaters => 'Prikaži ponovitve';
String get appSettings_showRepeaters => 'Prikaži repetitorje';
@override
String get appSettings_showRepeatersSubtitle =>
'Prikaži ponovljalne notranjosti na zemljeploscu';
String get appSettings_showRepeatersSubtitle => 'Prikaži repetitorje na mapi';
@override
String get appSettings_showChatNodes => 'Prikaži čakalne notranjosti';
String get appSettings_showChatNodes => 'Prikaži naprave za klepet';
@override
String get appSettings_showChatNodesSubtitle =>
'Prikaži pogovorni pike na zemljeploscu';
'Prikaži naprave na zemljevidu';
@override
String get appSettings_showOtherNodes => 'Pokaži druge vozlišča';
String get appSettings_showOtherNodes => 'Pokaži druge naprave';
@override
String get appSettings_showOtherNodesSubtitle =>
'Pokaži druge vrste notranjih elementov na zemljevalu.';
'Pokaži druge vrste naprav na zemljevidu.';
@override
String get appSettings_timeFilter => 'Filtri po času';
String get appSettings_timeFilter => 'Filter po času';
@override
String get appSettings_timeFilterShowAll => 'Pokaži vse notranje elemente';
String get appSettings_timeFilterShowAll => 'Pokaži vse naprave';
@override
String appSettings_timeFilterShowLast(int hours) {
return 'Pokaži notranjosti iz zadnjih $hours ur';
return 'Pokaži naprave v zadnjih $hours urah';
}
@override
String get appSettings_mapTimeFilter => 'Filtri časa zemljevida';
String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu';
@override
String get appSettings_showNodesDiscoveredWithin =>
'Pokaži notranje čepke, odkrivene v:';
'Pokaži naprave odkrite v:';
@override
String get appSettings_allTime => 'Vse čase';
String get appSettings_allTime => 'Brez omejitev';
@override
String get appSettings_lastHour => 'Minuto nazaj';
String get appSettings_lastHour => 'V zadnji uri';
@override
String get appSettings_last6Hours => 'Zadnjih 6 ur';
@@ -599,13 +604,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_last24Hours => 'Zadnjih 24 ur';
@override
String get appSettings_lastWeek => 'Lepošno';
String get appSettings_lastWeek => 'Prejšnji teden';
@override
String get appSettings_offlineMapCache => 'Omrezni Poudni Arhiv';
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
@override
String get appSettings_noAreaSelected => 'Nizkana označena površina';
String get appSettings_noAreaSelected => 'Območje ni izbrano';
@override
String appSettings_areaSelectedZoom(int minZoom, int maxZoom) {
@@ -613,79 +618,78 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get appSettings_debugCard => 'Napravi popravek';
String get appSettings_debugCard => 'Razhroščevanje';
@override
String get appSettings_appDebugLogging => 'Programski Log';
String get appSettings_appDebugLogging => 'Programski dnevnik';
@override
String get appSettings_appDebugLoggingSubtitle =>
'Log aplikacijske debug sporočila za odpravljanje težav';
'Dnevnik debug sporočil za odpravljanje težav';
@override
String get appSettings_appDebugLoggingEnabled =>
'Omogočeno zaznamovanje napak v aplikaciji';
'Beleženje napak v aplikaciji omogočeno';
@override
String get appSettings_appDebugLoggingDisabled =>
'Programski logi aplikacije so onemogočeni.';
'Beleženje napak v aplikacije onemogočeno.';
@override
String get contacts_title => 'Kontakti';
String get contacts_title => 'Stiki';
@override
String get contacts_noContacts => 'Še ni kontaktov.';
String get contacts_noContacts => 'Ni stikov.';
@override
String get contacts_contactsWillAppear =>
'Kontakti se bodo prikazali, ko naprave oglasijo.';
'Stiki se bodo prikazali, ko se naprave oglasijo.';
@override
String get contacts_searchContacts => 'Iskanje kontaktov...';
String get contacts_searchContacts => 'Iskanje stikov...';
@override
String get contacts_noUnreadContacts => 'Nerešeno kontaktov.';
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
@override
String get contacts_noContactsFound =>
'Niti ena oseba ali skupine ni najdena.';
String get contacts_noContactsFound => 'Stiki niso najdeni.';
@override
String get contacts_deleteContact => 'Izbrisati Kontakt';
String get contacts_deleteContact => 'Izbriši stik';
@override
String contacts_removeConfirm(String contactName) {
return 'Izbrisati $contactName iz kontaktov?';
return 'Izbrišem $contactName iz stikov?';
}
@override
String get contacts_manageRepeater => 'Upravljajte Ponovitve';
String get contacts_manageRepeater => 'Upravljaj Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
@override
String get contacts_roomLogin => 'Vnos v sobo';
String get contacts_roomLogin => 'Prijava v sobo';
@override
String get contacts_openChat => 'Odprta kleta';
String get contacts_openChat => 'Odpri klepet';
@override
String get contacts_editGroup => 'Uredi Skupino';
String get contacts_editGroup => 'Uredi skupino';
@override
String get contacts_deleteGroup => 'Izbrisati Skupino';
String get contacts_deleteGroup => 'Izbriši skupino';
@override
String contacts_deleteGroupConfirm(String groupName) {
return 'Odpovedati $groupName?';
return 'Izbriši $groupName?';
}
@override
String get contacts_newGroup => 'Novo skupino';
String get contacts_newGroup => 'Nova skupina';
@override
String get contacts_groupName => 'Skupina imena';
String get contacts_groupName => 'Ime skupine';
@override
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
@@ -696,53 +700,53 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get contacts_filterContacts => 'Filtri kontakt\\,...';
String get contacts_filterContacts => 'Filtriraj stik\\,...';
@override
String get contacts_noContactsMatchFilter =>
'Niti ena oseba ne ustreza vašemu kriteriju.';
'Noben stik ne ustreza vašemu kriteriju.';
@override
String get contacts_noMembers => 'Nič članov.';
String get contacts_noMembers => 'Ni članov.';
@override
String get contacts_lastSeenNow => 'Datum zadnjega vpisa zdaj';
String get contacts_lastSeenNow => 'Nazadnje viden zdaj';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Zadnjič videti $minutes minut nazaj';
return 'Zadnjič viden pred $minutes minutami';
}
@override
String get contacts_lastSeenHourAgo => 'Zadnjič ogledan pred 1 uro.';
String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Zadnjič videti $hours ur nazaj';
return 'Zadnjič viden pred $hours urami';
}
@override
String get contacts_lastSeenDayAgo => 'Zadnjič ogledan pred 1 dnem';
String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Zadnjič videti $days dni nazaj';
return 'Zadnjič viden pred $days dnem';
}
@override
String get channels_title => 'Kanali';
@override
String get channels_noChannelsConfigured => 'Nekonfigurirane kanale';
String get channels_noChannelsConfigured => 'Kanali še niso konfigurirani';
@override
String get channels_addPublicChannel => 'Dodaj Objavni Kanal';
String get channels_addPublicChannel => 'Dodaj javni kanal';
@override
String get channels_searchChannels => 'Poišči kanale...';
@override
String get channels_noChannelsFound => 'Niti kanalov najti ni.';
String get channels_noChannelsFound => 'Ne najdem kanalov.';
@override
String channels_channelIndex(int index) {
@@ -753,16 +757,16 @@ class AppLocalizationsSl extends AppLocalizations {
String get channels_hashtagChannel => 'Hashtag kanal';
@override
String get channels_public => 'javno';
String get channels_public => 'Javni';
@override
String get channels_private => 'Zasebno';
String get channels_private => 'Zasebni';
@override
String get channels_publicChannel => 'Ogljišna skupina';
String get channels_publicChannel => 'Javni kanal';
@override
String get channels_privateChannel => 'Zatemniščen kanal';
String get channels_privateChannel => 'Zasebni kanal';
@override
String get channels_editChannel => 'Uredi kanal';
@@ -772,7 +776,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String channels_deleteChannelConfirm(String name) {
return 'Izbrisati \"$name\"? To se ne da povrniti.';
return 'Izbrišem \"$name\"? To se ne da povrniti.';
}
@override
@@ -912,21 +916,21 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get chat_typeMessage => 'Vnesite sporočilo...';
String get chat_typeMessage => 'Vnesi sporočilo...';
@override
String chat_messageTooLong(int maxBytes) {
return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes bajt).';
return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).';
}
@override
String get chat_messageCopied => 'Pošljeno sporočilo';
String get chat_messageCopied => 'Sporočilo poslano';
@override
String get chat_messageDeleted => 'Pošiljanje sporočila izbrisano';
String get chat_messageDeleted => 'Sporočilo izbrisano';
@override
String get chat_retryingMessage => 'Ponovna poskus.';
String get chat_retryingMessage => 'Ponovni poskus.';
@override
String chat_retryCount(int current, int max) {
@@ -937,10 +941,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_sendGif => 'Pošlji GIF';
@override
String get chat_reply => 'Odpošlji';
String get chat_reply => 'Odgovori';
@override
String get chat_addReaction => 'Dodaj Reakcijo';
String get chat_addReaction => 'Dodaj reakcijo';
@override
String get chat_me => 'jaz';
@@ -961,19 +965,19 @@ class AppLocalizationsSl extends AppLocalizations {
String get gifPicker_title => 'Izberi GIF';
@override
String get gifPicker_searchHint => 'Iskalite GIF-e...';
String get gifPicker_searchHint => 'Išči GIF-e...';
@override
String get gifPicker_poweredBy => 'Naprodno z GIPHY';
String get gifPicker_poweredBy => 'Napredno z GIPHY';
@override
String get gifPicker_noGifsFound => 'Niti GIF-jev najti ni.';
String get gifPicker_noGifsFound => 'Ne najdem GIF-ov.';
@override
String get gifPicker_failedLoad => 'Neuspešno je naložilo GIF-e';
String get gifPicker_failedLoad => 'Neuspešno nalaganje GIF-a';
@override
String get gifPicker_failedSearch => 'Posodobit neuspešno.';
String get gifPicker_failedSearch => 'Iskanje neuspešno.';
@override
String get gifPicker_noInternet => 'Ni internetne povezave';
@@ -982,35 +986,35 @@ class AppLocalizationsSl extends AppLocalizations {
String get debugLog_appTitle => 'Log zapiske aplikacije';
@override
String get debugLog_bleTitle => 'Logarjev zapis BLE';
String get debugLog_bleTitle => 'Log zapis BLE';
@override
String get debugLog_copyLog => 'Kopiraj zapiske';
String get debugLog_copyLog => 'Kopiraj dnevnik';
@override
String get debugLog_clearLog => 'Pasters log';
String get debugLog_clearLog => 'Briši log';
@override
String get debugLog_copied => 'Kopirana belež poteka.';
String get debugLog_copied => 'Beležka kopirana.';
@override
String get debugLog_bleCopied => 'Kopirana beležke iz BLE';
String get debugLog_bleCopied => 'Kopirana beležka iz BLE';
@override
String get debugLog_noEntries => 'Še ni ustvarjenih debug zapisov.';
String get debugLog_noEntries => 'Ni ustvarjenih debug zapisov.';
@override
String get debugLog_enableInSettings =>
'Omogoči beleženje napak v aplikaciji v nastavitvah';
'Omogoči beleženje napak v nastavitvah aplikacije';
@override
String get debugLog_frames => 'Okna';
String get debugLog_frames => 'Okvirji';
@override
String get debugLog_rawLogRx => 'Svež Log-RX';
@override
String get debugLog_noBleActivity => 'Šele začnite z aktivnostjo BLE.';
String get debugLog_noBleActivity => 'Ni BLE aktivnosti.';
@override
String debugFrame_length(int count) {
@@ -1079,10 +1083,10 @@ class AppLocalizationsSl extends AppLocalizations {
'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.';
@override
String get chat_hopSingular => 'skoč';
String get chat_hopSingular => 'skok';
@override
String get chat_hopPlural => 'škrabec';
String get chat_hopPlural => 'skokov';
@override
String chat_hopsCount(int count) {
@@ -1103,7 +1107,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_noPathHistoryYet =>
'Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.';
'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.';
@override
String get chat_pathActions => 'Potni ukazi:';
@@ -1115,7 +1119,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.';
@override
String get chat_clearPath => 'Čista pot';
String get chat_clearPath => 'Počisti pot';
@override
String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.';
@@ -1133,7 +1137,7 @@ class AppLocalizationsSl extends AppLocalizations {
'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.';
@override
String get chat_fullPath => 'Polni pot';
String get chat_fullPath => 'Polna pot';
@override
String get chat_pathDetailsNotAvailable =>
@@ -1197,6 +1201,24 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Nerešeno: $count';
}
@override
String get chat_openLink => 'Odpreti povezavo?';
@override
String get chat_openLinkConfirmation =>
'Ali želite odpreti to povezavo v brskalniku?';
@override
String get chat_open => 'Odpri';
@override
String chat_couldNotOpenLink(String url) {
return 'Povezave ni bilo mogoče odpreti: $url';
}
@override
String get chat_invalidLink => 'Neveljavna oblika povezave';
@override
String get map_title => 'Mapa omrežja';
@@ -1994,13 +2016,13 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get repeater_cliQuickGetName => 'Dobiti ime';
String get repeater_cliQuickGetName => 'Pridobi ime';
@override
String get repeater_cliQuickGetRadio => 'Dobiti Radiopravo';
@override
String get repeater_cliQuickGetTx => 'Dobiti TX';
String get repeater_cliQuickGetTx => 'Pridobi TX';
@override
String get repeater_cliQuickNeighbors => 'Sosedi';
@@ -2012,7 +2034,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_cliQuickAdvertise => 'Oglasite';
@override
String get repeater_cliQuickClock => 'Urnik';
String get repeater_cliQuickClock => 'Ura';
@override
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
@@ -2135,7 +2157,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_cliHelpSetPerm =>
'Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).';
'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).';
@override
String get repeater_cliHelpGetBridgeType =>
@@ -2243,11 +2265,11 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_logging => 'Logiranje';
@override
String get repeater_neighborsRepeaterOnly => 'Sosedi (le za ponovitelja)';
String get repeater_neighborsRepeaterOnly => 'Sosedi (le za repetitorje)';
@override
String get repeater_regionManagementRepeaterOnly =>
'Upravljanje regij (zgolj za ponovitve)';
'Upravljanje regij (zgolj za repetitorje)';
@override
String get repeater_regionNote =>
@@ -2362,13 +2384,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get channelPath_messageDetails => 'Podrobnosti sporočila';
@override
String get channelPath_senderLabel => 'Pošiljalec';
String get channelPath_senderLabel => 'Pošiljatelj';
@override
String get channelPath_timeLabel => 'Čas';
String get channelPath_timeLabel => 'Ura';
@override
String get channelPath_repeatsLabel => 'Ponovi';
String get channelPath_repeatsLabel => 'Ponovitve';
@override
String channelPath_pathLabel(int index) {
@@ -2533,17 +2555,17 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get community_scanOrCreate =>
'Skenirajte QR kodo ali ustvarite skupnost za začetek.';
'Skeniraj QR kodo ali ustvari skupnost za začetek.';
@override
String get community_manageCommunities => 'Upravljajte skupnosti';
String get community_manageCommunities => 'Upravljanje skupnosti';
@override
String get community_delete => 'Opusti skupnost';
@override
String community_deleteConfirm(String name) {
return 'Zapustiti \"$name\"?';
return 'Zapusti \"$name\"?';
}
@override
@@ -2557,36 +2579,36 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Ponovno ustvari geslo';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Preberi znova';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Geslo za \"$name\" ponovno ustvarjeno';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Ažuriraj ključ';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Skrivnostno spremembo za \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skeniraj novo QR kodo za posodabljanje ključa za $name';
}
@override
String get community_addHashtagChannel => 'Dodaj Oznako Obštnine';
String get community_addHashtagChannel => 'Dodaj hashtag kanal';
@override
String get community_addHashtagChannelDesc =>
@@ -2600,7 +2622,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get community_regularHashtagDesc =>
'javna oznaka (kateri koli lahko sodelujejo)';
'javna oznaka (kdorkoli lahko sodeluje)';
@override
String get community_communityHashtag => 'Skupnostni hashtag';
@@ -2649,4 +2671,42 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nova skupina';
@override
String get pathTrace_you => 'Ti';
@override
String get pathTrace_failed => 'Sledenje poti ni uspelo.';
@override
String get pathTrace_notAvailable => 'Potni sled ni na voljo.';
@override
String get pathTrace_refreshTooltip => 'Osveži Path Trace.';
@override
String get contacts_pathTrace => 'Sledenje poti';
@override
String get contacts_ping => 'Pingati';
@override
String get contacts_repeaterPathTrace => 'Sledi poti do ponavljalnika';
@override
String get contacts_repeaterPing => 'Pinguj ponavljalnik';
@override
String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe';
@override
String get contacts_roomPing => 'Ping strežnik sobe';
@override
String get contacts_chatTraceRoute => 'Slediti poti žarkov';
@override
String contacts_pathTraceTo(String name) {
return 'Trace route to $name';
}
}
+69 -7
View File
@@ -441,6 +441,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Meddelanden';
@@ -1192,6 +1198,24 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Olästa: $count';
}
@override
String get chat_openLink => 'Öppna länk?';
@override
String get chat_openLinkConfirmation =>
'Vill du öppna den här länken i din webbläsare?';
@override
String get chat_open => 'Öppna';
@override
String chat_couldNotOpenLink(String url) {
return 'Kunde inte öppna länken: $url';
}
@override
String get chat_invalidLink => 'Ogiltigt länkformat';
@override
String get map_title => 'Nodkarta';
@@ -2541,32 +2565,32 @@ class AppLocalizationsSv extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerera hemlig kod';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Regenerera';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Lösenord återskapad för \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Uppdatera hemlighet';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Hemlighet uppdaterad för \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
}
@override
@@ -2632,4 +2656,42 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_newGroup => 'Ny grupp';
@override
String get pathTrace_you => 'Du';
@override
String get pathTrace_failed => 'Sökvägsföljning misslyckades.';
@override
String get pathTrace_notAvailable => 'Path trace ej tillgänglig.';
@override
String get pathTrace_refreshTooltip => 'Uppdatera Path Trace';
@override
String get contacts_pathTrace => 'Path Trace';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Vägspårning till repeater';
@override
String get contacts_repeaterPing => 'Ping-repeater';
@override
String get contacts_roomPathTrace => 'Vägspårning till rumserver';
@override
String get contacts_roomPing => 'Ping rumsserver';
@override
String get contacts_chatTraceRoute => 'Spåra rutt';
@override
String contacts_pathTraceTo(String name) {
return 'Spåra rutt till $name';
}
}
File diff suppressed because it is too large Load Diff
+68 -7
View File
@@ -432,6 +432,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => '通知';
@@ -1148,6 +1154,23 @@ class AppLocalizationsZh extends AppLocalizations {
return '未读:$count';
}
@override
String get chat_openLink => '打开链接?';
@override
String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?';
@override
String get chat_open => '打开';
@override
String chat_couldNotOpenLink(String url) {
return '无法打开链接:$url';
}
@override
String get chat_invalidLink => '链接格式无效';
@override
String get map_title => '节点地图';
@@ -2425,32 +2448,32 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => '重新生成密钥';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => '重新生成';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return '密码已重置为“$name';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => '更新密钥';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return '密码已更新为“$name';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return '扫描新的二维码更新\"$name\"的密码';
}
@override
@@ -2514,4 +2537,42 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get listFilter_newGroup => '新组';
@override
String get pathTrace_you => '';
@override
String get pathTrace_failed => '路径追踪失败。';
@override
String get pathTrace_notAvailable => '路径追踪不可用';
@override
String get pathTrace_refreshTooltip => '刷新路径追踪';
@override
String get contacts_pathTrace => '路径追踪';
@override
String get contacts_ping => 'ping';
@override
String get contacts_repeaterPathTrace => '路径追踪到中继器';
@override
String get contacts_repeaterPing => 'Ping 中继器';
@override
String get contacts_roomPathTrace => '路径追踪至房间服务器';
@override
String get contacts_roomPing => 'Ping 房间服务器';
@override
String get contacts_chatTraceRoute => '路径追踪';
@override
String contacts_pathTraceTo(String name) {
return '追踪路由到 $name';
}
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Link openen?",
"chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?",
"chat_open": "Openen",
"chat_couldNotOpenLink": "Kan link niet openen: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ongeldig linkformaat",
"map_title": "Node Map",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlaten",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
"community_communityHashtag": "Gemeenschappelijk Hashtag",
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
"community_forCommunity": "Voor {name}"
"community_forCommunity": "Voor {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Geheim hersteld voor \"{name}\"",
"community_regenerateSecret": "Regeneer Geheimwoord",
"community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.",
"community_regenerate": "Regeneer",
"community_updateSecret": "Bijwerken Geheime",
"community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
"community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Jij",
"pathTrace_failed": "Padtrace mislukt.",
"pathTrace_notAvailable": "Padtrace niet beschikbaar.",
"pathTrace_refreshTooltip": "Path Trace vernieuwen.",
"contacts_pathTrace": "Pad Traceren",
"contacts_ping": "Pingen",
"contacts_repeaterPathTrace": "Pad traceren naar repeater",
"contacts_repeaterPing": "Ping repeater",
"contacts_roomPathTrace": "Padtrace naar room server",
"contacts_roomPing": "Ping kamer server",
"contacts_chatTraceRoute": "Route traceren",
"contacts_pathTraceTo": "Trace route to {name}"
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Otworzyć link?",
"chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?",
"chat_open": "Otwórz",
"chat_couldNotOpenLink": "Nie można otworzyć linku: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Nieprawidłowy format linku",
"map_title": "Mapa węzłów",
"map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi",
"map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
"community_communityHashtag": "Hashtag Społeczności",
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
"community_forCommunity": "Dla {name}"
"community_forCommunity": "Dla {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerate": "Zregeneruj",
"community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"",
"community_regenerateSecret": "Zregeneruj sekret",
"community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.",
"community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"",
"community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
"community_updateSecret": "Zaktualizuj tajny klucz",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Ty",
"pathTrace_failed": "Śledzenie ścieżki nie powiodło się.",
"pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.",
"contacts_pathTrace": "Śledzenie Ścieżek",
"contacts_ping": "Pingować",
"contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera",
"contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego",
"contacts_roomPing": "Pinguj serwer pokoju",
"pathTrace_refreshTooltip": "Odśwież ścieżkę.",
"contacts_repeaterPing": "Repeater pingowy",
"contacts_pathTraceTo": "Śledź trasę do {name}",
"contacts_chatTraceRoute": "Śledź trasę promienia"
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Abrir link?",
"chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?",
"chat_open": "Abrir",
"chat_couldNotOpenLink": "Não foi possível abrir o link: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato de link inválido",
"map_title": "Mapa de Nós",
"map_noNodesWithLocation": "Não existem nós com dados de localização.",
"map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
"community_communityHashtag": "Hashtag da Comunidade",
"community_communityHashtagDesc": "Apenas para membros da comunidade",
"community_forCommunity": "Para {name}"
"community_forCommunity": "Para {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.",
"community_regenerateSecret": "Regenerar Senha Segura",
"community_secretRegenerated": "Senha secreta regenerada para \"{name}\"",
"community_regenerate": "Regenerar",
"community_secretUpdated": "Segredo atualizado para \"{name}\"",
"community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
"community_updateSecret": "Atualizar Segredo",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Você",
"pathTrace_failed": "Falha no rastreamento de caminho.",
"pathTrace_notAvailable": "Traçado de caminho não disponível.",
"pathTrace_refreshTooltip": "Atualizar Path Trace.",
"contacts_pathTrace": "Traçado de Caminho",
"contacts_ping": "Pingar",
"contacts_repeaterPathTrace": "Traçar caminho para repetidor",
"contacts_repeaterPing": "Pingar repetidor",
"contacts_roomPathTrace": "Traçar caminho para o servidor da sala",
"contacts_roomPing": "Pingar servidor da sala",
"contacts_chatTraceRoute": "Rastrear rota do caminho",
"contacts_pathTraceTo": "Rastrear rota para {name}"
}
+797
View File
@@ -0,0 +1,797 @@
{
"@@locale": "ru",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакты",
"nav_channels": "Каналы",
"nav_map": "Карта",
"common_cancel": "Отмена",
"common_ok": "OK",
"common_connect": "Коннект",
"common_unknownDevice": "Неизвестное устройство",
"common_save": "Сохранить",
"common_delete": "Удалить",
"common_close": "Закрыть",
"common_edit": "Изменить",
"common_add": "Добавить",
"common_settings": "Настройки",
"common_disconnect": "Отключить",
"common_connected": "Подключено",
"common_disconnected": "Отключено",
"common_create": "Создать",
"common_continue": "Продолжить",
"common_share": "Поделиться",
"common_copy": "Копировать",
"common_retry": "Повторить",
"common_hide": "Скрыть",
"common_remove": "Убрать",
"common_enable": "Включить",
"common_disable": "Выключить",
"common_reboot": "Перезагрузить",
"common_loading": "Загрузка...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} В",
"common_percentValue": "{percent}%",
"scanner_title": "MeshCore Open",
"scanner_scanning": "Поиск устройств...",
"scanner_connecting": "Подключение...",
"scanner_disconnecting": "Отключение...",
"scanner_notConnected": "Не подключено",
"scanner_connectedTo": "Подключено к {deviceName}",
"scanner_searchingDevices": "Поиск устройств MeshCore...",
"scanner_tapToScan": "Нажмите для поиска MeshCore устройств",
"scanner_connectionFailed": "Подключение не удалось: {error}",
"scanner_stop": "Стоп",
"scanner_scan": "Сканирование",
"device_quickSwitch": "Быстрое переключение",
"device_meshcore": "MeshCore",
"settings_title": "Настройки",
"settings_deviceInfo": "Информация об устройстве",
"settings_appSettings": "Настройки приложения",
"settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты",
"settings_nodeSettings": "Настройки ноды",
"settings_nodeName": "Имя ноды",
"settings_nodeNameNotSet": "Не установлено",
"settings_nodeNameHint": "Введите имя ноды",
"settings_nodeNameUpdated": "Имя обновлено",
"settings_radioSettings": "Настройки радио",
"settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения",
"settings_radioSettingsUpdated": "Настройки радио обновлены",
"settings_location": "Позиция",
"settings_locationSubtitle": "Координаты GPS",
"settings_locationUpdated": "Позиция и настройки GPS обновлены",
"settings_locationBothRequired": "Введите широту и долготу.",
"settings_locationInvalid": "Неверная широта или долгота.",
"settings_locationGPSEnable": "Включить GPS",
"settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.",
"settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)",
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
"settings_latitude": "Широта",
"settings_longitude": "Долгота",
"settings_privacyMode": "Режим конфиденциальности",
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
"settings_privacyModeEnabled": "Режим конфиденциальности включен",
"settings_privacyModeDisabled": "Режим конфиденциальности выключен",
"settings_actions": "Действия",
"settings_sendAdvertisement": "Отправить анонсирование",
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
"settings_advertisementSent": "Анонсирование отправлено",
"settings_syncTime": "Синхронизация времени",
"settings_syncTimeSubtitle": "Синхронизировать время с телефоном",
"settings_timeSynchronized": "Время синхронизировано",
"settings_refreshContacts": "Обновить контакты",
"settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства",
"settings_rebootDevice": "Перезагрузить устройство",
"settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore",
"settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.",
"settings_debug": "Отладка",
"settings_bleDebugLog": "Журнал отладки BLE",
"settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные",
"settings_appDebugLog": "Журнал отладки приложения",
"settings_appDebugLogSubtitle": "Сообщения отладки приложения",
"settings_about": "О программе",
"settings_aboutVersion": "MeshCore Open v{version}",
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
"settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.",
"settings_infoName": "Имя",
"settings_infoId": "ID",
"settings_infoStatus": "Статус",
"settings_infoBattery": "Батарея",
"settings_infoPublicKey": "Публичный ключ",
"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 МГц)",
"settings_bandwidth": "Полоса пропускания",
"settings_spreadingFactor": "Коэффициент расширения",
"settings_codingRate": "Коэффициент кодирования",
"settings_txPower": "Мощность передачи (дБм)",
"settings_txPowerHelper": "0 22",
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
"settings_longRange": "Дальний радиус",
"settings_fastSpeed": "Высокая скорость",
"settings_error": "Ошибка: {message}",
"appSettings_title": "Настройки приложения",
"appSettings_appearance": "Внешний вид",
"appSettings_theme": "Тема",
"appSettings_themeSystem": "Как в системе",
"appSettings_themeLight": "Светлая",
"appSettings_themeDark": "Тёмная",
"appSettings_language": "Язык",
"appSettings_languageSystem": "Как в системе",
"appSettings_languageEn": "Английский",
"appSettings_languageFr": "Французский",
"appSettings_languageEs": "Испанский",
"appSettings_languageDe": "Немецкий",
"appSettings_languagePl": "Польский",
"appSettings_languageSl": "Словенский",
"appSettings_languagePt": "Португальский",
"appSettings_languageIt": "Итальянский",
"appSettings_languageZh": "Китайский",
"appSettings_languageSv": "Шведский",
"appSettings_languageNl": "Нидерландский",
"appSettings_languageSk": "Словацкий",
"appSettings_languageBg": "Болгарский",
"appSettings_languageRu": "Русский",
"appSettings_notifications": "Уведомления",
"appSettings_enableNotifications": "Включить уведомления",
"appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях",
"appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено",
"appSettings_notificationsEnabled": "Уведомления включены",
"appSettings_notificationsDisabled": "Уведомления отключены",
"appSettings_messageNotifications": "Уведомления о сообщениях",
"appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений",
"appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах",
"appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах",
"appSettings_advertisementNotifications": "Уведомления об анонсированиях",
"appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод",
"appSettings_messaging": "Обмен сообщениями",
"appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток",
"appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки",
"appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток",
"appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться",
"appSettings_autoRouteRotation": "Автоматическое переключение маршрутов",
"appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки",
"appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено",
"appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено",
"appSettings_battery": "Батарея",
"appSettings_batteryChemistry": "Химия батареи",
"appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})",
"appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать",
"appSettings_batteryNmc": "18650 NMC (3.04.2 В)",
"appSettings_batteryLifepo4": "LiFePO4 (2.63.65 В)",
"appSettings_batteryLipo": "LiPo (3.04.2 В)",
"appSettings_mapDisplay": "Отображение карты",
"appSettings_showRepeaters": "Показывать репитеры",
"appSettings_showRepeatersSubtitle": "Отображать репитеры на карте",
"appSettings_showChatNodes": "Показывать чат-ноды",
"appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте",
"appSettings_showOtherNodes": "Показывать другие ноды",
"appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте",
"appSettings_timeFilter": "Фильтр по времени",
"appSettings_timeFilterShowAll": "Показывать все ноды",
"appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч",
"appSettings_mapTimeFilter": "Временной фильтр карты",
"appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:",
"appSettings_allTime": "Всё время",
"appSettings_lastHour": "Последний час",
"appSettings_last6Hours": "Последние 6 часов",
"appSettings_last24Hours": "Последние 24 часа",
"appSettings_lastWeek": "Последнюю неделю",
"appSettings_offlineMapCache": "Кэш офлайн-карты",
"appSettings_noAreaSelected": "Область не выбрана",
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})",
"appSettings_debugCard": "Отладка",
"appSettings_appDebugLogging": "Журнал отладки приложения",
"appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики",
"appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён",
"appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён",
"contacts_title": "Контакты",
"contacts_noContacts": "Контактов пока нет",
"contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения",
"contacts_searchContacts": "Поиск контактов...",
"contacts_noUnreadContacts": "Нет непрочитанных контактов",
"contacts_noContactsFound": "Контакты или группы не найдены",
"contacts_deleteContact": "Удалить контакт",
"contacts_removeConfirm": "Удалить {contactName} из контактов?",
"contacts_manageRepeater": "Управление репитером",
"contacts_manageRoom": "Управление сервером комнат",
"contacts_roomLogin": "Вход на сервер комнат",
"contacts_openChat": "Открыть чат",
"contacts_editGroup": "Изменить группу",
"contacts_deleteGroup": "Удалить группу",
"contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?",
"contacts_newGroup": "Новая группа",
"contacts_groupName": "Имя группы",
"contacts_groupNameRequired": "Имя группы обязательно",
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
"contacts_filterContacts": "Фильтр контактов...",
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
"contacts_noMembers": "Нет участников",
"contacts_lastSeenNow": "Видели только что",
"contacts_lastSeenMinsAgo": "Видели {minutes} мин назад",
"contacts_lastSeenHourAgo": "Видели 1 час назад",
"contacts_lastSeenHoursAgo": "Видели {hours} ч назад",
"contacts_lastSeenDayAgo": "Видели 1 день назад",
"contacts_lastSeenDaysAgo": "Видели {days} дн. назад",
"channels_title": "Каналы",
"channels_noChannelsConfigured": "Каналы не настроены",
"channels_addPublicChannel": "Добавить публичный канал",
"channels_searchChannels": "Поиск каналов...",
"channels_noChannelsFound": "Каналы не найдены",
"channels_channelIndex": "Канал {index}",
"channels_hashtagChannel": "Хэштег-канал",
"channels_public": "Публичный",
"channels_private": "Приватный",
"channels_publicChannel": "Публичный канал",
"channels_privateChannel": "Приватный канал",
"channels_editChannel": "Изменить канал",
"channels_deleteChannel": "Удалить канал",
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
"channels_channelDeleted": "Канал \"{name}\" удалён",
"channels_addChannel": "Добавить канал",
"channels_channelIndexLabel": "Индекс канала",
"channels_channelName": "Имя канала",
"channels_usePublicChannel": "Использовать публичный канал",
"channels_standardPublicPsk": "Стандартный публичный PSK",
"channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Сгенерировать случайный PSK",
"channels_enterChannelName": "Введите имя канала",
"channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа",
"channels_channelAdded": "Канал \"{name}\" добавлен",
"channels_editChannelTitle": "Изменить канал {index}",
"channels_smazCompression": "Сжатие SMAZ",
"channels_channelUpdated": "Канал \"{name}\" обновлён",
"channels_publicChannelAdded": "Публичный канал добавлен",
"channels_sortBy": "Сортировка",
"channels_sortManual": "Вручную",
"channels_sortAZ": "По алфавиту",
"channels_sortLatestMessages": "По последним сообщениям",
"channels_sortUnread": "По непрочитанным",
"channels_createPrivateChannel": "Создать приватный канал",
"channels_createPrivateChannelDesc": "Защищён секретным ключом.",
"channels_joinPrivateChannel": "Присоединиться к приватному каналу",
"channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.",
"channels_joinPublicChannel": "Присоединиться к публичному каналу",
"channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.",
"channels_joinHashtagChannel": "Присоединиться к хэштег-каналу",
"channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.",
"channels_scanQrCode": "Сканировать QR-код",
"channels_scanQrCodeComingSoon": "Скоро будет",
"channels_enterHashtag": "Введите хэштег",
"channels_hashtagHint": "например, #команда",
"chat_noMessages": "Сообщений пока нет",
"chat_sendMessageToStart": "Отправьте сообщение, чтобы начать",
"chat_originalMessageNotFound": "Исходное сообщение не найдено",
"chat_replyingTo": "Ответ для {name}",
"chat_replyTo": "Ответить {name}",
"chat_location": "Местоположение",
"chat_sendMessageTo": "Отправить сообщение {contactName}",
"chat_typeMessage": "Напишите сообщение...",
"chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).",
"chat_messageCopied": "Сообщение скопировано",
"chat_messageDeleted": "Сообщение удалено",
"chat_retryingMessage": "Повтор отправки сообщения",
"chat_retryCount": "Попытка {current}/{max}",
"chat_sendGif": "Отправить GIF",
"chat_reply": "Ответить",
"chat_addReaction": "Добавить реакцию",
"chat_me": "Я",
"emojiCategorySmileys": "Смайлы",
"emojiCategoryGestures": "Жесты",
"emojiCategoryHearts": "Сердечки",
"emojiCategoryObjects": "Предметы",
"gifPicker_title": "Выберите GIF",
"gifPicker_searchHint": "Поиск GIF...",
"gifPicker_poweredBy": "Работает на GIPHY",
"gifPicker_noGifsFound": "GIF не найдены",
"gifPicker_failedLoad": "Не удалось загрузить GIF",
"gifPicker_failedSearch": "Не удалось выполнить поиск GIF",
"gifPicker_noInternet": "Нет подключения к интернету",
"debugLog_appTitle": "Журнал отладки приложения",
"debugLog_bleTitle": "Журнал отладки BLE",
"debugLog_copyLog": "Копировать журнал",
"debugLog_clearLog": "Очистить журнал",
"debugLog_copied": "Журнал отладки скопирован",
"debugLog_bleCopied": "Журнал BLE скопирован",
"debugLog_noEntries": "Журнал отладки пока пуст",
"debugLog_enableInSettings": "Включите запись журнала отладки в настройках",
"debugLog_frames": "Фреймы",
"debugLog_rawLogRx": "Сырой журнал приёма",
"debugLog_noBleActivity": "Активность BLE пока отсутствует",
"debugFrame_length": "Длина фрейма: {count} байт",
"debugFrame_command": "Команда: 0x{value}",
"debugFrame_textMessageHeader": "Фрейм текстового сообщения:",
"debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}",
"debugFrame_timestamp": "- Временная метка: {timestamp}",
"debugFrame_flags": "- Флаги: 0x{value}",
"debugFrame_textType": "- Тип текста: {type} ({label})",
"debugFrame_textTypeCli": "CLI",
"debugFrame_textTypePlain": "Обычный",
"debugFrame_text": "- Текст: \"{text}\"",
"debugFrame_hexDump": "Шестнадцатеричный дамп:",
"chat_pathManagement": "Управление маршрутами",
"chat_routingMode": "Режим маршрутизации",
"chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"chat_forceFloodMode": "Принудительный режим рассылки",
"chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):",
"chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.",
"chat_hopSingular": "хоп",
"chat_hopPlural": "хопов",
"chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
"chat_successes": "успешно",
"chat_removePath": "Удалить маршрут",
"chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.",
"chat_pathActions": "Действия с маршрутом:",
"chat_setCustomPath": "Указать маршрут вручную",
"chat_setCustomPathSubtitle": "Вручную задать маршрут передачи",
"chat_clearPath": "Очистить маршрут",
"chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке",
"chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.",
"chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения",
"chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.",
"chat_fullPath": "Полный маршрут",
"chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.",
"chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}",
"chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.",
"chat_pathDeviceConfirmed": "Подтверждено устройством.",
"chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.",
"chat_type": "Тип",
"chat_path": "Маршрут",
"chat_publicKey": "Публичный ключ",
"chat_compressOutgoingMessages": "Сжимать исходящие сообщения",
"chat_floodForced": "Рассылка (принудительно)",
"chat_directForced": "Прямой (принудительно)",
"chat_hopsForced": "{count} хоп(ов) (принудительно)",
"chat_floodAuto": "Рассылка (авто)",
"chat_direct": "Прямой",
"chat_poiShared": "Точка интереса отправлена",
"chat_unread": "Непрочитанных: {count}",
"map_title": "Карта нод",
"map_noNodesWithLocation": "Нет нод с данными о местоположении",
"map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
"map_nodesCount": "Нод: {count}",
"map_pinsCount": "Меток: {count}",
"map_chat": "Чат",
"map_repeater": "Репитер",
"map_room": "Комната",
"map_sensor": "Сенсор",
"map_pinDm": "Метка (ЛС)",
"map_pinPrivate": "Метка (Приватная)",
"map_pinPublic": "Метка (Публичная)",
"map_lastSeen": "Последнее появление",
"map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
"map_from": "От",
"map_source": "Источник",
"map_flags": "Флаги",
"map_shareMarkerHere": "Поделиться меткой здесь",
"map_pinLabel": "Метка",
"map_label": "Подпись",
"map_pointOfInterest": "Точка интереса",
"map_sendToContact": "Отправить контакту",
"map_sendToChannel": "Отправить в канал",
"map_noChannelsAvailable": "Нет доступных каналов",
"map_publicLocationShare": "Публичная передача местоположения",
"map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.",
"map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками",
"map_filterNodes": "Фильтр нод",
"map_nodeTypes": "Типы нод",
"map_chatNodes": "Чат-ноды",
"map_repeaters": "Репитеры",
"map_otherNodes": "Другие ноды",
"map_keyPrefix": "Префикс ключа",
"map_filterByKeyPrefix": "Фильтр по префиксу ключа",
"map_publicKeyPrefix": "Префикс публичного ключа",
"map_markers": "Метки",
"map_showSharedMarkers": "Показывать общие метки",
"map_lastSeenTime": "Время последнего появления",
"map_sharedPin": "Общая метка",
"map_joinRoom": "Присоединиться к комнате",
"map_manageRepeater": "Управление репитером",
"mapCache_title": "Кэш офлайн-карты",
"mapCache_selectAreaFirst": "Сначала выберите область для кэширования",
"mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области",
"mapCache_downloadTilesTitle": "Загрузить плитки",
"mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?",
"mapCache_downloadAction": "Загрузить",
"mapCache_cachedTiles": "Закэшировано {count} плиток",
"mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)",
"mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш",
"mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?",
"mapCache_offlineCacheCleared": "Офлайн-кэш очищен",
"mapCache_noAreaSelected": "Область не выбрана",
"mapCache_cacheArea": "Область кэширования",
"mapCache_useCurrentView": "Использовать текущий вид",
"mapCache_zoomRange": "Диапазон масштаба",
"mapCache_estimatedTiles": "Оценочное количество плиток: {count}",
"mapCache_downloadedTiles": "Загружено {completed} из {total}",
"mapCache_downloadTilesButton": "Загрузить плитки",
"mapCache_clearCacheButton": "Очистить кэш",
"mapCache_failedDownloads": "Неудачных загрузок: {count}",
"mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
"time_justNow": "Только что",
"time_minutesAgo": "{minutes} мин назад",
"time_hoursAgo": "{hours} ч назад",
"time_daysAgo": "{days} дн. назад",
"time_hour": "час",
"time_hours": "часов",
"time_day": "день",
"time_days": "дней",
"time_week": "неделя",
"time_weeks": "недель",
"time_month": "месяц",
"time_months": "месяцев",
"time_minutes": "минут",
"time_allTime": "Всё время",
"dialog_disconnect": "Отключиться",
"dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
"login_repeaterLogin": "Вход в репитер",
"login_roomLogin": "Вход на сервер комнат",
"login_password": "Пароль",
"login_enterPassword": "Введите пароль",
"login_savePassword": "Сохранить пароль",
"login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве",
"login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.",
"login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.",
"login_routing": "Маршрутизация",
"login_routingMode": "Режим маршрутизации",
"login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"login_forceFloodMode": "Принудительный режим рассылки",
"login_managePaths": "Управление маршрутами",
"login_login": "Войти",
"login_attempt": "Попытка {current}/{max}",
"login_failed": "Ошибка входа: {error}",
"login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.",
"common_reload": "Обновить",
"common_clear": "Очистить",
"path_currentPath": "Текущий маршрут: {path}",
"path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
"path_enterCustomPath": "Введите маршрут вручную",
"path_currentPathLabel": "Текущий маршрут",
"path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.",
"path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)",
"path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)",
"path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)",
"path_selectFromContacts": "Или выберите из контактов:",
"path_noRepeatersFound": "Репитеры или серверы комнат не найдены.",
"path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.",
"path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}",
"path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.",
"path_setPath": "Установить маршрут",
"repeater_management": "Управление репитером",
"room_management": "Управление сервером комнат",
"repeater_managementTools": "Инструменты управления",
"repeater_status": "Статус",
"repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера",
"repeater_telemetry": "Телеметрия",
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Отправка команд репитеру",
"repeater_neighbours": "Соседи",
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_settings": "Настройки",
"repeater_settingsSubtitle": "Настройка параметров репитера",
"repeater_statusTitle": "Статус репитера",
"repeater_routingMode": "Режим маршрутизации",
"repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"repeater_forceFloodMode": "Принудительный режим рассылки",
"repeater_pathManagement": "Управление маршрутами",
"repeater_refresh": "Обновить",
"repeater_statusRequestTimeout": "Время ожидания статуса истекло.",
"repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}",
"repeater_systemInformation": "Системная информация",
"repeater_battery": "Батарея",
"repeater_clockAtLogin": "Время (при входе)",
"repeater_uptime": "Время работы",
"repeater_queueLength": "Длина очереди",
"repeater_debugFlags": "Флаги отладки",
"repeater_radioStatistics": "Радиостатистика",
"repeater_lastRssi": "Последний RSSI",
"repeater_lastSnr": "Последний SNR",
"repeater_noiseFloor": "Уровень шума",
"repeater_txAirtime": "Время эфира (передача)",
"repeater_rxAirtime": "Время эфира (приём)",
"repeater_packetStatistics": "Статистика пакетов",
"repeater_sent": "Отправлено",
"repeater_received": "Получено",
"repeater_duplicates": "Дубликаты",
"repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с",
"repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}",
"repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}",
"repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}",
"repeater_duplicatesTotal": "Всего: {total}",
"repeater_settingsTitle": "Настройки репитера",
"repeater_basicSettings": "Основные настройки",
"repeater_repeaterName": "Имя репитера",
"repeater_repeaterNameHelper": "Отображаемое имя этого репитера",
"repeater_adminPassword": "Пароль администратора",
"repeater_adminPasswordHelper": "Пароль с полным доступом",
"repeater_guestPassword": "Гостевой пароль",
"repeater_guestPasswordHelper": "Пароль для доступа только для чтения",
"repeater_radioSettings": "Настройки радио",
"repeater_frequencyMhz": "Частота (МГц)",
"repeater_frequencyHelper": "3002500 МГц",
"repeater_txPower": "Мощность передачи",
"repeater_txPowerHelper": "130 дБм",
"repeater_bandwidth": "Полоса пропускания",
"repeater_spreadingFactor": "Коэффициент расширения",
"repeater_codingRate": "Коэффициент кодирования",
"repeater_locationSettings": "Настройки местоположения",
"repeater_latitude": "Широта",
"repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)",
"repeater_longitude": "Долгота",
"repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)",
"repeater_features": "Функции",
"repeater_packetForwarding": "Пересылка пакетов",
"repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты",
"repeater_guestAccess": "Гостевой доступ",
"repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения",
"repeater_privacyMode": "Режим конфиденциальности",
"repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях",
"repeater_advertisementSettings": "Настройки анонсирования",
"repeater_localAdvertInterval": "Интервал локальных анонсирований",
"repeater_localAdvertIntervalMinutes": "{minutes} минут",
"repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)",
"repeater_floodAdvertIntervalHours": "{hours} часов",
"repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований",
"repeater_dangerZone": "Опасная зона",
"repeater_rebootRepeater": "Перезагрузить репитер",
"repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера",
"repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?",
"repeater_regenerateIdentityKey": "Пересоздать ключ идентификации",
"repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей",
"repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?",
"repeater_eraseFileSystem": "Стереть файловую систему",
"repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера",
"repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!",
"repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.",
"repeater_commandSent": "Команда отправлена: {command}",
"repeater_errorSendingCommand": "Ошибка отправки команды: {error}",
"repeater_confirm": "Подтвердить",
"repeater_settingsSaved": "Настройки успешно сохранены",
"repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}",
"repeater_refreshBasicSettings": "Обновить основные настройки",
"repeater_refreshRadioSettings": "Обновить настройки радио",
"repeater_refreshTxPower": "Обновить мощность передачи",
"repeater_refreshLocationSettings": "Обновить настройки местоположения",
"repeater_refreshPacketForwarding": "Обновить пересылку пакетов",
"repeater_refreshGuestAccess": "Обновить гостевой доступ",
"repeater_refreshPrivacyMode": "Обновить режим конфиденциальности",
"repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований",
"repeater_refreshed": "{label} обновлён",
"repeater_errorRefreshing": "Ошибка обновления {label}",
"repeater_cliTitle": "CLI репитера",
"repeater_debugNextCommand": "Отладка следующей команды",
"repeater_commandHelp": "Справка по командам",
"repeater_clearHistory": "Очистить историю",
"repeater_noCommandsSent": "Команды ещё не отправлялись",
"repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды",
"repeater_enterCommandHint": "Введите команду...",
"repeater_previousCommand": "Предыдущая команда",
"repeater_nextCommand": "Следующая команда",
"repeater_enterCommandFirst": "Сначала введите команду",
"repeater_cliCommandFrameTitle": "Фрейм CLI-команды",
"repeater_cliCommandError": "Ошибка: {error}",
"repeater_cliQuickGetName": "Получить имя",
"repeater_cliQuickGetRadio": "Получить радио",
"repeater_cliQuickGetTx": "Получить TX",
"repeater_cliQuickNeighbors": "Соседи",
"repeater_cliQuickVersion": "Версия",
"repeater_cliQuickAdvertise": "Анонсировать",
"repeater_cliQuickClock": "Время",
"repeater_cliHelpAdvert": "Отправляет пакет анонсирования",
"repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)",
"repeater_cliHelpClock": "Показывает текущее время по часам устройства.",
"repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.",
"repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.",
"repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.",
"repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.",
"repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)",
"repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.",
"repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)",
"repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)",
"repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.",
"repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.",
"repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».",
"repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.",
"repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.",
"repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)",
"repeater_cliHelpSetName": "Устанавливает имя в оповещениях.",
"repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)",
"repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)",
"repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.",
"repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.",
"repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).",
"repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.",
"repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.",
"repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.",
"repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.",
"repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.",
"repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.",
"repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).",
"repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).",
"repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)",
"repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow",
"repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.",
"repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.",
"repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.",
"repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4",
"repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.",
"repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.",
"repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.",
"repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»",
"repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.",
"repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)",
"repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)",
"repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)",
"repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)",
"repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.",
"repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.",
"repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.",
"repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.",
"repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.",
"repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.",
"repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек",
"repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.",
"repeater_commandsListTitle": "Список команд",
"repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».",
"repeater_general": "Общие",
"repeater_settingsCategory": "Настройки",
"repeater_bridge": "Мост",
"repeater_logging": "Журналирование",
"repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)",
"repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)",
"repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.",
"repeater_gpsManagement": "Управление GPS",
"repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.",
"telemetry_receivedData": "Полученные телеметрические данные",
"telemetry_requestTimeout": "Время ожидания телеметрии истекло.",
"telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}",
"telemetry_noData": "Данные телеметрии недоступны.",
"telemetry_channelTitle": "Канал {channel}",
"telemetry_batteryLabel": "Батарея",
"telemetry_voltageLabel": "Напряжение",
"telemetry_mcuTemperatureLabel": "Температура МК",
"telemetry_temperatureLabel": "Температура",
"telemetry_currentLabel": "Ток",
"telemetry_batteryValue": "{percent}% / {volts}В",
"telemetry_voltageValue": "{volts}В",
"telemetry_currentValue": "{amps}А",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
"neighbors_repeatersNeighbours": "Соседи репитеров",
"neighbors_noData": "Данные о соседях недоступны.",
"neighbors_unknownContact": "Неизвестный {pubkey}",
"neighbors_heardA ago": "Слышали: {time} назад",
"channelPath_title": "Путь пакета",
"channelPath_viewMap": "Посмотреть на карте",
"channelPath_otherObservedPaths": "Другие наблюдаемые пути",
"channelPath_repeaterHops": "Хопы через репитеры",
"channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.",
"channelPath_messageDetails": "Детали сообщения",
"channelPath_senderLabel": "Отправитель",
"channelPath_timeLabel": "Время",
"channelPath_repeatsLabel": "Повторы",
"channelPath_pathLabel": "Путь {index}",
"channelPath_observedLabel": "Наблюдаемый",
"channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}",
"channelPath_noLocationData": "Нет данных о местоположении",
"channelPath_timeWithDate": "{day}/{month} {time}",
"channelPath_timeOnly": "{time}",
"channelPath_unknownPath": "Неизвестный",
"channelPath_floodPath": "Рассылка",
"channelPath_directPath": "Прямой",
"channelPath_observedZeroOf": "0 из {total} хопов",
"channelPath_observedSomeOf": "{observed} из {total} хопов",
"channelPath_mapTitle": "Карта пути",
"channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.",
"channelPath_primaryPath": "Путь {index} (Основной)",
"channelPath_pathLabelTitle": "Путь",
"channelPath_observedPathHeader": "Наблюдаемый путь",
"channelPath_selectedPathLabel": "{label} • {prefixes}",
"channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.",
"channelPath_unknownRepeater": "Неизвестный репитер",
"community_title": "Сообщество",
"community_create": "Создать сообщество",
"community_createDesc": "Создать новое сообщество и поделиться через QR-код.",
"community_join": "Присоединиться",
"community_joinTitle": "Присоединиться к сообществу",
"community_joinConfirmation": "Вы хотите присоединиться к сообществу \"{name}\"?",
"community_scanQr": "Сканировать QR-код сообщества",
"community_scanInstructions": "Наведите камеру на QR-код сообщества",
"community_showQr": "Показать QR-код",
"community_publicChannel": "Публичный канал сообщества",
"community_hashtagChannel": "Хэштег-канал сообщества",
"community_name": "Имя сообщества",
"community_enterName": "Введите имя сообщества",
"community_created": "Сообщество \"{name}\" создано",
"community_joined": "Присоединились к сообществу \"{name}\"",
"community_qrTitle": "Поделиться сообществом",
"community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"",
"community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам",
"community_invalidQrCode": "Недопустимый QR-код сообщества",
"community_alreadyMember": "Уже участник",
"community_alreadyMemberMessage": "Вы уже участник сообщества \"{name}\".",
"community_addPublicChannel": "Добавить публичный канал сообщества",
"community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества",
"community_noCommunities": "Вы ещё не присоединились ни к одному сообществу",
"community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать",
"community_manageCommunities": "Управление сообществами",
"community_delete": "Покинуть сообщество",
"community_deleteConfirm": "Покинуть \"{name}\"?",
"community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.",
"community_deleted": "Покинули сообщество \"{name}\"",
"community_regenerateSecret": "Пересоздать секрет",
"community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.",
"community_regenerate": "Пересоздать",
"community_secretRegenerated": "Секрет пересоздан для \"{name}\"",
"community_updateSecret": "Обновить секрет",
"community_secretUpdated": "Секрет обновлён для \"{name}\"",
"community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"",
"community_addHashtagChannel": "Добавить хэштег-канал сообщества",
"community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества",
"community_selectCommunity": "Выбрать сообщество",
"community_regularHashtag": "Обычный хэштег",
"community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)",
"community_communityHashtag": "Хэштег сообщества",
"community_communityHashtagDesc": "Доступен только участникам сообщества",
"community_forCommunity": "Для {name}",
"listFilter_tooltip": "Фильтр и сортировка",
"listFilter_sortBy": "Сортировка по",
"listFilter_latestMessages": "Последние сообщения",
"listFilter_heardRecently": "Слышали недавно",
"listFilter_az": "По алфавиту",
"listFilter_filters": "Фильтры",
"listFilter_all": "Все",
"listFilter_users": "Пользователи",
"listFilter_repeaters": "Репитеры",
"listFilter_roomServers": "Серверы комнат",
"listFilter_unreadOnly": "Только непрочитанные",
"listFilter_newGroup": "Новая группа",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"chat_open": "Открыть",
"chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}",
"chat_openLink": "Открыть ссылку?",
"chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?",
"neighbors_heardAgo": "Слушал(а): {time} назад",
"chat_invalidLink": "Неправильный формат ссылки",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Вы",
"pathTrace_failed": "Путь трассировки не выполнен.",
"pathTrace_notAvailable": "Трассировка пути недоступна.",
"pathTrace_refreshTooltip": "Обновить Path Trace",
"contacts_pathTrace": "Трассировка пути",
"contacts_ping": "Пинговать",
"contacts_repeaterPathTrace": "Отследить путь к ретранслятору",
"contacts_repeaterPing": "Пинговать повторитель",
"contacts_roomPathTrace": "Трассировка пути к серверу комнаты",
"contacts_roomPing": "Пинговать сервер комнаты",
"contacts_chatTraceRoute": "Трассировка маршрута",
"contacts_pathTraceTo": "Показать маршрут к {name}"
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Otvoriť odkaz?",
"chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?",
"chat_open": "Otvoriť",
"chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Neplatný formát odkazu",
"map_title": "Mapa uzlov",
"map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe",
"map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
"community_communityHashtag": "Komunitný Hashtag",
"community_communityHashtagDesc": "Špecifické pre členov komunity",
"community_forCommunity": "Pre {name}"
"community_forCommunity": "Pre {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne",
"community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.",
"community_regenerate": "Znovu vygenerovať",
"community_regenerateSecret": "Zobraziť nový tajný kód",
"community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
"community_updateSecret": "Aktualizovať tajné heslo",
"community_secretUpdated": "Zmena tajnej slova pre \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Vy",
"pathTrace_failed": "Sledovanie cesty zlyhalo.",
"pathTrace_notAvailable": "Path trace nie je k dispozícii.",
"pathTrace_refreshTooltip": "Obnoviť Path Trace.",
"contacts_pathTrace": "Sledovanie lúčov",
"contacts_ping": "Pingovať",
"contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču",
"contacts_repeaterPing": "Pingovať opakovač",
"contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti",
"contacts_roomPing": "Ping server miestnosti",
"contacts_chatTraceRoute": "Sledovať trasu lúča",
"contacts_pathTraceTo": "Sledovať trasu k {name}"
}
+208 -140
View File
@@ -1,7 +1,7 @@
{
"@@locale": "sl",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakti",
"nav_contacts": "Stiki",
"nav_channels": "Kanali",
"nav_map": "Karta",
"common_cancel": "Prekliči",
@@ -69,49 +69,49 @@
},
"scanner_stop": "Prekliči",
"scanner_scan": "Skeniraj",
"device_quickSwitch": "Hitro preklopiti",
"device_quickSwitch": "Hitro preklop",
"device_meshcore": "MeshCore",
"settings_title": "Nastavitve",
"settings_deviceInfo": "Informacije o napravei",
"settings_appSettings": "Nastavitve aplikacije",
"settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.",
"settings_nodeSettings": "Nastavitve časa",
"settings_nodeName": "Ime omrežno mesto",
"settings_nodeNameNotSet": "Nezavedeno",
"settings_nodeNameHint": "Vnesite ime časa",
"settings_nodeSettings": "Nastavitev časa",
"settings_nodeName": "Ime node-a",
"settings_nodeNameNotSet": "Ni nastavljeno",
"settings_nodeNameHint": "Vnesite ime node-a",
"settings_nodeNameUpdated": "Ime posodobljeno",
"settings_radioSettings": "Nastavitve radija",
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširni faktor",
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor",
"settings_radioSettingsUpdated": "Radio nastavitve posodobljene",
"settings_location": "Lokacija",
"settings_locationSubtitle": "GPS koordinate",
"settings_locationUpdated": "Lokacija posodobljena",
"settings_locationBothRequired": "Vnesite širino in dolžino.",
"settings_locationInvalid": "Neveljna zemeljska širina ali dolžina.",
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
"settings_latitude": "Širina",
"settings_longitude": "Dolžina",
"settings_privacyMode": "Mod podjetja",
"settings_privacyMode": "Zasebnost",
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
"settings_privacyModeEnabled": "Privatni režim je omogočen.",
"settings_privacyModeDisabled": "Privatni režim je onemogočen.",
"settings_privacyModeEnabled": "Privatni način je omogočen.",
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
"settings_actions": "Akcije",
"settings_sendAdvertisement": "Pošlji Oglas",
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
"settings_advertisementSent": "Oglas poslan",
"settings_syncTime": "Ugasniti čas",
"settings_syncTimeSubtitle": "Nastavi uro naprave v čas telefona",
"settings_timeSynchronized": "Sinhronizirano po času",
"settings_syncTime": "Nastavi uro",
"settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona",
"settings_timeSynchronized": "Ura sinhronizirana",
"settings_refreshContacts": "Ponovno obišči kontakte",
"settings_refreshContactsSubtitle": "Ponovno naloži seznam kontaktov iz naprave",
"settings_rebootDevice": "Restart Naprave",
"settings_rebootDeviceSubtitle": "Ponovite zažetek naprave MeshCore",
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.",
"settings_debug": "Napravi popravek",
"settings_bleDebugLog": "Logarjev zapis BLE",
"settings_bleDebugLogSubtitle": "Navodila BLE, odgovori in surovo podatkovno",
"settings_appDebugLog": "Log zapiske aplikacije",
"settings_appDebugLogSubtitle": "Prijavni sporočila aplikacije",
"settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi",
"settings_rebootDevice": "Ponovni zagon naprave",
"settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo",
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.",
"settings_debug": "Debug",
"settings_bleDebugLog": "BLE debug log (razhroščevanje)",
"settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki",
"settings_appDebugLog": "Logi aplikacije",
"settings_appDebugLogSubtitle": "Debug sporočila aplikacije",
"settings_about": "Oglejte si",
"settings_aboutVersion": "MeshCore Open v{version}",
"@settings_aboutVersion": {
@@ -121,14 +121,14 @@
}
}
},
"settings_aboutLegalese": "MeshCore Odprtokodni Projekt 2024",
"settings_aboutDescription": "Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.",
"settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024",
"settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.",
"settings_infoName": "Ime",
"settings_infoId": "ID",
"settings_infoStatus": "Status",
"settings_infoBattery": "Baterija",
"settings_infoPublicKey": "Ključ javnega tipa",
"settings_infoContactsCount": "Število kontaktov",
"settings_infoPublicKey": "Javni ključ",
"settings_infoContactsCount": "Število stikov",
"settings_infoChannelCount": "Število kanalov",
"settings_presets": "Prednastavitve",
"settings_preset915Mhz": "915 MHz",
@@ -136,15 +136,15 @@
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvenca (MHz)",
"settings_frequencyHelper": "300,00 - 2500,00",
"settings_frequencyInvalid": "Neveljčna frekvenca (300-2500 MHz)",
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
"settings_bandwidth": "Pasovna širina",
"settings_spreadingFactor": "Razširitveni faktor",
"settings_codingRate": "Programska hitrost",
"settings_txPower": "TX Moč (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neveljaven TX moč (0-22 dBm)",
"settings_longRange": "Dolenje območje",
"settings_fastSpeed": "Hitra hitrost",
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
"settings_longRange": "DDolg doseg",
"settings_fastSpeed": "Visoka hitrost",
"settings_error": "Napaka: {message}",
"@settings_error": {
"placeholders": {
@@ -156,8 +156,8 @@
"appSettings_title": "Nastavitve aplikacije",
"appSettings_appearance": "Prikaži",
"appSettings_theme": "Tema",
"appSettings_themeSystem": "Predpomnilnik sistema",
"appSettings_themeLight": "Luč",
"appSettings_themeSystem": "Sistemska tema",
"appSettings_themeLight": "Svetlo",
"appSettings_themeDark": "Temno",
"appSettings_language": "Jezik",
"appSettings_languageSystem": "Sistemska privzeta vrednost",
@@ -174,8 +174,8 @@
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_notifications": "Obvestila",
"appSettings_enableNotifications": "Omogoči obvestila",
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena",
@@ -185,19 +185,19 @@
"appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala",
"appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala",
"appSettings_advertisementNotifications": "Opozorila o oglasih",
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so novi vozlišči odkrivljeni.",
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.",
"appSettings_messaging": "Komuniciranje",
"appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju",
"appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja",
"appSettings_pathsWillBeCleared": "Potnice bodo očiščene po 5 neuspešnih poskusih.",
"appSettings_pathsWillNotBeCleared": "Potniški poti ne bodo samodejno čiščeni.",
"appSettings_autoRouteRotation": "Avtomatsko Občutke in Rotacije",
"appSettings_autoRouteRotationSubtitle": "Med spreminjanjem med najboljšimi potmi in plovilnim načinom",
"appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.",
"appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.",
"appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti",
"appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom",
"appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno",
"appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno",
"appSettings_battery": "Baterija",
"appSettings_batteryChemistry": "Razem z možnostmi",
"appSettings_batteryChemistryPerDevice": "Nastavitve za naprave ({deviceName})",
"appSettings_batteryChemistry": "Kemija baterije",
"appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})",
"@appSettings_batteryChemistryPerDevice": {
"placeholders": {
"deviceName": {
@@ -205,20 +205,20 @@
}
}
},
"appSettings_batteryChemistryConnectFirst": "Povežite se z napravo za izbiro",
"appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo",
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
"appSettings_batteryLifepo4": "LiFePO4 (2,63,65 V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Prikaz zemljevide",
"appSettings_showRepeaters": "Prikaži ponovitve",
"appSettings_showRepeatersSubtitle": "Prikaži ponovljalne notranjosti na zemljeploscu",
"appSettings_showChatNodes": "Prikaži čakalne notranjosti",
"appSettings_showChatNodesSubtitle": "Prikaži pogovorni pike na zemljeploscu",
"appSettings_showOtherNodes": "Pokaži druge vozlišča",
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste notranjih elementov na zemljevalu.",
"appSettings_timeFilter": "Filtri po času",
"appSettings_timeFilterShowAll": "Pokaži vse notranje elemente",
"appSettings_timeFilterShowLast": "Pokaži notranjosti iz zadnjih {hours} ur",
"appSettings_mapDisplay": "Prikaz zemljevida",
"appSettings_showRepeaters": "Prikaži repetitorje",
"appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi",
"appSettings_showChatNodes": "Prikaži naprave za klepet",
"appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu",
"appSettings_showOtherNodes": "Pokaži druge naprave",
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.",
"appSettings_timeFilter": "Filter po času",
"appSettings_timeFilterShowAll": "Pokaži vse naprave",
"appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah",
"@appSettings_timeFilterShowLast": {
"placeholders": {
"hours": {
@@ -226,15 +226,15 @@
}
}
},
"appSettings_mapTimeFilter": "Filtri časa zemljevida",
"appSettings_showNodesDiscoveredWithin": "Pokaži notranje čepke, odkrivene v:",
"appSettings_allTime": "Vse čase",
"appSettings_lastHour": "Minuto nazaj",
"appSettings_mapTimeFilter": "Filter časa na zemljevidu",
"appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:",
"appSettings_allTime": "Brez omejitev",
"appSettings_lastHour": "V zadnji uri",
"appSettings_last6Hours": "Zadnjih 6 ur",
"appSettings_last24Hours": "Zadnjih 24 ur",
"appSettings_lastWeek": "Lepošno",
"appSettings_offlineMapCache": "Omrezni Poudni Arhiv",
"appSettings_noAreaSelected": "Nizkana označena površina",
"appSettings_lastWeek": "Prejšnji teden",
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
"appSettings_noAreaSelected": "Območje ni izbrano",
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
"@appSettings_areaSelectedZoom": {
"placeholders": {
@@ -246,19 +246,19 @@
}
}
},
"appSettings_debugCard": "Napravi popravek",
"appSettings_appDebugLogging": "Programski Log",
"appSettings_appDebugLoggingSubtitle": "Log aplikacijske debug sporočila za odpravljanje težav",
"appSettings_appDebugLoggingEnabled": "Omogočeno zaznamovanje napak v aplikaciji",
"appSettings_appDebugLoggingDisabled": "Programski logi aplikacije so onemogočeni.",
"contacts_title": "Kontakti",
"contacts_noContacts": "Še ni kontaktov.",
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"appSettings_debugCard": "Razhroščevanje",
"appSettings_appDebugLogging": "Programski dnevnik",
"appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav",
"appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno",
"appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.",
"contacts_title": "Stiki",
"contacts_noContacts": "Ni stikov.",
"contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.",
"contacts_searchContacts": "Iskanje stikov...",
"contacts_noUnreadContacts": "Ne prebrani stiki.",
"contacts_noContactsFound": "Stiki niso najdeni.",
"contacts_deleteContact": "Izbriši stik",
"contacts_removeConfirm": "Izbrišem {contactName} iz stikov?",
"@contacts_removeConfirm": {
"placeholders": {
"contactName": {
@@ -266,12 +266,12 @@
}
}
},
"contacts_manageRepeater": "Upravljajte Ponovitve",
"contacts_roomLogin": "Vnos v sobo",
"contacts_openChat": "Odprta kleta",
"contacts_editGroup": "Uredi Skupino",
"contacts_deleteGroup": "Izbrisati Skupino",
"contacts_deleteGroupConfirm": "Odpovedati {groupName}?",
"contacts_manageRepeater": "Upravljaj Ponovitve",
"contacts_roomLogin": "Prijava v sobo",
"contacts_openChat": "Odpri klepet",
"contacts_editGroup": "Uredi skupino",
"contacts_deleteGroup": "Izbriši skupino",
"contacts_deleteGroupConfirm": "Izbriši {groupName}?",
"@contacts_deleteGroupConfirm": {
"placeholders": {
"groupName": {
@@ -279,8 +279,8 @@
}
}
},
"contacts_newGroup": "Novo skupino",
"contacts_groupName": "Skupina imena",
"contacts_newGroup": "Nova skupina",
"contacts_groupName": "Ime skupine",
"contacts_groupNameRequired": "Ime skupine je obvezno.",
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
"@contacts_groupAlreadyExists": {
@@ -290,11 +290,11 @@
}
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Nič članov.",
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
"contacts_filterContacts": "Filtriraj stik\\,...",
"contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Ni članov.",
"contacts_lastSeenNow": "Nazadnje viden zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -302,8 +302,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Zadnjič ogledan pred 1 uro.",
"contacts_lastSeenHoursAgo": "Zadnjič videti {hours} ur nazaj",
"contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.",
"contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -311,8 +311,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Zadnjič ogledan pred 1 dnem",
"contacts_lastSeenDaysAgo": "Zadnjič videti {days} dni nazaj",
"contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem",
"contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -321,10 +321,10 @@
}
},
"channels_title": "Kanali",
"channels_noChannelsConfigured": "Nekonfigurirane kanale",
"channels_addPublicChannel": "Dodaj Objavni Kanal",
"channels_noChannelsConfigured": "Kanali še niso konfigurirani",
"channels_addPublicChannel": "Dodaj javni kanal",
"channels_searchChannels": "Poišči kanale...",
"channels_noChannelsFound": "Niti kanalov najti ni.",
"channels_noChannelsFound": "Ne najdem kanalov.",
"channels_channelIndex": "Kanal {index}",
"@channels_channelIndex": {
"placeholders": {
@@ -334,13 +334,13 @@
}
},
"channels_hashtagChannel": "Hashtag kanal",
"channels_public": "javno",
"channels_private": "Zasebno",
"channels_publicChannel": "Ogljišna skupina",
"channels_privateChannel": "Zatemniščen kanal",
"channels_public": "Javni",
"channels_private": "Zasebni",
"channels_publicChannel": "Javni kanal",
"channels_privateChannel": "Zasebni kanal",
"channels_editChannel": "Uredi kanal",
"channels_deleteChannel": "Pošlji kanal",
"channels_deleteChannelConfirm": "Izbrisati \"{name}\"? To se ne da povrniti.",
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
"@channels_deleteChannelConfirm": {
"placeholders": {
"name": {
@@ -424,8 +424,8 @@
}
}
},
"chat_typeMessage": "Vnesite sporočilo...",
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} bajt).",
"chat_typeMessage": "Vnesi sporočilo...",
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).",
"@chat_messageTooLong": {
"placeholders": {
"maxBytes": {
@@ -433,9 +433,9 @@
}
}
},
"chat_messageCopied": "Pošljeno sporočilo",
"chat_messageDeleted": "Pošiljanje sporočila izbrisano",
"chat_retryingMessage": "Ponovna poskus.",
"chat_messageCopied": "Sporočilo poslano",
"chat_messageDeleted": "Sporočilo izbrisano",
"chat_retryingMessage": "Ponovni poskus.",
"chat_retryCount": "Ponovit {current}/{max}",
"@chat_retryCount": {
"placeholders": {
@@ -448,31 +448,31 @@
}
},
"chat_sendGif": "Pošlji GIF",
"chat_reply": "Odpošlji",
"chat_addReaction": "Dodaj Reakcijo",
"chat_reply": "Odgovori",
"chat_addReaction": "Dodaj reakcijo",
"chat_me": "jaz",
"emojiCategorySmileys": "Emoji",
"emojiCategoryGestures": "Gestikulacije",
"emojiCategoryHearts": "Srce",
"emojiCategoryObjects": "Predmeti",
"gifPicker_title": "Izberi GIF",
"gifPicker_searchHint": "Iskalite GIF-e...",
"gifPicker_poweredBy": "Naprodno z GIPHY",
"gifPicker_noGifsFound": "Niti GIF-jev najti ni.",
"gifPicker_failedLoad": "Neuspešno je naložilo GIF-e",
"gifPicker_failedSearch": "Posodobit neuspešno.",
"gifPicker_searchHint": "Išči GIF-e...",
"gifPicker_poweredBy": "Napredno z GIPHY",
"gifPicker_noGifsFound": "Ne najdem GIF-ov.",
"gifPicker_failedLoad": "Neuspešno nalaganje GIF-a",
"gifPicker_failedSearch": "Iskanje neuspešno.",
"gifPicker_noInternet": "Ni internetne povezave",
"debugLog_appTitle": "Log zapiske aplikacije",
"debugLog_bleTitle": "Logarjev zapis BLE",
"debugLog_copyLog": "Kopiraj zapiske",
"debugLog_clearLog": "Pasters log",
"debugLog_copied": "Kopirana belež poteka.",
"debugLog_bleCopied": "Kopirana beležke iz BLE",
"debugLog_noEntries": "Še ni ustvarjenih debug zapisov.",
"debugLog_enableInSettings": "Omogoči beleženje napak v aplikaciji v nastavitvah",
"debugLog_frames": "Okna",
"debugLog_bleTitle": "Log zapis BLE",
"debugLog_copyLog": "Kopiraj dnevnik",
"debugLog_clearLog": "Briši log",
"debugLog_copied": "Beležka kopirana.",
"debugLog_bleCopied": "Kopirana beležka iz BLE",
"debugLog_noEntries": "Ni ustvarjenih debug zapisov.",
"debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije",
"debugLog_frames": "Okvirji",
"debugLog_rawLogRx": "Svež Log-RX",
"debugLog_noBleActivity": "Šele začnite z aktivnostjo BLE.",
"debugLog_noBleActivity": "Ni BLE aktivnosti.",
"debugFrame_length": "Izhodni rob: {count} bajtov",
"@debugFrame_length": {
"placeholders": {
@@ -542,8 +542,8 @@
"chat_forceFloodMode": "Nasilje obvezati v način",
"chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):",
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
"chat_hopSingular": "skoč",
"chat_hopPlural": "škrabec",
"chat_hopSingular": "skok",
"chat_hopPlural": "skokov",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"@chat_hopsCount": {
"placeholders": {
@@ -554,16 +554,16 @@
},
"chat_successes": "Uspešni",
"chat_removePath": "Izbriši pot",
"chat_noPathHistoryYet": "Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
"chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
"chat_pathActions": "Potni ukazi:",
"chat_setCustomPath": "Nastavi Prilozeno Pot",
"chat_setCustomPathSubtitle": "Ročno določite potniško pot.",
"chat_clearPath": "Čista pot",
"chat_clearPath": "Počisti pot",
"chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.",
"chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.",
"chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.",
"chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.",
"chat_fullPath": "Polni pot",
"chat_fullPath": "Polna pot",
"chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.",
"chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
"@chat_pathSetHops": {
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Odpreti povezavo?",
"chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?",
"chat_open": "Odpri",
"chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Neveljavna oblika povezave",
"map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
@@ -1092,13 +1104,13 @@
}
}
},
"repeater_cliQuickGetName": "Dobiti ime",
"repeater_cliQuickGetName": "Pridobi ime",
"repeater_cliQuickGetRadio": "Dobiti Radiopravo",
"repeater_cliQuickGetTx": "Dobiti TX",
"repeater_cliQuickGetTx": "Pridobi TX",
"repeater_cliQuickNeighbors": "Sosedi",
"repeater_cliQuickVersion": "Različica",
"repeater_cliQuickAdvertise": "Oglasite",
"repeater_cliQuickClock": "Urnik",
"repeater_cliQuickClock": "Ura",
"repeater_cliHelpAdvert": "Pošlje paket oglasov",
"repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)",
"repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.",
@@ -1130,7 +1142,7 @@
"repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.",
"repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).",
"repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).",
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
"repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow",
"repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.",
"repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.",
@@ -1159,8 +1171,8 @@
"repeater_settingsCategory": "Nastavitve",
"repeater_bridge": "Most",
"repeater_logging": "Logiranje",
"repeater_neighborsRepeaterOnly": "Sosedi (le za ponovitelja)",
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za ponovitve)",
"repeater_neighborsRepeaterOnly": "Sosedi (le za repetitorje)",
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)",
"repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.",
"repeater_gpsManagement": "Upravljanje GPS",
"repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.",
@@ -1232,9 +1244,9 @@
"channelPath_repeaterHops": "Skoki ponovitelja",
"channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.",
"channelPath_messageDetails": "Podrobnosti sporočila",
"channelPath_senderLabel": "Pošiljalec",
"channelPath_timeLabel": "Čas",
"channelPath_repeatsLabel": "Ponovi",
"channelPath_senderLabel": "Pošiljatelj",
"channelPath_timeLabel": "Ura",
"channelPath_repeatsLabel": "Ponovitve",
"channelPath_pathLabel": "Pot {index}",
"channelPath_observedLabel": "Opazovani",
"channelPath_observedPathTitle": "Opazovana pot {index} • {hops}",
@@ -1466,23 +1478,79 @@
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
"community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
"community_manageCommunities": "Upravljajte skupnosti",
"community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.",
"community_manageCommunities": "Upravljanje skupnosti",
"community_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapustiti \"{name}\"?",
"community_deleteConfirm": "Zapusti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
"community_addHashtagChannel": "Dodaj hashtag kanal",
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
"community_selectCommunity": "Izberi skupnost",
"community_regularHashtag": "Oznaka s hashtagom",
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
"community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"community_forCommunity": "Za {name}"
"community_forCommunity": "Za {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno",
"community_regenerateSecret": "Ponovno ustvari geslo",
"community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
"community_regenerate": "Preberi znova",
"community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}",
"community_updateSecret": "Ažuriraj ključ",
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Ti",
"pathTrace_failed": "Sledenje poti ni uspelo.",
"pathTrace_notAvailable": "Potni sled ni na voljo.",
"pathTrace_refreshTooltip": "Osveži Path Trace.",
"contacts_pathTrace": "Sledenje poti",
"contacts_ping": "Pingati",
"contacts_repeaterPathTrace": "Sledi poti do ponavljalnika",
"contacts_repeaterPing": "Pinguj ponavljalnik",
"contacts_roomPathTrace": "Sledenje poti do strežnika sobe",
"contacts_roomPing": "Ping strežnik sobe",
"contacts_chatTraceRoute": "Slediti poti žarkov",
"contacts_pathTraceTo": "Trace route to {name}"
}
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "Öppna länk?",
"chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?",
"chat_open": "Öppna",
"chat_couldNotOpenLink": "Kunde inte öppna länken: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ogiltigt länkformat",
"map_title": "Nodkarta",
"map_noNodesWithLocation": "Inga noder med platsinformation",
"map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Lämnade community \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"community_communityHashtag": "Community Hashtag"
"community_communityHashtag": "Community Hashtag",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerate": "Regenerera",
"community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.",
"community_secretRegenerated": "Lösenord återskapad för \"{name}\"",
"community_regenerateSecret": "Regenerera hemlig kod",
"community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
"community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
"community_updateSecret": "Uppdatera hemlighet",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Du",
"pathTrace_failed": "Sökvägsföljning misslyckades.",
"pathTrace_notAvailable": "Path trace ej tillgänglig.",
"pathTrace_refreshTooltip": "Uppdatera Path Trace",
"contacts_pathTrace": "Path Trace",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Vägspårning till repeater",
"contacts_repeaterPing": "Ping-repeater",
"contacts_roomPathTrace": "Vägspårning till rumserver",
"contacts_roomPing": "Ping rumsserver",
"contacts_chatTraceRoute": "Spåra rutt",
"contacts_pathTraceTo": "Spåra rutt till {name}"
}
+1557
View File
File diff suppressed because it is too large Load Diff
+70 -2
View File
@@ -604,6 +604,18 @@
}
}
},
"chat_openLink": "打开链接?",
"chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?",
"chat_open": "打开",
"chat_couldNotOpenLink": "无法打开链接:{url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "链接格式无效",
"map_title": "节点地图",
"map_noNodesWithLocation": "没有具有位置数据的节点",
"map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "已退出社区 \"{name}\"",
@@ -1484,5 +1498,59 @@
"community_regularHashtagDesc": "公共话题(任何人都可以加入)",
"community_communityHashtag": "社区标签",
"community_communityHashtagDesc": "仅限社区成员使用",
"community_forCommunity": "对于 {name}"
"community_forCommunity": "对于 {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecret": "重新生成密钥",
"community_secretRegenerated": "密码已重置为“{name}”",
"community_regenerate": "重新生成",
"community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。",
"community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码",
"community_updateSecret": "更新密钥",
"community_secretUpdated": "密码已更新为“{name}”",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "你",
"pathTrace_failed": "路径追踪失败。",
"pathTrace_notAvailable": "路径追踪不可用",
"pathTrace_refreshTooltip": "刷新路径追踪",
"contacts_pathTrace": "路径追踪",
"contacts_ping": "ping",
"contacts_repeaterPathTrace": "路径追踪到中继器",
"contacts_repeaterPing": "Ping 中继器",
"contacts_roomPathTrace": "路径追踪至房间服务器",
"contacts_roomPing": "Ping 房间服务器",
"contacts_chatTraceRoute": "路径追踪",
"contacts_pathTraceTo": "追踪路由到 {name}"
}
+40
View File
@@ -102,6 +102,46 @@ class Contact {
return parts.join(',');
}
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
Uint8List? get traceRouteBytes {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if(pathLength <= 0) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
}
if(type == advTypeRepeater || type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = publicKey[0];
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len-1-i] = pathBytes[i];
}
}
} else {
if(pathBytes.length < 2) {
return pathBytes[0] == 0 ? null : pathBytes;
}
final len = (pathBytes.length + pathBytes.length-1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length-1) {
traceBytes[len-1-i] = pathBytes[i];
}
}
}
return traceBytes;
}
Uint8List get _pathBytesForDisplay {
if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0);
+12
View File
@@ -471,6 +471,10 @@ class AppSettingsScreen extends StatelessWidget {
return context.l10n.appSettings_languageSk;
case 'bg':
return context.l10n.appSettings_languageBg;
case 'ru':
return context.l10n.appSettings_languageRu;
case 'uk':
return context.l10n.appSettings_languageUk;
default:
return context.l10n.appSettings_languageSystem;
}
@@ -547,6 +551,14 @@ class AppSettingsScreen extends StatelessWidget {
title: Text(context.l10n.appSettings_languageBg),
value: 'bg',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageRu),
value: 'ru',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageUk),
value: 'uk',
),
],
),
),
+184 -105
View File
@@ -3,11 +3,15 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/link_handler.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
@@ -15,6 +19,7 @@ import '../models/channel_message.dart';
import '../utils/emoji_utils.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -33,42 +38,51 @@ class ChannelChatScreen extends StatefulWidget {
class _ChannelChatScreenState extends State<ChannelChatScreen> {
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final ChatScrollController _scrollController = ChatScrollController();
final FocusNode _textFieldFocusNode = FocusNode();
ChannelMessage? _replyingToMessage;
final Map<String, GlobalKey> _messageKeys = {};
bool _isLoadingOlder = false;
@override
void initState() {
super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<MeshCoreConnector>().setActiveChannel(widget.channel.index);
// Scroll to bottom when opening channel chat - use SchedulerBinding for next frame
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
}
}
Future<void> _loadOlderMessages() async {
if (_isLoadingOlder) return;
setState(() => _isLoadingOlder = true);
final connector = context.read<MeshCoreConnector>();
await connector.loadOlderChannelMessages(widget.channel.index);
if (mounted) {
setState(() => _isLoadingOlder = false);
}
}
@override
void dispose() {
context.read<MeshCoreConnector>().setActiveChannel(null);
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
_textFieldFocusNode.dispose();
_textController.dispose();
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void _setReplyingTo(ChannelMessage message) {
setState(() {
_replyingToMessage = message;
@@ -153,10 +167,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, connector, child) {
final messages = connector.getChannelMessages(widget.channel);
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
if (messages.isEmpty) {
return Center(
child: Column(
@@ -190,20 +200,51 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.scrollToBottomIfAtBottom();
});
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),
),
),
);
}
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,
),
],
);
},
),
@@ -241,7 +282,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
@@ -255,15 +298,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
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,
),
),
),
const SizedBox(height: 4),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
@@ -272,60 +320,84 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
else if (gifId != null)
GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
fallbackTextColor: isOutgoing
? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
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),
),
)
else
Text(
message.text,
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 (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'via ${_formatPathPrefixes(displayPath)}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
Padding(
padding: gifId != null
? const EdgeInsets.symmetric(horizontal: 8)
: EdgeInsets.zero,
child: Text(
'via ${_formatPathPrefixes(displayPath)}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
),
],
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
if (message.repeatCount > 0) ...[
const SizedBox(width: 6),
Icon(Icons.repeat, size: 12, color: Colors.grey[600]),
const SizedBox(width: 2),
Padding(
padding: gifId != null
? const EdgeInsets.only(left: 8, right: 8, bottom: 4)
: EdgeInsets.zero,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${message.repeatCount}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
if (message.repeatCount > 0) ...[
const SizedBox(width: 6),
Icon(Icons.repeat, size: 12, color: Colors.grey[600]),
const SizedBox(width: 2),
Text(
'${message.repeatCount}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
if (isOutgoing) ...[
const SizedBox(width: 4),
Icon(
message.status == ChannelMessageStatus.sent
? Icons.check
: message.status == ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color: message.status == ChannelMessageStatus.failed
? Colors.red
: Colors.grey[600],
),
],
],
if (isOutgoing) ...[
const SizedBox(width: 4),
Icon(
message.status == ChannelMessageStatus.sent
? Icons.check
: message.status == ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color: message.status == ChannelMessageStatus.failed
? Colors.red
: Colors.grey[600],
),
],
],
),
),
],
),
@@ -364,8 +436,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: colorScheme.surfaceContainerHighest,
fallbackTextColor: previewTextColor,
width: 120,
height: 80,
maxSize: 80,
),
);
} else if (poi != null) {
@@ -690,14 +761,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Row(
children: [
Expanded(
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),
width: 160,
height: 110,
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),
@@ -711,9 +784,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return TextField(
controller: _textController,
focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder(
@@ -803,14 +878,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message);
},
),
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message);
},
),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.common_copy),
@@ -852,9 +929,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
void _sendReaction(ChannelMessage message, String emoji) {
final connector = context.read<MeshCoreConnector>();
// Send reaction with full messageId to find target, but parser will extract
// lightweight reactionKey (timestamp_senderPrefix) for deduplication
final reactionText = 'r:${message.messageId}:$emoji';
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
if (emojiIndex == null) return; // Unknown emoji, skip
final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000;
final hash = ReactionHelper.computeReactionHash(timestampSecs, message.senderName, message.text);
final reactionText = 'r:$hash:$emojiIndex';
connector.sendChannelMessage(widget.channel, reactionText);
}
File diff suppressed because it is too large Load Diff
+176 -91
View File
@@ -5,11 +5,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
@@ -20,6 +24,7 @@ import 'map_screen.dart';
import '../utils/emoji_utils.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/path_selection_dialog.dart';
import '../utils/app_logger.dart';
@@ -36,25 +41,44 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
final _textController = TextEditingController();
final _scrollController = ScrollController();
final _scrollController = ChatScrollController();
final _textFieldFocusNode = FocusNode();
bool _isLoadingOlder = false;
@override
void initState() {
super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<MeshCoreConnector>().setActiveContact(widget.contact.publicKeyHex);
// Scroll to bottom when opening chat use SchedulerBinding for next frame
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
}
}
Future<void> _loadOlderMessages() async {
if (_isLoadingOlder) return;
setState(() => _isLoadingOlder = true);
final connector = context.read<MeshCoreConnector>();
await connector.loadOlderMessages(widget.contact.publicKeyHex);
if (mounted) {
setState(() => _isLoadingOlder = false);
}
}
@override
void dispose() {
context.read<MeshCoreConnector>().setActiveContact(null);
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
_textFieldFocusNode.dispose();
_textController.dispose();
_scrollController.dispose();
super.dispose();
@@ -167,9 +191,16 @@ class _ChatScreenState extends State<ChatScreen> {
return Column(
children: [
Expanded(
child: messages.isEmpty
? _buildEmptyState()
: _buildMessageList(messages, connector),
child: Stack(
children: [
messages.isEmpty
? _buildEmptyState()
: _buildMessageList(messages, connector),
JumpToBottomButton(
scrollController: _scrollController,
),
],
),
),
_buildInputBar(connector),
],
@@ -201,13 +232,37 @@ class _ChatScreenState extends State<ChatScreen> {
}
Widget _buildMessageList(List<Message> messages, MeshCoreConnector connector) {
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.scrollToBottomIfAtBottom();
});
return ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
itemCount: messages.length,
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;
Contact contact = widget.contact;
final message = messages[index];
final message = reversedMessages[messageIndex];
String fourByteHex = '';
if (widget.contact.type == advTypeRoom) {
contact = _resolveContactFrom4Bytes(
@@ -256,13 +311,15 @@ class _ChatScreenState extends State<ChatScreen> {
return Row(
children: [
Expanded(
child: GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: colorScheme.surfaceContainerHighest,
fallbackTextColor:
colorScheme.onSurface.withValues(alpha: 0.6),
width: 160,
height: 110,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: colorScheme.surfaceContainerHighest,
fallbackTextColor:
colorScheme.onSurface.withValues(alpha: 0.6),
maxSize: 160,
),
),
),
const SizedBox(width: 8),
@@ -276,9 +333,11 @@ class _ChatScreenState extends State<ChatScreen> {
return TextField(
controller: _textController,
focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: const OutlineInputBorder(),
@@ -336,16 +395,6 @@ class _ChatScreenState extends State<ChatScreen> {
text,
);
_textController.clear();
Future.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
});
}
@@ -802,14 +851,16 @@ class _ChatScreenState extends State<ChatScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message);
},
),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message, contact);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.common_copy),
@@ -883,25 +934,29 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
void _showEmojiPicker(Message message) {
void _showEmojiPicker(Message message, Contact senderContact) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => EmojiPicker(
onEmojiSelected: (emoji) {
_sendReaction(message, emoji);
_sendReaction(message, senderContact, emoji);
},
),
);
}
void _sendReaction(Message message, String emoji) {
void _sendReaction(Message message, Contact senderContact, String emoji) {
final connector = context.read<MeshCoreConnector>();
// Send reaction with messageId if available, otherwise use lightweight format
// Parser will extract reactionKey (timestamp_senderPrefix) for deduplication
final messageId = message.messageId ??
'${message.timestamp.millisecondsSinceEpoch}_${message.senderKeyHex.substring(0, 8)}';
final reactionText = 'r:$messageId:$emoji';
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
if (emojiIndex == null) return; // Unknown emoji, skip
final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000;
// For room servers, include sender name (like channels) since multiple users
// For 1:1 chats, sender is implicit (null)
final senderName = widget.contact.type == advTypeRoom ? senderContact.name : null;
final hash = ReactionHelper.computeReactionHash(timestampSecs, senderName, message.text);
final reactionText = 'r:$hash:$emojiIndex';
connector.sendMessage(widget.contact, reactionText);
}
}
@@ -957,7 +1012,9 @@ class _MessageBubble extends StatelessWidget {
],
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
@@ -969,75 +1026,103 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Text(
senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
Padding(
padding: gifId != null
? const EdgeInsets.only(left: 8, top: 4, bottom: 4)
: EdgeInsets.zero,
child: Text(
senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
),
const SizedBox(height: 4),
if (gifId == null) const SizedBox(height: 4),
],
if (poi != null)
_buildPoiMessage(context, poi, textColor, metaColor)
else if (gifId != null)
GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: bubbleColor,
fallbackTextColor: textColor.withValues(alpha: 0.7),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: textColor.withValues(alpha: 0.7),
),
)
else
Text(
messageText,
Linkify(
text: messageText,
style: TextStyle(
color: textColor,
),
linkStyle: const TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) => LinkHandler.handleLinkTap(context, link.url),
),
if (isOutgoing && message.retryCount > 0) ...[
const SizedBox(height: 4),
Text(
context.l10n.chat_retryCount(message.retryCount, 4),
style: TextStyle(
fontSize: 10,
color: metaColor,
fontWeight: FontWeight.w500,
Padding(
padding: gifId != null
? const EdgeInsets.symmetric(horizontal: 8)
: EdgeInsets.zero,
child: Text(
context.l10n.chat_retryCount(message.retryCount, 4),
style: TextStyle(
fontSize: 10,
color: metaColor,
fontWeight: FontWeight.w500,
),
),
),
],
const SizedBox(height: 4),
Wrap(
spacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 10,
color: metaColor,
),
),
if (isOutgoing) ...[
const SizedBox(width: 4),
_buildStatusIcon(metaColor),
],
if (message.tripTimeMs != null &&
message.status == MessageStatus.delivered) ...[
const SizedBox(width: 4),
Icon(
Icons.speed,
size: 10,
color: isOutgoing ? metaColor : Colors.green[700],
),
Padding(
padding: gifId != null
? const EdgeInsets.only(left: 8, right: 8, bottom: 4)
: EdgeInsets.zero,
child: Wrap(
spacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 9,
color: isOutgoing ? metaColor : Colors.green[700],
fontSize: 10,
color: metaColor,
),
),
if (isOutgoing) ...[
const SizedBox(width: 4),
_buildStatusIcon(metaColor),
],
if (message.tripTimeMs != null &&
message.status == MessageStatus.delivered) ...[
const SizedBox(width: 4),
Icon(
Icons.speed,
size: 10,
color: isOutgoing ? metaColor : Colors.green[700],
),
Text(
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
style: TextStyle(
fontSize: 9,
color: isOutgoing ? metaColor : Colors.green[700],
),
),
],
],
],
),
),
],
),
+76 -21
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/widgets/path_trace_dialog.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -51,7 +53,7 @@ class _ContactsScreenState extends State<ContactsScreen>
final ContactGroupStore _groupStore = ContactGroupStore();
List<ContactGroup> _groups = [];
Timer? _searchDebounce;
@override
void initState() {
super.initState();
@@ -313,6 +315,14 @@ class _ContactsScreenState extends State<ContactsScreen>
return matchesContactQuery(contact, _searchQuery);
}).toList();
// Filter out own node from the list
if (connector.selfPublicKey != null) {
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
filtered = filtered.where((contact) {
return contact.publicKeyHex != selfPubKeyHex;
}).toList();
}
if (_typeFilter != ContactTypeFilter.all) {
filtered = filtered.where(_matchesTypeFilter).toList();
}
@@ -752,7 +762,19 @@ class _ContactsScreenState extends State<ContactsScreen>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isRepeater)
if (isRepeater) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
onTap: () {
showDialog(context: context, builder: (context) {
return PathTraceDialog(
title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing,
path: contact.traceRouteBytes ?? Uint8List(0),
);
});
}
),
ListTile(
leading: const Icon(Icons.cell_tower, color: Colors.orange),
title: Text(context.l10n.contacts_manageRepeater),
@@ -761,7 +783,19 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRepeaterLogin(context, contact);
},
)
else if (isRoom) ...[
]else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
onTap: () {
showDialog(context: context, builder: (context) {
return PathTraceDialog(
title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing,
path: contact.traceRouteBytes ?? Uint8List(0),
);
});
}
),
ListTile(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
@@ -778,7 +812,20 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRoomLogin(context, contact, RoomLoginDestination.management);
},
),
] else
] else ...[
if(contact.pathLength > 0)
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
showDialog(context: context, builder: (context) {
return PathTraceDialog(
title: context.l10n.contacts_pathTraceTo(contact.name),
path: contact.traceRouteBytes ?? Uint8List(0),
);
});
}
),
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -798,6 +845,7 @@ class _ContactsScreenState extends State<ContactsScreen>
_confirmDelete(context, connector, contact);
},
),
],
],
),
),
@@ -852,8 +900,6 @@ class _ContactTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
@@ -861,23 +907,32 @@ class _ContactTile extends StatelessWidget {
),
title: Text(contact.name),
subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
'${contact.typeLabel}${contact.pathLabel} ${contact.shortPubKeyHex}',
),
trailing: 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]),
// Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
),
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
],
),
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]),
),
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
],
),
),
onTap: onTap,
onLongPress: onLongPress,
+1
View File
@@ -354,6 +354,7 @@ class _MapScreenState extends State<MapScreen> {
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showFilterDialog(context, settingsService),
tooltip: context.l10n.map_filterNodes,
child: const Icon(Icons.filter_list),
),
),
+1 -1
View File
@@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
repeater.shortPubKeyHex,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
+39 -10
View File
@@ -8,9 +8,47 @@ import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
/// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatelessWidget {
class ScannerScreen extends StatefulWidget {
const ScannerScreen({super.key});
@override
State<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
late final VoidCallback _connectionListener;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connectionListener = () {
if (connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
} else if (connector.state == MeshCoreConnectionState.connected && !_changedNavigation) {
_changedNavigation = true;
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ContactsScreen(),
),
);
}
}
};
connector.addListener(_connectionListener);
}
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -161,15 +199,6 @@ final l10n = context.l10n;
? result.device.platformName
: result.advertisementData.advName;
await connector.connect(result.device, displayName: name);
if (context.mounted && connector.isConnected) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ContactsScreen(),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
+4
View File
@@ -156,6 +156,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
case cmdSendTracePath:
return 'CMD_SEND_TRACE_PATH';
default:
return null;
}
@@ -195,6 +197,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_INFO';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
case pushCodeTraceData:
return 'PUSH_CODE_TRACE_DATA';
default:
return null;
}
+3 -9
View File
@@ -112,7 +112,7 @@ class NotificationService {
await _notifications.show(
contactId?.hashCode ?? 0,
'New message from $contactName',
message.length > 100 ? '${message.substring(0, 100)}...' : message,
message,
notificationDetails,
payload: 'message:$contactId',
);
@@ -203,10 +203,10 @@ class NotificationService {
macOS: macDetails,
);
final preview = _truncateMessage(message, 30);
final preview = message.trim();
final body = preview.isEmpty
? 'Received new message'
: 'Received new message: $preview';
: preview;
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
@@ -217,12 +217,6 @@ class NotificationService {
);
}
String _truncateMessage(String message, int maxLength) {
final trimmed = message.trim();
if (trimmed.length <= maxLength) return trimmed;
return '${trimmed.substring(0, maxLength)}...';
}
void _onNotificationTapped(NotificationResponse response) {
final payload = response.payload;
if (payload != null) {
+10 -10
View File
@@ -12,32 +12,32 @@ class EmojiPicker extends StatelessWidget {
static const List<String> quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥'];
static const List<String> _smileys = [
static const List<String> smileys = [
'😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘',
'😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏',
'😒', '😞', '😔', '😟', '😕', '🙁', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡',
'🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶',
];
static const List<String> _gestures = [
static const List<String> gestures = [
'👍', '👎', '👊', '', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '🤌', '🤏', '👈', '👉', '👆',
'👇', '☝️', '👋', '🤚', '🖐️', '', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳',
'👇', '☝️', '👋', '🤚', '🖐️', '', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪',
];
static const List<String> _hearts = [
static const List<String> hearts = [
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❤️‍🔥', '❤️‍🩹', '💕', '💞', '💓', '💗',
'💖', '💘', '💝', '💟', '💌', '💢', '💥', '💫', '💦', '💨', '🕳️', '💬', '👁️‍🗨️', '🗨️', '🗯️', '💭',
];
static const List<String> _objects = [
static const List<String> objects = [
'🎉', '🎊', '🎈', '🎁', '🎀', '🪅', '🪆', '🏆', '🥇', '🥈', '🥉', '', '', '🥎', '🏀', '🏐',
'🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', '🏓', '🏸', '🥊', '🥋', '🥅', '', '🔥',
'', '🌟', '', '', '💡', '🔦', '🏮', '🪔', '📱', '💻', '', '📷', '📺', '📻', '🎵', '🎶',
'', '🌟', '', '', '💡', '🔦', '🏮', '🪔', '📱', '💻', '', '📷', '📺', '📻', '🎵', '🎶', '🚀',
];
Map<String, List<String>> _emojiCategories(AppLocalizations l10n) {
return {
l10n.emojiCategorySmileys: _smileys,
l10n.emojiCategoryGestures: _gestures,
l10n.emojiCategoryHearts: _hearts,
l10n.emojiCategoryObjects: _objects,
l10n.emojiCategorySmileys: smileys,
l10n.emojiCategoryGestures: gestures,
l10n.emojiCategoryHearts: hearts,
l10n.emojiCategoryObjects: objects,
};
}
+42 -25
View File
@@ -6,16 +6,14 @@ class GifMessage extends StatefulWidget {
final String url;
final Color backgroundColor;
final Color fallbackTextColor;
final double width;
final double height;
final double maxSize;
const GifMessage({
super.key,
required this.url,
required this.backgroundColor,
required this.fallbackTextColor,
this.width = 200,
this.height = 140,
this.maxSize = 200,
});
@override
@@ -122,6 +120,28 @@ class _GifMessageState extends State<GifMessage> {
@override
Widget build(BuildContext context) {
// Calculate display size based on image aspect ratio
// Use 4:3 placeholder aspect ratio during loading to minimize layout shifts
double displayWidth = widget.maxSize;
double displayHeight = widget.maxSize * 0.75;
if (_image != null) {
final imageWidth = _image!.width.toDouble();
final imageHeight = _image!.height.toDouble();
final aspectRatio = imageWidth / imageHeight;
// Fit within maxSize, calculating dimensions from aspect ratio
if (aspectRatio >= 1) {
// Wider than tall: constrain by width
displayWidth = widget.maxSize;
displayHeight = displayWidth / aspectRatio;
} else {
// Taller than wide: constrain by height
displayHeight = widget.maxSize;
displayWidth = displayHeight * aspectRatio;
}
}
Widget content;
if (_error != null) {
@@ -151,33 +171,30 @@ class _GifMessageState extends State<GifMessage> {
} else {
content = RawImage(
image: _image,
fit: BoxFit.cover,
width: widget.width,
height: widget.height,
fit: BoxFit.contain,
width: displayWidth,
height: displayHeight,
);
}
return GestureDetector(
onTap: _togglePause,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
color: widget.backgroundColor,
width: widget.width,
height: widget.height,
child: Stack(
fit: StackFit.expand,
children: [
content,
if (_isPaused && _image != null)
Container(
color: Colors.black.withValues(alpha: 0.2),
child: const Center(
child: Icon(Icons.pause, color: Colors.white70, size: 28),
),
child: Container(
color: widget.backgroundColor,
width: displayWidth,
height: displayHeight,
child: Stack(
fit: StackFit.expand,
children: [
content,
if (_isPaused && _image != null)
Container(
color: Colors.black.withValues(alpha: 0.2),
child: const Center(
child: Icon(Icons.pause, color: Colors.white70, size: 28),
),
],
),
),
],
),
),
);
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import '../helpers/chat_scroll_controller.dart';
class JumpToBottomButton extends StatelessWidget {
final ChatScrollController scrollController;
const JumpToBottomButton({
super.key,
required this.scrollController,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: scrollController.showJumpToBottom,
builder: (context, show, _) {
if (!show) return const SizedBox.shrink();
return Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.small(
onPressed: scrollController.jumpToBottom,
child: const Icon(Icons.keyboard_arrow_down),
),
);
},
);
}
}
+226
View File
@@ -0,0 +1,226 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
import '../widgets/snr_indicator.dart';
import '../l10n/l10n.dart';
class PathTraceDialog extends StatefulWidget {
const PathTraceDialog({
super.key,
required this.title,
required this.path,
});
final String title;
final Uint8List path;
@override
State<PathTraceDialog> createState() => _PathTraceDialogState();
}
class _PathTraceDialogState extends State<PathTraceDialog> {
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
bool _isLoading = false;
bool _failed2Loaded = false;
bool _hasData = false;
Uint8List _pathData = Uint8List(0);
Uint8List _snrData = Uint8List(0) ;
Map<int, Contact> _pathContacts = {};
@override
void initState() {
super.initState();
_setupFrameListener();
_doPathTrace();
}
@override
void dispose() {
_frameSubscription?.cancel();
_timeoutTimer?.cancel();
super.dispose();
}
Future<void> _doPathTrace() async {
if(mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
});
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
0, //flags
0, //auth
payload: widget.path,
);
connector.sendFrame(frame);
}
void _setupFrameListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
Uint8List tagData = Uint8List(4);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutSeconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
});
}
// Check if it's a binary response
if (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);
}
}
});
}
Future<void> _handleTraceResponse(Uint8List frame)async {
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();
Map<int, Contact> pathContacts = {};
connector.contacts.where((c) => c.type != advTypeChat).forEach((
repeater,
) {
for (var neighbourData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([neighbourData]),
)) {
pathContacts[neighbourData] = repeater;
}
}
});
setState(() {
_isLoading = false;
_hasData = true;
_pathData = pathData;
_snrData = snrData;
_pathContacts = pathContacts;
});
}
String formatDirectionText(int index) {
if (index == 0 || index == _snrData.length - 1) {
if (index == 0) {
return context.l10n.pathTrace_you;
} else {
return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}";
}
} else {
return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}";
}
}
String formatDirectionSubText(int index) {
if (index == 0 || index == _snrData.length - 1) {
if (index == 0) {
return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}";
} else {
return context.l10n.pathTrace_you;
}
} else {
return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}";
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog(
title: Column( children: [
FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))),
if(_failed2Loaded)
Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),),
],
),
content: SafeArea(
child: RefreshIndicator(
onRefresh: _doPathTrace,
child: !_hasData
? Center(
child: Text(l10n.pathTrace_notAvailable),
)
: ListView.builder(
itemCount: _snrData.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made),
title: Text(
formatDirectionText(index), style: const TextStyle(fontSize: 14),
),
subtitle: Text(
formatDirectionSubText(index),
style: const TextStyle(fontSize: 14),
),
trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0),
onTap: () {
// Handle item tap
},
),
if (index < _snrData.length - 1) const Divider(height: 0.0),
],
);
},
),
),
),
actions: [
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _doPathTrace,
tooltip: l10n.pathTrace_refreshTooltip,
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.common_close),
),
],
);
}
}
+3 -1
View File
@@ -322,7 +322,9 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
},
onSubmitted: (_) => _handleLogin(),
autofocus: _passwordController.text.isEmpty,
autofocus: !(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
CheckboxListTile(
+6 -2
View File
@@ -261,7 +261,8 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
child: CircularProgressIndicator(),
),
)
: Column(
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -292,7 +293,9 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
),
),
onSubmitted: (_) => _handleLogin(),
autofocus: _passwordController.text.isEmpty,
autofocus: !(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
CheckboxListTile(
@@ -382,6 +385,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -9,9 +9,9 @@ import flutter_blue_plus_darwin
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -19,8 +19,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
+42
View File
@@ -0,0 +1,42 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end
+74
View File
@@ -0,0 +1,74 @@
PODS:
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- mobile_scanner (6.0.2):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
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`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- 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`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
EXTERNAL SOURCES:
flutter_blue_plus_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
FlutterMacOS:
:path: Flutter/ephemeral
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
SPEC CHECKSUMS:
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
COCOAPODS: 1.16.2
+97 -1
View File
@@ -27,6 +27,8 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */; };
D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -60,11 +62,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
00F1FE94A1827B8A00BD3DB9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "meshcore_open.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meshcore_open.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -76,8 +80,14 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
4172BCCDFD1E1404F7155426 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -85,6 +95,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -92,6 +103,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -125,6 +137,7 @@
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
73DBB8BFF247FD65EEC878CC /* Pods */,
);
sourceTree = "<group>";
};
@@ -172,9 +185,25 @@
path = Runner;
sourceTree = "<group>";
};
73DBB8BFF247FD65EEC878CC /* Pods */ = {
isa = PBXGroup;
children = (
BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */,
4172BCCDFD1E1404F7155426 /* Pods-Runner.release.xcconfig */,
00F1FE94A1827B8A00BD3DB9 /* Pods-Runner.profile.xcconfig */,
96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */,
EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */,
D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */,
B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -186,6 +215,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
7DEC542F9A4811B2EEDCB8C1 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@@ -204,11 +234,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
79D67F01E273245A9C69C0B6 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
306490712F2EAA29CA421662 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -291,6 +323,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
306490712F2EAA29CA421662 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -329,6 +378,50 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
79D67F01E273245A9C69C0B6 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
7DEC542F9A4811B2EEDCB8C1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -380,6 +473,7 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -394,6 +488,7 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -408,6 +503,7 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
+3
View File
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+148 -12
View File
@@ -97,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
@@ -157,10 +165,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
file:
dependency: transitive
description:
@@ -234,10 +242,10 @@ packages:
dependency: transitive
description:
name: flutter_blue_plus_winrt
sha256: "0c87ca5bdf1a110d42847edeca8fbb11a9701738dc8526aefbb2a115bea29aef"
sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80"
url: "https://pub.dev"
source: hosted
version: "0.0.10"
version: "0.0.16"
flutter_cache_manager:
dependency: "direct main"
description:
@@ -262,6 +270,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_linkify:
dependency: "direct main"
description:
name: flutter_linkify
sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -317,6 +333,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: "direct main"
description:
@@ -361,10 +393,10 @@ packages:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
version: "4.10.0"
latlong2:
dependency: "direct main"
description:
@@ -397,6 +429,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
linkify:
dependency: transitive
description:
name: linkify
sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
lints:
dependency: transitive
description:
@@ -421,6 +461,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.2"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@@ -461,6 +509,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.11"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
nested:
dependency: transitive
description:
@@ -469,6 +525,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e"
url: "https://pub.dev"
source: hosted
version: "9.2.5"
octo_image:
dependency: transitive
description:
@@ -521,10 +585,10 @@ packages:
dependency: transitive
description:
name: path_provider_foundation
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -613,6 +677,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
qr:
dependency: transitive
description:
@@ -649,10 +721,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
url: "https://pub.dev"
source: hosted
version: "2.4.18"
version: "2.4.20"
shared_preferences_foundation:
dependency: transitive
description:
@@ -818,6 +890,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
url: "https://pub.dev"
source: hosted
version: "6.3.6"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: "direct main"
description:
@@ -907,5 +1043,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"
+3 -1
View File
@@ -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: 0.4.0+4
version: 5.0.0+5
environment:
sdk: ^3.9.2
@@ -55,6 +55,8 @@ dependencies:
package_info_plus: ^8.0.0
mobile_scanner: ^6.0.0 # 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
dev_dependencies:
flutter_test:
+404
View File
@@ -0,0 +1,404 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/helpers/reaction_helper.dart';
import 'package:meshcore_open/widgets/emoji_picker.dart';
void main() {
group('ReactionHelper', () {
group('reactionEmojis', () {
test('should contain all emoji categories', () {
final emojis = ReactionHelper.reactionEmojis;
// Should contain quickEmojis
for (final emoji in EmojiPicker.quickEmojis) {
expect(emojis.contains(emoji), isTrue, reason: 'Missing quick emoji: $emoji');
}
// Should contain smileys
for (final emoji in EmojiPicker.smileys) {
expect(emojis.contains(emoji), isTrue, reason: 'Missing smiley: $emoji');
}
// Should contain gestures
for (final emoji in EmojiPicker.gestures) {
expect(emojis.contains(emoji), isTrue, reason: 'Missing gesture: $emoji');
}
// Should contain hearts
for (final emoji in EmojiPicker.hearts) {
expect(emojis.contains(emoji), isTrue, reason: 'Missing heart: $emoji');
}
// Should contain objects
for (final emoji in EmojiPicker.objects) {
expect(emojis.contains(emoji), isTrue, reason: 'Missing object: $emoji');
}
});
test('should fit in 1 byte (max 256 emojis)', () {
expect(ReactionHelper.reactionEmojis.length, lessThanOrEqualTo(256));
});
});
group('emojiToIndex', () {
test('should return 2-char hex for valid emoji', () {
// First emoji (thumbs up) should be index 0
expect(ReactionHelper.emojiToIndex('👍'), equals('00'));
// Second emoji (heart) should be index 1
expect(ReactionHelper.emojiToIndex('❤️'), equals('01'));
});
test('should return null for unknown emoji', () {
expect(ReactionHelper.emojiToIndex('🦄'), isNull); // Not in list
expect(ReactionHelper.emojiToIndex('invalid'), isNull);
expect(ReactionHelper.emojiToIndex(''), isNull);
});
test('should return lowercase hex', () {
final index = ReactionHelper.emojiToIndex('👍');
expect(index, matches(RegExp(r'^[0-9a-f]{2}$')));
});
});
group('indexToEmoji', () {
test('should return emoji for valid index', () {
expect(ReactionHelper.indexToEmoji('00'), equals('👍'));
expect(ReactionHelper.indexToEmoji('01'), equals('❤️'));
});
test('should return null for invalid index', () {
expect(ReactionHelper.indexToEmoji('ff'), isNull); // Index 255, out of range
expect(ReactionHelper.indexToEmoji('zz'), isNull); // Invalid hex
expect(ReactionHelper.indexToEmoji(''), isNull); // Empty string
// Note: indexToEmoji parses any valid hex; length validation is done by parseReaction's regex
});
test('should handle case insensitivity', () {
// Both uppercase and lowercase should work
expect(ReactionHelper.indexToEmoji('0a'), isNotNull);
expect(ReactionHelper.indexToEmoji('0A'), isNotNull);
});
});
group('emoji round-trip', () {
test('all emojis should round-trip correctly', () {
for (int i = 0; i < ReactionHelper.reactionEmojis.length; i++) {
final emoji = ReactionHelper.reactionEmojis[i];
final index = ReactionHelper.emojiToIndex(emoji);
expect(index, isNotNull, reason: 'emojiToIndex failed for $emoji');
final decoded = ReactionHelper.indexToEmoji(index!);
expect(decoded, equals(emoji), reason: 'Round-trip failed for $emoji (index $index)');
}
});
});
group('computeReactionHash', () {
test('should return 4-char hex hash', () {
final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world');
expect(hash, matches(RegExp(r'^[0-9a-f]{4}$')));
});
test('should be deterministic', () {
final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello');
final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello');
expect(hash1, equals(hash2));
});
test('should differ for different inputs', () {
final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello');
final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Bob', 'Hello');
final hash3 = ReactionHelper.computeReactionHash(1234567891, 'Alice', 'Hello');
final hash4 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'World');
expect(hash1, isNot(equals(hash2))); // Different sender
expect(hash1, isNot(equals(hash3))); // Different timestamp
expect(hash1, isNot(equals(hash4))); // Different text
});
test('should use first 5 chars of text', () {
final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world');
final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello there');
expect(hash1, equals(hash2)); // Same first 5 chars
});
test('should handle short text', () {
final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hi');
expect(hash, matches(RegExp(r'^[0-9a-f]{4}$')));
});
test('should handle empty text', () {
final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', '');
expect(hash, matches(RegExp(r'^[0-9a-f]{4}$')));
});
});
group('computeReactionHash with null sender (1:1 chats)', () {
test('should return 4-char hex hash', () {
final hash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello world');
expect(hash, matches(RegExp(r'^[0-9a-f]{4}$')));
});
test('should be deterministic', () {
final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello');
final hash2 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello');
expect(hash1, equals(hash2));
});
test('should differ for different inputs', () {
final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello');
final hash2 = ReactionHelper.computeReactionHash(1234567891, null, 'Hello');
final hash3 = ReactionHelper.computeReactionHash(1234567890, null, 'World');
expect(hash1, isNot(equals(hash2))); // Different timestamp
expect(hash1, isNot(equals(hash3))); // Different text
});
test('should differ from hash with sender name', () {
// Null sender hash doesn't include sender, so should differ
final nullSenderHash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello');
final withSenderHash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello');
expect(nullSenderHash, isNot(equals(withSenderHash)));
});
test('1:1 chat flow: sender and receiver compute same hash', () {
// Alice sends "Hello" at timestamp 1234567890
// Bob receives it and wants to react
// Bob computes hash the same way Alice's app will match it
const timestamp = 1234567890;
const messageText = 'Hello there!';
// Bob (sender of reaction) computes hash with null sender
final bobHash = ReactionHelper.computeReactionHash(timestamp, null, messageText);
// Alice (receiver of reaction) computes hash for her outgoing message
final aliceHash = ReactionHelper.computeReactionHash(timestamp, null, messageText);
expect(bobHash, equals(aliceHash));
});
});
group('parseReaction', () {
test('should parse valid reaction format', () {
final info = ReactionHelper.parseReaction('r:a1b2:00');
expect(info, isNotNull);
expect(info!.targetHash, equals('a1b2'));
expect(info.emoji, equals('👍'));
});
test('should return null for invalid format', () {
expect(ReactionHelper.parseReaction('invalid'), isNull);
expect(ReactionHelper.parseReaction('r:abc:00'), isNull); // Hash too short
expect(ReactionHelper.parseReaction('r:abcde:00'), isNull); // Hash too long
expect(ReactionHelper.parseReaction('r:a1b2:0'), isNull); // Index too short
expect(ReactionHelper.parseReaction('r:a1b2:000'), isNull); // Index too long
expect(ReactionHelper.parseReaction('R:a1b2:00'), isNull); // Uppercase R
expect(ReactionHelper.parseReaction('r:A1B2:00'), isNull); // Uppercase hash
expect(ReactionHelper.parseReaction(''), isNull);
});
test('should return null for invalid emoji index', () {
// Index ff (255) is likely out of range
expect(ReactionHelper.parseReaction('r:a1b2:ff'), isNull);
});
test('should decode emoji correctly', () {
// Encode thumbs up and verify decode
final index = ReactionHelper.emojiToIndex('👍');
final info = ReactionHelper.parseReaction('r:dead:$index');
expect(info, isNotNull);
expect(info!.emoji, equals('👍'));
});
});
group('full reaction flow', () {
test('should encode and decode reaction correctly', () {
// Simulate sending a reaction
const timestamp = 1234567890;
const senderName = 'Alice';
const messageText = 'Hello world!';
const emoji = '🎉';
// Compute hash (sender side)
final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText);
// Encode emoji (sender side)
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
expect(emojiIndex, isNotNull);
// Build reaction text (sender side)
final reactionText = 'r:$hash:$emojiIndex';
// Parse reaction (receiver side)
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
expect(info!.targetHash, equals(hash));
expect(info.emoji, equals(emoji));
// Verify receiver can match the hash
final receiverHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText);
expect(receiverHash, equals(info.targetHash));
});
test('reaction text should be 9 bytes', () {
final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello');
final index = ReactionHelper.emojiToIndex('👍')!;
final reactionText = 'r:$hash:$index';
// r: (2) + hash (4) + : (1) + index (2) = 9 bytes
expect(reactionText.length, equals(9));
});
test('1:1 chat: Bob reacts to Alice message', () {
// Alice sends "Hello" to Bob at timestamp 1234567890
const timestamp = 1234567890;
const aliceName = 'Alice';
const messageText = 'Hello';
const emoji = '👍';
// On Bob's device: message.isOutgoing = false, so senderName = contact.name = Alice
final bobSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText);
final emojiIndex = ReactionHelper.emojiToIndex(emoji)!;
final reactionText = 'r:$bobSideHash:$emojiIndex';
// Alice receives the reaction
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
// On Alice's device: message.isOutgoing = true, so senderName = selfName = Alice
final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText);
// Hashes should match!
expect(info!.targetHash, equals(aliceSideHash));
expect(info.emoji, equals(emoji));
});
test('1:1 chat: Alice reacts to Bob message', () {
// Bob sends "Hi there" to Alice at timestamp 9876543210
const timestamp = 9876543210;
const bobName = 'Bob';
const messageText = 'Hi there';
const emoji = '❤️';
// On Alice's device: message.isOutgoing = false, so senderName = contact.name = Bob
final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText);
final emojiIndex = ReactionHelper.emojiToIndex(emoji)!;
final reactionText = 'r:$aliceSideHash:$emojiIndex';
// Bob receives the reaction
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
// On Bob's device: message.isOutgoing = true, so senderName = selfName = Bob
final bobSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText);
// Hashes should match!
expect(info!.targetHash, equals(bobSideHash));
expect(info.emoji, equals(emoji));
});
test('room server: user reacts to message from another user', () {
// In a room server, Charlie sends "Hello room" at timestamp 1111111111
// Alice wants to react to it
const timestamp = 1111111111;
const charlieName = 'Charlie';
const messageText = 'Hello room';
const emoji = '🎉';
// Alice computes hash including sender name (room servers are multi-user)
final aliceHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText);
final emojiIndex = ReactionHelper.emojiToIndex(emoji)!;
final reactionText = 'r:$aliceHash:$emojiIndex';
// Verify format
expect(reactionText.length, equals(9));
expect(reactionText, matches(RegExp(r'^r:[0-9a-f]{4}:[0-9a-f]{2}$')));
// Bob (another user in the room) receives the reaction
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
// Bob computes hash for Charlie's message the same way
final bobHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText);
// Hashes should match!
expect(info!.targetHash, equals(bobHash));
expect(info.emoji, equals(emoji));
});
test('room server: hash differs from 1:1 hash for same message content', () {
// Same timestamp and text, but room server includes sender name
const timestamp = 1234567890;
const senderName = 'Dave';
const messageText = 'Hello';
// Room server hash (with sender name)
final roomHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText);
// 1:1 hash (without sender name)
final directHash = ReactionHelper.computeReactionHash(timestamp, null, messageText);
// They should be different!
expect(roomHash, isNot(equals(directHash)));
});
test('room server: different senders produce different hashes', () {
// Two users send the exact same message at the same time in a room
const timestamp = 1234567890;
const messageText = 'Hello';
final aliceHash = ReactionHelper.computeReactionHash(timestamp, 'Alice', messageText);
final bobHash = ReactionHelper.computeReactionHash(timestamp, 'Bob', messageText);
// Different senders = different hashes (even with same content)
expect(aliceHash, isNot(equals(bobHash)));
});
test('room server: self message reaction works', () {
// Alice sends "My message" at timestamp 2222222222
// Bob wants to react to it
const timestamp = 2222222222;
const aliceName = 'Alice';
const messageText = 'My message';
const emoji = '👍';
// Bob computes hash for Alice's message
final bobHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText);
final emojiIndex = ReactionHelper.emojiToIndex(emoji)!;
final reactionText = 'r:$bobHash:$emojiIndex';
// Alice receives the reaction and matches against her outgoing message
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
// Alice computes hash using her selfName
final aliceHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText);
// Hashes should match!
expect(info!.targetHash, equals(aliceHash));
});
test('channel: same logic as room server', () {
// Channel messages also use sender name in hash
const timestamp = 3333333333;
const senderName = 'Eve';
const messageText = 'Channel msg';
const emoji = '🔥';
// Compute hash with sender name
final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText);
final emojiIndex = ReactionHelper.emojiToIndex(emoji)!;
final reactionText = 'r:$hash:$emojiIndex';
// Parse and verify
final info = ReactionHelper.parseReaction(reactionText);
expect(info, isNotNull);
expect(info!.emoji, equals(emoji));
// Another user computes the same hash
final otherUserHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText);
expect(info.targetHash, equals(otherUserHash));
});
});
});
}
+4 -1
View File
@@ -466,7 +466,7 @@ def fmt_duration(seconds: float) -> str:
def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]:
"""Find keys that are in source but not in target (excluding metadata keys)."""
"""Find keys that are in source but not in target, or have empty values (excluding metadata keys)."""
missing = []
for key in source_data:
if key == "@@locale":
@@ -475,6 +475,9 @@ def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any])
continue
if key not in target_data:
missing.append(key)
elif isinstance(target_data.get(key), str) and target_data[key].strip() == "":
# Also include keys with empty string values
missing.append(key)
return missing
+32 -84
View File
@@ -1,121 +1,69 @@
{
"bg": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"de": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"es": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"fr": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"it": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"nl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"pl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"pt": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"ru": [
"appSettings_languageUk"
],
"sk": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"sl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"sv": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
],
"uk": [
"appSettings_languageRu"
],
"zh": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
"appSettings_languageRu",
"appSettings_languageUk"
]
}
@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <flutter_blue_plus_winrt/flutter_blue_plus_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterBluePlusPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterBluePlusPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
+1
View File
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_blue_plus_winrt
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST