Compare commits

..

85 Commits

Author SHA1 Message Date
Zach 92d2b224e7 fix: address PR review issues
- Fix memory leak by adding dispose() to remove connection listener
- Fix typo: changedNavgation -> _changedNavigation
- Add mounted check before navigation to prevent errors
- Remove overly aggressive _handleDisconnection() call on battery request failure
- Only reset battery flag on error to allow retry without disconnecting
2026-01-28 21:29:18 -07:00
Winston Lowe 34a6b5d895 Added error catching to requestBatteryStatus
to call _handleDisconnection when it fails update.

Updated ScannerScreen to manage navigation state logic on connection.
2026-01-28 20:13:40 -08:00
Zach 8b0bdd9a46 fix: update PRODUCT_BUNDLE_IDENTIFIER to com.monitormx.meshcoreopen 2026-01-24 01:37:19 -07:00
zjs81 45d914de57 chore: update version to 5.0.0+5 in pubspec.yaml 2026-01-24 01:26:23 -07:00
Zach 2c49534955 feat: add url_launcher_ios dependency and update project configuration 2026-01-24 01:24:56 -07:00
Zach c56cf9c3ed feat: add CocoaPods support for macOS and iOS, including necessary configurations and dependencies 2026-01-24 01:07:18 -07:00
zjs81 fee4cd13be chore: update version to 0.4.5+4 in pubspec.yaml 2026-01-24 00:52:15 -07:00
zjs81 a53d5ccfb6 Merge pull request #69 from spfmoby/better-french-translations2
More french translation updates
2026-01-24 00:50:11 -07:00
zjs81 e5d06b1c7e Merge pull request #102 from zjs81/pr-94
Pr 94
2026-01-24 00:46:48 -07:00
zjs81 e95a55e4f0 feat: add Ukrainian localization support and improve string formatting 2026-01-24 00:45:01 -07:00
zjs81 422ca941c2 Merge remote-tracking branch 'origin/main' into pr-94 2026-01-24 00:42:29 -07:00
zjs81 3098d860e9 Merge pull request #101 from zjs81/anupoh/main
Anupoh/main
2026-01-24 00:32:29 -07:00
zjs81 f0d34f7503 Update Russian localization for improved pluralization and add new chat link handling messages
- Enhanced pluralization rules for "hops" in various contexts to better reflect Russian grammar.
- Added new localization strings for chat link handling, including error messages and confirmation prompts.
- Ensured consistency in the use of plural forms across the application.
2026-01-24 00:27:45 -07:00
zjs81 daa0c3f9c3 Merge branch 'main' into anupoh/main 2026-01-24 00:22:28 -07:00
zjs81 09e1cd2b8d fix: improve BLE scanning reliability and filter out own node from contacts list improve text scaling 2026-01-24 00:17:18 -07:00
zjs81 fa514533eb feat: add ChatScrollController and JumpToBottomButton for improved chat scrolling experience
- Implemented ChatScrollController to manage scroll behavior and visibility of jump-to-bottom button.
- Added functionality to automatically scroll to the bottom when the keyboard opens.
- Created JumpToBottomButton widget that appears when the user scrolls up, allowing quick navigation back to the bottom of the chat.
2026-01-23 17:56:06 -07:00
zjs81 75b8b8af70 Merge pull request #60 from 446564/missing-tooltips
update tooltips
2026-01-23 16:47:31 -07:00
spfmoby 115667a27c More french translation updates6 2026-01-23 17:39:59 +01:00
spfmoby cfb51d96ff More french translation updates6 2026-01-23 17:39:49 +01:00
anupoh 75356fe20d Russian translation for the app
I've prepared the Russian localization files for the app. It would be great if localization were included in the app. Thanx a lot!
2026-01-23 16:58:16 +07:00
megadimich c43df67fac Ukrainian localization files 2026-01-22 15:08:42 +00:00
spfmoby e2b9b58d7d More french translation updates5 2026-01-22 10:25:42 +01:00
spfmoby d6794bc8d7 More french translation updates4 2026-01-22 08:45:54 +01:00
spfmoby 72216e2cf7 More french translation updates3 2026-01-22 08:21:09 +01:00
spfmoby 2a2275ec31 More french translation updates2 2026-01-22 08:16:58 +01:00
spfmoby dff037535d More french translation updates 2026-01-21 18:13:24 +01:00
zjs81 297e609b3e fix: replace RadioListTile with RadioGroup for better state management in community selection 2026-01-20 22:40:42 -07:00
zjs81 20171c491f fix: update iOS platform version and enable sentence capitalization in chat input fields 2026-01-20 22:28:37 -07:00
zjs81 cc43f4d198 Merge pull request #65 from zjs81/fix/message-length-safety-margin
fix: add safety margin to text message overhead calculations
2026-01-20 21:51:53 -07:00
zjs81 537384ea5b fix: add safety margin to text message overhead calculations 2026-01-20 21:50:35 -07:00
zjs81 a0be63b2e7 feat: integrate link handling in chat screen with linkify support
- Added flutter_linkify package to auto-detect and linkify URLs in chat messages.
- Implemented LinkHandler class to manage link tap confirmations and URL launching.
- Updated chat_screen.dart to use Linkify for displaying message text with links.
- Registered url_launcher plugin for handling URL launches across platforms.
- Updated pubspec.yaml and pubspec.lock to include new dependencies.
- Cleaned up untranslated.json by removing unused translations.
2026-01-20 21:42:54 -07:00
zjs81 1cc887e5bb Merge pull request #61 from 446564/remove-rcvd
remove msg notify prefix when preview avail
2026-01-20 21:11:08 -07:00
446564 26d9029538 remove msg notify prefix when preview avail
this removes the 'Received new message: ' prefix from notications
when there is a message preview available
2026-01-20 17:35:14 -08:00
446564 30bcbedf5e update tooltips
add missing tooltip:
- channels, add channel button
- map, filter nodes button
2026-01-20 17:21:44 -08:00
zjs81 3fdd8f5eaf chore: Update version to 0.4.0+4 in pubspec.yaml 2026-01-19 20:58:11 -07:00
zjs81 f4ec732de8 feat: Add community management features with QR code scanning
- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
2026-01-19 20:56:07 -07:00
zjs81 f790604d23 Merge pull request #42 from wel97459/dev-neighbours
Added Neighbors to the repeater hub and a screen to display the Neighbors
2026-01-19 19:17:00 -07:00
zjs81 8e3b563aba revert translate.py 2026-01-19 19:14:48 -07:00
zjs81 ee3b0a3126 Add untranslated messages file and update localization keys
- Added `untranslated.json` to track untranslated messages.
- Updated localization keys in various language files to use camelCase format for consistency.
- Modified `neighbours_screen.dart` to reference updated localization keys.
2026-01-19 19:13:22 -07:00
zjs81 31d633ee0b Merge main into dev-neighbours 2026-01-19 19:09:03 -07:00
zjs81 c269365d81 Merge pull request #48 from wel97459/dev-gps
Added GPS enable and GPS interval settings.
2026-01-19 19:02:13 -07:00
zjs81 9a9f59e53f localization: update GPS settings messages for clarity and consistency across multiple languages 2026-01-19 19:00:30 -07:00
zjs81 9cb667fad0 localization: fix punctuation in GPS interval settings for Spanish and Portuguese 2026-01-19 19:00:24 -07:00
zjs81 3fef594fe5 localization: update GPS settings messages and improve handling of custom variables 2026-01-19 18:56:06 -07:00
zjs81 8387304d2a Merge main into dev-gps
- Resolved localization conflicts by keeping both GPS settings and room management strings
- Merged room management features from main
- Merged map and contacts screen updates from main
2026-01-19 18:51:02 -07:00
zjs81 2acba9eb84 Merge pull request #51 from wel97459/dev-roomManagement
Added room server management
2026-01-19 18:34:51 -07:00
zjs81 30ba1799e1 localization: update room management strings in multiple languages and refactor room login handling 2026-01-19 18:29:53 -07:00
zjs81 13f9c5058a Merge branch 'main' into dev-roomManagement 2026-01-19 18:25:00 -07:00
Winston Lowe 98fc2d6e0a Updated gps setting to follow state of companion. 2026-01-19 16:57:46 -08:00
Winston Lowe 2becbb342c Added buildGetCustomVarsFrame
And added update to refreshDeviceInfo and _requestDeviceInfo.
Added parsing of Custom Vars
2026-01-19 16:55:39 -08:00
zjs81 5b2d5a494c Merge pull request #47 from ericszimmermann/main
Disable Map rotation
2026-01-19 09:26:29 -07:00
Winston Lowe 153736d36e added roomserver management 2026-01-18 21:21:33 -08:00
Winston Lowe 6c8a149e1b fix a few translations and used _neighbourCount 2026-01-18 12:01:57 -08:00
Winston Lowe b41ccee4f9 Merge branch 'main' into dev-neighbours 2026-01-18 11:27:19 -08:00
Winston Lowe 04a713bb76 Added a basic neighbours screen for repeaters 2026-01-18 11:17:47 -08:00
Winston Lowe 714aecd7e6 Added GPS enable and interval settings 2026-01-18 01:05:46 -08:00
Winston Lowe 2e1a5e0fbf added CMD_SET_CUSTOM_VAR to BLE debug 2026-01-18 01:03:45 -08:00
Winston Lowe 1f0b7d8d7b added buildSetCustomVarFrame and setCustomVar 2026-01-18 01:02:48 -08:00
ericszimmermann dffea23ce2 Merge branch 'zjs81:main' into main 2026-01-17 20:47:56 +01:00
zjs81 e0a8fb7ec0 Merge pull request #44 from mtlynch/gh-build
Add a Github Action to build code in CI
2026-01-17 11:39:37 -07:00
zjs81 06fc08c41f Merge pull request #45 from mtlynch/flutter-analyze
Fix issues flagged by flutter analyze
2026-01-17 11:38:08 -07:00
ericz c22bfed680 Merge branch 'disable_map_rotation'
Disable Map Rotation.
2026-01-17 19:30:52 +01:00
zjs81 316c76e5b4 Merge pull request #46 from ericszimmermann/main
German translation V2
2026-01-17 11:20:54 -07:00
ericz 4b215ad574 Disable Map rotation 2026-01-17 17:14:39 +01:00
ericz 09e60cebd9 German translation V2 2026-01-17 17:03:39 +01:00
Michael Lynch 6782347cf4 Fix issues flagged by flutter analyze
This fixes code quality issues that flutter analyze catches and adds a CI step to Github Actions to flag on any future issues.
2026-01-17 11:00:34 -05:00
Michael Lynch 1726119c3e Add a Github Action to build code in CI
This adds a CI workflow in Github Actions to verify that the flutter builds compile for all supported platforms.

I tried adding Windows, but it currently fails, so I excluded it from this initial set.
2026-01-17 10:48:46 -05:00
zjs81 988806dccd Merge pull request #41 from mtlynch/show-error
Show repeater login error in login dialog
2026-01-16 19:10:15 -07:00
zjs81 14ff8250c0 Add support for private and hashtag channels in localization and channel management
- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels.
- Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags.
- Implemented PSK derivation from hashtags using SHA256 in the Channel model.
- Improved the translation script to handle missing keys and translate all locales efficiently.
2026-01-16 19:06:39 -07:00
Michael Lynch 2a04ebb8b6 Show repeater login error in login dialog 2026-01-16 09:35:02 -05:00
zjs81 a14462978d Replace Column with SingleChildScrollView in RepeaterLoginDialog for better layout handling 2026-01-15 21:49:54 -07:00
zjs81 df7fb45683 Merge pull request #38 from wel97459/dev-contactsPubkey
Added public key in contacts list and in the repeater hub
2026-01-15 19:26:53 -07:00
zjs81 f01eff07ff Merge pull request #37 from wel97459/dev-map
Fix map centering
2026-01-15 19:20:20 -07:00
zjs81 7cc7183e0c Refactor map initialization and zoom calculation logic in MapScreen 2026-01-15 19:15:42 -07:00
zjs81 a6b2756d0d Ran flutter format on the file 2026-01-15 19:11:13 -07:00
zjs81 614f3d4601 Add signing configuration support in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 7c33647119 Add key.properties support for signing configuration in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 fde8b686f5 Merge pull request #28 from spfmoby/better-french-translations
Replace Publicité by Annonce in the french translations
2026-01-15 18:30:55 -07:00
zjs81 9bc3a27b53 Merge pull request #30 from dennis1248/main
Update Dutch translations
2026-01-15 18:30:02 -07:00
Winston Lowe a8f387b0da Fix map centering weirdly
When nodes or markers are outside of the main area of interest.
2026-01-14 19:38:01 -08:00
Winston Lowe dd1a73c247 Repeater hub now show public key at the top 2026-01-14 19:34:41 -08:00
Winston Lowe e36f6b7eb9 changed contects list to show public keys of contect 2026-01-14 19:33:07 -08:00
Dennis ten Hoove fcef82be63 Update Dutch translations
This solves many of the most obvious errors and inconsistensies in the initial translation.
2026-01-13 11:53:54 +01:00
spfmoby 6ddb8f1a3d more fr translations / .arb and .dart synced 2026-01-13 08:27:01 +01:00
spfmoby 7a22223756 Replace Publicité by Annonce in the french translations 2026-01-12 10:18:18 +01:00
94 changed files with 21585 additions and 1717 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Build
on:
push:
pull_request:
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: flutter pub get
- run: flutter build apk --release --no-pub
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build ios --release --no-codesign --no-pub
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Install Linux build deps
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: flutter pub get
- run: flutter build linux --release --no-pub
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build macos --release --no-pub
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build web --release --no-pub
+23
View File
@@ -0,0 +1,23 @@
name: Flutter Analyze
on:
pull_request:
push:
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze --fatal-infos --fatal-warnings
+1
View File
@@ -70,6 +70,7 @@ secrets.dart
**/android/local.properties
**/android/.externalNativeBuild/
*.jks
key.properties
keystore.properties
# Generated files
+25 -3
View File
@@ -1,3 +1,5 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +7,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
@@ -40,11 +48,25 @@ android {
// }
}
signingConfigs {
create("release") {
val storeFilePath = keystoreProperties["storeFile"] as String?
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = keystoreProperties["storePassword"] as String?
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
+12
View File
@@ -16,6 +16,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="meshcore_open"
@@ -64,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
Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+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>
+7
View File
@@ -53,5 +53,12 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<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>
+1
View File
@@ -3,3 +3,4 @@ template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
untranslated-messages-file: untranslated.json
File diff suppressed because it is too large Load Diff
+67 -32
View File
@@ -20,7 +20,8 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) {
final value = <int>[];
@@ -38,13 +39,19 @@ class BufferReader {
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readUInt16LE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() =>
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt32LE() =>
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt24BE() {
var value = (readByte() << 16) | (readByte() << 8) | readByte();
@@ -63,21 +70,25 @@ class BufferWriter {
void writeBytes(Uint8List bytes) => _builder.add(bytes);
void writeUInt16LE(int num) {
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little);
final bytes = Uint8List(2)
..buffer.asByteData().setUint16(0, num, Endian.little);
writeBytes(bytes);
}
void writeUInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setUint32(0, num, Endian.little);
writeBytes(bytes);
}
void writeInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setInt32(0, num, Endian.little);
writeBytes(bytes);
}
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeString(String string) =>
writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeCString(String string, int maxLength) {
final bytes = Uint8List(maxLength);
@@ -118,6 +129,8 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
// Text message types
@@ -152,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -166,7 +180,6 @@ const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
// Contact/advertisement types
const int advTypeChat = 1;
const int advTypeRepeater = 2;
@@ -182,8 +195,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;
@@ -233,10 +246,7 @@ class ParsedContactText {
final Uint8List senderPrefix;
final String text;
const ParsedContactText({
required this.senderPrefix,
required this.text,
});
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
@@ -265,10 +275,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
return null;
}
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim();
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text =
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim();
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
@@ -362,7 +379,8 @@ Uint8List buildSendTextMsgFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
@@ -444,7 +462,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
// Format: [cmd][name...]
Uint8List buildSetAdvertNameFrame(String name) {
final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
final nameLen = nameBytes.length < maxNameSize
? nameBytes.length
: maxNameSize - 1;
final writer = BufferWriter();
writer.writeByte(cmdSetAdvertName);
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
@@ -461,6 +481,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
return writer.toBytes();
}
Uint8List buildSetCustomVarFrame(String value) {
final writer = BufferWriter();
writer.writeByte(cmdSetCustomVar);
writer.writeString(value);
writer.writeByte(0);
return writer.toBytes();
}
// Build CMD_REBOOT frame
// Format: [cmd]["reboot"]
Uint8List buildRebootFrame() {
@@ -544,7 +572,9 @@ Uint8List buildUpdateContactPathFrame(
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
@@ -575,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -598,9 +633,11 @@ int calculateLoRaAirtime({
final crc = 1; // CRC enabled
final de = lowDataRateOptimize ? 1 : 0;
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final numerator =
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final denominator = 4 * (spreadingFactor - 2 * de);
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
var payloadSymbols =
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
if (payloadSymbols < 0) {
payloadSymbols = 8;
@@ -647,7 +684,8 @@ Uint8List buildSendCliCommandFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
@@ -661,10 +699,7 @@ Uint8List buildSendCliCommandFrame(
// Build a telemetry request frame
// Format: [cmd][pub_key x32][payload]
Uint8List buildSendBinaryReq(
Uint8List repeaterPubKey, {
Uint8List? payload,
}) {
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
final writer = BufferWriter();
writer.writeByte(cmdSendBinaryReq);
writer.writeBytes(repeaterPubKey);
@@ -672,4 +707,4 @@ Uint8List buildSendBinaryReq(
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,
),
);
}
}
}
}
+199 -1
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за да се появят на картата.",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.",
"common_reload": "Презареди",
"common_clear": "Изчисти",
"path_currentPath": "Текущ път: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Повторители",
"listFilter_roomServers": "Сървъри на стая",
"listFilter_unreadOnly": "Само непрочетените",
"listFilter_newGroup": "Нова група"
"listFilter_newGroup": "Нова група",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbours": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbours": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
"channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.",
"channels_joinPublicChannel": "Присъединете се към Публичния канал",
"channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.",
"channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал",
"channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.",
"channels_scanQrCode": "Сканирайте QR код",
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
"channels_enterHashtag": "Въведете хаштаг",
"channels_hashtagHint": "напр. #отбор",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Слушано преди {time}.",
"neighbors_unknownContact": "Неизвестна {pubkey}",
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Общност",
"common_ok": "Добре",
"community_createDesc": "Създайте нова общност и я споделете чрез QR код.",
"community_create": "Създай общност",
"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_join": "Присъедини се",
"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_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Остави общността \"{name}\"",
"community_addHashtagChannel": "Добави общностен хаштаг",
"community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност",
"community_selectCommunity": "Изберете общност",
"community_regularHashtag": "Обикновен хаштаг",
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"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}\""
}
+330 -132
View File
@@ -69,7 +69,7 @@
},
"scanner_stop": "Stopp",
"scanner_scan": "Scannen",
"device_quickSwitch": "Schneller Umschalten",
"device_quickSwitch": "Schnelles Umschalten",
"device_meshcore": "MeshCore",
"settings_title": "Einstellungen",
"settings_deviceInfo": "Geräteinformationen",
@@ -78,7 +78,7 @@
"settings_nodeSettings": "Knoten-Einstellungen",
"settings_nodeName": "Knotenname",
"settings_nodeNameNotSet": "Nicht festgelegt",
"settings_nodeNameHint": "Gib den Knotenamen ein",
"settings_nodeNameHint": "Gebe den Knotenamen ein",
"settings_nodeNameUpdated": "Name aktualisiert",
"settings_radioSettings": "Funk Einstellungen",
"settings_radioSettingsSubtitle": "Frequenz, Leistung, Verbreitungsfaktor",
@@ -90,17 +90,17 @@
"settings_locationInvalid": "Ungültige Breiten- oder Längengrade.",
"settings_latitude": "Breitengrad",
"settings_longitude": "Längengrad",
"settings_privacyMode": "Privatschutzzustand",
"settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Anzeigen",
"settings_privacyModeToggle": "Aktivieren Sie den Datenschutzzustand, um Ihren Namen und Ihre Standortdaten in Anzeigen zu verbergen.",
"settings_privacyModeEnabled": "Privatschutzzustand aktiviert",
"settings_privacyMode": "Privatsphäreeinstellung",
"settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen",
"settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.",
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
"settings_actions": "Aktionen",
"settings_sendAdvertisement": "Senden Sie Anzeige",
"settings_sendAdvertisementSubtitle": "Sendungsstatus jetzt",
"settings_advertisementSent": "Anzeige gesendet",
"settings_syncTime": "Synchronisierungszeit",
"settings_syncTimeSubtitle": "Stelle die Gerätewielfalt auf die Uhrzeit des Telefons ein",
"settings_sendAdvertisement": "Sende eine Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
"settings_advertisementSent": "Ankündigung gesendet",
"settings_syncTime": "Zeitsynchronisierung",
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
"settings_timeSynchronized": "Zeit synchronisiert",
"settings_refreshContacts": "Kontakte aktualisieren",
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
@@ -128,8 +128,8 @@
"settings_infoStatus": "Status",
"settings_infoBattery": "Akku",
"settings_infoPublicKey": "Öffentlicher Schlüssel",
"settings_infoContactsCount": "Kontakte Anzahl",
"settings_infoChannelCount": "Kanalanzahl",
"settings_infoContactsCount": "Anzahl Kontakte",
"settings_infoChannelCount": "Anzahl Kanäle",
"settings_presets": "Voreinstellungen",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
@@ -139,11 +139,11 @@
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
"settings_bandwidth": "Bandbreite",
"settings_spreadingFactor": "Verteilungsfaktor",
"settings_codingRate": "Programmierpauschale",
"settings_codingRate": "Kodierungsrate",
"settings_txPower": "TX-Leistung (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
"settings_longRange": "Langreich",
"settings_longRange": "Grosse Reichweite",
"settings_fastSpeed": "Schnelle Geschwindigkeit",
"settings_error": "Fehler: {message}",
"@settings_error": {
@@ -157,7 +157,7 @@
"appSettings_appearance": "Aussehen",
"appSettings_theme": "Theme",
"appSettings_themeSystem": "Systemstandard",
"appSettings_themeLight": "Helligkeit",
"appSettings_themeLight": "Hell",
"appSettings_themeDark": "Dunkel",
"appSettings_language": "Sprache",
"appSettings_languageSystem": "Systemstandard",
@@ -176,19 +176,19 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Benachrichtigungen",
"appSettings_enableNotifications": "Benachrichtigungen aktivieren",
"appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Anzeigen",
"appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen",
"appSettings_notificationPermissionDenied": "Erlaubnis zur Benachrichtigung verweigert",
"appSettings_notificationsEnabled": "Benachrichtigungen aktiviert",
"appSettings_notificationsDisabled": "Benachrichtigungen deaktiviert",
"appSettings_messageNotifications": "Nachrichtenbenachrichtigungen",
"appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Nachrichten",
"appSettings_channelMessageNotifications": "Kanal-Nachrichten-Benachrichtigungen",
"appSettings_messageNotifications": "Direktnachrichten Benachrichtigungen",
"appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Direktnachrichten",
"appSettings_channelMessageNotifications": "Kanalnachrichten Benachrichtigungen",
"appSettings_channelMessageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfangen von Kanalnachrichten",
"appSettings_advertisementNotifications": "Werbeanzeigenbenachrichtigungen",
"appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen",
"appSettings_advertisementNotificationsSubtitle": "Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.",
"appSettings_messaging": "Nachrichten",
"appSettings_clearPathOnMaxRetry": "Klares Pfad bei Max Wiederholungsversuch",
"appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen des Kontaktpfads nach 5 fehlgeschlagenen Sendeverboten",
"appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen",
"appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen",
"appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.",
"appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.",
"appSettings_autoRouteRotation": "Automatische Routenrotation",
@@ -226,10 +226,10 @@
}
}
},
"appSettings_mapTimeFilter": "Kartent Zeitfilter",
"appSettings_mapTimeFilter": "Karten Zeitfilter",
"appSettings_showNodesDiscoveredWithin": "Zeige Knoten, die innerhalb von:",
"appSettings_allTime": "Alle Zeit",
"appSettings_lastHour": "Letzter Stunde",
"appSettings_allTime": "Ganzer Zeitverlauf",
"appSettings_lastHour": "Letzte Stunde",
"appSettings_last6Hours": "Letzte 6 Stunden",
"appSettings_last24Hours": "Letzte 24 Stunden",
"appSettings_lastWeek": "Letzte Woche",
@@ -252,13 +252,13 @@
"appSettings_appDebugLoggingEnabled": "App-Debug-Protokollierung aktiviert",
"appSettings_appDebugLoggingDisabled": "App-Debug-Protokollierung deaktiviert",
"contacts_title": "Kontakte",
"contacts_noContacts": "No Contacts noch",
"contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte Werbung machen.",
"contacts_noContacts": "Noch keine Kontakte vorhanden.",
"contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.",
"contacts_searchContacts": "Suche Kontakte...",
"contacts_noUnreadContacts": "Keine ungeklärten Kontakte",
"contacts_noUnreadContacts": "Keine ungesehene Kontakte",
"contacts_noContactsFound": "Keine Kontakte oder Gruppen gefunden.",
"contacts_deleteContact": "Löschen Sie Kontakt",
"contacts_removeConfirm": "Entfernen {contactName} aus den Kontakten?",
"contacts_deleteContact": "Lösche den Kontakt",
"contacts_removeConfirm": "{contactName} aus den Kontakten entfernen?",
"@contacts_removeConfirm": {
"placeholders": {
"contactName": {
@@ -266,12 +266,12 @@
}
}
},
"contacts_manageRepeater": "Wiederholung verwalten",
"contacts_manageRepeater": "Wiederholungen verwalten",
"contacts_roomLogin": "Raum-Login",
"contacts_openChat": "Öffnen Sie Chat",
"contacts_editGroup": "Gruppen bearbeiten",
"contacts_openChat": "Öffne Chat",
"contacts_editGroup": "Gruppe bearbeiten",
"contacts_deleteGroup": "Löschen Gruppe",
"contacts_deleteGroupConfirm": "Löschen Sie \"{groupName}\"?",
"contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?",
"@contacts_deleteGroupConfirm": {
"placeholders": {
"groupName": {
@@ -293,8 +293,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "Letztes Ansehen jetzt",
"contacts_lastSeenMinsAgo": "Letzte Sichtung {minutes} Minuten her.",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -303,7 +303,7 @@
}
},
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Aktivität vor {hours} Stunden.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -339,8 +339,8 @@
"channels_publicChannel": "Öffentlicher Kanal",
"channels_privateChannel": "Privater Kanal",
"channels_editChannel": "Kanal bearbeiten",
"channels_deleteChannel": "Löschen Sie Kanal",
"channels_deleteChannelConfirm": "Löschen \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"channels_deleteChannel": "Lösche den Kanal",
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"@channels_deleteChannelConfirm": {
"placeholders": {
"name": {
@@ -373,7 +373,7 @@
}
}
},
"channels_editChannelTitle": "Bearbeiteten Kanal {index}",
"channels_editChannelTitle": "Bearbeiteter Kanal {index}",
"@channels_editChannelTitle": {
"placeholders": {
"index": {
@@ -392,10 +392,10 @@
},
"channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt",
"channels_sortBy": "Sortiere nach",
"channels_sortManual": "Manuelle",
"channels_sortManual": "Manuell",
"channels_sortAZ": "A bis Z",
"channels_sortLatestMessages": "Letzte Nachrichten",
"channels_sortUnread": "Unlescht",
"channels_sortUnread": "Ungelesen",
"chat_noMessages": "Noch keine Nachrichten.",
"chat_sendMessageToStart": "Eine Nachricht senden, um anzufangen.",
"chat_originalMessageNotFound": "Originalmeldung nicht gefunden",
@@ -407,7 +407,7 @@
}
}
},
"chat_replyTo": "Antworten Sie {name}",
"chat_replyTo": "Antwort an {name}",
"@chat_replyTo": {
"placeholders": {
"name": {
@@ -436,7 +436,7 @@
"chat_messageCopied": "Nachricht kopiert",
"chat_messageDeleted": "Nachricht gelöscht",
"chat_retryingMessage": "Versuche es erneut.",
"chat_retryCount": "Versuchen {current}/{max}",
"chat_retryCount": "Versuche {current}/{max}",
"@chat_retryCount": {
"placeholders": {
"current": {
@@ -457,22 +457,22 @@
"emojiCategoryObjects": "Objekte",
"gifPicker_title": "Wähle ein GIF",
"gifPicker_searchHint": "Suche nach GIFs...",
"gifPicker_poweredBy": "Angetrieben von GIPHY",
"gifPicker_poweredBy": "Bereitgestellt von GIPHY",
"gifPicker_noGifsFound": "Keine GIFs gefunden",
"gifPicker_failedLoad": "GIF-Dateien konnten nicht geladen werden.",
"gifPicker_failedLoad": "GIF-Datei konnten nicht geladen werden.",
"gifPicker_failedSearch": "Suche nach GIFs fehlgeschlagen",
"gifPicker_noInternet": "Keine Internetverbindung",
"debugLog_appTitle": "App-Debug-Protokoll",
"debugLog_bleTitle": "BLE-Debug-Protokoll",
"debugLog_copyLog": "Kopieren Sie Protokoll",
"debugLog_clearLog": "Log löschen",
"debugLog_copyLog": "Kopieren des Protokolls",
"debugLog_clearLog": "Protokoll löschen",
"debugLog_copied": "Debug-Protokoll kopiert",
"debugLog_bleCopied": "BLE-Protokoll kopiert",
"debugLog_noEntries": "No Debug-Protokolle noch verfügbar",
"debugLog_enableInSettings": "Aktivieren Sie das App-Debug-Logging in den Einstellungen",
"debugLog_frames": "Rahmen",
"debugLog_rawLogRx": "Roh-Log-RX",
"debugLog_noBleActivity": "No BLE-Aktivität bisher",
"debugLog_noBleActivity": "Bisher keine BLE-Aktivität",
"debugFrame_length": "Rahmenlänge: {count} Bytes",
"@debugFrame_length": {
"placeholders": {
@@ -539,12 +539,12 @@
"chat_pathManagement": "Pfadverwaltung",
"chat_routingMode": "Routenmodus",
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"chat_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"chat_forceFloodMode": "Flut-Modus erzwingen",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
"chat_hopSingular": "Springe",
"chat_hopPlural": "Hops",
"chat_hopsCount": "{count} {count, plural, =1{Hop} other{Hops}}",
"chat_hopSingular": "Sprung",
"chat_hopPlural": "Sprünge",
"chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -552,17 +552,17 @@
}
}
},
"chat_successes": "Erfolgreiche",
"chat_successes": "Erfolgreich",
"chat_removePath": "Pfad entfernen",
"chat_noPathHistoryYet": "Noe eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_pathActions": "Pfadaktionen:",
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
"chat_setCustomPathSubtitle": "Manuelle Routenpfad festlegen",
"chat_clearPath": "Klares Pfad",
"chat_clearPathSubtitle": "Zwinge bei nächster Sendung eine erneute Entdeckung durch.",
"chat_pathCleared": "Pfad freigelegt. Nächste Nachricht wird Route neu entdecken.",
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
"chat_clearPath": "Pfad zurücksetzen",
"chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.",
"chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.",
"chat_floodModeSubtitle": "Verwende den Routingschalter in der App-Leiste",
"chat_floodModeEnabled": "Flutmodus aktiviert. Über den Routing-Icon in der App-Leiste wieder aktivieren.",
"chat_floodModeEnabled": "Flutmodus aktiviert.",
"chat_fullPath": "Vollständiger Pfad",
"chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.",
"chat_pathSetHops": "Pfad gesetzt: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -576,15 +576,15 @@
}
}
},
"chat_pathSavedLocally": "Gespeichert lokal. Mit Verbinden zum Synchronisieren.",
"chat_pathSavedLocally": "Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.",
"chat_pathDeviceConfirmed": "Gerät bestätigt.",
"chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.",
"chat_type": "Gib ein",
"chat_type": "Gebe ein",
"chat_path": "Pfad",
"chat_publicKey": "Öffentlicher Schlüssel",
"chat_compressOutgoingMessages": "Komprimieren ausgehende Nachrichten",
"chat_floodForced": "Überschwemmung (erzwungen)",
"chat_directForced": "Direkt (gezwungen)",
"chat_compressOutgoingMessages": "Komprimieren ausgehender Nachrichten",
"chat_floodForced": "Geflutet (erzwungen)",
"chat_directForced": "Direkt (erzwungen)",
"chat_hopsForced": "{count} Sprünge (erzwungen)",
"@chat_hopsForced": {
"placeholders": {
@@ -593,10 +593,10 @@
}
}
},
"chat_floodAuto": "Überschwemmung (automatisch)",
"chat_floodAuto": "Geflutet (automatisch)",
"chat_direct": "Direkt",
"chat_poiShared": "Gemeinsamer POI",
"chat_unread": "Unlescht: {count}",
"chat_poiShared": "Geteilter POI",
"chat_unread": "Ungelesen: {count}",
"@chat_unread": {
"placeholders": {
"count": {
@@ -604,9 +604,21 @@
}
}
},
"map_title": "Knotenkarte",
"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\nteilen,\num auf der Karte\nerscheinen.",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
"map_nodesCount": "Knoten: {count}",
"@map_nodesCount": {
"placeholders": {
@@ -623,24 +635,24 @@
}
}
},
"map_chat": "Chat",
"map_repeater": "Wiederholung",
"map_chat": "Benutzer",
"map_repeater": "Repeater",
"map_room": "Raum",
"map_sensor": "Sensor",
"map_pinDm": "Sperren (DM)",
"map_pinPrivate": "Privat-Pin",
"map_pinPublic": "Öffentliche Taste (PIN)",
"map_pinDm": "Pin (Kontakt)",
"map_pinPrivate": "Pin (Channel)",
"map_pinPublic": "Pin (Public)",
"map_lastSeen": "Letzte Sichtung",
"map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?",
"map_from": "Von",
"map_source": "Quelle",
"map_flags": "Flaggen",
"map_shareMarkerHere": "Teilen Sie hier das Marker.",
"map_pinLabel": "Kennzeichnungslabel",
"map_flags": "Flags",
"map_shareMarkerHere": "Teilen Sie den Marker hier.",
"map_pinLabel": "Pin Name",
"map_label": "Label",
"map_pointOfInterest": "Punkt von Interesse",
"map_sendToContact": "Senden an Kontakt",
"map_sendToChannel": "Senden Sie Kanal",
"map_sendToChannel": "Senden an Kanal",
"map_noChannelsAvailable": "Keine Kanäle verfügbar",
"map_publicLocationShare": "Öffentliche Standortfreigabe",
"map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.",
@@ -652,25 +664,25 @@
}
},
"map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen",
"map_filterNodes": "Filter Knoten",
"map_filterNodes": "Knotenfilter",
"map_nodeTypes": "Knotentypen",
"map_chatNodes": "Chat-Knoten",
"map_repeaters": "Wiederholer",
"map_repeaters": "Repeater",
"map_otherNodes": "Andere Knoten",
"map_keyPrefix": "Schlüsselpräfix",
"map_filterByKeyPrefix": "Filter nach Schlüsselpräfix",
"map_publicKeyPrefix": "Öffentlicher Schlüsselpräfix",
"map_publicKeyPrefix": "Schlüsselpräfix",
"map_markers": "Marker",
"map_showSharedMarkers": "Zeige gemeinsam genutzte Marker",
"map_lastSeenTime": "Letzte Sichtung",
"map_sharedPin": "Gemeinsames Passwort",
"map_joinRoom": "Beitreten Sie dem Raum",
"map_manageRepeater": "Wiederholung verwalten",
"map_manageRepeater": "Repeater verwalten",
"mapCache_title": "Offline-Karten-Cache",
"mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.",
"mapCache_noTilesToDownload": "Keine Tiles für diese Region zum Herunterladen verfügbar.",
"mapCache_downloadTilesTitle": "Herunterladen von Tiles",
"mapCache_downloadTilesPrompt": "Laden {count} Tiles für den Offline-Bereich herunter?",
"mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.",
"mapCache_downloadTilesTitle": "Herunterladen von Kacheln",
"mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?",
"@mapCache_downloadTilesPrompt": {
"placeholders": {
"count": {
@@ -679,7 +691,7 @@
}
},
"mapCache_downloadAction": "Herunterladen",
"mapCache_cachedTiles": "Zwischengespeicherte {count} Fliesen",
"mapCache_cachedTiles": "Zwischengespeicherte {count} Kacheln",
"@mapCache_cachedTiles": {
"placeholders": {
"count": {
@@ -687,7 +699,7 @@
}
}
},
"mapCache_cachedTilesWithFailed": "Zwischengespeicherte {downloaded} Tiles ({failed} fehlgeschlagen)",
"mapCache_cachedTilesWithFailed": "Zwischengespeicherte {downloaded} Kacheln ({failed} fehlgeschlagen)",
"@mapCache_cachedTilesWithFailed": {
"placeholders": {
"downloaded": {
@@ -698,7 +710,7 @@
}
}
},
"mapCache_clearOfflineCacheTitle": "Leeren Offline-Cache",
"mapCache_clearOfflineCacheTitle": "Leere Offline-Cache",
"mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenraster entfernen?",
"mapCache_offlineCacheCleared": "Offline-Cache gelöscht",
"mapCache_noAreaSelected": "Kein Bereich ausgewählt",
@@ -724,7 +736,7 @@
}
}
},
"mapCache_downloadTilesButton": "Herunterladen von Tiles",
"mapCache_downloadTilesButton": "Herunterladen von Kacheln",
"mapCache_clearCacheButton": "Cache leeren",
"mapCache_failedDownloads": "Fehlgeschlagene Downloads: {count}",
"@mapCache_failedDownloads": {
@@ -785,10 +797,10 @@
"time_month": "Monat",
"time_months": "Monate",
"time_minutes": "Minuten",
"time_allTime": "Alle Zeit",
"time_allTime": "Ganzer Zeitraum",
"dialog_disconnect": "Trennen",
"dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?",
"login_repeaterLogin": "Wiederholungseingang anmelden",
"login_repeaterLogin": "Beim Repeater anmelden",
"login_roomLogin": "Raum-Login",
"login_password": "Passwort",
"login_enterPassword": "Passwort eingeben",
@@ -799,7 +811,7 @@
"login_routing": "Routen",
"login_routingMode": "Routenmodus",
"login_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"login_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"login_forceFloodMode": "Flut-Modus erzwingen",
"login_managePaths": "Pfadverwaltung",
"login_login": "Anmelden",
"login_attempt": "Versuche {current}/{max}",
@@ -821,9 +833,10 @@
}
}
},
"login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.",
"common_reload": "Neu laden",
"common_clear": "Löschen",
"path_currentPath": "Aktiger Pfad: {path}",
"path_currentPath": "Aktiver Pfad: {path}",
"@path_currentPath": {
"placeholders": {
"path": {
@@ -839,9 +852,9 @@
}
}
},
"path_enterCustomPath": "Gib Pfad an",
"path_enterCustomPath": "Gebe Pfad ein",
"path_currentPathLabel": "Aktueller Pfad",
"path_hexPrefixInstructions": "Gib für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
@@ -858,7 +871,7 @@
},
"path_tooLong": "Pfad zu lang. Maximal 64 Hops erlaubt.",
"path_setPath": "Pfad festlegen",
"repeater_management": "Wiederholungselement-Verwaltung",
"repeater_management": "Repeater-Verwaltung",
"repeater_managementTools": "Verwaltungs-Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, Statistiken und Nachbarn anzeigen",
@@ -867,11 +880,11 @@
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Sende Befehle an den Repeater",
"repeater_settings": "Einstellungen",
"repeater_settingsSubtitle": "Wiederholungsparameter konfigurieren",
"repeater_statusTitle": "Wiederholungszustand",
"repeater_settingsSubtitle": "Repeater-parameter konfigurieren",
"repeater_statusTitle": "Repeaterstatus",
"repeater_routingMode": "Routenmodus",
"repeater_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"repeater_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"repeater_forceFloodMode": "Flut-Modus erzwingen",
"repeater_pathManagement": "Pfadverwaltung",
"repeater_refresh": "Aktualisieren",
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
@@ -963,9 +976,9 @@
}
}
},
"repeater_settingsTitle": "Wiederholungseinstellungen",
"repeater_settingsTitle": "Repeater Einstellungen",
"repeater_basicSettings": "Grundlegende Einstellungen",
"repeater_repeaterName": "Wiederholungseintrag",
"repeater_repeaterName": "Repeater Name",
"repeater_repeaterNameHelper": "Anzeigename für diesen Repeater",
"repeater_adminPassword": "Admin-Passwort",
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
@@ -978,7 +991,7 @@
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bandbreite",
"repeater_spreadingFactor": "Verteilungsfaktor",
"repeater_codingRate": "Programmierpauschale",
"repeater_codingRate": "Kodierungsrate",
"repeater_locationSettings": "Standort Einstellungen",
"repeater_latitude": "Breitengrad",
"repeater_latitudeHelper": "Dezimalgrad (z.B. 37,7749)",
@@ -989,10 +1002,10 @@
"repeater_packetForwardingSubtitle": "Aktivieren Sie den Repeater, um Pakete weiterzuleiten.",
"repeater_guestAccess": "Gastzugriff",
"repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen",
"repeater_privacyMode": "Privatschutzzustand",
"repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Anzeigen",
"repeater_advertisementSettings": "Werbe Einstellungen",
"repeater_localAdvertInterval": "Lokaler Werbeintervall",
"repeater_privacyMode": "Privatsphäreeinstellung",
"repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen",
"repeater_advertisementSettings": "Ankündigungseinstellungen",
"repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen",
"repeater_localAdvertIntervalMinutes": "{minutes} Minuten",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1001,7 +1014,7 @@
}
}
},
"repeater_floodAdvertInterval": "Überschwemmungsanzeige-Intervall",
"repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen",
"repeater_floodAdvertIntervalHours": "{hours} Stunden",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1010,7 +1023,7 @@
}
}
},
"repeater_encryptedAdvertInterval": "Verschlüsselte Werbeintervall",
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
"repeater_dangerZone": "Gefahrenzone",
"repeater_rebootRepeater": "Neustart Repeater",
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
@@ -1050,12 +1063,12 @@
},
"repeater_refreshBasicSettings": "Grundlegende Einstellungen aktualisieren",
"repeater_refreshRadioSettings": "Radio-Einstellungen aktualisieren",
"repeater_refreshTxPower": "Batterie-Strom aktualisieren",
"repeater_refreshTxPower": "Sendeleistung aktualisieren",
"repeater_refreshLocationSettings": "Aktualisieren Sie die Standort Einstellungen",
"repeater_refreshPacketForwarding": "Aktualisieren Paketweiterleitung",
"repeater_refreshGuestAccess": "Aktualisieren Sie den Gastzugriff",
"repeater_refreshPrivacyMode": "Wiederherstellen des Datenschutzzustands",
"repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Werbe Einstellungen",
"repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen",
"repeater_refreshed": "{label} wurde aktualisiert",
"@repeater_refreshed": {
"placeholders": {
@@ -1072,10 +1085,10 @@
}
}
},
"repeater_cliTitle": "Wiederholung CLI",
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
"repeater_commandHelp": "Hilfe",
"repeater_clearHistory": "Löschung der Historie",
"repeater_clearHistory": "Löschen der Historie",
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
@@ -1096,37 +1109,37 @@
"repeater_cliQuickGetTx": "Erhalte TX",
"repeater_cliQuickNeighbors": "Nachbarn",
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Werben",
"repeater_cliQuickAdvertise": "Ankündigungen",
"repeater_cliQuickClock": "Uhr",
"repeater_cliHelpAdvert": "Sendet ein Werbepaket",
"repeater_cliHelpAdvert": "Sendet eine Ankündigung",
"repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)",
"repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.",
"repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.",
"repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.",
"repeater_cliHelpClearStats": "Setzt verschiedene Statistikkalkulate auf Null zurück.",
"repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.",
"repeater_cliHelpSetAf": "Legt den Luftzeitfaktor fest.",
"repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)",
"repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.",
"repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber kann nicht in den Raum geschickt werden. (nur lesen möglich).",
"repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).",
"repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)",
"repeater_cliHelpSetIntThresh": "Legt den Interferenzeniveau (in dB) fest. Der Standardwert ist 14. Auf 0 setzen, um die Erkennung von Kanalinterferenzen zu deaktivieren.",
"repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetMultiAcks": "Aktiviert oder deaktiviert die Funktion 'Doppel-ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Werbe-Paket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Werbungspakets fest. Auf 0 setzen, um es zu deaktivieren.",
"repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.",
"repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)",
"repeater_cliHelpSetName": "Legt den Anzeigenamen fest.",
"repeater_cliHelpSetLat": "Legt die Breitengrad-Angabe der Werbekarte fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Werbe-Map fest. (dezimale Grad)",
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
"repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.",
"repeater_cliHelpSetBridgeSource": "Wählen Sie, ob die Brücke empfangene oder gesendete Pakete erneut übertragen soll.",
"repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.",
"repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.",
"repeater_cliHelpSetBridgeSecret": "Richte das Espnow-Brücken-Geheimnis ein.",
"repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.",
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
@@ -1134,9 +1147,9 @@
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Werbung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
"repeater_cliHelpRegion": "(Serien nur) Listet alle definierten Regionen und aktuelle Hochwassermissungen auf.",
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
@@ -1228,12 +1241,12 @@
"channelPath_title": "Paketpfad",
"channelPath_viewMap": "Karte anzeigen",
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
"channelPath_repeaterHops": "Wiederholungs-Sprünge",
"channelPath_repeaterHops": "Repeater-Sprünge",
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
"channelPath_messageDetails": "Nachrichtsdetails",
"channelPath_senderLabel": "Sender",
"channelPath_timeLabel": "Zeit",
"channelPath_repeatsLabel": "Wiederholung",
"channelPath_repeatsLabel": "Wiederholungen",
"channelPath_pathLabel": "Pfad {index}",
"channelPath_observedLabel": "Beobachtet",
"channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}",
@@ -1271,7 +1284,7 @@
}
},
"channelPath_unknownPath": "Unbekannt",
"channelPath_floodPath": "Überschwemmung",
"channelPath_floodPath": "Geflutet",
"channelPath_directPath": "Direkt",
"channelPath_observedZeroOf": "0 von {total} Sprüngen",
"@channelPath_observedZeroOf": {
@@ -1327,13 +1340,198 @@
"listFilter_tooltip": "Filteren und sortieren",
"listFilter_sortBy": "Sortiere nach",
"listFilter_latestMessages": "Letzte Nachrichten",
"listFilter_heardRecently": "Hörte kürzlich",
"listFilter_heardRecently": "Kürzlich gehört",
"listFilter_az": "A-Z",
"listFilter_filters": "Filtere",
"listFilter_all": "Alle",
"listFilter_users": "Benutzer",
"listFilter_repeaters": "Wiederholer",
"listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_newGroup": "Neue Gruppe"
"listFilter_newGroup": "Neue Gruppe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
"channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.",
"channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei",
"channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.",
"channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei",
"channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.",
"channels_scanQrCode": "Scannen Sie einen QR-Code",
"channels_scanQrCodeComingSoon": "Bald verfügbar",
"channels_enterHashtag": "Gib Hashtag ein",
"channels_hashtagHint": "z.B. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hörte: {time} vor her.",
"neighbors_unknownContact": "Unbekannte {pubkey}",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
"settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.",
"contacts_manageRoom": "Raum-Server verwalten",
"room_management": "Raum-Server-Verwaltung",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_create": "Erstelle Community",
"community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.",
"community_join": "Beitreten",
"community_joinTitle": "Tritt der Community bei",
"community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?",
"community_scanQr": "Scannen Sie die Community QR-Code",
"community_scanInstructions": "Richten Sie die Kamera auf einen Community-QR-Code.",
"community_showQr": "Zeige QR-Code",
"community_publicChannel": "Community Öffentlich",
"community_enterName": "Bitte Community-Name eingeben",
"community_title": "Community",
"community_created": "Community \"{name}\" wurde erstellt",
"community_joined": "Community \"{name}\" beigetreten",
"community_qrTitle": "Teile Community",
"community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.",
"community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_invalidQrCode": "Ungültiger Community-QR-Code",
"community_alreadyMember": "Bereits registriert",
"community_alreadyMemberMessage": "Sie sind bereits Mitglied von \"{name}\".",
"community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu",
"community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen",
"community_noCommunities": "Noch keiner Community beigetreten",
"community_scanOrCreate": "Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.",
"community_manageCommunities": "Verwalten von Communities",
"community_delete": "Verlasse Community",
"community_deleteConfirm": "\"{name}\" verlassen?",
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlassen",
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
"community_selectCommunity": "Wählen Sie Community",
"community_regularHashtag": "Regulärer Hashtag",
"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_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": "Geheime Wiederherstellung 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": "Neu generieren Sie das Geheimnis",
"community_secretUpdated": "Geheime für \"{name}\" aktualisiert",
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
"community_updateSecret": "Aktualisieren Sie das Geheimnis"
}
+171 -3
View File
@@ -8,6 +8,7 @@
"nav_map": "Map",
"common_cancel": "Cancel",
"common_ok": "OK",
"common_connect": "Connect",
"common_unknownDevice": "Unknown Device",
"common_save": "Save",
@@ -83,9 +84,13 @@
"settings_radioSettingsUpdated": "Radio settings updated",
"settings_location": "Location",
"settings_locationSubtitle": "GPS coordinates",
"settings_locationUpdated": "Location updated",
"settings_locationUpdated": "Location and GPS settings updated",
"settings_locationBothRequired": "Enter both latitude and longitude.",
"settings_locationInvalid": "Invalid latitude or longitude.",
"settings_locationGPSEnable": "GPS Enable",
"settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.",
"settings_locationIntervalSec": "Interval for GPS (Seconds)",
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_privacyMode": "Privacy Mode",
@@ -253,7 +258,8 @@
}
},
"contacts_manageRepeater": "Manage Repeater",
"contacts_roomLogin": "Room Login",
"contacts_manageRoom": "Manage Room Server",
"contacts_roomLogin": "Room Server Login",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Edit Group",
"contacts_deleteGroup": "Delete Group",
@@ -361,6 +367,18 @@
"channels_sortAZ": "A-Z",
"channels_sortLatestMessages": "Latest messages",
"channels_sortUnread": "Unread",
"channels_createPrivateChannel": "Create a Private Channel",
"channels_createPrivateChannelDesc": "Secured with a secret key.",
"channels_joinPrivateChannel": "Join a Private Channel",
"channels_joinPrivateChannelDesc": "Manually enter a secret key.",
"channels_joinPublicChannel": "Join the Public Channel",
"channels_joinPublicChannelDesc": "Anyone can join this channel.",
"channels_joinHashtagChannel": "Join a Hashtag Channel",
"channels_joinHashtagChannelDesc": "Anyone can join hashtag channels.",
"channels_scanQrCode": "Scan a QR Code",
"channels_scanQrCodeComingSoon": "Coming soon",
"channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team",
"chat_noMessages": "No messages yet",
"chat_sendMessageToStart": "Send a message to get started",
@@ -532,6 +550,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",
@@ -685,7 +713,7 @@
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
"login_enterPassword": "Enter password",
"login_savePassword": "Save password",
@@ -711,6 +739,8 @@
"error": {"type": "String"}
}
},
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
"common_reload": "Reload",
"common_clear": "Clear",
@@ -746,6 +776,7 @@
"path_setPath": "Set Path",
"repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_managementTools": "Management Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
@@ -753,6 +784,8 @@
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Send commands to the repeater",
"repeater_neighbours": "Neighbors",
"repeater_neighboursSubtitle": "View zero hop neighbors.",
"repeater_settings": "Settings",
"repeater_settingsSubtitle": "Configure repeater parameters",
@@ -1055,6 +1088,29 @@
"fahrenheit": {"type": "String"}
}
},
"neighbors_receivedData": "Received Neighbours Data",
"neighbors_requestTimedOut": "Neighbours request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}",
"@neighbors_errorLoading": {
"placeholders": {
"error": {"type": "String"}
}
},
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
"neighbors_noData": "No neighbours data available.",
"neighbors_unknownContact": "Unknown {pubkey}",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {"type": "String"}
}
},
"neighbors_heardAgo": "Heard: {time} ago",
"@neighbors_heardAgo": {
"placeholders": {
"time": {"type": "String"}
}
},
"channelPath_title": "Packet Path",
"channelPath_viewMap": "View map",
"channelPath_otherObservedPaths": "Other Observed Paths",
@@ -1129,6 +1185,118 @@
},
"channelPath_noHopDetailsAvailable": "No hop details available for this packet.",
"channelPath_unknownRepeater": "Unknown Repeater",
"community_title": "Community",
"community_create": "Create Community",
"community_createDesc": "Create a new community and share via QR code.",
"community_join": "Join",
"community_joinTitle": "Join Community",
"community_joinConfirmation": "Do you want to join the community \"{name}\"?",
"@community_joinConfirmation": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanQr": "Scan Community QR",
"community_scanInstructions": "Point the camera at a community QR code",
"community_showQr": "Show QR Code",
"community_publicChannel": "Community Public",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_enterName": "Enter community name",
"community_created": "Community \"{name}\" created",
"@community_created": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_joined": "Joined community \"{name}\"",
"@community_joined": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_qrTitle": "Share Community",
"community_qrInstructions": "Scan this QR code to join \"{name}\"",
"@community_qrInstructions": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community",
"community_invalidQrCode": "Invalid community QR code",
"community_alreadyMember": "Already a Member",
"community_alreadyMemberMessage": "You are already a member of \"{name}\".",
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addPublicChannel": "Add Community Public Channel",
"community_addPublicChannelHint": "Automatically add the public channel for this community",
"community_noCommunities": "No communities joined yet",
"community_scanOrCreate": "Scan a QR code or create a community to get started",
"community_manageCommunities": "Manage Communities",
"community_delete": "Leave Community",
"community_deleteConfirm": "Leave \"{name}\"?",
"@community_deleteConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_deleteChannelsWarning": "This will also delete {count} channel(s) and their messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Left community \"{name}\"",
"@community_deleted": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerateSecret": "Regenerate Secret",
"community_regenerateSecretConfirm": "Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerate": "Regenerate",
"community_secretRegenerated": "Secret regenerated for \"{name}\"",
"@community_secretRegenerated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_updateSecret": "Update Secret",
"community_secretUpdated": "Secret updated for \"{name}\"",
"@community_secretUpdated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"",
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addHashtagChannel": "Add Community Hashtag",
"community_addHashtagChannelDesc": "Add a hashtag channel for this community",
"community_selectCommunity": "Select Community",
"community_regularHashtag": "Regular Hashtag",
"community_regularHashtagDesc": "Public hashtag (anyone can join)",
"community_communityHashtag": "Community Hashtag",
"community_communityHashtagDesc": "Private to community members",
"community_forCommunity": "For {name}",
"@community_forCommunity": {
"placeholders": {
"name": {"type": "String"}
}
},
"listFilter_tooltip": "Filter and sort",
"listFilter_sortBy": "Sort by",
"listFilter_latestMessages": "Latest messages",
+199 -1
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",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.",
"common_reload": "Recargar",
"common_clear": "Borrar",
"path_currentPath": "Ruta actual: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de la sala",
"listFilter_unreadOnly": "Solo sin leer",
"listFilter_newGroup": "Nuevo grupo"
"listFilter_newGroup": "Nuevo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vecinos",
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
"channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.",
"channels_joinPublicChannel": "Únete al Canal Público",
"channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.",
"channels_joinHashtagChannel": "Únete a un Canal con Hashtag",
"channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.",
"channels_scanQrCode": "Escanear un Código QR",
"channels_scanQrCodeComingSoon": "Próximamente",
"channels_enterHashtag": "Introducir hashtag",
"channels_hashtagHint": "ej. #equipo",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clave pública desconocida {pubkey}",
"neighbors_heardAgo": "Escuchado: {time} hace atrás",
"settings_locationGPSEnable": "Habilitar GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para 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",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Crear Comunidad",
"community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.",
"community_title": "Comunidad",
"community_join": "Únete",
"community_joinTitle": "Únete a la comunidad",
"community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?",
"community_scanQr": "Escanear Código QR de la Comunidad",
"community_scanInstructions": "Apunte la cámara a un código QR de la comunidad",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidad Pública",
"community_hashtagChannel": "Hashtag de la Comunidad",
"community_name": "Nombre de la comunidad",
"common_ok": "De acuerdo",
"community_enterName": "Introducir nombre de comunidad",
"community_created": "Comunidad \"{name}\" creada",
"community_joined": "Se unió a la comunidad \"{name}\"",
"community_qrTitle": "Compartir Comunidad",
"community_qrInstructions": "Escanear este código QR para unirte a {name}",
"community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad",
"community_invalidQrCode": "Código QR de comunidad no válido",
"community_alreadyMember": "Ya eres Miembro",
"community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".",
"community_addPublicChannel": "Añadir Canal Público de la Comunidad",
"community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.",
"community_noCommunities": "Aún no se han unido comunidades.",
"community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar",
"community_manageCommunities": "Gestionar Comunidades",
"community_delete": "Salir de la Comunidad",
"community_deleteConfirm": "¿Salir de \"{name}\"?",
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
"community_addHashtagChannel": "Añadir Hashtag de la Comunidad",
"community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad",
"community_selectCommunity": "Seleccionar Comunidad",
"community_regularHashtag": "Etiqueta de Hashtag Regular",
"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_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"
}
+244 -46
View File
@@ -91,12 +91,12 @@
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_privacyMode": "Mode de confidentialité",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les publicités.",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
"settings_privacyModeEnabled": "Mode de confidentialité activé",
"settings_privacyModeDisabled": "Mode de confidentialité désactivé",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Envoyer la publicité",
"settings_sendAdvertisement": "S'annoncer",
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
"settings_advertisementSent": "Annonce envoyée",
"settings_syncTime": "Temps de synchronisation",
@@ -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",
@@ -176,7 +176,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Notifications",
"appSettings_enableNotifications": "Activer les Notifications",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les publicités",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces",
"appSettings_notificationPermissionDenied": "Permission de notification refusée",
"appSettings_notificationsEnabled": "Notifications activées",
"appSettings_notificationsDisabled": "Notifications désactivées",
@@ -184,7 +184,7 @@
"appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages",
"appSettings_channelMessageNotifications": "Notifications des Messages de Canal",
"appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal",
"appSettings_advertisementNotifications": "Notifications publicitaires",
"appSettings_advertisementNotifications": "Notifications d'annonces",
"appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds",
"appSettings_messaging": "Messagerie",
"appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry",
@@ -192,7 +192,7 @@
"appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.",
"appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.",
"appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode inondation",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)",
"appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée",
"appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée",
"appSettings_battery": "Batterie",
@@ -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": {
@@ -539,7 +539,7 @@
"chat_pathManagement": "Gestion des chemins",
"chat_routingMode": "Mode de routage",
"chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"chat_forceFloodMode": "Mode Inondation Forcée",
"chat_forceFloodMode": "Mode tout le réseau forcé",
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
"chat_hopSingular": "Sautez",
@@ -562,7 +562,7 @@
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
"chat_floodModeEnabled": "Le mode inondation est activé. Réactiver via l'icône de routage dans la barre d'outils.",
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
"chat_fullPath": "Chemin complet",
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
"chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,7 +583,7 @@
"chat_path": "Chemin",
"chat_publicKey": "Clé Publique",
"chat_compressOutgoingMessages": "Compresser les messages sortants",
"chat_floodForced": "Inondation (forcée)",
"chat_floodForced": "Tout le réseau (forcée)",
"chat_directForced": "Direct (forcé)",
"chat_hopsForced": "{count} sauts (forcés)",
"@chat_hopsForced": {
@@ -593,7 +593,7 @@
}
}
},
"chat_floodAuto": "Inondation (auto)",
"chat_floodAuto": "Tout le réseau (auto)",
"chat_direct": "Afficher",
"chat_poiShared": "Point d'intérêt Partagé",
"chat_unread": "Non lu : {count}",
@@ -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": {
@@ -799,7 +811,7 @@
"login_routing": "Redirection",
"login_routingMode": "Mode de routage",
"login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"login_forceFloodMode": "Mode Inondation Forcée",
"login_forceFloodMode": "Mode tout le réseau forcé",
"login_managePaths": "Gérer les chemins",
"login_login": "Connexion",
"login_attempt": "Essayer {current}/{max}",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.",
"common_reload": "Recharger",
"common_clear": "Effacer",
"path_currentPath": "Chemin actuel : {path}",
@@ -871,7 +884,7 @@
"repeater_statusTitle": "État du répétiteur",
"repeater_routingMode": "Mode de routage",
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"repeater_forceFloodMode": "Mode de submersion forcée",
"repeater_forceFloodMode": "Mode tout le réseau forcé",
"repeater_pathManagement": "Gestion des chemins",
"repeater_refresh": "Rafraîchir",
"repeater_statusRequestTimeout": "Demande de statut délai dépassé.",
@@ -898,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": {
@@ -916,7 +929,7 @@
}
}
},
"repeater_packetTxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +943,7 @@
}
}
},
"repeater_packetRxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +957,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Inondation : {flood}, Direct : {direct}",
"repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -990,9 +1003,9 @@
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
"repeater_privacyMode": "Mode de confidentialité",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"repeater_advertisementSettings": "Paramètres de Publicité",
"repeater_localAdvertInterval": "Intervalle Publicité Locale",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"repeater_advertisementSettings": "Paramètres d'annonces",
"repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)",
"repeater_localAdvertIntervalMinutes": "{minutes} minutes",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1001,7 +1014,7 @@
}
}
},
"repeater_floodAdvertInterval": "Intervalle de Publicité Inondation",
"repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)",
"repeater_floodAdvertIntervalHours": "{hours} heures",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1010,8 +1023,8 @@
}
}
},
"repeater_encryptedAdvertInterval": "Intervalle publicitaire crypté",
"repeater_dangerZone": "Zone d'alerte",
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone dangereuse",
"repeater_rebootRepeater": "Redémarrer Répéteur",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
@@ -1055,7 +1068,7 @@
"repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets",
"repeater_refreshGuestAccess": "Rafraîchir l'accès invité",
"repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres de la Publicité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces",
"repeater_refreshed": "{label} rafraîchi",
"@repeater_refreshed": {
"placeholders": {
@@ -1098,7 +1111,7 @@
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Publier",
"repeater_cliQuickClock": "Horloge",
"repeater_cliHelpAdvert": "Envoie un paquet publicitaire",
"repeater_cliHelpAdvert": "Envoie un paquet d'annonce",
"repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)",
"repeater_cliHelpClock": "Affiche l'heure actuelle par l'horloge de chaque appareil.",
"repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.",
@@ -1107,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.",
@@ -1115,12 +1128,12 @@
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
"repeater_cliHelpSetName": "Définit le nom de la publicité.",
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
"repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)",
"repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)",
"repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.",
"repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode inondation pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.",
"repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.",
"repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.",
@@ -1134,9 +1147,9 @@
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des publicités sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations de débordement actuelles.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
"repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"",
"repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.",
@@ -1271,7 +1284,7 @@
}
},
"channelPath_unknownPath": "Inconnu",
"channelPath_floodPath": "Inondation",
"channelPath_floodPath": "Tout le réseau",
"channelPath_directPath": "Afficher",
"channelPath_observedZeroOf": "0 de {total} sauts",
"@channelPath_observedZeroOf": {
@@ -1326,14 +1339,199 @@
"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": {
"type": "String"
}
}
},
"repeater_neighbours": "Voisins",
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
"channels_createPrivateChannel": "Créer un Canal Privé",
"channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.",
"channels_joinPublicChannel": "Rejoindre le canal public",
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"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": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Communauté",
"community_create": "Créer une Communauté",
"community_createDesc": "Créer une nouvelle communauté et la partager via QR code.",
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
"community_name": "Nom de la communauté",
"community_enterName": "Entrez le nom de la communauté",
"community_created": "Communauté \"{name}\" créée",
"community_joined": "Rejoint la communauté \"{name}\"",
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag 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_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}\""
}
+199 -1
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",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
"common_reload": "Ricaricare",
"common_clear": "Cancella",
"path_currentPath": "Percorso corrente: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Ripetitori",
"listFilter_roomServers": "Server della stanza",
"listFilter_unreadOnly": "Solo non letto",
"listFilter_newGroup": "Nuovo gruppo"
"listFilter_newGroup": "Nuovo gruppo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vicini",
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Sentito: {time} fa",
"neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}",
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci Server Camera",
"room_management": "Gestione del Server di Camera",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"community_scanQr": "Scansiona il QR Code della Community",
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
"community_showQr": "Mostra il codice QR",
"community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
"community_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"community_deleteConfirm": "Uscire da \"{name}\"?",
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
"community_addHashtagChannel": "Aggiungi Hashtag della Community",
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare",
"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_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}\""
}
+487 -3
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'),
];
@@ -150,6 +154,12 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get common_cancel;
/// No description provided for @common_ok.
///
/// In en, this message translates to:
/// **'OK'**
String get common_ok;
/// No description provided for @common_connect.
///
/// In en, this message translates to:
@@ -465,7 +475,7 @@ abstract class AppLocalizations {
/// No description provided for @settings_locationUpdated.
///
/// In en, this message translates to:
/// **'Location updated'**
/// **'Location and GPS settings updated'**
String get settings_locationUpdated;
/// No description provided for @settings_locationBothRequired.
@@ -480,6 +490,30 @@ abstract class AppLocalizations {
/// **'Invalid latitude or longitude.'**
String get settings_locationInvalid;
/// No description provided for @settings_locationGPSEnable.
///
/// In en, this message translates to:
/// **'GPS Enable'**
String get settings_locationGPSEnable;
/// No description provided for @settings_locationGPSEnableSubtitle.
///
/// In en, this message translates to:
/// **'Enables GPS to automatically update location.'**
String get settings_locationGPSEnableSubtitle;
/// No description provided for @settings_locationIntervalSec.
///
/// In en, this message translates to:
/// **'Interval for GPS (Seconds)'**
String get settings_locationIntervalSec;
/// No description provided for @settings_locationIntervalInvalid.
///
/// In en, this message translates to:
/// **'Interval must be at least 60 seconds, and less than 86400 seconds.'**
String get settings_locationIntervalInvalid;
/// No description provided for @settings_latitude.
///
/// In en, this message translates to:
@@ -1284,10 +1318,16 @@ abstract class AppLocalizations {
/// **'Manage Repeater'**
String get contacts_manageRepeater;
/// No description provided for @contacts_manageRoom.
///
/// In en, this message translates to:
/// **'Manage Room Server'**
String get contacts_manageRoom;
/// No description provided for @contacts_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get contacts_roomLogin;
/// No description provided for @contacts_openChat.
@@ -1596,6 +1636,78 @@ abstract class AppLocalizations {
/// **'Unread'**
String get channels_sortUnread;
/// No description provided for @channels_createPrivateChannel.
///
/// In en, this message translates to:
/// **'Create a Private Channel'**
String get channels_createPrivateChannel;
/// No description provided for @channels_createPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Secured with a secret key.'**
String get channels_createPrivateChannelDesc;
/// No description provided for @channels_joinPrivateChannel.
///
/// In en, this message translates to:
/// **'Join a Private Channel'**
String get channels_joinPrivateChannel;
/// No description provided for @channels_joinPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Manually enter a secret key.'**
String get channels_joinPrivateChannelDesc;
/// No description provided for @channels_joinPublicChannel.
///
/// In en, this message translates to:
/// **'Join the Public Channel'**
String get channels_joinPublicChannel;
/// No description provided for @channels_joinPublicChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join this channel.'**
String get channels_joinPublicChannelDesc;
/// No description provided for @channels_joinHashtagChannel.
///
/// In en, this message translates to:
/// **'Join a Hashtag Channel'**
String get channels_joinHashtagChannel;
/// No description provided for @channels_joinHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join hashtag channels.'**
String get channels_joinHashtagChannelDesc;
/// No description provided for @channels_scanQrCode.
///
/// In en, this message translates to:
/// **'Scan a QR Code'**
String get channels_scanQrCode;
/// No description provided for @channels_scanQrCodeComingSoon.
///
/// In en, this message translates to:
/// **'Coming soon'**
String get channels_scanQrCodeComingSoon;
/// No description provided for @channels_enterHashtag.
///
/// In en, this message translates to:
/// **'Enter hashtag'**
String get channels_enterHashtag;
/// No description provided for @channels_hashtagHint.
///
/// In en, this message translates to:
/// **'e.g. #team'**
String get channels_hashtagHint;
/// No description provided for @chat_noMessages.
///
/// In en, this message translates to:
@@ -2118,6 +2230,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:
@@ -2600,7 +2742,7 @@ abstract class AppLocalizations {
/// No description provided for @login_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get login_roomLogin;
/// No description provided for @login_password.
@@ -2687,6 +2829,12 @@ abstract class AppLocalizations {
/// **'Login failed: {error}'**
String login_failed(String error);
/// No description provided for @login_failedMessage.
///
/// In en, this message translates to:
/// **'Login failed. Either the password is incorrect or the repeater is unreachable.'**
String get login_failedMessage;
/// No description provided for @common_reload.
///
/// In en, this message translates to:
@@ -2789,6 +2937,12 @@ abstract class AppLocalizations {
/// **'Repeater Management'**
String get repeater_management;
/// No description provided for @room_management.
///
/// In en, this message translates to:
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_managementTools.
///
/// In en, this message translates to:
@@ -2831,6 +2985,18 @@ abstract class AppLocalizations {
/// **'Send commands to the repeater'**
String get repeater_cliSubtitle;
/// No description provided for @repeater_neighbours.
///
/// In en, this message translates to:
/// **'Neighbors'**
String get repeater_neighbours;
/// No description provided for @repeater_neighboursSubtitle.
///
/// In en, this message translates to:
/// **'View zero hop neighbors.'**
String get repeater_neighboursSubtitle;
/// No description provided for @repeater_settings.
///
/// In en, this message translates to:
@@ -3970,6 +4136,48 @@ abstract class AppLocalizations {
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
/// **'Received Neighbours Data'**
String get neighbors_receivedData;
/// No description provided for @neighbors_requestTimedOut.
///
/// In en, this message translates to:
/// **'Neighbours request timed out.'**
String get neighbors_requestTimedOut;
/// No description provided for @neighbors_errorLoading.
///
/// In en, this message translates to:
/// **'Error loading neighbors: {error}'**
String neighbors_errorLoading(String error);
/// No description provided for @neighbors_repeatersNeighbours.
///
/// In en, this message translates to:
/// **'Repeaters Neighbours'**
String get neighbors_repeatersNeighbours;
/// No description provided for @neighbors_noData.
///
/// In en, this message translates to:
/// **'No neighbours data available.'**
String get neighbors_noData;
/// No description provided for @neighbors_unknownContact.
///
/// In en, this message translates to:
/// **'Unknown {pubkey}'**
String neighbors_unknownContact(String pubkey);
/// No description provided for @neighbors_heardAgo.
///
/// In en, this message translates to:
/// **'Heard: {time} ago'**
String neighbors_heardAgo(String time);
/// No description provided for @channelPath_title.
///
/// In en, this message translates to:
@@ -4138,6 +4346,276 @@ abstract class AppLocalizations {
/// **'Unknown Repeater'**
String get channelPath_unknownRepeater;
/// No description provided for @community_title.
///
/// In en, this message translates to:
/// **'Community'**
String get community_title;
/// No description provided for @community_create.
///
/// In en, this message translates to:
/// **'Create Community'**
String get community_create;
/// No description provided for @community_createDesc.
///
/// In en, this message translates to:
/// **'Create a new community and share via QR code.'**
String get community_createDesc;
/// No description provided for @community_join.
///
/// In en, this message translates to:
/// **'Join'**
String get community_join;
/// No description provided for @community_joinTitle.
///
/// In en, this message translates to:
/// **'Join Community'**
String get community_joinTitle;
/// No description provided for @community_joinConfirmation.
///
/// In en, this message translates to:
/// **'Do you want to join the community \"{name}\"?'**
String community_joinConfirmation(String name);
/// No description provided for @community_scanQr.
///
/// In en, this message translates to:
/// **'Scan Community QR'**
String get community_scanQr;
/// No description provided for @community_scanInstructions.
///
/// In en, this message translates to:
/// **'Point the camera at a community QR code'**
String get community_scanInstructions;
/// No description provided for @community_showQr.
///
/// In en, this message translates to:
/// **'Show QR Code'**
String get community_showQr;
/// No description provided for @community_publicChannel.
///
/// In en, this message translates to:
/// **'Community Public'**
String get community_publicChannel;
/// No description provided for @community_hashtagChannel.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_hashtagChannel;
/// No description provided for @community_name.
///
/// In en, this message translates to:
/// **'Community Name'**
String get community_name;
/// No description provided for @community_enterName.
///
/// In en, this message translates to:
/// **'Enter community name'**
String get community_enterName;
/// No description provided for @community_created.
///
/// In en, this message translates to:
/// **'Community \"{name}\" created'**
String community_created(String name);
/// No description provided for @community_joined.
///
/// In en, this message translates to:
/// **'Joined community \"{name}\"'**
String community_joined(String name);
/// No description provided for @community_qrTitle.
///
/// In en, this message translates to:
/// **'Share Community'**
String get community_qrTitle;
/// No description provided for @community_qrInstructions.
///
/// In en, this message translates to:
/// **'Scan this QR code to join \"{name}\"'**
String community_qrInstructions(String name);
/// No description provided for @community_hashtagPrivacyHint.
///
/// In en, this message translates to:
/// **'Community hashtag channels are only joinable by members of the community'**
String get community_hashtagPrivacyHint;
/// No description provided for @community_invalidQrCode.
///
/// In en, this message translates to:
/// **'Invalid community QR code'**
String get community_invalidQrCode;
/// No description provided for @community_alreadyMember.
///
/// In en, this message translates to:
/// **'Already a Member'**
String get community_alreadyMember;
/// No description provided for @community_alreadyMemberMessage.
///
/// In en, this message translates to:
/// **'You are already a member of \"{name}\".'**
String community_alreadyMemberMessage(String name);
/// No description provided for @community_addPublicChannel.
///
/// In en, this message translates to:
/// **'Add Community Public Channel'**
String get community_addPublicChannel;
/// No description provided for @community_addPublicChannelHint.
///
/// In en, this message translates to:
/// **'Automatically add the public channel for this community'**
String get community_addPublicChannelHint;
/// No description provided for @community_noCommunities.
///
/// In en, this message translates to:
/// **'No communities joined yet'**
String get community_noCommunities;
/// No description provided for @community_scanOrCreate.
///
/// In en, this message translates to:
/// **'Scan a QR code or create a community to get started'**
String get community_scanOrCreate;
/// No description provided for @community_manageCommunities.
///
/// In en, this message translates to:
/// **'Manage Communities'**
String get community_manageCommunities;
/// No description provided for @community_delete.
///
/// In en, this message translates to:
/// **'Leave Community'**
String get community_delete;
/// No description provided for @community_deleteConfirm.
///
/// In en, this message translates to:
/// **'Leave \"{name}\"?'**
String community_deleteConfirm(String name);
/// No description provided for @community_deleteChannelsWarning.
///
/// In en, this message translates to:
/// **'This will also delete {count} channel(s) and their messages.'**
String community_deleteChannelsWarning(int count);
/// No description provided for @community_deleted.
///
/// In en, this message translates to:
/// **'Left community \"{name}\"'**
String community_deleted(String name);
/// No description provided for @community_regenerateSecret.
///
/// In en, this message translates to:
/// **'Regenerate Secret'**
String get community_regenerateSecret;
/// No description provided for @community_regenerateSecretConfirm.
///
/// In en, this message translates to:
/// **'Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.'**
String community_regenerateSecretConfirm(String name);
/// No description provided for @community_regenerate.
///
/// In en, this message translates to:
/// **'Regenerate'**
String get community_regenerate;
/// No description provided for @community_secretRegenerated.
///
/// In en, this message translates to:
/// **'Secret regenerated for \"{name}\"'**
String community_secretRegenerated(String name);
/// No description provided for @community_updateSecret.
///
/// In en, this message translates to:
/// **'Update Secret'**
String get community_updateSecret;
/// No description provided for @community_secretUpdated.
///
/// In en, this message translates to:
/// **'Secret updated for \"{name}\"'**
String community_secretUpdated(String name);
/// No description provided for @community_scanToUpdateSecret.
///
/// In en, this message translates to:
/// **'Scan the new QR code to update the secret for \"{name}\"'**
String community_scanToUpdateSecret(String name);
/// No description provided for @community_addHashtagChannel.
///
/// In en, this message translates to:
/// **'Add Community Hashtag'**
String get community_addHashtagChannel;
/// No description provided for @community_addHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Add a hashtag channel for this community'**
String get community_addHashtagChannelDesc;
/// No description provided for @community_selectCommunity.
///
/// In en, this message translates to:
/// **'Select Community'**
String get community_selectCommunity;
/// No description provided for @community_regularHashtag.
///
/// In en, this message translates to:
/// **'Regular Hashtag'**
String get community_regularHashtag;
/// No description provided for @community_regularHashtagDesc.
///
/// In en, this message translates to:
/// **'Public hashtag (anyone can join)'**
String get community_regularHashtagDesc;
/// No description provided for @community_communityHashtag.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_communityHashtag;
/// No description provided for @community_communityHashtagDesc.
///
/// In en, this message translates to:
/// **'Private to community members'**
String get community_communityHashtagDesc;
/// No description provided for @community_forCommunity.
///
/// In en, this message translates to:
/// **'For {name}'**
String community_forCommunity(String name);
/// No description provided for @listFilter_tooltip.
///
/// In en, this message translates to:
@@ -4231,9 +4709,11 @@ class _AppLocalizationsDelegate
'nl',
'pl',
'pt',
'ru',
'sk',
'sl',
'sv',
'uk',
'zh',
].contains(locale.languageCode);
@@ -4262,12 +4742,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();
}
+286
View File
@@ -23,6 +23,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_cancel => 'Отказ';
@override
String get common_ok => 'Добре';
@override
String get common_connect => 'Свържи се';
@@ -201,6 +204,20 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_locationInvalid => 'Невалидна ширина или дължина.';
@override
String get settings_locationGPSEnable => 'Активиране на GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Активирайте автоматичното актуализиране на местоположението чрез GPS.';
@override
String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)';
@override
String get settings_locationIntervalInvalid =>
'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.';
@override
String get settings_latitude => 'Широчина';
@@ -650,6 +667,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get contacts_manageRoom => 'Управление на сървър за стая';
@override
String get contacts_roomLogin => 'Вход в стаята';
@@ -830,6 +850,45 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_sortUnread => 'Непрочетено';
@override
String get channels_createPrivateChannel => 'Създай Частен Канал';
@override
String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.';
@override
String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал';
@override
String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.';
@override
String get channels_joinPublicChannel =>
'Присъединете се към Публичния канал';
@override
String get channels_joinPublicChannelDesc =>
'Всеки може да се присъедини към този канал.';
@override
String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал';
@override
String get channels_joinHashtagChannelDesc =>
'Всеки може да се присъедини към хаштаговите канали.';
@override
String get channels_scanQrCode => 'Сканирайте QR код';
@override
String get channels_scanQrCodeComingSoon => 'Ще излезе скоро';
@override
String get channels_enterHashtag => 'Въведете хаштаг';
@override
String get channels_hashtagHint => 'напр. #отбор';
@override
String get chat_noMessages => 'Няма съобщения.';
@@ -1148,6 +1207,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 => 'Карта на възлите';
@@ -1475,6 +1552,10 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Входът не беше успешен: $error';
}
@override
String get login_failedMessage =>
'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.';
@override
String get common_reload => 'Презареди';
@@ -1544,6 +1625,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_management => 'Управление на повторители';
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_managementTools => 'Инструменти за управление';
@@ -1567,6 +1651,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbours => 'Съседи';
@override
String get repeater_neighboursSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
String get repeater_settings => 'Настройки';
@@ -2252,6 +2343,33 @@ class AppLocalizationsBg extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Получени данни за съседи';
@override
String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.';
@override
String neighbors_errorLoading(String error) {
return 'Грешка при зареждане на съседи: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
@override
String get neighbors_noData => 'Няма налични данни за съседи.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Неизвестна $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Слушано преди $time.';
}
@override
String get channelPath_title => 'Пътеки пъзел';
@@ -2355,6 +2473,174 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Неизвестен повторител';
@override
String get community_title => 'Общност';
@override
String get community_create => 'Създай общност';
@override
String get community_createDesc =>
'Създайте нова общност и я споделете чрез QR код.';
@override
String get community_join => 'Присъедини се';
@override
String get community_joinTitle => 'Присъедини се към общността';
@override
String community_joinConfirmation(String name) {
return 'Искате ли да се присъедините към общността \"$name\"?';
}
@override
String get community_scanQr => 'Сканирайте QR кода на общността';
@override
String get community_scanInstructions =>
'Насочете камерата към QR код на общността';
@override
String get community_showQr => 'Покажи QR код';
@override
String get community_publicChannel => 'Обществено общност';
@override
String get community_hashtagChannel => 'Хаштаг на общността';
@override
String get community_name => 'Име на общността';
@override
String get community_enterName => 'Въведете име на общността';
@override
String community_created(String name) {
return 'Общността \"$name\" е създадена';
}
@override
String community_joined(String name) {
return 'Присъединено общност \"$name\"';
}
@override
String get community_qrTitle => 'Споделяне в общността';
@override
String community_qrInstructions(String name) {
return 'Сканирайте този QR код, за да се присъедините към $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Хаштаг каналите на общността са достъпни само за членове на общността';
@override
String get community_invalidQrCode => 'Невалиден QR код на общността';
@override
String get community_alreadyMember => 'Вече съм член';
@override
String community_alreadyMemberMessage(String name) {
return 'Вие вече сте член на \"$name\".';
}
@override
String get community_addPublicChannel => 'Добави публичен общностен канал';
@override
String get community_addPublicChannelHint =>
'Автоматично добавете публичния канал за тази общност.';
@override
String get community_noCommunities => 'Няма присъединени общности още.';
@override
String get community_scanOrCreate =>
'Сканирайте QR код или създайте общност, за да започнете.';
@override
String get community_manageCommunities => 'Управление на общности';
@override
String get community_delete => 'Напусни общността';
@override
String community_deleteConfirm(String name) {
return 'Напускате \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Това ще изтрие също $count канал(а) и техните съобщения.';
}
@override
String community_deleted(String name) {
return 'Остави общността \"$name\"';
}
@override
String get community_regenerateSecret => 'Регенерейрай секрет';
@override
String community_regenerateSecretConfirm(String name) {
return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
}
@override
String get community_regenerate => 'Регенерация';
@override
String community_secretRegenerated(String name) {
return 'Секретно презареждане за \"$name\"';
}
@override
String get community_updateSecret => 'Актуализирай тайна';
@override
String community_secretUpdated(String name) {
return 'Секретно обновено за \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
}
@override
String get community_addHashtagChannel => 'Добави общностен хаштаг';
@override
String get community_addHashtagChannelDesc =>
'Добавете хаштаг канал за тази общност';
@override
String get community_selectCommunity => 'Изберете общност';
@override
String get community_regularHashtag => 'Обикновен хаштаг';
@override
String get community_regularHashtagDesc =>
'Общ хаштаг (всеки може да се присъедини)';
@override
String get community_communityHashtag => 'Общностен хаштаг';
@override
String get community_communityHashtagDesc => 'Само за членове на общността';
@override
String community_forCommunity(String name) {
return 'За $name';
}
@override
String get listFilter_tooltip => 'Филтрирайте и сортирайте';
File diff suppressed because it is too large Load Diff
+285 -3
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_cancel => 'Cancel';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connect';
@@ -190,7 +193,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_locationSubtitle => 'GPS coordinates';
@override
String get settings_locationUpdated => 'Location updated';
String get settings_locationUpdated => 'Location and GPS settings updated';
@override
String get settings_locationBothRequired =>
@@ -199,6 +202,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_locationInvalid => 'Invalid latitude or longitude.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
@override
String get settings_latitude => 'Latitude';
@@ -641,7 +658,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_manageRepeater => 'Manage Repeater';
@override
String get contacts_roomLogin => 'Room Login';
String get contacts_manageRoom => 'Manage Room Server';
@override
String get contacts_roomLogin => 'Room Server Login';
@override
String get contacts_openChat => 'Open Chat';
@@ -818,6 +838,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_sortUnread => 'Unread';
@override
String get channels_createPrivateChannel => 'Create a Private Channel';
@override
String get channels_createPrivateChannelDesc => 'Secured with a secret key.';
@override
String get channels_joinPrivateChannel => 'Join a Private Channel';
@override
String get channels_joinPrivateChannelDesc => 'Manually enter a secret key.';
@override
String get channels_joinPublicChannel => 'Join the Public Channel';
@override
String get channels_joinPublicChannelDesc => 'Anyone can join this channel.';
@override
String get channels_joinHashtagChannel => 'Join a Hashtag Channel';
@override
String get channels_joinHashtagChannelDesc =>
'Anyone can join hashtag channels.';
@override
String get channels_scanQrCode => 'Scan a QR Code';
@override
String get channels_scanQrCodeComingSoon => 'Coming soon';
@override
String get channels_enterHashtag => 'Enter hashtag';
@override
String get channels_hashtagHint => 'e.g. #team';
@override
String get chat_noMessages => 'No messages yet';
@@ -1129,6 +1186,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';
@@ -1402,7 +1477,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get login_repeaterLogin => 'Repeater Login';
@override
String get login_roomLogin => 'Room Login';
String get login_roomLogin => 'Room Server Login';
@override
String get login_password => 'Password';
@@ -1453,6 +1528,10 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Login failed: $error';
}
@override
String get login_failedMessage =>
'Login failed. Either the password is incorrect or the repeater is unreachable.';
@override
String get common_reload => 'Reload';
@@ -1520,6 +1599,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_management => 'Repeater Management';
@override
String get room_management => 'Room Server Management';
@override
String get repeater_managementTools => 'Management Tools';
@@ -1543,6 +1625,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbours => 'Neighbors';
@override
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@@ -2216,6 +2304,33 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Received Neighbours Data';
@override
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
@override
String neighbors_errorLoading(String error) {
return 'Error loading neighbors: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
@override
String get neighbors_noData => 'No neighbours data available.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unknown $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Heard: $time ago';
}
@override
String get channelPath_title => 'Packet Path';
@@ -2319,6 +2434,173 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Unknown Repeater';
@override
String get community_title => 'Community';
@override
String get community_create => 'Create Community';
@override
String get community_createDesc =>
'Create a new community and share via QR code.';
@override
String get community_join => 'Join';
@override
String get community_joinTitle => 'Join Community';
@override
String community_joinConfirmation(String name) {
return 'Do you want to join the community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Community QR';
@override
String get community_scanInstructions =>
'Point the camera at a community QR code';
@override
String get community_showQr => 'Show QR Code';
@override
String get community_publicChannel => 'Community Public';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Community Name';
@override
String get community_enterName => 'Enter community name';
@override
String community_created(String name) {
return 'Community \"$name\" created';
}
@override
String community_joined(String name) {
return 'Joined community \"$name\"';
}
@override
String get community_qrTitle => 'Share Community';
@override
String community_qrInstructions(String name) {
return 'Scan this QR code to join \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag channels are only joinable by members of the community';
@override
String get community_invalidQrCode => 'Invalid community QR code';
@override
String get community_alreadyMember => 'Already a Member';
@override
String community_alreadyMemberMessage(String name) {
return 'You are already a member of \"$name\".';
}
@override
String get community_addPublicChannel => 'Add Community Public Channel';
@override
String get community_addPublicChannelHint =>
'Automatically add the public channel for this community';
@override
String get community_noCommunities => 'No communities joined yet';
@override
String get community_scanOrCreate =>
'Scan a QR code or create a community to get started';
@override
String get community_manageCommunities => 'Manage Communities';
@override
String get community_delete => 'Leave Community';
@override
String community_deleteConfirm(String name) {
return 'Leave \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'This will also delete $count channel(s) and their messages.';
}
@override
String community_deleted(String name) {
return 'Left community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate 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.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Add Community Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Add a hashtag channel for this community';
@override
String get community_selectCommunity => 'Select Community';
@override
String get community_regularHashtag => 'Regular Hashtag';
@override
String get community_regularHashtagDesc => 'Public hashtag (anyone can join)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Private to community members';
@override
String community_forCommunity(String name) {
return 'For $name';
}
@override
String get listFilter_tooltip => 'Filter and sort';
+289
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'De acuerdo';
@override
String get common_connect => 'Conectar';
@@ -200,6 +203,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitud o longitud inválidos.';
@override
String get settings_locationGPSEnable => 'Habilitar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override
String get settings_latitude => 'Latitud';
@@ -648,6 +665,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestionar Repetidor';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override
String get contacts_roomLogin => 'Inicio de Sala';
@@ -829,6 +849,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_sortUnread => 'Sin leer';
@override
String get channels_createPrivateChannel => 'Crear un Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Cifrado con una clave secreta.';
@override
String get channels_joinPrivateChannel => 'Únete a un Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Introducir manualmente una clave secreta.';
@override
String get channels_joinPublicChannel => 'Únete al Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Cualquiera puede unirse a este canal.';
@override
String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Cualquiera puede unirse a los canales de hashtag.';
@override
String get channels_scanQrCode => 'Escanear un Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Próximamente';
@override
String get channels_enterHashtag => 'Introducir hashtag';
@override
String get channels_hashtagHint => 'ej. #equipo';
@override
String get chat_noMessages => 'Aún no hay mensajes';
@@ -1144,6 +1204,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';
@@ -1472,6 +1550,10 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Inicio fallido: $error';
}
@override
String get login_failedMessage =>
'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.';
@override
String get common_reload => 'Recargar';
@@ -1541,6 +1623,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_management => 'Gestión de Repetidores';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@@ -1564,6 +1649,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbours => 'Vecinos';
@override
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@@ -2248,6 +2339,34 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
@override
String get neighbors_requestTimedOut =>
'Los vecinos solicitan que se desconecte.';
@override
String neighbors_errorLoading(String error) {
return 'Error al cargar vecinos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clave pública desconocida $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Escuchado: $time hace atrás';
}
@override
String get channelPath_title => 'Ruta del Paquete';
@@ -2351,6 +2470,176 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconocido';
@override
String get community_title => 'Comunidad';
@override
String get community_create => 'Crear Comunidad';
@override
String get community_createDesc =>
'Crear una nueva comunidad y compartir a través de código QR.';
@override
String get community_join => 'Únete';
@override
String get community_joinTitle => 'Únete a la comunidad';
@override
String community_joinConfirmation(String name) {
return '¿Quieres unirte a la comunidad \"$name\"?';
}
@override
String get community_scanQr => 'Escanear Código QR de la Comunidad';
@override
String get community_scanInstructions =>
'Apunte la cámara a un código QR de la comunidad';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidad Pública';
@override
String get community_hashtagChannel => 'Hashtag de la Comunidad';
@override
String get community_name => 'Nombre de la comunidad';
@override
String get community_enterName => 'Introducir nombre de comunidad';
@override
String community_created(String name) {
return 'Comunidad \"$name\" creada';
}
@override
String community_joined(String name) {
return 'Se unió a la comunidad \"$name\"';
}
@override
String get community_qrTitle => 'Compartir Comunidad';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para unirte a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad';
@override
String get community_invalidQrCode => 'Código QR de comunidad no válido';
@override
String get community_alreadyMember => 'Ya eres Miembro';
@override
String community_alreadyMemberMessage(String name) {
return 'Ya eres miembro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Añadir Canal Público de la Comunidad';
@override
String get community_addPublicChannelHint =>
'Añade automáticamente el canal público para esta comunidad.';
@override
String get community_noCommunities => 'Aún no se han unido comunidades.';
@override
String get community_scanOrCreate =>
'Escanear un código QR o crear una comunidad para comenzar';
@override
String get community_manageCommunities => 'Gestionar Comunidades';
@override
String get community_delete => 'Salir de la Comunidad';
@override
String community_deleteConfirm(String name) {
return '¿Salir de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Esto también eliminará $count canal(es) y sus mensajes.';
}
@override
String community_deleted(String name) {
return 'Has salido de la comunidad \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Código secreto regenerado para \"$name\"';
}
@override
String get community_updateSecret => 'Actualizar Contraseña';
@override
String community_secretUpdated(String name) {
return 'Confidencialidad actualizada para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
}
@override
String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad';
@override
String get community_addHashtagChannelDesc =>
'Añadir un canal con hashtag para esta comunidad';
@override
String get community_selectCommunity => 'Seleccionar Comunidad';
@override
String get community_regularHashtag => 'Etiqueta de Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (cualquiera puede unirse)';
@override
String get community_communityHashtag => 'Hashtag de la Comunidad';
@override
String get community_communityHashtagDesc =>
'Exclusivo para miembros de la comunidad';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar y ordenar';
+339 -46
View File
@@ -23,6 +23,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_cancel => 'Annuler';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connecter';
@@ -200,6 +203,21 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitude ou longitude invalide.';
@override
String get settings_locationGPSEnable => 'Activer le GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Activer la mise à jour automatique de la position via GPS';
@override
String get settings_locationIntervalSec =>
'Intervalle de mise-à-jour du GPS (Secondes)';
@override
String get settings_locationIntervalInvalid =>
'L\'intervalle doit être compris entre 60 et 86400 secondes.';
@override
String get settings_latitude => 'Latitude';
@@ -211,11 +229,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_privacyModeSubtitle =>
'Cacher le nom/l\'emplacement dans les publicités';
'Cacher le nom/l\'emplacement dans les annonces';
@override
String get settings_privacyModeToggle =>
'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les publicités.';
'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.';
@override
String get settings_privacyModeEnabled => 'Mode de confidentialité activé';
@@ -228,7 +246,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_actions => 'Actions';
@override
String get settings_sendAdvertisement => 'Envoyer la publicité';
String get settings_sendAdvertisement => 'S\'annoncer';
@override
String get settings_sendAdvertisementSubtitle =>
@@ -255,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';
@@ -438,7 +456,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_enableNotificationsSubtitle =>
'Recevoir des notifications pour les messages et les publicités';
'Recevoir des notifications pour les messages et les annonces';
@override
String get appSettings_notificationPermissionDenied =>
@@ -467,7 +485,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_advertisementNotifications =>
'Notifications publicitaires';
'Notifications d\'annonces';
@override
String get appSettings_advertisementNotificationsSubtitle =>
@@ -498,7 +516,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_autoRouteRotationSubtitle =>
'Alterner entre les meilleurs chemins et le mode inondation';
'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)';
@override
String get appSettings_autoRouteRotationEnabled =>
@@ -649,6 +667,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gérer le répétiteur';
@override
String get contacts_manageRoom => 'Gérer le Room Server';
@override
String get contacts_roomLogin => 'Connexion Salle';
@@ -667,7 +688,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';
@@ -691,27 +712,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
@@ -825,11 +846,51 @@ 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';
@override
String get channels_createPrivateChannel => 'Créer un Canal Privé';
@override
String get channels_createPrivateChannelDesc =>
'Sécurisé avec une clé secrète.';
@override
String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé';
@override
String get channels_joinPrivateChannelDesc =>
'Entrer manuellement une clé secrète.';
@override
String get channels_joinPublicChannel => 'Rejoindre le canal public';
@override
String get channels_joinPublicChannelDesc =>
'Tout le monde peut rejoindre ce canal.';
@override
String get channels_joinHashtagChannel => 'Rejoindre un Canal Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'N\'importe qui peut rejoindre les canaux #hashtag.';
@override
String get channels_scanQrCode => 'Scanner un code QR';
@override
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
@override
String get channels_enterHashtag => 'Entrez le hashtag';
@override
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@@ -876,7 +937,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String chat_retryCount(int current, int max) {
return 'Réessayer $current/$max';
return 'Essai $current/$max';
}
@override
@@ -1016,7 +1077,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
@override
String get chat_forceFloodMode => 'Mode Inondation Forcée';
String get chat_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get chat_recentAckPaths =>
@@ -1080,7 +1141,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_floodModeEnabled =>
'Le mode inondation est activé. Réactiver via l\'icône de routage dans la barre d\'outils.';
'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.';
@override
String get chat_fullPath => 'Chemin complet';
@@ -1125,7 +1186,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Compresser les messages sortants';
@override
String get chat_floodForced => 'Inondation (forcée)';
String get chat_floodForced => 'Tout le réseau (forcée)';
@override
String get chat_directForced => 'Direct (forcé)';
@@ -1136,7 +1197,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get chat_floodAuto => 'Inondation (auto)';
String get chat_floodAuto => 'Tout le réseau (auto)';
@override
String get chat_direct => 'Afficher';
@@ -1149,6 +1210,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';
@@ -1311,7 +1390,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
@@ -1365,11 +1444,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) {
@@ -1460,7 +1539,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
@override
String get login_forceFloodMode => 'Mode Inondation Forcée';
String get login_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get login_managePaths => 'Gérer les chemins';
@@ -1478,6 +1557,10 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Connexion échouée : $error';
}
@override
String get login_failedMessage =>
'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.';
@override
String get common_reload => 'Recharger';
@@ -1547,6 +1630,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_management => 'Gestion des répétiteurs';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Outils de Gestion';
@@ -1570,6 +1656,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
@override
String get repeater_neighbours => 'Voisins';
@override
String get repeater_neighboursSubtitle =>
'Afficher les voisins de saut nuls.';
@override
String get repeater_settings => 'Paramètres';
@@ -1588,7 +1681,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Auto (utiliser le chemin sauvegardé)';
@override
String get repeater_forceFloodMode => 'Mode de submersion forcée';
String get repeater_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get repeater_pathManagement => 'Gestion des chemins';
@@ -1651,7 +1744,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(
@@ -1665,17 +1758,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String repeater_packetTxTotal(int total, String flood, String direct) {
return 'Total : $total, Inondation : $flood, Direct : $direct';
return 'Total : $total, Tout le réseau : $flood, Direct : $direct';
}
@override
String repeater_packetRxTotal(int total, String flood, String direct) {
return 'Total : $total, Inondation : $flood, Direct : $direct';
return 'Total : $total, Tout le réseau : $flood, Direct : $direct';
}
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Inondation : $flood, Direct : $direct';
return 'Tout le réseau : $flood, Direct : $direct';
}
@override
@@ -1771,13 +1864,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_privacyModeSubtitle =>
'Cacher le nom/l\'emplacement dans les publicités';
'Cacher le nom/l\'emplacement dans les annonces';
@override
String get repeater_advertisementSettings => 'Paramètres de Publicité';
String get repeater_advertisementSettings => 'Paramètres d\'annonces';
@override
String get repeater_localAdvertInterval => 'Intervalle Publicité Locale';
String get repeater_localAdvertInterval =>
'Intervalle des annonces Locale (0 saut)';
@override
String repeater_localAdvertIntervalMinutes(int minutes) {
@@ -1786,7 +1880,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_floodAdvertInterval =>
'Intervalle de Publicité Inondation';
'Intervalle des annonces à tout le réseau (flood)';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@@ -1795,10 +1889,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_encryptedAdvertInterval =>
'Intervalle publicitaire crypté';
'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';
@@ -1886,7 +1980,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_refreshAdvertisementSettings =>
'Rafraîchir les Paramètres de la Publicité';
'Rafraîchir les Paramètres des annonces';
@override
String repeater_refreshed(String label) {
@@ -1960,7 +2054,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_cliQuickClock => 'Horloge';
@override
String get repeater_cliHelpAdvert => 'Envoie un paquet publicitaire';
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
@override
String get repeater_cliHelpReboot =>
@@ -1995,7 +2089,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 =>
@@ -2026,7 +2120,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")';
@override
String get repeater_cliHelpSetName => 'Définit le nom de la publicité.';
String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.';
@override
String get repeater_cliHelpSetLat =>
@@ -2046,7 +2140,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetTxDelay =>
'Définit un facteur multiplié par le temps de fonctionnement en mode inondation pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).';
'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).';
@override
String get repeater_cliHelpSetDirectTxDelay =>
@@ -2101,7 +2195,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpNeighbors =>
'Affiche une liste d\'autres nœuds répétiteurs entendus via des publicités sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
@override
String get repeater_cliHelpNeighborRemove =>
@@ -2109,7 +2203,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpRegion =>
'(série uniquement) Liste toutes les régions définies et les autorisations de débordement actuelles.';
'(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).';
@override
String get repeater_cliHelpRegionLoad =>
@@ -2261,6 +2355,34 @@ class AppLocalizationsFr extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Données des voisins reçues';
@override
String get neighbors_requestTimedOut => 'Les voisins demandent un délai.';
@override
String neighbors_errorLoading(String error) {
return 'Erreur lors du chargement des voisins : $error';
}
@override
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
@override
String get neighbors_noData =>
'Aucune donnée concernant les voisins disponible.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clé publique inconnue $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Écouté : $time auparavant';
}
@override
String get channelPath_title => 'Chemin de paquet';
@@ -2319,7 +2441,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channelPath_unknownPath => 'Inconnu';
@override
String get channelPath_floodPath => 'Inondation';
String get channelPath_floodPath => 'Tout le réseau';
@override
String get channelPath_directPath => 'Afficher';
@@ -2364,6 +2486,177 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Répéteur Inconnu';
@override
String get community_title => 'Communauté';
@override
String get community_create => 'Créer une Communauté';
@override
String get community_createDesc =>
'Créer une nouvelle communauté et la partager via QR code.';
@override
String get community_join => 'Rejoindre';
@override
String get community_joinTitle => 'Rejoindre la communauté';
@override
String community_joinConfirmation(String name) {
return 'Souhaitez-vous rejoindre la communauté \"$name\" ?';
}
@override
String get community_scanQr => 'Scanner la communauté QR';
@override
String get community_scanInstructions =>
'Pointez l\'appareil photo vers un code QR communautaire.';
@override
String get community_showQr => 'Afficher le QR Code';
@override
String get community_publicChannel => 'Communauté Publique';
@override
String get community_hashtagChannel => 'Hashtag Communauté';
@override
String get community_name => 'Nom de la communauté';
@override
String get community_enterName => 'Entrez le nom de la communauté';
@override
String community_created(String name) {
return 'Communauté \"$name\" créée';
}
@override
String community_joined(String name) {
return 'Rejoint la communauté \"$name\"';
}
@override
String get community_qrTitle => 'Partager Communauté';
@override
String community_qrInstructions(String name) {
return 'Scanner ce QR code pour rejoindre $name';
}
@override
String get community_hashtagPrivacyHint =>
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
@override
String get community_invalidQrCode => 'Code QR de communauté non valide';
@override
String get community_alreadyMember => 'Déjà membre';
@override
String community_alreadyMemberMessage(String name) {
return 'Vous êtes déjà membre de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Ajouter un Canal Public de la Communauté';
@override
String get community_addPublicChannelHint =>
'Ajouter automatiquement le canal public pour cette communauté';
@override
String get community_noCommunities =>
'Aucun groupe n\'a été rejoint pour le moment.';
@override
String get community_scanOrCreate =>
'Scanner un code QR ou créer une communauté pour commencer';
@override
String get community_manageCommunities => 'Gérer les Communautés';
@override
String get community_delete => 'Quitter la communauté';
@override
String community_deleteConfirm(String name) {
return 'Quitter \"$name\" ?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Cela supprimera également $count canal/canaux et leurs messages.';
}
@override
String community_deleted(String name) {
return 'Communauté \"$name\" quittée';
}
@override
String get community_regenerateSecret => 'Régénérer le secret';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Régénérer';
@override
String community_secretRegenerated(String name) {
return 'Mot de passe secret régénéré pour \"$name\"';
}
@override
String get community_updateSecret => 'Mettre à jour le secret';
@override
String community_secretUpdated(String name) {
return 'Modification secrète mise à jour pour \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
}
@override
String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté';
@override
String get community_addHashtagChannelDesc =>
'Ajouter un canal hashtag pour cette communauté';
@override
String get community_selectCommunity => 'Sélectionner Communauté';
@override
String get community_regularHashtag => 'Hashtag régulier';
@override
String get community_regularHashtagDesc =>
'Hashtag public (tout le monde peut rejoindre)';
@override
String get community_communityHashtag => 'Hashtag de la communauté';
@override
String get community_communityHashtagDesc =>
'Exclusif aux membres de la communauté';
@override
String community_forCommunity(String name) {
return 'Pour $name';
}
@override
String get listFilter_tooltip => 'Filtrer et trier';
@@ -2371,7 +2664,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';
@@ -2392,11 +2685,11 @@ 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';
}
+289
View File
@@ -23,6 +23,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_cancel => 'Annulla';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connetti';
@@ -200,6 +203,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitudine o longitudine non valida.';
@override
String get settings_locationGPSEnable => 'Abilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Abilita l\'aggiornamento automatico della posizione tramite GPS.';
@override
String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)';
@override
String get settings_locationIntervalInvalid =>
'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.';
@override
String get settings_latitude => 'Latitudine';
@@ -646,6 +663,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestisci Ripetitore';
@override
String get contacts_manageRoom => 'Gestisci Server Camera';
@override
String get contacts_roomLogin => 'Login Camera';
@@ -827,6 +847,46 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_sortUnread => 'Non letto';
@override
String get channels_createPrivateChannel => 'Crea un Canale Privato';
@override
String get channels_createPrivateChannelDesc =>
'Protetta con una chiave segreta.';
@override
String get channels_joinPrivateChannel => 'Unisciti a un Canale Privato';
@override
String get channels_joinPrivateChannelDesc =>
'Inserire manualmente una chiave segreta.';
@override
String get channels_joinPublicChannel => 'Unisciti al Canale Pubblico';
@override
String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.';
@override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.';
@override
String get channels_scanQrCode => 'Scansiona un codice QR';
@override
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override
String get channels_enterHashtag => 'Inserisci hashtag';
@override
String get channels_hashtagHint => 'es. #team';
@override
String get chat_noMessages => 'Nessun messaggio ancora';
@@ -1143,6 +1203,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';
@@ -1470,6 +1548,10 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Accesso fallito: $error';
}
@override
String get login_failedMessage =>
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
@override
String get common_reload => 'Ricaricare';
@@ -1539,6 +1621,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_management => 'Gestione Ripetitori';
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@@ -1562,6 +1647,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbours => 'Vicini';
@override
String get repeater_neighboursSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
String get repeater_settings => 'Impostazioni';
@@ -2248,6 +2340,33 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
@override
String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.';
@override
String neighbors_errorLoading(String error) {
return 'Errore nel caricamento dei vicini: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Chiave pubblica sconosciuta $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Sentito: $time fa';
}
@override
String get channelPath_title => 'Percorso Pacchetto';
@@ -2351,6 +2470,176 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override
String get community_title => 'Comunità';
@override
String get community_create => 'Crea Comunità';
@override
String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.';
@override
String get community_join => 'Unisciti';
@override
String get community_joinTitle => 'Unisciti alla Community';
@override
String community_joinConfirmation(String name) {
return 'Vuoi unirti alla community \"$name\"?';
}
@override
String get community_scanQr => 'Scansiona il QR Code della Community';
@override
String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità';
@override
String get community_showQr => 'Mostra il codice QR';
@override
String get community_publicChannel => 'Comunità Pubblica';
@override
String get community_hashtagChannel => 'Hashtag della Comunità';
@override
String get community_name => 'Nome della Comunità';
@override
String get community_enterName => 'Inserisci il nome della comunità';
@override
String community_created(String name) {
return 'Comunità \"$name\" creata';
}
@override
String community_joined(String name) {
return 'Unito alla comunità \"$name\"';
}
@override
String get community_qrTitle => 'Condividi Comunità';
@override
String community_qrInstructions(String name) {
return 'Scansiona questo codice QR per unirti a $name';
}
@override
String get community_hashtagPrivacyHint =>
'I canali hashtag della community sono accessibili solo ai membri della community';
@override
String get community_invalidQrCode => 'Codice QR della community non valido';
@override
String get community_alreadyMember => 'Già membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".';
}
@override
String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità';
@override
String get community_addPublicChannelHint =>
'Aggiungi automaticamente il canale pubblico per questa community';
@override
String get community_noCommunities => 'Nessun gruppo aggiunto finora';
@override
String get community_scanOrCreate =>
'Scansiona un codice QR o crea una community per iniziare.';
@override
String get community_manageCommunities => 'Gestisci Comunità';
@override
String get community_delete => 'Lascia la Comunità';
@override
String community_deleteConfirm(String name) {
return 'Uscire da \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
}
@override
String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"';
}
@override
String get community_regenerateSecret => 'Ri genera la chiave segreta';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Rigenera';
@override
String community_secretRegenerated(String name) {
return 'Codice segreto rigenerato per \"$name\"';
}
@override
String get community_updateSecret => 'Aggiorna Segreto';
@override
String community_secretUpdated(String name) {
return 'Segreto aggiornato per \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
}
@override
String get community_addHashtagChannel => 'Aggiungi Hashtag della Community';
@override
String get community_addHashtagChannelDesc =>
'Aggiungi un canale con hashtag per questa community';
@override
String get community_selectCommunity => 'Seleziona Comunità';
@override
String get community_regularHashtag => 'Hashtag regolare';
@override
String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)';
@override
String get community_communityHashtag => 'Hashtag della Comunità';
@override
String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità';
@override
String community_forCommunity(String name) {
return 'Per $name';
}
@override
String get listFilter_tooltip => 'Filtra e ordina';
+379 -92
View File
@@ -15,7 +15,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get nav_contacts => 'Contacten';
@override
String get nav_channels => 'Kanaal';
String get nav_channels => 'Kanalen';
@override
String get nav_map => 'Kaart';
@@ -23,6 +23,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_cancel => 'Annuleren';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Verbinden';
@@ -39,7 +42,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_close => 'Sluiten';
@override
String get common_edit => 'Bewerk';
String get common_edit => 'Bewerken';
@override
String get common_add => 'Toevoegen';
@@ -48,13 +51,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_settings => 'Instellingen';
@override
String get common_disconnect => 'Verbinden verbreken';
String get common_disconnect => 'Verbinding verbreken';
@override
String get common_connected => 'Verbonden';
@override
String get common_disconnected => 'Ontkoppeld';
String get common_disconnected => 'Verbinding verbroken';
@override
String get common_create => 'Maak';
@@ -78,7 +81,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_remove => 'Verwijderen';
@override
String get common_enable => 'Aktivatie';
String get common_enable => 'Activeren';
@override
String get common_disable => 'Uitschakelen';
@@ -87,7 +90,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_reboot => 'Herstarten';
@override
String get common_loading => 'Laad...';
String get common_loading => 'Laden...';
@override
String get common_notAvailable => '';
@@ -168,7 +171,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_nodeNameNotSet => 'Niet ingesteld';
@override
String get settings_nodeNameHint => 'Voer knooppuntnaam in';
String get settings_nodeNameHint => 'Voer nodenaam in';
@override
String get settings_nodeNameUpdated => 'Naam bijgewerkt';
@@ -200,6 +203,20 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_locationInvalid =>
'Ongeldige breedtegraad of lengtegraad.';
@override
String get settings_locationGPSEnable => 'GPS inschakelen';
@override
String get settings_locationGPSEnableSubtitle =>
'Activeer automatisch locatieupdates via GPS.';
@override
String get settings_locationIntervalSec => 'Interval voor GPS (Seconden)';
@override
String get settings_locationIntervalInvalid =>
'De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.';
@override
String get settings_latitude => 'Breedtegraad';
@@ -312,7 +329,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_infoContactsCount => 'Aantal Contacten';
@override
String get settings_infoChannelCount => 'Kanaal Aantal';
String get settings_infoChannelCount => 'Aantal Kanalen';
@override
String get settings_presets => 'Presets';
@@ -354,10 +371,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_longRange => 'Lang Bereik';
String get settings_longRange => 'Lange Afstand';
@override
String get settings_fastSpeed => 'Snelle Snelheid';
String get settings_fastSpeed => 'Hoge Snelheid';
@override
String settings_error(String message) {
@@ -377,7 +394,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_themeSystem => 'Standaardinstelling';
@override
String get appSettings_themeLight => 'Helder';
String get appSettings_themeLight => 'Licht';
@override
String get appSettings_themeDark => 'Donker';
@@ -469,13 +486,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_advertisementNotificationsSubtitle =>
'Toon notificatie wanneer nieuwe knooppunten worden ontdekt';
'Toon notificatie wanneer nieuwe nodes worden ontdekt';
@override
String get appSettings_messaging => 'Berichten';
@override
String get appSettings_clearPathOnMaxRetry => 'Duidelijke Pad op Max Retry';
String get appSettings_clearPathOnMaxRetry => 'Wis Pad op Max Retry';
@override
String get appSettings_clearPathOnMaxRetrySubtitle =>
@@ -490,19 +507,19 @@ class AppLocalizationsNl extends AppLocalizations {
'Padoms worden niet automatisch verwijderd';
@override
String get appSettings_autoRouteRotation => 'Automatische Route Rotatie';
String get appSettings_autoRouteRotation => 'Route Automatisch Roteren';
@override
String get appSettings_autoRouteRotationSubtitle =>
'Wissel tussen de beste paden en floodmodus over.';
'Verwissel tussen beste pad en floodmodus.';
@override
String get appSettings_autoRouteRotationEnabled =>
'Automatische routeplanning rotatie ingeschakeld';
'Automatische route rotatie ingeschakeld';
@override
String get appSettings_autoRouteRotationDisabled =>
'Automatische routeplanning rotatie is uitgeschakeld';
'Automatische route rotatie is uitgeschakeld';
@override
String get appSettings_battery => 'Batterij';
@@ -532,11 +549,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_mapDisplay => 'Kaartweergave';
@override
String get appSettings_showRepeaters => 'Toon Herhalingen';
String get appSettings_showRepeaters => 'Toon Repeaters';
@override
String get appSettings_showRepeatersSubtitle =>
'Toon herhalende knoopjes op de kaart';
'Toon repeaternodes op de kaart';
@override
String get appSettings_showChatNodes => 'Chat Nodes tonen';
@@ -546,21 +563,21 @@ class AppLocalizationsNl extends AppLocalizations {
'Chatnodes weergeven op de kaart';
@override
String get appSettings_showOtherNodes => 'Toon Andere Knopen';
String get appSettings_showOtherNodes => 'Toon Andere Nodes';
@override
String get appSettings_showOtherNodesSubtitle =>
'Toon andere knooptypes op de kaart';
'Toon andere nodetypes op de kaart';
@override
String get appSettings_timeFilter => 'Filter op tijd';
@override
String get appSettings_timeFilterShowAll => 'Alle knooppunten tonen';
String get appSettings_timeFilterShowAll => 'Alle nodes tonen';
@override
String appSettings_timeFilterShowLast(int hours) {
return 'Toon knopen van de laatste $hours uur';
return 'Toon nodes van de laatste $hours uur';
}
@override
@@ -568,10 +585,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_showNodesDiscoveredWithin =>
'Toon knooppunten ontdekt binnen:';
'Toon nodes ontdekt binnen:';
@override
String get appSettings_allTime => 'Alle tijd';
String get appSettings_allTime => 'Altijd';
@override
String get appSettings_lastHour => 'Laat uur';
@@ -642,7 +659,10 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Beheer Herhaling';
String get contacts_manageRepeater => 'Beheer Repeater';
@override
String get contacts_manageRoom => 'Beheer Ruimte Server';
@override
String get contacts_roomLogin => 'Ruimte Inloggen';
@@ -690,7 +710,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Laast gezien $minutes minuten geleden';
return 'Laatst gezien $minutes minuten geleden';
}
@override
@@ -824,6 +844,46 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
@override
String get channels_joinPrivateChannelDesc =>
'Handmatig een geheime sleutel invoeren.';
@override
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
@override
String get channels_joinPublicChannelDesc =>
'Iedereen kan dit kanaal aanmelden.';
@override
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
@override
String get channels_joinHashtagChannelDesc =>
'Iedereen kan lid worden van hashtag-kanalen.';
@override
String get channels_scanQrCode => 'Scan een QR-code';
@override
String get channels_scanQrCodeComingSoon => 'Komt later';
@override
String get channels_enterHashtag => 'Voer hashtag in';
@override
String get channels_hashtagHint => 'bijv. #team';
@override
String get chat_noMessages => 'Nog geen berichten.';
@@ -1009,7 +1069,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
@override
String get chat_forceFloodMode => 'Dwing Overstromingsmodus';
String get chat_forceFloodMode => 'Dwing Floodsmodus';
@override
String get chat_recentAckPaths => 'Recente ACK Paden (tik om te gebruiken):';
@@ -1071,7 +1131,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_floodModeEnabled =>
'Overstromingsmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
'Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
@override
String get chat_fullPath => 'Volledige Pad';
@@ -1115,18 +1175,18 @@ class AppLocalizationsNl extends AppLocalizations {
'Verzenden van uitgaande berichten comprimeren';
@override
String get chat_floodForced => 'Overstroming (gedwongen)';
String get chat_floodForced => 'Flood (afgedwongen)';
@override
String get chat_directForced => 'Direct (gedwongen)';
String get chat_directForced => 'Direct (afgedwongen)';
@override
String chat_hopsForced(int count) {
return '$count sprongen (gedwongen)';
return '$count hops (afgedwongen)';
}
@override
String get chat_floodAuto => 'Overstroming (auto)';
String get chat_floodAuto => 'Flood (auto)';
@override
String get chat_direct => 'Direct';
@@ -1139,11 +1199,29 @@ 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';
@override
String get map_noNodesWithLocation => 'Geen knopen met locatiegegevens';
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
@override
String get map_nodesNeedGps =>
@@ -1163,7 +1241,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_chat => 'Chat';
@override
String get map_repeater => 'Herhaling';
String get map_repeater => 'Repeater';
@override
String get map_room => 'Ruimte';
@@ -1230,19 +1308,19 @@ class AppLocalizationsNl extends AppLocalizations {
'Verbind met een apparaat om markers te delen';
@override
String get map_filterNodes => 'Filter Knopen';
String get map_filterNodes => 'Filter Nodes';
@override
String get map_nodeTypes => 'Node Types';
String get map_nodeTypes => 'Nodetypes';
@override
String get map_chatNodes => 'Chat Nodes';
String get map_chatNodes => 'Chatnodes';
@override
String get map_repeaters => 'Herhalingen';
String get map_repeaters => 'Repeaters';
@override
String get map_otherNodes => 'Andere knooppunten';
String get map_otherNodes => 'Andere Nodes';
@override
String get map_keyPrefix => 'Prefix sleutel';
@@ -1269,7 +1347,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_joinRoom => 'Sluit Kamer';
@override
String get map_manageRepeater => 'Beheer Herhaling';
String get map_manageRepeater => 'Beheer Repeater';
@override
String get mapCache_title => 'Offline Kaarten Cache';
@@ -1412,7 +1490,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?';
@override
String get login_repeaterLogin => 'Herhaalders Inloggen';
String get login_repeaterLogin => 'Inloggen Repeater';
@override
String get login_roomLogin => 'Ruimte Inloggen';
@@ -1432,7 +1510,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Voer het wachtwoord van de herhaling in om instellingen en status te openen.';
'Voer het wachtwoord van de repeater in om instellingen en status te openen.';
@override
String get login_roomDescription =>
@@ -1448,7 +1526,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get login_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
@override
String get login_forceFloodMode => 'Dwing Overstromingsmodus';
String get login_forceFloodMode => 'Dwing Floodmodus Af';
@override
String get login_managePaths => 'Padbeheer';
@@ -1466,6 +1544,10 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Inloggen mislukt: $error';
}
@override
String get login_failedMessage =>
'Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.';
@override
String get common_reload => 'Opnieuw laden';
@@ -1513,8 +1595,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get path_selectFromContacts => 'Of select contacten:';
@override
String get path_noRepeatersFound =>
'Geen herhalingen of zaalservers gevonden.';
String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.';
@override
String get path_customPathsRequire =>
@@ -1533,7 +1614,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get path_setPath => 'Stel Pad in';
@override
String get repeater_management => 'Beheer Herhalingen';
String get repeater_management => 'Beheer Repeaters';
@override
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_managementTools => 'Beheerinstrumenten';
@@ -1556,7 +1640,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cli => 'CLI';
@override
String get repeater_cliSubtitle => 'Verzend commando\'s naar de herhaaldere';
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
@override
String get repeater_neighbours => 'Buren';
@override
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
@override
String get repeater_settings => 'Instellingen';
@@ -1565,7 +1655,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
@override
String get repeater_statusTitle => 'Status herhalen';
String get repeater_statusTitle => 'Status repeater';
@override
String get repeater_routingMode => 'Routeerwijze';
@@ -1575,7 +1665,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Automatisch (gebruik opgeslagen pad)';
@override
String get repeater_forceFloodMode => 'Dwing Overloopmodus';
String get repeater_forceFloodMode => 'Dwing Floodmodus Af';
@override
String get repeater_pathManagement => 'Beheer van paden';
@@ -1592,7 +1682,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_systemInformation => 'Systeem Informatie';
String get repeater_systemInformation => 'Systeeminformatie';
@override
String get repeater_battery => 'Batterij';
@@ -1607,10 +1697,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_queueLength => 'Wachttijd';
@override
String get repeater_debugFlags => 'Debug Flags';
String get repeater_debugFlags => 'Debugvlaggen';
@override
String get repeater_radioStatistics => 'Radio Statistieken';
String get repeater_radioStatistics => 'Radiostatistieken';
@override
String get repeater_lastRssi => 'Laatste RSSI';
@@ -1619,7 +1709,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_lastSnr => 'Laatste SNR';
@override
String get repeater_noiseFloor => 'Ruishoordniveau';
String get repeater_noiseFloor => 'Ruisvloer';
@override
String get repeater_txAirtime => 'TX Airtime';
@@ -1628,7 +1718,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_rxAirtime => 'RX Airtime';
@override
String get repeater_packetStatistics => 'Pakket Statistieken';
String get repeater_packetStatistics => 'Pakketstatistieken';
@override
String get repeater_sent => 'Verzonden';
@@ -1637,7 +1727,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_received => 'Ontvangen';
@override
String get repeater_duplicates => 'Dubbele';
String get repeater_duplicates => 'Duplicaat';
@override
String repeater_daysHoursMinsSecs(
@@ -1651,17 +1741,17 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String repeater_packetTxTotal(int total, String flood, String direct) {
return 'Totaal: $total, Overstroming: $flood, Direct: $direct';
return 'Totaal: $total, Flood: $flood, Direct: $direct';
}
@override
String repeater_packetRxTotal(int total, String flood, String direct) {
return 'Totaal: $total, Overstroming: $flood, Direct: $direct';
return 'Totaal: $total, Flood: $flood, Direct: $direct';
}
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Overstroming: $flood, Direct: $direct';
return 'Flood: $flood, Direct: $direct';
}
@override
@@ -1670,16 +1760,16 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Herstelinstellingen';
String get repeater_settingsTitle => 'Repeater Instellingen';
@override
String get repeater_basicSettings => 'Basisinstellingen';
@override
String get repeater_repeaterName => 'Herhaalnaam';
String get repeater_repeaterName => 'Repeaternaam';
@override
String get repeater_repeaterNameHelper => 'Weergave naam voor deze herhaling';
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
@override
String get repeater_adminPassword => 'Admin wachtwoord';
@@ -1712,7 +1802,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_bandwidth => 'Bandbreedte';
@override
String get repeater_spreadingFactor => 'Spreadsnelheid';
String get repeater_spreadingFactor => 'Spreidingsfactor';
@override
String get repeater_codingRate => 'Codeertarief';
@@ -1736,11 +1826,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_features => 'Kenmerken';
@override
String get repeater_packetForwarding => 'Pakketdoorstrooming';
String get repeater_packetForwarding => 'Pakketdoorvoering';
@override
String get repeater_packetForwardingSubtitle =>
'Herstel activeren om pakketten door te sturen';
'Repeater instellen om pakketten door te sturen';
@override
String get repeater_guestAccess => 'Toegang voor Gasten';
@@ -1750,7 +1840,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Toegestane leesbeheer toegang voor gasten.';
@override
String get repeater_privacyMode => 'Privacy Mode';
String get repeater_privacyMode => 'Privacy Modus';
@override
String get repeater_privacyModeSubtitle =>
@@ -1768,8 +1858,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_floodAdvertInterval =>
'Advertentie Interval bij overstroming';
String get repeater_floodAdvertInterval => 'Flood Advertentie Interval';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@@ -1784,11 +1873,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_dangerZone => 'Gevaarzone';
@override
String get repeater_rebootRepeater => 'Herstart Herhaalder';
String get repeater_rebootRepeater => 'Herstart Repeater';
@override
String get repeater_rebootRepeaterSubtitle =>
'De herstart van het herhalerapparaat';
String get repeater_rebootRepeaterSubtitle => 'Herstart het Repeaterapparaat';
@override
String get repeater_rebootRepeaterConfirm =>
@@ -1804,18 +1892,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
'Dit genereert een nieuwe identiteit voor de herhaling. Doorgaan?';
'Dit genereert een nieuwe identiteit voor de repeater. Doorgaan?';
@override
String get repeater_eraseFileSystem => 'Verwijder Besturingssysteem';
@override
String get repeater_eraseFileSystemSubtitle =>
'Formateer het herhalende bestandsysteem';
'Formateer het bestandsysteem van de repeater';
@override
String get repeater_eraseFileSystemConfirm =>
'WAARSCHUWING: Dit zal alle gegevens op de herhaling wissen. Dit kan niet worden teruggedraaid!';
'WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!';
@override
String get repeater_eraseSerialOnly =>
@@ -1847,7 +1935,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_refreshRadioSettings =>
'Radiozenders Instellingen Bijwerken';
'Radiozender Instellingen Verversen';
@override
String get repeater_refreshTxPower => 'Nieuw laden TX-vermogen';
@@ -1881,7 +1969,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_cliTitle => 'Herhaling CLI';
String get repeater_cliTitle => 'Repeater CLI';
@override
String get repeater_debugNextCommand => 'Debug Volgende Commando';
@@ -1972,7 +2060,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetRepeat =>
'Activeert of deactiveert de herhalerrol voor dit knoop.';
'Activeert of deactiveert de repeater rol van deze node.';
@override
String get repeater_cliHelpSetAllowReadOnly =>
@@ -1980,7 +2068,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetFloodMax =>
'Stelt het maximale aantal hops van een inkomend overlastpakket in (indien >= max, wordt het pakket niet doorgestuurd)';
'Stelt het maximale aantal hops van een inkomend floodpakket in (indien >= max, wordt het pakket niet doorgestuurd)';
@override
String get repeater_cliHelpSetIntThresh =>
@@ -1992,7 +2080,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetMultiAcks =>
'Activeert of deactiveert de functie \'dubbele ACKs\'.';
'Activeert of deactiveert de functie \'duplicate ACKs\'.';
@override
String get repeater_cliHelpSetAdvertInterval =>
@@ -2000,7 +2088,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetFloodAdvertInterval =>
'Stelt het timerinterval in uren in om een overstromingsadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.';
'Stelt het timerinterval in uren in om een floodadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.';
@override
String get repeater_cliHelpSetGuestPassword =>
@@ -2091,7 +2179,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpRegion =>
'(reeks alleen) Lijst alle gedefinieerde regio\'s en huidige overstromingsrechten.';
'(Alleen Serieel) Lijst alle gedefinieerde regio\'s en huidige floodrechten.';
@override
String get repeater_cliHelpRegionLoad =>
@@ -2136,11 +2224,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cliHelpGpsOnOff => 'Schakel de GPS-standby aan/uit.';
@override
String get repeater_cliHelpGpsSync => 'Synchroniseer knooptime met GPS-klok.';
String get repeater_cliHelpGpsSync => 'Synchroniseer node met GPS-klok.';
@override
String get repeater_cliHelpGpsSetLoc =>
'Stel de positie van het knoop vast naar GPS-coördinaten en sla de voorkeuren op.';
'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.';
@override
String get repeater_cliHelpGpsAdvert =>
@@ -2170,11 +2258,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_logging => 'Logging';
@override
String get repeater_neighborsRepeaterOnly => 'Buren (Alleen herhaald)';
String get repeater_neighborsRepeaterOnly => 'Buren (Alleen repeaters)';
@override
String get repeater_regionManagementRepeaterOnly =>
'Regiobeheer (Alleen voor Repeater)';
'Regiobeheer (Alleen Repeater)';
@override
String get repeater_regionNote =>
@@ -2241,6 +2329,34 @@ class AppLocalizationsNl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ontvangen Buurdata';
@override
String get neighbors_requestTimedOut =>
'Buren vragen om tijdelijk uitgeschakeld.';
@override
String neighbors_errorLoading(String error) {
return 'Fout bij het laden van buren: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Onbekende $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Horen: $time geleden';
}
@override
String get channelPath_title => 'Pakketpad';
@@ -2251,7 +2367,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_otherObservedPaths => 'Overige Waargenomen Paden';
@override
String get channelPath_repeaterHops => 'Herhalingstapjes';
String get channelPath_repeaterHops => 'Repeater Hops';
@override
String get channelPath_noHopDetails =>
@@ -2267,7 +2383,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_timeLabel => 'Tijd';
@override
String get channelPath_repeatsLabel => 'Herhalen';
String get channelPath_repeatsLabel => 'Repeats';
@override
String channelPath_pathLabel(int index) {
@@ -2299,7 +2415,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_unknownPath => 'Onbekend';
@override
String get channelPath_floodPath => 'Overstroming';
String get channelPath_floodPath => 'Flood';
@override
String get channelPath_directPath => 'Direct';
@@ -2319,7 +2435,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channelPath_noRepeaterLocations =>
'Geen herhaler locaties beschikbaar voor deze route.';
'Geen repeaters beschikbaar voor deze route.';
@override
String channelPath_primaryPath(int index) {
@@ -2342,7 +2458,178 @@ class AppLocalizationsNl extends AppLocalizations {
'Geen details beschikbaar voor dit pakket.';
@override
String get channelPath_unknownRepeater => 'Onbekend Herhaalaar';
String get channelPath_unknownRepeater => 'Onbekend Repeater';
@override
String get community_title => 'Gemeenschap';
@override
String get community_create => 'Maak Gemeenschap';
@override
String get community_createDesc =>
'Maak een nieuwe community en deel deze via QR-code.';
@override
String get community_join => 'Sluit aan';
@override
String get community_joinTitle => 'Worden lid van de community';
@override
String community_joinConfirmation(String name) {
return 'Wil je je aansluiten bij de community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Gemeenschap QR';
@override
String get community_scanInstructions =>
'Richt de camera op een gemeenschappelijke QR-code';
@override
String get community_showQr => 'Toon QR-code';
@override
String get community_publicChannel => 'Gemeenschap Openbaar';
@override
String get community_hashtagChannel => 'Gemeenschappelijk Hashtag';
@override
String get community_name => 'Gemeenschapnaam';
@override
String get community_enterName => 'Voer de gemeenschapsnaam in';
@override
String community_created(String name) {
return 'Gemeenschap \"$name\" is aangemaakt';
}
@override
String community_joined(String name) {
return 'Gevonden in de community \"$name\"';
}
@override
String get community_qrTitle => 'Deel Gemeenschap';
@override
String community_qrInstructions(String name) {
return 'Scan deze QR-code om je aan te sluiten bij $name';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community';
@override
String get community_invalidQrCode => 'Ongeldige community QR-code';
@override
String get community_alreadyMember => 'Alleen al lid';
@override
String community_alreadyMemberMessage(String name) {
return 'U bent al lid van \"$name\".';
}
@override
String get community_addPublicChannel =>
'Voeg een Openbaar Gemeenschapskanaal toe';
@override
String get community_addPublicChannelHint =>
'Automatisch de publieke kanaal toevoegen voor deze community';
@override
String get community_noCommunities =>
'Nog geen gemeenschappen zijn bijgesloten.';
@override
String get community_scanOrCreate =>
'Scan een QR-code of een community aanmaken om te beginnen';
@override
String get community_manageCommunities => 'Beheer Gemeenschappen';
@override
String get community_delete => 'Laat Gemeenschap';
@override
String community_deleteConfirm(String name) {
return '\"$name\" verlaten?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Dit verwijdert ook $count kanaal/kanalen en hun berichten.';
}
@override
String community_deleted(String name) {
return 'Community \"$name\" verlaten';
}
@override
String get community_regenerateSecret => 'Regeneer Geheimwoord';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.';
}
@override
String get community_regenerate => 'Regeneer';
@override
String community_secretRegenerated(String name) {
return 'Geheim hersteld voor \"$name\"';
}
@override
String get community_updateSecret => 'Bijwerken Geheime';
@override
String community_secretUpdated(String name) {
return 'Geheim gewijzigd voor \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken';
}
@override
String get community_addHashtagChannel => 'Voeg Community Hashtag toe';
@override
String get community_addHashtagChannelDesc =>
'Voeg een hashtag-kanaal toe aan deze community';
@override
String get community_selectCommunity => 'Selecteer Gemeenschap';
@override
String get community_regularHashtag => 'Gewone Hashtag';
@override
String get community_regularHashtagDesc =>
'Open hashtag (iedereen kan deelnemen)';
@override
String get community_communityHashtag => 'Gemeenschappelijk Hashtag';
@override
String get community_communityHashtagDesc =>
'Alleen zichtbaar voor leden van de community';
@override
String community_forCommunity(String name) {
return 'Voor $name';
}
@override
String get listFilter_tooltip => 'Filteren en sorteren';
@@ -2369,10 +2656,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get listFilter_users => 'Gebruikers';
@override
String get listFilter_repeaters => 'Herhalingen';
String get listFilter_repeaters => 'Repeaters';
@override
String get listFilter_roomServers => 'Kamervirtualisatie';
String get listFilter_roomServers => 'Roomservers';
@override
String get listFilter_unreadOnly => 'Alleen ongelezen';
+290
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_cancel => 'Anuluj';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Połącz';
@@ -202,6 +205,20 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_locationInvalid =>
'Nieprawidłowa szerokość geograficzna lub długość geograficzna.';
@override
String get settings_locationGPSEnable => 'Włącz GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.';
@override
String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.';
@override
String get settings_latitude => 'Szerokość';
@@ -649,6 +666,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@override
String get contacts_roomLogin => 'Logowanie do pokoju';
@@ -828,6 +848,46 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_sortUnread => 'Niezgłoszone';
@override
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpieczone kluczem szyfrowym.';
@override
String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału';
@override
String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.';
@override
String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.';
@override
String get channels_joinPublicChannelDesc =>
'Każdy może dołączyć do tego kanału.';
@override
String get channels_joinHashtagChannel =>
'Dołącz do kanału oznaczanego hashtagiem';
@override
String get channels_joinHashtagChannelDesc =>
'Każdy może dołączyć do kanałów z hashtagami.';
@override
String get channels_scanQrCode => 'Skanuj kod QR';
@override
String get channels_scanQrCodeComingSoon => 'Wkrótce';
@override
String get channels_enterHashtag => 'Wprowadź hashtag';
@override
String get channels_hashtagHint => 'np. #zespół';
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
@@ -1145,6 +1205,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';
@@ -1474,6 +1552,10 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Zalogowanie się nie powiodło: $error';
}
@override
String get login_failedMessage =>
'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.';
@override
String get common_reload => 'Ponownie załadować';
@@ -1543,6 +1625,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_management => 'Zarządzanie Powtórzami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@@ -1566,6 +1651,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbours => 'Sąsiedzi';
@override
String get repeater_neighboursSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
String get repeater_settings => 'Ustawienia';
@@ -2246,6 +2338,34 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
@override
String get neighbors_requestTimedOut =>
'Sąsiedzi proszą o wyłączenie timingu.';
@override
String neighbors_errorLoading(String error) {
return 'Błąd podczas ładowania sąsiadów: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nieznana $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Usłyszano: $time temu';
}
@override
String get channelPath_title => 'Ścieżka pakietu';
@@ -2349,6 +2469,176 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nieznany Powtarzacz';
@override
String get community_title => 'Społeczność';
@override
String get community_create => 'Utwórz Społeczność';
@override
String get community_createDesc =>
'Utwórz nową społeczność i udostępnij za pomocą kodu QR.';
@override
String get community_join => 'Dołącz';
@override
String get community_joinTitle => 'Dołącz do społeczności';
@override
String community_joinConfirmation(String name) {
return 'Czy chcesz dołączyć do społeczności \"$name\"?';
}
@override
String get community_scanQr => 'Skanuj QR kod społeczności';
@override
String get community_scanInstructions =>
'Skieruj kamerę w kierunku kodu QR społeczności.';
@override
String get community_showQr => 'Pokaż kod QR';
@override
String get community_publicChannel => 'Społeczność Publiczna';
@override
String get community_hashtagChannel => 'Hashtag Społeczności';
@override
String get community_name => 'Nazwa Społeczności';
@override
String get community_enterName => 'Wprowadź nazwę społeczności';
@override
String community_created(String name) {
return 'Społeczność \"$name\" została utworzona';
}
@override
String community_joined(String name) {
return 'Dołączył do społeczności \"$name\"';
}
@override
String get community_qrTitle => 'Dziel się Społecznością';
@override
String community_qrInstructions(String name) {
return 'Skanuj ten kod QR, aby dołączyć $name';
}
@override
String get community_hashtagPrivacyHint =>
'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności';
@override
String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.';
@override
String get community_alreadyMember => 'Już jesteś członkiem.';
@override
String community_alreadyMemberMessage(String name) {
return 'Jesteś już członkiem \"$name\".';
}
@override
String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności';
@override
String get community_addPublicChannelHint =>
'Automatycznie dodaj kanał publiczny dla tej społeczności.';
@override
String get community_noCommunities =>
'Nie dołączono jeszcze żadnych społeczności.';
@override
String get community_scanOrCreate =>
'Skanuj kod QR lub utwórz społeczność, aby zacząć.';
@override
String get community_manageCommunities => 'Zarządzaj Grupami';
@override
String get community_delete => 'Opuszczenie Społeczności';
@override
String community_deleteConfirm(String name) {
return 'Opuścić \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.';
}
@override
String community_deleted(String name) {
return 'Opuszczono społeczność \"$name\"';
}
@override
String get community_regenerateSecret => 'Zregeneruj sekret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
}
@override
String get community_regenerate => 'Zregeneruj';
@override
String community_secretRegenerated(String name) {
return 'Hasło ponownie wygenerowane dla \"$name\"';
}
@override
String get community_updateSecret => 'Zaktualizuj tajny klucz';
@override
String community_secretUpdated(String name) {
return 'Hasło zaktualizowane dla \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj hashtag społeczności';
@override
String get community_addHashtagChannelDesc =>
'Dodaj kanał z hashtagiem dla tej społeczności';
@override
String get community_selectCommunity => 'Wybierz społeczność';
@override
String get community_regularHashtag => 'Hashtag regular';
@override
String get community_regularHashtagDesc =>
'Publiczny hashtag (każdy może dołączyć)';
@override
String get community_communityHashtag => 'Hashtag Społeczności';
@override
String get community_communityHashtagDesc =>
'Dostępne tylko dla członków społeczności';
@override
String community_forCommunity(String name) {
return 'Dla $name';
}
@override
String get listFilter_tooltip => 'Filtruj i sortuj';
+291
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Conectar';
@@ -201,6 +204,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitude ou longitude inválidos.';
@override
String get settings_locationGPSEnable => 'Ativar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita a atualização automática da localização via GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.';
@override
String get settings_latitude => 'Latitude';
@@ -649,6 +666,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gerenciar Repetidor';
@override
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
@override
String get contacts_roomLogin => 'Login no Quarto';
@@ -829,6 +849,46 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_sortUnread => 'Não lido';
@override
String get channels_createPrivateChannel => 'Criar um Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Protegido com uma chave secreta.';
@override
String get channels_joinPrivateChannel => 'Junte-se a um Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Inserir uma chave secreta manualmente.';
@override
String get channels_joinPublicChannel => 'Junte-se ao Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Qualquer pessoa pode entrar neste canal.';
@override
String get channels_joinHashtagChannel => 'Junte-se a um Canal com Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Qualquer pessoa pode participar de canais com hashtag.';
@override
String get channels_scanQrCode => 'Digitalizar um Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Em breve';
@override
String get channels_enterHashtag => 'Insira hashtag';
@override
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
@@ -1144,6 +1204,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';
@@ -1472,6 +1550,10 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Login falhou: $error';
}
@override
String get login_failedMessage =>
'Falha no login. A senha está incorreta ou o repetidor está inacessível.';
@override
String get common_reload => 'Recarregar';
@@ -1541,6 +1623,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_management => 'Gerenciamento de Repetidor';
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@@ -1564,6 +1649,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbours => 'Vizinhos';
@override
String get repeater_neighboursSubtitle =>
'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@@ -2248,6 +2340,34 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
@override
String get neighbors_requestTimedOut =>
'Vizinhos solicitam tempo limite esgotado.';
@override
String neighbors_errorLoading(String error) {
return 'Erro ao carregar vizinhos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@override
String neighbors_unknownContact(String pubkey) {
return '$pubkey Desconhecido';
}
@override
String neighbors_heardAgo(String time) {
return 'Ouvido: $time atrás';
}
@override
String get channelPath_title => 'Rótulo de Caminho de Pacote';
@@ -2351,6 +2471,177 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconhecido';
@override
String get community_title => 'Comunidade';
@override
String get community_create => 'Criar Comunidade';
@override
String get community_createDesc =>
'Crie uma nova comunidade e compartilhe via código QR.';
@override
String get community_join => 'Junte-se';
@override
String get community_joinTitle => 'Junte-se à Comunidade';
@override
String community_joinConfirmation(String name) {
return 'Você gostaria de se juntar à comunidade \"$name\"?';
}
@override
String get community_scanQr => 'Digitalizar a QR Code da Comunidade';
@override
String get community_scanInstructions =>
'Aponte a câmera para um código QR da comunidade';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidade Pública';
@override
String get community_hashtagChannel => 'Hashtag da Comunidade';
@override
String get community_name => 'Nome da Comunidade';
@override
String get community_enterName => 'Insira o nome da comunidade';
@override
String community_created(String name) {
return 'Comunidade \"$name\" criada';
}
@override
String community_joined(String name) {
return 'Juntou-se à comunidade \"$name\"';
}
@override
String get community_qrTitle => 'Partilhar Comunidade';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para juntar-se a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade';
@override
String get community_invalidQrCode => 'Código QR da comunidade inválido';
@override
String get community_alreadyMember => 'Já é Membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Você já é membro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Adicionar Canal Público da Comunidade';
@override
String get community_addPublicChannelHint =>
'Adicionar automaticamente o canal público para esta comunidade';
@override
String get community_noCommunities =>
'Ainda não foram adicionadas comunidades.';
@override
String get community_scanOrCreate =>
'Escaneie um código QR ou crie uma comunidade para começar.';
@override
String get community_manageCommunities => 'Gerenciar Comunidades';
@override
String get community_delete => 'Deixar Comunidade';
@override
String community_deleteConfirm(String name) {
return 'Sair de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Isso também excluirá $count canal/canais e suas mensagens.';
}
@override
String community_deleted(String name) {
return 'Saiu da comunidade \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerar Senha Segura';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Senha secreta regenerada para \"$name\"';
}
@override
String get community_updateSecret => 'Atualizar Segredo';
@override
String community_secretUpdated(String name) {
return 'Segredo atualizado para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
}
@override
String get community_addHashtagChannel => 'Adicionar Hashtag da Comunidade';
@override
String get community_addHashtagChannelDesc =>
'Adicionar um canal de hashtag para esta comunidade';
@override
String get community_selectCommunity => 'Selecione Comunidade';
@override
String get community_regularHashtag => 'Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (qualquer pessoa pode participar)';
@override
String get community_communityHashtag => 'Hashtag da Comunidade';
@override
String get community_communityHashtagDesc =>
'Apenas para membros da comunidade';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar e ordenar';
File diff suppressed because it is too large Load Diff
+288 -1
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_cancel => 'Zrušiť';
@override
String get common_ok => 'OK\nDobre';
@override
String get common_connect => 'Pripojiť';
@@ -200,6 +203,20 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.';
@override
String get settings_locationGPSEnable => 'Aktivovať GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Povolí automatické aktualizovanie polohy pomocou GPS.';
@override
String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.';
@override
String get settings_latitude => 'Súradnica';
@@ -642,6 +659,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
@override
String get contacts_manageRoom => 'Spravovať server miestnosti';
@override
String get contacts_roomLogin => 'Prihlásenie do miestnosti';
@@ -824,6 +844,45 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_sortUnread => 'Nezriadené';
@override
String get channels_createPrivateChannel => 'Vytvorte súkromný kanál';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpečené pomocou tajného kľúča.';
@override
String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu';
@override
String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.';
@override
String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu';
@override
String get channels_joinPublicChannelDesc =>
'Któvek sátó na tutó kanalizovát.';
@override
String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu';
@override
String get channels_joinHashtagChannelDesc =>
'Ktoekolikoľvek sa môže pridať do hashtag kanálov.';
@override
String get channels_scanQrCode => 'Skenujte QR kód';
@override
String get channels_scanQrCodeComingSoon => 'Čoskoro';
@override
String get channels_enterHashtag => 'Zadajte hashtag';
@override
String get channels_hashtagHint => 'napr. #tím';
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@@ -1061,7 +1120,7 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_clearPathSubtitle =>
'Znovu nájsť vynútene pri nasledujacej pošlite';
'Znovu nájsť vynútene pri nasledujúcej pošlite';
@override
String get chat_pathCleared =>
@@ -1141,6 +1200,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';
@@ -1468,6 +1545,10 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Prihlásenie zlyhalo: $error';
}
@override
String get login_failedMessage =>
'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.';
@override
String get common_reload => 'Načítať';
@@ -1537,6 +1618,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_management => 'Správa opakérov';
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_managementTools => 'Nástroje na správu';
@@ -1560,6 +1644,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbours => 'Súsezný';
@override
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@@ -2237,6 +2327,34 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
@override
String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.';
@override
String neighbors_errorLoading(String error) {
return 'Chyba pri načítaní susedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
'Nie je dostupná žiadna informácia o susedoch.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Neznáma $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Počuli sme to: $time dozadu';
}
@override
String get channelPath_title => 'Cesta balíka';
@@ -2340,6 +2458,175 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Neznáme opakovače';
@override
String get community_title => 'Komunita';
@override
String get community_create => 'Vytvoriť komunitu';
@override
String get community_createDesc =>
'Vytvorte novú komunitu a zdieľajte cez QR kód.';
@override
String get community_join => 'Pripojiť';
@override
String get community_joinTitle => 'Pripojiť sa k spoločenstvu';
@override
String community_joinConfirmation(String name) {
return 'Chceš sa pridať do komunity \"$name\"?';
}
@override
String get community_scanQr => 'Skontrolujte komunitný QR kód';
@override
String get community_scanInstructions =>
'Zamerte kameru na komunitný QR kód.';
@override
String get community_showQr => 'Zobraziť QR kód';
@override
String get community_publicChannel => 'Komunita verejná';
@override
String get community_hashtagChannel => 'Komunitný Hashtag';
@override
String get community_name => 'Komunita';
@override
String get community_enterName => 'Zadajte názov komunity';
@override
String community_created(String name) {
return 'Komunita \"$name\" vytvorená';
}
@override
String community_joined(String name) {
return 'Pripojená komunita \"$name\"';
}
@override
String get community_qrTitle => 'Zdieľť komunitu';
@override
String community_qrInstructions(String name) {
return 'Skenejte tento QR kód, aby ste sa pripojili k $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtagové kanály komunity sú prístupné len členom komunity';
@override
String get community_invalidQrCode => 'Neplatná QR kód komunity.';
@override
String get community_alreadyMember => 'Už ste členom.';
@override
String community_alreadyMemberMessage(String name) {
return 'Vy ste už členom \"$name\".';
}
@override
String get community_addPublicChannel => 'Pridať verejný komunikačný kanál';
@override
String get community_addPublicChannelHint =>
'Automaticky prida verejný kanál pre túto komunitu.';
@override
String get community_noCommunities =>
'Zatiaľ ste sa nepripojili k žiadnej komunite';
@override
String get community_scanOrCreate =>
'Skene QR kód alebo vytvor komunitu na začiatok.';
@override
String get community_manageCommunities => 'Spravovať komunity';
@override
String get community_delete => 'Nechajte komunitu';
@override
String community_deleteConfirm(String name) {
return 'Opustiť \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.';
}
@override
String community_deleted(String name) {
return 'Opustená komunita \"$name\"';
}
@override
String get community_regenerateSecret => 'Zobraziť nový tajný kód';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Znovu vygenerovať';
@override
String community_secretRegenerated(String name) {
return 'Záznam pre \"$name\" bol regenerovaný tajne';
}
@override
String get community_updateSecret => 'Aktualizovať tajné heslo';
@override
String community_secretUpdated(String name) {
return 'Zmena tajnej slova pre \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
}
@override
String get community_addHashtagChannel => 'Pridať komunitný hashtag';
@override
String get community_addHashtagChannelDesc =>
'Pridajte hashtagový kanál pre túto komunitu.';
@override
String get community_selectCommunity => 'Vyberte komunitu';
@override
String get community_regularHashtag => 'Zvyčajný hashtag';
@override
String get community_regularHashtagDesc =>
'Veľký hashtag (ktočokoľvek sa môže pridať)';
@override
String get community_communityHashtag => 'Komunitný Hashtag';
@override
String get community_communityHashtagDesc => 'Špecifické pre členov komunity';
@override
String community_forCommunity(String name) {
return 'Pre $name';
}
@override
String get listFilter_tooltip => 'Filtrovať a triediť';
+291 -4
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_cancel => 'Prekliči';
@override
String get common_ok => 'V redu';
@override
String get common_connect => 'Poveži se';
@@ -200,6 +203,20 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_locationInvalid =>
'Neveljna zemeljska širina ali dolžina.';
@override
String get settings_locationGPSEnable => 'Omogoči GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Omogoči samodejno posodabljanje lokacije z GPS-jem.';
@override
String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)';
@override
String get settings_locationIntervalInvalid =>
'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.';
@override
String get settings_latitude => 'Širina';
@@ -435,7 +452,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_enableNotificationsSubtitle =>
'Prejmujte obvestila o sporočilih in oglasih';
'Prejmite obvestila o sporočilih in oglasih';
@override
String get appSettings_notificationPermissionDenied =>
@@ -631,7 +648,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_noContactsFound =>
'Niti ena osebe ali skupine ni najdena.';
'Niti ena oseba ali skupine ni najdena.';
@override
String get contacts_deleteContact => 'Izbrisati Kontakt';
@@ -644,6 +661,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Upravljajte Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
@override
String get contacts_roomLogin => 'Vnos v sobo';
@@ -680,7 +700,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_noContactsMatchFilter =>
'Niti ena osebe ne ustreza vašemu kriteriju.';
'Niti ena oseba ne ustreza vašemu kriteriju.';
@override
String get contacts_noMembers => 'Nič članov.';
@@ -824,6 +844,45 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channels_sortUnread => 'Nerešeno';
@override
String get channels_createPrivateChannel => 'Ustvari zasebno kanal.';
@override
String get channels_createPrivateChannelDesc =>
'Varno zaklenjeno s skrivnim ključem.';
@override
String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini';
@override
String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.';
@override
String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu';
@override
String get channels_joinPublicChannelDesc =>
'Kdor karkoli je, lahko se pridruži tej skupini.';
@override
String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom';
@override
String get channels_joinHashtagChannelDesc =>
'Kdor karkoli, lahko se pridruži hashtag kanalom.';
@override
String get channels_scanQrCode => 'Skeniraj QR kodo';
@override
String get channels_scanQrCodeComingSoon => 'Prihajajoča';
@override
String get channels_enterHashtag => 'Vnesite hashtag';
@override
String get channels_hashtagHint => 'npr. #ekipa';
@override
String get chat_noMessages => 'Še ni sporočil.';
@@ -1138,6 +1197,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';
@@ -1147,7 +1224,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_nodesNeedGps =>
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.';
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.';
@override
String map_nodesCount(int count) {
@@ -1469,6 +1546,10 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Prijava je bila neuspešna: $error';
}
@override
String get login_failedMessage =>
'Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.';
@override
String get common_reload => 'Ponovno naloži';
@@ -1537,6 +1618,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_management => 'Upravljanje ponovitve';
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_managementTools => 'Upravne orodje';
@@ -1561,6 +1645,12 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_cliSubtitle =>
'Pošlji ukazne povelje na ponovitveno enoto.';
@override
String get repeater_neighbours => 'Sosedi';
@override
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
@override
String get repeater_settings => 'Nastavitve';
@@ -2242,6 +2332,34 @@ class AppLocalizationsSl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Prejeto podatke o sosedih';
@override
String get neighbors_requestTimedOut =>
'Sosedi zahtevajo izklop po dogovoru.';
@override
String neighbors_errorLoading(String error) {
return 'Napaka pri obnašanju sosedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
@override
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nepoznano $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Udeleženec je prejel sporočilo $time nazaj.';
}
@override
String get channelPath_title => 'Pot do paketa';
@@ -2345,6 +2463,175 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nepoznati ponovitelj';
@override
String get community_title => 'Skupnost';
@override
String get community_create => 'Ustvari skupnost';
@override
String get community_createDesc =>
'Ustvari novo skupnost in jo deli preko QR kode.';
@override
String get community_join => 'Pridružiti se';
@override
String get community_joinTitle => 'Pridružite se skupnosti';
@override
String community_joinConfirmation(String name) {
return 'Želiš se pridružiti skupnosti \"$name\"?';
}
@override
String get community_scanQr => 'Skeniraj QR kode skupnosti';
@override
String get community_scanInstructions =>
'Nasmerite kamero s skupnostnim QR kodom.';
@override
String get community_showQr => 'Pokaži QR kodo';
@override
String get community_publicChannel => 'Skupnostna javna';
@override
String get community_hashtagChannel => 'Skupnostni hashtag';
@override
String get community_name => 'Komunitarne ime';
@override
String get community_enterName => 'Vnesite ime skupnosti';
@override
String community_created(String name) {
return 'Skupnost \"$name\" je bila ustvarila.';
}
@override
String community_joined(String name) {
return 'Prilojen k skupnosti \"$name\"';
}
@override
String get community_qrTitle => 'Delite skupnost';
@override
String community_qrInstructions(String name) {
return 'Skenirajte to QR kodo za vključitev $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtag kanali skupnosti so dostopni samo članom skupnosti';
@override
String get community_invalidQrCode => 'Neveljaven QR koden skupnosti';
@override
String get community_alreadyMember => 'Že član';
@override
String community_alreadyMemberMessage(String name) {
return 'Kljub temu ste že član/ka $name.';
}
@override
String get community_addPublicChannel => 'Dodaj Objavni Kanal Komunitarja';
@override
String get community_addPublicChannelHint =>
'Samodejno dodaj javni kanal za to skupnost.';
@override
String get community_noCommunities => 'Še nobena skupnost se ni pridružila.';
@override
String get community_scanOrCreate =>
'Skenirajte QR kodo ali ustvarite skupnost za začetek.';
@override
String get community_manageCommunities => 'Upravljajte skupnosti';
@override
String get community_delete => 'Opusti skupnost';
@override
String community_deleteConfirm(String name) {
return 'Zapustiti \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.';
}
@override
String community_deleted(String name) {
return 'Zapustil skupnost \"$name\"';
}
@override
String get community_regenerateSecret => 'Preberi nov tajni kôd';
@override
String community_regenerateSecretConfirm(String name) {
return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.';
}
@override
String get community_regenerate => 'Preberi znova';
@override
String community_secretRegenerated(String name) {
return 'Tajna za \"$name\" ponovno ustvarjena';
}
@override
String get community_updateSecret => 'Ažurniraj tajno';
@override
String community_secretUpdated(String name) {
return 'Skrivnostno spremembo za \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skeniraj nov kôd QR za posodabljanje tajne za $name';
}
@override
String get community_addHashtagChannel => 'Dodaj Oznako Obštnine';
@override
String get community_addHashtagChannelDesc =>
'Dodajte hashtag kanal za to skupnost.';
@override
String get community_selectCommunity => 'Izberi skupnost';
@override
String get community_regularHashtag => 'Oznaka s hashtagom';
@override
String get community_regularHashtagDesc =>
'javna oznaka (kateri koli lahko sodelujejo)';
@override
String get community_communityHashtag => 'Skupnostni hashtag';
@override
String get community_communityHashtagDesc =>
'Izključeno za uporabnike skupnosti';
@override
String community_forCommunity(String name) {
return 'Za $name';
}
@override
String get listFilter_tooltip => 'Filtri in vrstiči';
+287
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_cancel => 'Avbryt';
@override
String get common_ok => 'Okej';
@override
String get common_connect => 'Anslut';
@@ -199,6 +202,20 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_locationInvalid => 'Ogiltig latitud eller longitud.';
@override
String get settings_locationGPSEnable => 'Aktivera GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.';
@override
String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)';
@override
String get settings_locationIntervalInvalid =>
'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.';
@override
String get settings_latitude => 'Latitud';
@@ -638,6 +655,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Hantera Upprepare';
@override
String get contacts_manageRoom => 'Hantera Rumserver';
@override
String get contacts_roomLogin => 'Rum Inloggning';
@@ -817,6 +837,46 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_sortUnread => 'Oläst';
@override
String get channels_createPrivateChannel => 'Skapa en privat kanal';
@override
String get channels_createPrivateChannelDesc =>
'Skyddat med en hemlig nyckel.';
@override
String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal';
@override
String get channels_joinPrivateChannelDesc =>
'Ange en hemlig nyckel manuellt.';
@override
String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen';
@override
String get channels_joinPublicChannelDesc =>
'Vem som helst kan gå med i denna kanal.';
@override
String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal';
@override
String get channels_joinHashtagChannelDesc =>
'Väldigt enkelt att gå med i hashtag-kanaler.';
@override
String get channels_scanQrCode => 'Skanna en QR-kod';
@override
String get channels_scanQrCodeComingSoon => 'Kommer snart';
@override
String get channels_enterHashtag => 'Ange hashtag';
@override
String get channels_hashtagHint => 't.ex. #team';
@override
String get chat_noMessages => 'Inga meddelanden ännu';
@@ -1132,6 +1192,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';
@@ -1457,6 +1535,10 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Inloggning misslyckades: $error';
}
@override
String get login_failedMessage =>
'Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.';
@override
String get common_reload => 'Ladda om';
@@ -1525,6 +1607,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_management => 'Återuppspelarens Hantering';
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@@ -1548,6 +1633,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbours => 'Grannar';
@override
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@@ -2225,6 +2316,33 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Mottagna grannars data';
@override
String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.';
@override
String neighbors_errorLoading(String error) {
return 'Fel vid inläsning av grannar: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Okänd $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hördes: $time sedan';
}
@override
String get channelPath_title => 'Paketväg';
@@ -2328,6 +2446,175 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Okänt Upprepare';
@override
String get community_title => 'Gemenskap';
@override
String get community_create => 'Skapa Gemenskap';
@override
String get community_createDesc =>
'Skapa en ny gemenskap och dela via QR-kod.';
@override
String get community_join => 'Gå med';
@override
String get community_joinTitle => 'Gå med i gemenskapen';
@override
String community_joinConfirmation(String name) {
return 'Vill du gå med i communityn \"$name\"?';
}
@override
String get community_scanQr => 'Skanna Gemenskapens QR';
@override
String get community_scanInstructions =>
'Rikta kameran mot en QR-kod i communityn';
@override
String get community_showQr => 'Visa QR-kod';
@override
String get community_publicChannel => 'Föreningens Offentliga';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Gemenskapens namn';
@override
String get community_enterName => 'Ange communities namn';
@override
String community_created(String name) {
return 'Community \"$name\" har skapats';
}
@override
String community_joined(String name) {
return 'Medlem i communityn \"$name\"';
}
@override
String get community_qrTitle => 'Dela Gemenskap';
@override
String community_qrInstructions(String name) {
return 'Skanna denna QR-kod för att gå med i \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community-hashtagkanaler kan endast nås av medlemmar i communityn';
@override
String get community_invalidQrCode => 'Ogiltig community QR-kod';
@override
String get community_alreadyMember => 'Är redan medlem';
@override
String community_alreadyMemberMessage(String name) {
return 'Du är redan medlem av \"$name\".';
}
@override
String get community_addPublicChannel =>
'Lägg till Gemenskapskanal (Offentlig)';
@override
String get community_addPublicChannelHint =>
'Lägg automatiskt till den offentliga kanalen för denna community';
@override
String get community_noCommunities => 'Inga gemenskaper har anslutats ännu';
@override
String get community_scanOrCreate =>
'Skanna en QR-kod eller skapa en community för att komma igång';
@override
String get community_manageCommunities => 'Hantera Gemenskaper';
@override
String get community_delete => 'Lämna Gemenskap';
@override
String community_deleteConfirm(String name) {
return 'Lämna \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.';
}
@override
String community_deleted(String name) {
return 'Lämnade community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerera hemlig kod';
@override
String community_regenerateSecretConfirm(String name) {
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 => 'Regenerera';
@override
String community_secretRegenerated(String name) {
return 'Lösenord återskapad för \"$name\"';
}
@override
String get community_updateSecret => 'Uppdatera hemlighet';
@override
String community_secretUpdated(String name) {
return 'Hemlighet uppdaterad för \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
}
@override
String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Lägg till en hashtag-kanal för denna community';
@override
String get community_selectCommunity => 'Välj Gemenskap';
@override
String get community_regularHashtag => 'Vanlig Hash Tag';
@override
String get community_regularHashtagDesc =>
'Offentlig hashtag (alla kan gå med)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Endast för medlemmar';
@override
String community_forCommunity(String name) {
return 'För $name';
}
@override
String get listFilter_tooltip => 'Filtrera och sortera';
File diff suppressed because it is too large Load Diff
+271
View File
@@ -23,6 +23,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_cancel => '取消';
@override
String get common_ok => '好的';
@override
String get common_connect => '连接';
@@ -196,6 +199,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_locationInvalid => '无效的纬度或经度。';
@override
String get settings_locationGPSEnable => '启用GPS';
@override
String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。';
@override
String get settings_locationIntervalSec => 'GPS 间隔(秒)';
@override
String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。';
@override
String get settings_latitude => '纬度';
@@ -611,6 +626,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_manageRepeater => '管理重复项';
@override
String get contacts_manageRoom => '管理房间服务器';
@override
String get contacts_roomLogin => '房间登录';
@@ -789,6 +807,42 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channels_sortUnread => '未读';
@override
String get channels_createPrivateChannel => '创建私聊频道';
@override
String get channels_createPrivateChannelDesc => '使用密钥保护。';
@override
String get channels_joinPrivateChannel => '加入私密频道';
@override
String get channels_joinPrivateChannelDesc => '手动输入密钥。';
@override
String get channels_joinPublicChannel => '加入公共频道';
@override
String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。';
@override
String get channels_joinHashtagChannel => '加入标签频道';
@override
String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。';
@override
String get channels_scanQrCode => '扫描二维码';
@override
String get channels_scanQrCodeComingSoon => '即将到来';
@override
String get channels_enterHashtag => '输入标签';
@override
String get channels_hashtagHint => '例如 #团队';
@override
String get chat_noMessages => '目前还没有消息';
@@ -1094,6 +1148,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 => '节点地图';
@@ -1411,6 +1482,9 @@ class AppLocalizationsZh extends AppLocalizations {
return '登录失败:$error';
}
@override
String get login_failedMessage => '登录失败。密码不正确或中继器不可达。';
@override
String get common_reload => '重新加载';
@@ -1474,6 +1548,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_management => '重复器管理';
@override
String get room_management => '房间服务器管理';
@override
String get repeater_managementTools => '管理工具';
@@ -1495,6 +1572,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_cliSubtitle => '发送命令到重复器';
@override
String get repeater_neighbours => '邻居';
@override
String get repeater_neighboursSubtitle => '查看零跳邻居。';
@override
String get repeater_settings => '设置';
@@ -2125,6 +2208,33 @@ class AppLocalizationsZh extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => '收到邻居数据';
@override
String get neighbors_requestTimedOut => '邻居请求超时处理。';
@override
String neighbors_errorLoading(String error) {
return '加载邻居时出错:$error';
}
@override
String get neighbors_repeatersNeighbours => '重复器邻居';
@override
String get neighbors_noData => '没有可用的邻居数据。';
@override
String neighbors_unknownContact(String pubkey) {
return '未知$pubkey';
}
@override
String neighbors_heardAgo(String time) {
return '听到的时间:$time前';
}
@override
String get channelPath_title => '数据包路径';
@@ -2225,6 +2335,167 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channelPath_unknownRepeater => '未知重复器';
@override
String get community_title => '社区';
@override
String get community_create => '创建社区';
@override
String get community_createDesc => '创建新的社区并可通过二维码分享。';
@override
String get community_join => '加入';
@override
String get community_joinTitle => '加入社区';
@override
String community_joinConfirmation(String name) {
return '您想加入社区 \"$name\" 吗?';
}
@override
String get community_scanQr => '扫描社区二维码';
@override
String get community_scanInstructions => '将相机对准社区二维码';
@override
String get community_showQr => '显示二维码';
@override
String get community_publicChannel => '社区公开';
@override
String get community_hashtagChannel => '社区标签';
@override
String get community_name => '社区名称';
@override
String get community_enterName => '请输入社区名称';
@override
String community_created(String name) {
return '社区“$name”已创建';
}
@override
String community_joined(String name) {
return '加入社区 \"$name\"';
}
@override
String get community_qrTitle => '分享社区';
@override
String community_qrInstructions(String name) {
return '扫描此二维码加入$name';
}
@override
String get community_hashtagPrivacyHint => '社区标签频道仅社区成员可加入';
@override
String get community_invalidQrCode => '无效的社区二维码';
@override
String get community_alreadyMember => '已经是会员了';
@override
String community_alreadyMemberMessage(String name) {
return '您已经是 \"$name\" 的会员。';
}
@override
String get community_addPublicChannel => '添加社区公共频道';
@override
String get community_addPublicChannelHint => '自动添加该社区的公共频道';
@override
String get community_noCommunities => '尚未加入任何社区';
@override
String get community_scanOrCreate => '扫描二维码或创建社区开始';
@override
String get community_manageCommunities => '管理社群';
@override
String get community_delete => '退出社区';
@override
String community_deleteConfirm(String name) {
return '退出 \"$name\"';
}
@override
String community_deleteChannelsWarning(int count) {
return '这也将删除 $count 个频道及其消息。';
}
@override
String community_deleted(String name) {
return '已退出社区 \"$name\"';
}
@override
String get community_regenerateSecret => '重新生成密钥';
@override
String community_regenerateSecretConfirm(String name) {
return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。';
}
@override
String get community_regenerate => '重新生成';
@override
String community_secretRegenerated(String name) {
return '密码已重置为“$name';
}
@override
String get community_updateSecret => '更新密钥';
@override
String community_secretUpdated(String name) {
return '密码已更新为“$name';
}
@override
String community_scanToUpdateSecret(String name) {
return '扫描新的二维码更新\"$name\"的密码';
}
@override
String get community_addHashtagChannel => '添加社区标签';
@override
String get community_addHashtagChannelDesc => '添加一个话题频道给此社区';
@override
String get community_selectCommunity => '选择社区';
@override
String get community_regularHashtag => '常规话题标签';
@override
String get community_regularHashtagDesc => '公共话题(任何人都可以加入)';
@override
String get community_communityHashtag => '社区标签';
@override
String get community_communityHashtagDesc => '仅限社区成员使用';
@override
String community_forCommunity(String name) {
return '对于 $name';
}
@override
String get listFilter_tooltip => '筛选和排序';
+288 -90
View File
@@ -2,7 +2,7 @@
"@@locale": "nl",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacten",
"nav_channels": "Kanaal",
"nav_channels": "Kanalen",
"nav_map": "Kaart",
"common_cancel": "Annuleren",
"common_connect": "Verbinden",
@@ -10,12 +10,12 @@
"common_save": "Opslaan",
"common_delete": "Verwijderen",
"common_close": "Sluiten",
"common_edit": "Bewerk",
"common_edit": "Bewerken",
"common_add": "Toevoegen",
"common_settings": "Instellingen",
"common_disconnect": "Verbinden verbreken",
"common_disconnect": "Verbinding verbreken",
"common_connected": "Verbonden",
"common_disconnected": "Ontkoppeld",
"common_disconnected": "Verbinding verbroken",
"common_create": "Maak",
"common_continue": "Doorgaan",
"common_share": "Delen",
@@ -23,10 +23,10 @@
"common_retry": "Nogmaals proberen",
"common_hide": "Verbergen",
"common_remove": "Verwijderen",
"common_enable": "Aktivatie",
"common_enable": "Activeren",
"common_disable": "Uitschakelen",
"common_reboot": "Herstarten",
"common_loading": "Laad...",
"common_loading": "Laden...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
@@ -78,7 +78,7 @@
"settings_nodeSettings": "Node Instellingen",
"settings_nodeName": "Node Naam",
"settings_nodeNameNotSet": "Niet ingesteld",
"settings_nodeNameHint": "Voer knooppuntnaam in",
"settings_nodeNameHint": "Voer nodenaam in",
"settings_nodeNameUpdated": "Naam bijgewerkt",
"settings_radioSettings": "Radio Instellingen",
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
@@ -129,7 +129,7 @@
"settings_infoBattery": "Batterij",
"settings_infoPublicKey": "Openbare Sleutel",
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Kanaal Aantal",
"settings_infoChannelCount": "Aantal Kanalen",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
@@ -143,8 +143,8 @@
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_longRange": "Lang Bereik",
"settings_fastSpeed": "Snelle Snelheid",
"settings_longRange": "Lange Afstand",
"settings_fastSpeed": "Hoge Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -157,7 +157,7 @@
"appSettings_appearance": "Uiterlijk",
"appSettings_theme": "Thema",
"appSettings_themeSystem": "Standaardinstelling",
"appSettings_themeLight": "Helder",
"appSettings_themeLight": "Licht",
"appSettings_themeDark": "Donker",
"appSettings_language": "Taal",
"appSettings_languageSystem": "Standaardinstelling",
@@ -185,16 +185,16 @@
"appSettings_channelMessageNotifications": "Kanaal Bericht Meldingen",
"appSettings_channelMessageNotificationsSubtitle": "Toon notificatie bij het ontvangen van kanaalberichten",
"appSettings_advertisementNotifications": "Advertentie-meldingen",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe knooppunten worden ontdekt",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe nodes worden ontdekt",
"appSettings_messaging": "Berichten",
"appSettings_clearPathOnMaxRetry": "Duidelijke Pad op Max Retry",
"appSettings_clearPathOnMaxRetry": "Wis Pad op Max Retry",
"appSettings_clearPathOnMaxRetrySubtitle": "Reset contactpad na 5 mislukte verzendpogingen",
"appSettings_pathsWillBeCleared": "De paden worden na 5 mislukte pogingen leeggehaald.",
"appSettings_pathsWillNotBeCleared": "Padoms worden niet automatisch verwijderd",
"appSettings_autoRouteRotation": "Automatische Route Rotatie",
"appSettings_autoRouteRotationSubtitle": "Wissel tussen de beste paden en floodmodus over.",
"appSettings_autoRouteRotationEnabled": "Automatische routeplanning rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische routeplanning rotatie is uitgeschakeld",
"appSettings_autoRouteRotation": "Route Automatisch Roteren",
"appSettings_autoRouteRotationSubtitle": "Verwissel tussen beste pad en floodmodus.",
"appSettings_autoRouteRotationEnabled": "Automatische route rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische route rotatie is uitgeschakeld",
"appSettings_battery": "Batterij",
"appSettings_batteryChemistry": "Batterijchemie",
"appSettings_batteryChemistryPerDevice": "Instellen per apparaat ({deviceName})",
@@ -210,15 +210,15 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Kaartweergave",
"appSettings_showRepeaters": "Toon Herhalingen",
"appSettings_showRepeatersSubtitle": "Toon herhalende knoopjes op de kaart",
"appSettings_showRepeaters": "Toon Repeaters",
"appSettings_showRepeatersSubtitle": "Toon repeaternodes op de kaart",
"appSettings_showChatNodes": "Chat Nodes tonen",
"appSettings_showChatNodesSubtitle": "Chatnodes weergeven op de kaart",
"appSettings_showOtherNodes": "Toon Andere Knopen",
"appSettings_showOtherNodesSubtitle": "Toon andere knooptypes op de kaart",
"appSettings_showOtherNodes": "Toon Andere Nodes",
"appSettings_showOtherNodesSubtitle": "Toon andere nodetypes op de kaart",
"appSettings_timeFilter": "Filter op tijd",
"appSettings_timeFilterShowAll": "Alle knooppunten tonen",
"appSettings_timeFilterShowLast": "Toon knopen van de laatste {hours} uur",
"appSettings_timeFilterShowAll": "Alle nodes tonen",
"appSettings_timeFilterShowLast": "Toon nodes van de laatste {hours} uur",
"@appSettings_timeFilterShowLast": {
"placeholders": {
"hours": {
@@ -227,8 +227,8 @@
}
},
"appSettings_mapTimeFilter": "Filter tijd op kaart",
"appSettings_showNodesDiscoveredWithin": "Toon knooppunten ontdekt binnen:",
"appSettings_allTime": "Alle tijd",
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
"appSettings_allTime": "Altijd",
"appSettings_lastHour": "Laat uur",
"appSettings_last6Hours": "laatste 6 uur",
"appSettings_last24Hours": "De laatste 24 uur",
@@ -266,7 +266,7 @@
}
}
},
"contacts_manageRepeater": "Beheer Herhaling",
"contacts_manageRepeater": "Beheer Repeater",
"contacts_roomLogin": "Ruimte Inloggen",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Groep bewerken",
@@ -294,7 +294,7 @@
"contacts_noContactsMatchFilter": "Geen contacten matchen met uw filter",
"contacts_noMembers": "Geen leden",
"contacts_lastSeenNow": "Laatste keer gezien nu",
"contacts_lastSeenMinsAgo": "Laast gezien {minutes} minuten geleden",
"contacts_lastSeenMinsAgo": "Laatst gezien {minutes} minuten geleden",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -539,7 +539,7 @@
"chat_pathManagement": "Beheer van Paden",
"chat_routingMode": "Routeerwijze",
"chat_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"chat_forceFloodMode": "Dwing Overstromingsmodus",
"chat_forceFloodMode": "Dwing Floodsmodus",
"chat_recentAckPaths": "Recente ACK Paden (tik om te gebruiken):",
"chat_pathHistoryFull": "De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.",
"chat_hopSingular": "Hop",
@@ -562,7 +562,7 @@
"chat_clearPathSubtitle": "Dwing herontdekking bij volgende verzending",
"chat_pathCleared": "Pad is vrijgegeven. Volgende bericht herontdekt route.",
"chat_floodModeSubtitle": "Gebruik de route-schakelaar in de app-balk",
"chat_floodModeEnabled": "Overstromingsmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_floodModeEnabled": "Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_fullPath": "Volledige Pad",
"chat_pathDetailsNotAvailable": "De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.",
"chat_pathSetHops": "Pad ingesteld: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,9 +583,9 @@
"chat_path": "Pad",
"chat_publicKey": "Openbare Sleutel",
"chat_compressOutgoingMessages": "Verzenden van uitgaande berichten comprimeren",
"chat_floodForced": "Overstroming (gedwongen)",
"chat_directForced": "Direct (gedwongen)",
"chat_hopsForced": "{count} sprongen (gedwongen)",
"chat_floodForced": "Flood (afgedwongen)",
"chat_directForced": "Direct (afgedwongen)",
"chat_hopsForced": "{count} hops (afgedwongen)",
"@chat_hopsForced": {
"placeholders": {
"count": {
@@ -593,7 +593,7 @@
}
}
},
"chat_floodAuto": "Overstroming (auto)",
"chat_floodAuto": "Flood (auto)",
"chat_direct": "Direct",
"chat_poiShared": "Gedeelde POI",
"chat_unread": "Nieuw: {count}",
@@ -604,8 +604,20 @@
}
}
},
"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 knopen met locatiegegevens",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
"map_nodesCount": "Nodes: {count}",
"@map_nodesCount": {
@@ -624,7 +636,7 @@
}
},
"map_chat": "Chat",
"map_repeater": "Herhaling",
"map_repeater": "Repeater",
"map_room": "Ruimte",
"map_sensor": "Sensor",
"map_pinDm": "Verzenden als bericht (DM)",
@@ -652,11 +664,11 @@
}
},
"map_connectToShareMarkers": "Verbind met een apparaat om markers te delen",
"map_filterNodes": "Filter Knopen",
"map_nodeTypes": "Node Types",
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Herhalingen",
"map_otherNodes": "Andere knooppunten",
"map_filterNodes": "Filter Nodes",
"map_nodeTypes": "Nodetypes",
"map_chatNodes": "Chatnodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Andere Nodes",
"map_keyPrefix": "Prefix sleutel",
"map_filterByKeyPrefix": "Filteren op sleutelvoorgemeld",
"map_publicKeyPrefix": "Openbare sleutelvoorgemeld",
@@ -665,7 +677,7 @@
"map_lastSeenTime": "Laatste Bekeken Tijd",
"map_sharedPin": "Gedeelde pin",
"map_joinRoom": "Sluit Kamer",
"map_manageRepeater": "Beheer Herhaling",
"map_manageRepeater": "Beheer Repeater",
"mapCache_title": "Offline Kaarten Cache",
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
"mapCache_noTilesToDownload": "Geen tiles te downloaden voor dit gebied.",
@@ -788,18 +800,18 @@
"time_allTime": "Alle tijd",
"dialog_disconnect": "Verbinden verbreken",
"dialog_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
"login_repeaterLogin": "Herhaalders Inloggen",
"login_repeaterLogin": "Inloggen Repeater",
"login_roomLogin": "Ruimte Inloggen",
"login_password": "Wachtwoord",
"login_enterPassword": "Wachtwoord invoeren",
"login_savePassword": "Wachtwoord opslaan",
"login_savePasswordSubtitle": "Het wachtwoord wordt veilig op dit apparaat opgeslagen.",
"login_repeaterDescription": "Voer het wachtwoord van de herhaling in om instellingen en status te openen.",
"login_repeaterDescription": "Voer het wachtwoord van de repeater in om instellingen en status te openen.",
"login_roomDescription": "Voer het wachtwoord van de kamer in om toegang te krijgen tot instellingen en status.",
"login_routing": "Routing",
"login_routingMode": "Routeerwijze",
"login_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"login_forceFloodMode": "Dwing Overstromingsmodus",
"login_forceFloodMode": "Dwing Floodmodus Af",
"login_managePaths": "Padbeheer",
"login_login": "Inloggen",
"login_attempt": "Poging {current}/{max}",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.",
"common_reload": "Opnieuw laden",
"common_clear": "Schoonmaken",
"path_currentPath": "Huidige pad: {path}",
@@ -846,7 +859,7 @@
"path_labelHexPrefixes": "Pad (hex-voorkeursletters)",
"path_helperMaxHops": "Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)",
"path_selectFromContacts": "Of select contacten:",
"path_noRepeatersFound": "Geen herhalingen of zaalservers gevonden.",
"path_noRepeatersFound": "Geen repeaters of roomservers gevonden.",
"path_customPathsRequire": "Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.",
"path_invalidHexPrefixes": "Ongeldige hex-voorkeursletters: {prefixes}",
"@path_invalidHexPrefixes": {
@@ -858,20 +871,20 @@
},
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
"path_setPath": "Stel Pad in",
"repeater_management": "Beheer Herhalingen",
"repeater_management": "Beheer Repeaters",
"repeater_managementTools": "Beheerinstrumenten",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
"repeater_telemetry": "Telemetry",
"repeater_telemetrySubtitle": "Bekijk telemetrie van sensoren en systeemgegevens",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Verzend commando's naar de herhaaldere",
"repeater_cliSubtitle": "Verzend commando's naar de repeater",
"repeater_settings": "Instellingen",
"repeater_settingsSubtitle": "Configureer repeaterparameters",
"repeater_statusTitle": "Status herhalen",
"repeater_statusTitle": "Status repeater",
"repeater_routingMode": "Routeerwijze",
"repeater_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"repeater_forceFloodMode": "Dwing Overloopmodus",
"repeater_forceFloodMode": "Dwing Floodmodus Af",
"repeater_pathManagement": "Beheer van paden",
"repeater_refresh": "Vernieuwen",
"repeater_statusRequestTimeout": "Statusverzoek is uitgevallen.",
@@ -883,22 +896,22 @@
}
}
},
"repeater_systemInformation": "Systeem Informatie",
"repeater_systemInformation": "Systeeminformatie",
"repeater_battery": "Batterij",
"repeater_clockAtLogin": "Tijd (bij aanmelden)",
"repeater_uptime": "Beschikbaarheid",
"repeater_queueLength": "Wachttijd",
"repeater_debugFlags": "Debug Flags",
"repeater_radioStatistics": "Radio Statistieken",
"repeater_debugFlags": "Debugvlaggen",
"repeater_radioStatistics": "Radiostatistieken",
"repeater_lastRssi": "Laatste RSSI",
"repeater_lastSnr": "Laatste SNR",
"repeater_noiseFloor": "Ruishoordniveau",
"repeater_noiseFloor": "Ruisvloer",
"repeater_txAirtime": "TX Airtime",
"repeater_rxAirtime": "RX Airtime",
"repeater_packetStatistics": "Pakket Statistieken",
"repeater_packetStatistics": "Pakketstatistieken",
"repeater_sent": "Verzonden",
"repeater_received": "Ontvangen",
"repeater_duplicates": "Dubbele",
"repeater_duplicates": "Duplicaat",
"repeater_daysHoursMinsSecs": "{days} dagen {hours} uur {minutes} minuten {seconds} seconden",
"@repeater_daysHoursMinsSecs": {
"placeholders": {
@@ -916,7 +929,7 @@
}
}
},
"repeater_packetTxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetTxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +943,7 @@
}
}
},
"repeater_packetRxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetRxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +957,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Overstroming: {flood}, Direct: {direct}",
"repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -963,10 +976,10 @@
}
}
},
"repeater_settingsTitle": "Herstelinstellingen",
"repeater_settingsTitle": "Repeater Instellingen",
"repeater_basicSettings": "Basisinstellingen",
"repeater_repeaterName": "Herhaalnaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze herhaling",
"repeater_repeaterName": "Repeaternaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
"repeater_adminPassword": "Admin wachtwoord",
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
"repeater_guestPassword": "Wachtwoord Gast",
@@ -977,7 +990,7 @@
"repeater_txPower": "TX Power",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bandbreedte",
"repeater_spreadingFactor": "Spreadsnelheid",
"repeater_spreadingFactor": "Spreidingsfactor",
"repeater_codingRate": "Codeertarief",
"repeater_locationSettings": "Locatie Instellingen",
"repeater_latitude": "Breedtegraad",
@@ -985,11 +998,11 @@
"repeater_longitude": "Lengtegraad",
"repeater_longitudeHelper": "Graadseconden (bijv. -122.4194)",
"repeater_features": "Kenmerken",
"repeater_packetForwarding": "Pakketdoorstrooming",
"repeater_packetForwardingSubtitle": "Herstel activeren om pakketten door te sturen",
"repeater_packetForwarding": "Pakketdoorvoering",
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
"repeater_guestAccess": "Toegang voor Gasten",
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
"repeater_privacyMode": "Privacy Mode",
"repeater_privacyMode": "Privacy Modus",
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"repeater_advertisementSettings": "Advertentie Instellingen",
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
@@ -1001,7 +1014,7 @@
}
}
},
"repeater_floodAdvertInterval": "Advertentie Interval bij overstroming",
"repeater_floodAdvertInterval": "Flood Advertentie Interval",
"repeater_floodAdvertIntervalHours": "{hours} uur",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1012,15 +1025,15 @@
},
"repeater_encryptedAdvertInterval": "Versleutelde Advertentie Interval",
"repeater_dangerZone": "Gevaarzone",
"repeater_rebootRepeater": "Herstart Herhaalder",
"repeater_rebootRepeaterSubtitle": "De herstart van het herhalerapparaat",
"repeater_rebootRepeater": "Herstart Repeater",
"repeater_rebootRepeaterSubtitle": "Herstart het Repeaterapparaat",
"repeater_rebootRepeaterConfirm": "Ben je er zeker van dat je deze repeater opnieuw wilt opstarten?",
"repeater_regenerateIdentityKey": "Identiteit sleutel opnieuw genereren",
"repeater_regenerateIdentityKeySubtitle": "Nieuwe publieke/private sleutelpaar genereren",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de herhaling. Doorgaan?",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de repeater. Doorgaan?",
"repeater_eraseFileSystem": "Verwijder Besturingssysteem",
"repeater_eraseFileSystemSubtitle": "Formateer het herhalende bestandsysteem",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de herhaling wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
"repeater_commandSent": "Commando verzonden: {command}",
"@repeater_commandSent": {
@@ -1049,7 +1062,7 @@
}
},
"repeater_refreshBasicSettings": "Basisinstellingen vernieuwen",
"repeater_refreshRadioSettings": "Radiozenders Instellingen Bijwerken",
"repeater_refreshRadioSettings": "Radiozender Instellingen Verversen",
"repeater_refreshTxPower": "Nieuw laden TX-vermogen",
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
@@ -1072,7 +1085,7 @@
}
}
},
"repeater_cliTitle": "Herhaling CLI",
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Debug Volgende Commando",
"repeater_commandHelp": "Help",
"repeater_clearHistory": "Verwijder Geschiedenis",
@@ -1106,14 +1119,14 @@
"repeater_cliHelpClearStats": "Reset verschillende statistiek-tellers naar nul.",
"repeater_cliHelpSetAf": "Stelt de luchtvaartfactor in.",
"repeater_cliHelpSetTx": "Stelt LoRa zendvermogen in dBm. (om te wijzigen)",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de herhalerrol voor dit knoop.",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de repeater rol van deze node.",
"repeater_cliHelpSetAllowReadOnly": "(Kamervisie) Als 'aan', dan wordt inloggen met een blanco wachtwoord toegestaan, maar kan niet naar de kamervisie Posten. (alleen lezen mogelijk).",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend overlastpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend floodpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetIntThresh": "Stelt de Interferentiewaarde (in dB) in. Standaardwaarde is 14. Stel in op 0 om het detecteren van kanaalinterferentie uit te schakelen.",
"repeater_cliHelpSetAgcResetInterval": "Stelt het interval in om de Auto Gain Controller te resetten. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'dubbele ACKs'.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'duplicate ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Stelt het timerinterval in minuten in om een lokale (zero-hop) advertentiepakket te versturen. Stel in op 0 om uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een overstromingsadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een floodadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetGuestPassword": "Stelt/past de gastenwacht aan of wijzigt deze. (voor herstelcontacten kunnen gastelogins de \"Get Stats\" verzoek verzenden)",
"repeater_cliHelpSetName": "Stelt de advertentietitel in.",
"repeater_cliHelpSetLat": "Stelt de breedtegraad van de advertentiekaart in. (graadrijssysteem)",
@@ -1136,7 +1149,7 @@
"repeater_cliHelpLogErase": "Verwijdert de pakketlogs uit het bestandssysteem.",
"repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.",
"repeater_cliHelpRegion": "(reeks alleen) Lijst alle gedefinieerde regio's en huidige overstromingsrechten.",
"repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.",
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
"repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"",
"repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.",
@@ -1148,8 +1161,8 @@
"repeater_cliHelpRegionSave": "Bewaar de lijst/kaart van de regio's naar de opslag.",
"repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.",
"repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
"repeater_cliHelpGpsSync": "Synchroniseer knooptime met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van het knoop vast naar GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen",
"repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
"repeater_commandsListTitle": "Commandenlijst",
@@ -1158,8 +1171,8 @@
"repeater_settingsCategory": "Instellingen",
"repeater_bridge": "Bruggen",
"repeater_logging": "Logging",
"repeater_neighborsRepeaterOnly": "Buren (Alleen herhaald)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen voor Repeater)",
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)",
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
"repeater_gpsManagement": "Beheer GPS",
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
@@ -1228,12 +1241,12 @@
"channelPath_title": "Pakketpad",
"channelPath_viewMap": "Kaart bekijken",
"channelPath_otherObservedPaths": "Overige Waargenomen Paden",
"channelPath_repeaterHops": "Herhalingstapjes",
"channelPath_repeaterHops": "Repeater Hops",
"channelPath_noHopDetails": "De details van de pakket zijn niet verstrekt.",
"channelPath_messageDetails": "Details Bericht",
"channelPath_senderLabel": "Afzender",
"channelPath_timeLabel": "Tijd",
"channelPath_repeatsLabel": "Herhalen",
"channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Pad {index}",
"channelPath_observedLabel": "Waargenomen",
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
@@ -1271,7 +1284,7 @@
}
},
"channelPath_unknownPath": "Onbekend",
"channelPath_floodPath": "Overstroming",
"channelPath_floodPath": "Flood",
"channelPath_directPath": "Direct",
"channelPath_observedZeroOf": "0 van {total} sprongen",
"@channelPath_observedZeroOf": {
@@ -1293,7 +1306,7 @@
}
},
"channelPath_mapTitle": "Padkaart",
"channelPath_noRepeaterLocations": "Geen herhaler locaties beschikbaar voor deze route.",
"channelPath_noRepeaterLocations": "Geen repeaters beschikbaar voor deze route.",
"channelPath_primaryPath": "Pad {index} (Hoofdtype)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1323,7 +1336,7 @@
}
},
"channelPath_noHopDetailsAvailable": "Geen details beschikbaar voor dit pakket.",
"channelPath_unknownRepeater": "Onbekend Herhaalaar",
"channelPath_unknownRepeater": "Onbekend Repeater",
"listFilter_tooltip": "Filteren en sorteren",
"listFilter_sortBy": "Sorteren door",
"listFilter_latestMessages": "Recente berichten",
@@ -1332,8 +1345,193 @@
"listFilter_filters": "Filters",
"listFilter_all": "Alles",
"listFilter_users": "Gebruikers",
"listFilter_repeaters": "Herhalingen",
"listFilter_roomServers": "Kamervirtualisatie",
"listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Roomservers",
"listFilter_unreadOnly": "Alleen ongelezen",
"listFilter_newGroup": "Nieuwe groep"
"listFilter_newGroup": "Nieuwe groep",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Buren",
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbours": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Onbekende {pubkey}",
"neighbors_heardAgo": "Horen: {time} geleden",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Gemeenschap",
"common_ok": "OK",
"community_createDesc": "Maak een nieuwe community en deel deze via QR-code.",
"community_create": "Maak Gemeenschap",
"community_join": "Sluit aan",
"community_joinTitle": "Worden lid van de community",
"community_joinConfirmation": "Wil je je aansluiten bij de community \"{name}\"?",
"community_scanQr": "Scan Gemeenschap QR",
"community_scanInstructions": "Richt de camera op een gemeenschappelijke QR-code",
"community_showQr": "Toon QR-code",
"community_publicChannel": "Gemeenschap Openbaar",
"community_hashtagChannel": "Gemeenschappelijk Hashtag",
"community_name": "Gemeenschapnaam",
"community_enterName": "Voer de gemeenschapsnaam in",
"community_created": "Gemeenschap \"{name}\" is aangemaakt",
"community_joined": "Gevonden in de community \"{name}\"",
"community_qrTitle": "Deel Gemeenschap",
"community_qrInstructions": "Scan deze QR-code om je aan te sluiten bij {name}",
"community_hashtagPrivacyHint": "Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community",
"community_invalidQrCode": "Ongeldige community QR-code",
"community_alreadyMember": "Alleen al lid",
"community_alreadyMemberMessage": "U bent al lid van \"{name}\".",
"community_addPublicChannel": "Voeg een Openbaar Gemeenschapskanaal toe",
"community_addPublicChannelHint": "Automatisch de publieke kanaal toevoegen voor deze community",
"community_noCommunities": "Nog geen gemeenschappen zijn bijgesloten.",
"community_scanOrCreate": "Scan een QR-code of een community aanmaken om te beginnen",
"community_manageCommunities": "Beheer Gemeenschappen",
"community_delete": "Laat Gemeenschap",
"community_deleteConfirm": "\"{name}\" verlaten?",
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlaten",
"community_addHashtagChannel": "Voeg Community Hashtag toe",
"community_addHashtagChannelDesc": "Voeg een hashtag-kanaal toe aan deze community",
"community_selectCommunity": "Selecteer Gemeenschap",
"community_regularHashtag": "Gewone Hashtag",
"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_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"
}
+199 -1
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.",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.",
"common_reload": "Ponownie załadować",
"common_clear": "Wyczyść",
"path_currentPath": "Aktualny ścieżka: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Powtarzacze",
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa"
"listFilter_newGroup": "Nowa grupa",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Sąsiedzi",
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
"channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału",
"channels_joinPublicChannel": "Dołącz do kanału publicznego.",
"channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.",
"channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem",
"channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.",
"channels_scanQrCode": "Skanuj kod QR",
"channels_scanQrCodeComingSoon": "Wkrótce",
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Usłyszano: {time} temu",
"neighbors_unknownContact": "Nieznana {pubkey}",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
"room_management": "Zarządzanie Serwerem Pokoju",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.",
"community_title": "Społeczność",
"community_create": "Utwórz Społeczność",
"common_ok": "OK",
"community_join": "Dołącz",
"community_joinTitle": "Dołącz do społeczności",
"community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?",
"community_scanQr": "Skanuj QR kod społeczności",
"community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.",
"community_showQr": "Pokaż kod QR",
"community_publicChannel": "Społeczność Publiczna",
"community_hashtagChannel": "Hashtag Społeczności",
"community_name": "Nazwa Społeczności",
"community_enterName": "Wprowadź nazwę społeczności",
"community_created": "Społeczność \"{name}\" została utworzona",
"community_joined": "Dołączył do społeczności \"{name}\"",
"community_qrTitle": "Dziel się Społecznością",
"community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}",
"community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności",
"community_invalidQrCode": "Nieprawidłowy kod QR społeczności.",
"community_alreadyMember": "Już jesteś członkiem.",
"community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".",
"community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności",
"community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.",
"community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.",
"community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.",
"community_manageCommunities": "Zarządzaj Grupami",
"community_delete": "Opuszczenie Społeczności",
"community_deleteConfirm": "Opuścić \"{name}\"?",
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
"community_addHashtagChannel": "Dodaj hashtag społeczności",
"community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności",
"community_selectCommunity": "Wybierz społeczność",
"community_regularHashtag": "Hashtag regular",
"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_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"
}
+199 -1
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",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.",
"common_reload": "Recarregar",
"common_clear": "Limpar",
"path_currentPath": "Caminho atual: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de sala",
"listFilter_unreadOnly": "Apenas não lido",
"listFilter_newGroup": "Novo grupo"
"listFilter_newGroup": "Novo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
"channels_createPrivateChannel": "Criar um Canal Privado",
"channels_joinPrivateChannel": "Junte-se a um Canal Privado",
"channels_joinPublicChannel": "Junte-se ao Canal Público",
"channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.",
"channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag",
"channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.",
"channels_scanQrCode": "Digitalizar um Código QR",
"channels_scanQrCodeComingSoon": "Em breve",
"channels_enterHashtag": "Insira hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Ouvido: {time} atrás",
"neighbors_unknownContact": "{pubkey} Desconhecido",
"settings_locationGPSEnable": "Ativar GPS",
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"contacts_manageRoom": "Gerenciar Servidor de Sala",
"room_management": "Gerenciamento de Servidor de Sala",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Comunidade",
"community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.",
"common_ok": "OK",
"community_create": "Criar Comunidade",
"community_join": "Junte-se",
"community_joinTitle": "Junte-se à Comunidade",
"community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?",
"community_scanQr": "Digitalizar a QR Code da Comunidade",
"community_scanInstructions": "Aponte a câmera para um código QR da comunidade",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidade Pública",
"community_hashtagChannel": "Hashtag da Comunidade",
"community_name": "Nome da Comunidade",
"community_enterName": "Insira o nome da comunidade",
"community_created": "Comunidade \"{name}\" criada",
"community_joined": "Juntou-se à comunidade \"{name}\"",
"community_qrTitle": "Partilhar Comunidade",
"community_qrInstructions": "Escanear este código QR para juntar-se a {name}",
"community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade",
"community_invalidQrCode": "Código QR da comunidade inválido",
"community_alreadyMember": "Já é Membro",
"community_alreadyMemberMessage": "Você já é membro de \"{name}\".",
"community_addPublicChannel": "Adicionar Canal Público da Comunidade",
"community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade",
"community_noCommunities": "Ainda não foram adicionadas comunidades.",
"community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.",
"community_manageCommunities": "Gerenciar Comunidades",
"community_delete": "Deixar Comunidade",
"community_deleteConfirm": "Sair de \"{name}\"?",
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
"community_addHashtagChannel": "Adicionar Hashtag da Comunidade",
"community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade",
"community_selectCommunity": "Selecione Comunidade",
"community_regularHashtag": "Hashtag Regular",
"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_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"
}
+778
View File
@@ -0,0 +1,778 @@
{
"@@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": "Неправильный формат ссылки"
}
+200 -2
View File
@@ -559,7 +559,7 @@
"chat_setCustomPath": "Nastaviť vlastnú cestu",
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
"chat_clearPath": "Vyčistiš cestu",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
@@ -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.",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.",
"common_reload": "Načítať",
"common_clear": "Zmazať",
"path_currentPath": "Aktívna cesta: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Opakovadlá",
"listFilter_roomServers": "Servéry miestnosti",
"listFilter_unreadOnly": "Nezaregistrované len",
"listFilter_newGroup": "Nová skupina"
"listFilter_newGroup": "Nová skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbours": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
"channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.",
"channels_joinPublicChannel": "Pripojte sa k verejnému kanálu",
"channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.",
"channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu",
"channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.",
"channels_scanQrCode": "Skenujte QR kód",
"channels_scanQrCodeComingSoon": "Čoskoro",
"channels_enterHashtag": "Zadajte hashtag",
"channels_hashtagHint": "napr. #tím",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Počuli sme to: {time} dozadu",
"neighbors_unknownContact": "Neznáma {pubkey}",
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
"contacts_manageRoom": "Spravovať server miestnosti",
"room_management": "Správa servera miestnosti",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Vytvoriť komunitu",
"community_title": "Komunita",
"community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.",
"community_join": "Pripojiť",
"community_joinTitle": "Pripojiť sa k spoločenstvu",
"community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?",
"community_scanQr": "Skontrolujte komunitný QR kód",
"community_scanInstructions": "Zamerte kameru na komunitný QR kód.",
"community_showQr": "Zobraziť QR kód",
"common_ok": "OK\nDobre",
"community_publicChannel": "Komunita verejná",
"community_hashtagChannel": "Komunitný Hashtag",
"community_name": "Komunita",
"community_enterName": "Zadajte názov komunity",
"community_created": "Komunita \"{name}\" vytvorená",
"community_joined": "Pripojená komunita \"{name}\"",
"community_qrTitle": "Zdieľť komunitu",
"community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.",
"community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity",
"community_invalidQrCode": "Neplatná QR kód komunity.",
"community_alreadyMember": "Už ste členom.",
"community_alreadyMemberMessage": "Vy ste už členom \"{name}\".",
"community_addPublicChannel": "Pridať verejný komunikačný kanál",
"community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.",
"community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite",
"community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.",
"community_manageCommunities": "Spravovať komunity",
"community_delete": "Nechajte komunitu",
"community_deleteConfirm": "Opustiť \"{name}\"?",
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
"community_addHashtagChannel": "Pridať komunitný hashtag",
"community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.",
"community_selectCommunity": "Vyberte komunitu",
"community_regularHashtag": "Zvyčajný hashtag",
"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_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}\""
}
+203 -5
View File
@@ -176,7 +176,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih",
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena",
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
@@ -256,7 +256,7 @@
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.",
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"@contacts_removeConfirm": {
@@ -291,7 +291,7 @@
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.",
"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",
@@ -604,9 +604,21 @@
}
}
},
"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 prikazajo na zemljeobrazniku.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
"map_nodesCount": "Omize: {count}",
"@map_nodesCount": {
"placeholders": {
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.",
"common_reload": "Ponovno naloži",
"common_clear": "Ponoviti",
"path_currentPath": "Trenutna pot: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Ponovitve",
"listFilter_roomServers": "Smeti za prostore",
"listFilter_unreadOnly": "Nezbrani samo",
"listFilter_newGroup": "Nova skupina"
"listFilter_newGroup": "Nova skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbours": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
"channels_createPrivateChannel": "Ustvari zasebno kanal.",
"channels_joinPublicChannel": "Pridružite se javnemu kanalu",
"channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.",
"channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom",
"channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.",
"channels_scanQrCode": "Skeniraj QR kodo",
"channels_scanQrCodeComingSoon": "Prihajajoča",
"channels_enterHashtag": "Vnesite hashtag",
"channels_hashtagHint": "npr. #ekipa",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Nepoznano {pubkey}",
"neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Ustvari novo skupnost in jo deli preko QR kode.",
"community_title": "Skupnost",
"common_ok": "V redu",
"community_create": "Ustvari skupnost",
"community_joinTitle": "Pridružite se skupnosti",
"community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?",
"community_scanQr": "Skeniraj QR kode skupnosti",
"community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.",
"community_showQr": "Pokaži QR kodo",
"community_publicChannel": "Skupnostna javna",
"community_hashtagChannel": "Skupnostni hashtag",
"community_name": "Komunitarne ime",
"community_enterName": "Vnesite ime skupnosti",
"community_join": "Pridružiti se",
"community_created": "Skupnost \"{name}\" je bila ustvarila.",
"community_joined": "Prilojen k skupnosti \"{name}\"",
"community_qrTitle": "Delite skupnost",
"community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.",
"community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti",
"community_invalidQrCode": "Neveljaven QR koden skupnosti",
"community_alreadyMember": "Že član",
"community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.",
"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_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapustiti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
"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_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"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": "Tajna za \"{name}\" ponovno ustvarjena",
"community_regenerateSecret": "Preberi nov tajni kôd",
"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 nov kôd QR za posodabljanje tajne za {name}",
"community_updateSecret": "Ažurniraj tajno",
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\""
}
+199 -1
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",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.",
"common_reload": "Ladda om",
"common_clear": "Rensa",
"path_currentPath": "Nuvarande sökväg: {path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "Upprepare",
"listFilter_roomServers": "Rumservrar",
"listFilter_unreadOnly": "Endast oinlästa",
"listFilter_newGroup": "Ny grupp"
"listFilter_newGroup": "Ny grupp",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Grannar",
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbours": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
"channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.",
"channels_joinPublicChannel": "Gå med i den Offentliga Kanalen",
"channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.",
"channels_joinHashtagChannel": "Gå med i en Hashtagkanal",
"channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.",
"channels_scanQrCode": "Skanna en QR-kod",
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hördes: {time} sedan",
"neighbors_unknownContact": "Okänd {pubkey}",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Skapa Gemenskap",
"community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.",
"common_ok": "Okej",
"community_title": "Gemenskap",
"community_join": "Gå med",
"community_joinTitle": "Gå med i gemenskapen",
"community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?",
"community_scanQr": "Skanna Gemenskapens QR",
"community_scanInstructions": "Rikta kameran mot en QR-kod i communityn",
"community_showQr": "Visa QR-kod",
"community_publicChannel": "Föreningens Offentliga",
"community_name": "Gemenskapens namn",
"community_enterName": "Ange communities namn",
"community_created": "Community \"{name}\" har skapats",
"community_joined": "Medlem i communityn \"{name}\"",
"community_qrTitle": "Dela Gemenskap",
"community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"",
"community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn",
"community_hashtagChannel": "Community Hashtag",
"community_invalidQrCode": "Ogiltig community QR-kod",
"community_alreadyMember": "Är redan medlem",
"community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".",
"community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)",
"community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community",
"community_noCommunities": "Inga gemenskaper har anslutats ännu",
"community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång",
"community_manageCommunities": "Hantera Gemenskaper",
"community_delete": "Lämna Gemenskap",
"community_deleteConfirm": "Lämna \"{name}\"?",
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Lämnade community \"{name}\"",
"community_addHashtagChannel": "Lägg till Gemenskapens Hashtag",
"community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community",
"community_selectCommunity": "Välj Gemenskap",
"community_regularHashtag": "Vanlig Hash Tag",
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"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"
}
+1538
View File
File diff suppressed because it is too large Load Diff
+199 -1
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才能在地图上显示",
@@ -821,6 +833,7 @@
}
}
},
"login_failedMessage": "登录失败。密码不正确或中继器不可达。",
"common_reload": "重新加载",
"common_clear": "清除",
"path_currentPath": "当前路径:{path}",
@@ -1335,5 +1348,190 @@
"listFilter_repeaters": "重复器",
"listFilter_roomServers": "房间服务器",
"listFilter_unreadOnly": "未读消息",
"listFilter_newGroup": "新组"
"listFilter_newGroup": "新组",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "查看零跳邻居。",
"repeater_neighbours": "邻居",
"neighbors_receivedData": "收到邻居数据",
"neighbors_requestTimedOut": "邻居请求超时处理。",
"neighbors_errorLoading": "加载邻居时出错:{error}",
"neighbors_repeatersNeighbours": "重复器邻居",
"neighbors_noData": "没有可用的邻居数据。",
"channels_joinPrivateChannel": "加入私密频道",
"channels_createPrivateChannelDesc": "使用密钥保护。",
"channels_joinPrivateChannelDesc": "手动输入密钥。",
"channels_createPrivateChannel": "创建私聊频道",
"channels_joinPublicChannel": "加入公共频道",
"channels_joinPublicChannelDesc": "任何人都可以加入这个频道。",
"channels_joinHashtagChannel": "加入标签频道",
"channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。",
"channels_scanQrCode": "扫描二维码",
"channels_scanQrCodeComingSoon": "即将到来",
"channels_enterHashtag": "输入标签",
"channels_hashtagHint": "例如 #团队",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "听到的时间:{time}前",
"neighbors_unknownContact": "未知{pubkey}",
"settings_locationGPSEnable": "启用GPS",
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
"settings_locationIntervalSec": "GPS 间隔(秒)",
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。",
"contacts_manageRoom": "管理房间服务器",
"room_management": "房间服务器管理",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "创建社区",
"community_title": "社区",
"community_createDesc": "创建新的社区并可通过二维码分享。",
"common_ok": "好的",
"community_join": "加入",
"community_joinTitle": "加入社区",
"community_joinConfirmation": "您想加入社区 \"{name}\" 吗?",
"community_scanQr": "扫描社区二维码",
"community_scanInstructions": "将相机对准社区二维码",
"community_showQr": "显示二维码",
"community_publicChannel": "社区公开",
"community_hashtagChannel": "社区标签",
"community_name": "社区名称",
"community_enterName": "请输入社区名称",
"community_created": "社区“{name}”已创建",
"community_joined": "加入社区 \"{name}\"",
"community_qrTitle": "分享社区",
"community_qrInstructions": "扫描此二维码加入{name}",
"community_hashtagPrivacyHint": "社区标签频道仅社区成员可加入",
"community_invalidQrCode": "无效的社区二维码",
"community_alreadyMember": "已经是会员了",
"community_alreadyMemberMessage": "您已经是 \"{name}\" 的会员。",
"community_addPublicChannel": "添加社区公共频道",
"community_addPublicChannelHint": "自动添加该社区的公共频道",
"community_noCommunities": "尚未加入任何社区",
"community_scanOrCreate": "扫描二维码或创建社区开始",
"community_manageCommunities": "管理社群",
"community_delete": "退出社区",
"community_deleteConfirm": "退出 \"{name}\"",
"community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "已退出社区 \"{name}\"",
"community_addHashtagChannel": "添加社区标签",
"community_addHashtagChannelDesc": "添加一个话题频道给此社区",
"community_selectCommunity": "选择社区",
"community_regularHashtag": "常规话题标签",
"community_regularHashtagDesc": "公共话题(任何人都可以加入)",
"community_communityHashtag": "社区标签",
"community_communityHashtagDesc": "仅限社区成员使用",
"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}”"
}
+1 -1
View File
@@ -27,7 +27,7 @@ void main() async {
final storage = StorageService();
final connector = MeshCoreConnector();
final pathHistoryService = PathHistoryService(storage);
final retryService = MessageRetryService(storage);
final retryService = MessageRetryService();
final appSettingsService = AppSettingsService();
final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService();
+41
View File
@@ -1,5 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
class Channel {
@@ -61,6 +64,44 @@ class Channel {
return bytes;
}
/// Derive PSK from hashtag name using SHA256.
/// The hashtag is normalized to include '#' prefix.
/// Returns first 16 bytes of SHA256 hash as PSK.
static Uint8List derivePskFromHashtag(String hashtag) {
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
return Uint8List.fromList(hash.sublist(0, 16));
}
/// Derive PSK for community public channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
///
/// This creates a channel that is "public" only to members who have
/// the community secret. Outsiders see only opaque IDs.
static Uint8List deriveCommunityPublicPsk(Uint8List secret) {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
///
/// Community hashtag channels are deterministic for all members
/// (same name => same id) but impossible to enumerate/guess without K.
static Uint8List deriveCommunityHashtagPsk(Uint8List secret, String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Normalize a hashtag name for consistent community PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
static String formatPskHex(Uint8List psk) {
return _bytesToHex(psk);
}
+243
View File
@@ -0,0 +1,243 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
/// distributed via QR code. Members can create Community Public Channels
/// and Community Hashtag Channels that are opaque to outsiders.
class Community {
/// Unique identifier for local storage
final String id;
/// Display name for the community
final String name;
/// The 32-byte shared secret (K)
final Uint8List secret;
/// Timestamp when the community was created/joined
final DateTime createdAt;
/// List of hashtag channel names (without #) that have been added
final List<String> hashtagChannels;
Community({
required this.id,
required this.name,
required this.secret,
required this.createdAt,
List<String>? hashtagChannels,
}) : hashtagChannels = hashtagChannels ?? [];
/// Generate a new community with a random 32-byte secret
factory Community.create({
required String id,
required String name,
}) {
final random = Random.secure();
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: DateTime.now(),
);
}
/// Parse a community from QR code JSON data
factory Community.fromQrData(String id, String qrData) {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') {
throw const FormatException('Invalid QR code type');
}
if (json['v'] != 1) {
throw const FormatException('Unsupported QR code version');
}
final name = json['name'] as String;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) {
throw const FormatException('Invalid secret length');
}
return Community(
id: id,
name: name,
secret: Uint8List.fromList(secret),
createdAt: DateTime.now(),
);
}
/// Parse a community from storage JSON
factory Community.fromJson(Map<String, dynamic> json) {
return Community(
id: json['id'] as String,
name: json['name'] as String,
secret: base64Decode(json['secret'] as String),
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
hashtagChannels: (json['hashtag_channels'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
/// Convert to JSON for storage
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'secret': base64Encode(secret),
'created_at': createdAt.millisecondsSinceEpoch,
'hashtag_channels': hashtagChannels,
};
}
/// Generate QR code JSON payload for sharing
String toQrJson() {
return jsonEncode({
'v': 1,
'type': 'meshcore_community',
'name': name,
'k': base64Url.encode(secret),
});
}
/// Derive the public Community ID from the secret.
/// This is safe to display/log since it's one-way derived.
/// CID = SHA256("community:v1" || K)
String get communityId {
final data = utf8.encode('community:v1') + secret;
final hash = crypto.sha256.convert(data).bytes;
return _bytesToHex(Uint8List.fromList(hash));
}
/// Short version of community ID for display (first 8 chars)
String get shortCommunityId => communityId.substring(0, 8);
/// Derive PSK for community public channel.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
Uint8List deriveCommunityPublicPsk() {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
Uint8List deriveCommunityHashtagPsk(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Check if QR data is valid community data
static bool isValidQrData(String data) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return false;
if (json['v'] != 1) return false;
if (json['name'] == null || (json['name'] as String).isEmpty) {
return false;
}
if (json['k'] == null) return false;
final secret = base64Url.decode(json['k'] as String);
return secret.length == 32;
} catch (_) {
return false;
}
}
/// Normalize a hashtag name for consistent PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
if (hashtagChannels.contains(normalized)) {
return this;
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: [...hashtagChannels, normalized],
);
}
/// Remove a hashtag channel from this community's list
Community removeHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: hashtagChannels.where((h) => h != normalized).toList(),
);
}
/// Create a copy of this community with a new secret
Community withNewSecret(Uint8List newSecret) {
return Community(
id: id,
name: name,
secret: newSecret,
createdAt: createdAt,
hashtagChannels: hashtagChannels,
);
}
/// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() {
final random = Random.secure();
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
}
/// Extract secret from QR data (for updating existing community)
static Uint8List? extractSecretFromQrData(String qrData) {
try {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return null;
if (json['v'] != 1) return null;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) return null;
return Uint8List.fromList(secret);
} catch (_) {
return null;
}
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Community &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
+168 -94
View File
@@ -3,11 +3,14 @@ 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/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
@@ -15,6 +18,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 +37,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 +166,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 +199,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 +281,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 +297,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 +319,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 +435,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 +760,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 +783,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(
File diff suppressed because it is too large Load Diff
+153 -75
View File
@@ -5,11 +5,14 @@ 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/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 +23,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 +40,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 +190,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 +231,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 +310,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 +332,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 +394,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,
);
}
});
}
@@ -957,7 +1005,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 +1019,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],
),
),
],
],
],
),
),
],
),
@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../widgets/qr_scanner_widget.dart';
/// Screen for scanning community QR codes to join communities.
///
/// After successful scan, the user can:
/// 1. Join the community (saves to local storage)
/// 2. Optionally add the Community Public Channel to the device
class CommunityQrScannerScreen extends StatefulWidget {
const CommunityQrScannerScreen({super.key});
@override
State<CommunityQrScannerScreen> createState() =>
_CommunityQrScannerScreenState();
}
class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
final CommunityStore _communityStore = CommunityStore();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
? const Center(child: CircularProgressIndicator())
: QrScannerWidget(
onScanned: (data) => _handleScannedData(context, data),
validator: Community.isValidQrData,
onValidationFailed: (_) => _showInvalidQrError(context),
instructions: context.l10n.community_scanInstructions,
),
);
}
Future<void> _handleScannedData(BuildContext context, String data) async {
if (_isProcessing) return;
setState(() {
_isProcessing = true;
});
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
// Check if this community already exists
final existing = await _communityStore.findByCommunityId(
community.communityId,
);
if (existing != null) {
if (context.mounted) {
_showAlreadyMemberDialog(context, existing);
}
return;
}
// Show confirmation dialog
if (context.mounted) {
await _showJoinConfirmationDialog(context, community);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
);
}
void _showAlreadyMemberDialog(BuildContext context, Community community) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.community_alreadyMember),
content: Text(
context.l10n.community_alreadyMemberMessage(community.name),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: Text(context.l10n.common_ok),
),
],
),
);
}
Future<void> _showJoinConfirmationDialog(
BuildContext context,
Community community,
) async {
bool addPublicChannel = true;
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(context.l10n.community_joinTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.community_joinConfirmation(community.name)),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.groups,
color: Theme.of(dialogContext).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
community.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'ID: ${community.shortCommunityId}...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
CheckboxListTile(
value: addPublicChannel,
onChanged: (value) {
setDialogState(() {
addPublicChannel = value ?? true;
});
},
title: Text(context.l10n.community_addPublicChannel),
subtitle: Text(context.l10n.community_addPublicChannelHint),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(context.l10n.community_join),
),
],
),
),
);
if (result == true && context.mounted) {
await _joinCommunity(context, community, addPublicChannel);
} else if (context.mounted) {
// User cancelled - go back
Navigator.pop(context);
}
}
Future<void> _joinCommunity(
BuildContext context,
Community community,
bool addPublicChannel,
) async {
// Save community to local storage
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
if (addPublicChannel && context.mounted) {
final connector = context.read<MeshCoreConnector>();
final nextIndex = _findNextAvailableChannelIndex(connector);
if (nextIndex != null) {
final psk = community.deriveCommunityPublicPsk();
final channelName = '${community.name} Public';
connector.setChannel(nextIndex, channelName, psk);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
),
);
// Return to previous screen
Navigator.pop(context, community);
}
}
int? _findNextAvailableChannelIndex(MeshCoreConnector connector) {
final usedIndices = connector.channels.map((c) => c.index).toSet();
for (int i = 0; i < connector.maxChannels; i++) {
if (!usedIndices.contains(i)) return i;
}
return null;
}
}
+212 -93
View File
@@ -27,13 +27,15 @@ import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination {
chat,
management,
}
class ContactsScreen extends StatefulWidget {
final bool hideBackButton;
const ContactsScreen({
super.key,
this.hideBackButton = false,
});
const ContactsScreen({super.key, this.hideBackButton = false});
@override
State<ContactsScreen> createState() => _ContactsScreenState();
@@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
top: false,
child: QuickSwitchBar(
selectedIndex: 0,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
),
),
),
@@ -168,8 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
}
final filteredAndSorted = _filterAndSortContacts(contacts, connector);
final filteredGroups =
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts);
final filteredGroups = _showUnreadOnly
? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
return Column(
children: [
@@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
@@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
final group = filteredGroups[index];
return _buildGroupTile(context, group, contacts);
}
final contact = filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(contact);
final contact =
filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(
contact,
);
return _ContactTile(
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
onTap: () => _openChat(context, contact),
onLongPress: () => _showContactOptions(context, connector, contact),
onLongPress: () =>
_showContactOptions(context, connector, contact),
);
},
),
@@ -255,40 +266,61 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<ContactGroup> _filterAndSortGroups(List<ContactGroup> groups, List<Contact> contacts) {
List<ContactGroup> _filterAndSortGroups(
List<ContactGroup> groups,
List<Contact> contacts,
) {
final query = _searchQuery.trim().toLowerCase();
final contactsByKey = <String, Contact>{};
for (final contact in contacts) {
contactsByKey[contact.publicKeyHex] = contact;
}
final filtered = groups.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) return true;
}
return false;
}).where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
}).toList();
final filtered = groups
.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) {
return true;
}
}
return false;
})
.where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
})
.toList();
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return filtered;
}
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) {
List<Contact> _filterAndSortContacts(
List<Contact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) {
if (_searchQuery.isEmpty) return true;
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();
}
@@ -301,19 +333,27 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_sortOption) {
case ContactSortOption.lastSeen:
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)));
filtered.sort(
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
);
break;
case ContactSortOption.recentMessages:
filtered.sort((a, b) {
final aMessages = connector.getMessages(a);
final bMessages = connector.getMessages(b);
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
final aLastMsg = aMessages.isEmpty
? DateTime(1970)
: aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty
? DateTime(1970)
: bMessages.last.timestamp;
return bLastMsg.compareTo(aLastMsg);
});
break;
case ContactSortOption.name:
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
}
@@ -340,7 +380,11 @@ class _ContactsScreenState extends State<ContactsScreen>
: contact.lastSeen;
}
Widget _buildGroupTile(BuildContext context, ContactGroup group, List<Contact> contacts) {
Widget _buildGroupTile(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final memberContacts = _resolveGroupContacts(group, contacts);
final subtitle = _formatGroupMembers(context, memberContacts);
return ListTile(
@@ -359,7 +403,10 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<Contact> _resolveGroupContacts(ContactGroup group, List<Contact> contacts) {
List<Contact> _resolveGroupContacts(
ContactGroup group,
List<Contact> contacts,
) {
final byKey = <String, Contact>{};
for (final contact in contacts) {
byKey[contact.publicKeyHex] = contact;
@@ -371,7 +418,9 @@ class _ContactsScreenState extends State<ContactsScreen>
resolved.add(contact);
}
}
resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
resolved.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return resolved;
}
@@ -387,7 +436,7 @@ class _ContactsScreenState extends State<ContactsScreen>
if (contact.type == advTypeRepeater) {
_showRepeaterLogin(context, contact);
} else if (contact.type == advTypeRoom) {
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
} else {
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
Navigator.push(
@@ -403,17 +452,13 @@ class _ContactsScreenState extends State<ContactsScreen>
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const MapScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
@@ -429,10 +474,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
),
);
},
@@ -440,18 +483,23 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showRoomLogin(BuildContext context, Contact room) {
void _showRoomLogin(
BuildContext context,
Contact room,
RoomLoginDestination destination,
) {
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(contact: room),
builder: (context) => destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
: ChatScreen(contact: room),
),
);
},
@@ -459,7 +507,11 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showGroupOptions(BuildContext context, ContactGroup group, List<Contact> contacts) {
void _showGroupOptions(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final members = _resolveGroupContacts(group, contacts);
showModalBottomSheet(
context: context,
@@ -478,7 +530,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteGroup,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDeleteGroup(context, group);
@@ -522,7 +577,10 @@ class _ContactsScreenState extends State<ContactsScreen>
});
await _saveGroups();
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -548,10 +606,16 @@ class _ContactsScreenState extends State<ContactsScreen>
final filteredContacts = filterQuery.isEmpty
? sortedContacts
: sortedContacts
.where((contact) => matchesContactQuery(contact, filterQuery))
.toList();
.where(
(contact) => matchesContactQuery(contact, filterQuery),
)
.toList();
return AlertDialog(
title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup),
title: Text(
isEditing
? context.l10n.contacts_editGroup
: context.l10n.contacts_newGroup,
),
content: SizedBox(
width: double.maxFinite,
child: Column(
@@ -582,12 +646,18 @@ class _ContactsScreenState extends State<ContactsScreen>
SizedBox(
height: 240,
child: filteredContacts.isEmpty
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter))
? Center(
child: Text(
context.l10n.contacts_noContactsMatchFilter,
),
)
: ListView.builder(
itemCount: filteredContacts.length,
itemBuilder: (context, index) {
final contact = filteredContacts[index];
final isSelected = selectedKeys.contains(contact.publicKeyHex);
final isSelected = selectedKeys.contains(
contact.publicKeyHex,
);
return CheckboxListTile(
value: isSelected,
title: Text(contact.name),
@@ -618,7 +688,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)),
SnackBar(
content: Text(context.l10n.contacts_groupNameRequired),
),
);
return;
}
@@ -628,13 +700,19 @@ class _ContactsScreenState extends State<ContactsScreen>
});
if (exists) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))),
SnackBar(
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
),
);
return;
}
setState(() {
if (isEditing) {
final index = _groups.indexWhere((g) => g.name == group.name);
final index = _groups.indexWhere(
(g) => g.name == group.name,
);
if (index != -1) {
_groups[index] = ContactGroup(
name: name,
@@ -642,7 +720,12 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
} else {
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList()));
_groups.add(
ContactGroup(
name: name,
memberKeys: selectedKeys.toList(),
),
);
}
});
await _saveGroups();
@@ -650,7 +733,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
}
},
child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create),
child: Text(
isEditing
? context.l10n.common_save
: context.l10n.common_create,
),
),
],
);
@@ -682,16 +769,24 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRepeaterLogin(context, contact);
},
)
else if (isRoom)
else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
},
)
else
),
ListTile(
leading: const Icon(Icons.room_preferences, color: Colors.orange),
title: Text(context.l10n.room_management),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact, RoomLoginDestination.management);
},
),
] else
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -702,7 +797,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDelete(context, connector, contact);
@@ -734,7 +832,10 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
connector.removeContact(contact);
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -759,28 +860,41 @@ 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),
child: _buildContactAvatar(contact),
),
title: Text(contact.name),
subtitle: Text('${contact.typeLabel}${contact.pathLabel}'),
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]),
subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
),
// 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,
@@ -790,10 +904,7 @@ class _ContactTile extends StatelessWidget {
Widget _buildContactAvatar(Contact contact) {
final emoji = firstEmoji(contact.name);
if (emoji != null) {
return Text(
emoji,
style: const TextStyle(fontSize: 18),
);
return Text(emoji, style: const TextStyle(fontSize: 18));
}
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
}
@@ -832,13 +943,21 @@ class _ContactTile extends StatelessWidget {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow;
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1 ? context.l10n.contacts_lastSeenHourAgo : context.l10n.contacts_lastSeenHoursAgo(hours);
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1 ? context.l10n.contacts_lastSeenDayAgo : context.l10n.contacts_lastSeenDaysAgo(days);
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
}
+292 -130
View File
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@@ -47,6 +49,8 @@ class _MapScreenState extends State<MapScreen> {
final Set<String> _hiddenMarkerIds = {};
Set<String> _removedMarkerIds = {};
bool _isSelectingPoi = false;
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
@override
void initState() {
@@ -67,9 +71,39 @@ class _MapScreenState extends State<MapScreen> {
if (!mounted) return;
setState(() {
_removedMarkerIds = ids;
_removedMarkersLoaded = true;
});
}
double _standardDeviation(List<double> values) {
if (values.length <= 1) {
return 0.0;
}
final mean = values.reduce((a, b) => a + b) / values.length;
double sumSquaredDiff = 0.0;
for (final value in values) {
final diff = value - mean;
sumSquaredDiff += diff * diff;
}
// Sample standard deviation (n-1) most appropriate here
final variance = sumSquaredDiff / (values.length - 1);
return sqrt(variance);
}
// Calculate zoom level based on the spread of points (std deviation in degrees)
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
final maxSpread = max(latStdDev, lonStdDev);
if (maxSpread <= 0) return 13.0;
// Approzimate: each zoom level halves the visible area
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
return zoom.clamp(4.0, 15.0);
}
@override
Widget build(BuildContext context) {
return Consumer2<MeshCoreConnector, AppSettingsService>(
@@ -80,10 +114,12 @@ class _MapScreenState extends State<MapScreen> {
final highlightPosition = widget.highlightPosition;
final sharedMarkers = settings.mapShowMarkers
? _collectSharedMarkers(connector)
.where((marker) =>
!_hiddenMarkerIds.contains(marker.id) &&
!_removedMarkerIds.contains(marker.id))
.toList()
.where(
(marker) =>
!_hiddenMarkerIds.contains(marker.id) &&
!_removedMarkerIds.contains(marker.id),
)
.toList()
: <_SharedMarker>[];
// Filter by time
@@ -91,16 +127,18 @@ class _MapScreenState extends State<MapScreen> {
final filteredByTime = settings.mapTimeFilterHours == 0
? contacts
: contacts.where((c) {
final hoursSinceLastSeen =
now.difference(c.lastSeen).inHours;
final hoursSinceLastSeen = now.difference(c.lastSeen).inHours;
return hoursSinceLastSeen <= settings.mapTimeFilterHours;
}).toList();
// Filter by key prefix
final keyPrefix = settings.mapKeyPrefix.trim();
final filteredByKeyPrefix = (settings.mapKeyPrefixEnabled && keyPrefix.isNotEmpty)
final filteredByKeyPrefix =
(settings.mapKeyPrefixEnabled && keyPrefix.isNotEmpty)
? filteredByTime.where((c) {
return c.publicKeyHex.toLowerCase().startsWith(keyPrefix.toLowerCase());
return c.publicKeyHex.toLowerCase().startsWith(
keyPrefix.toLowerCase(),
);
}).toList()
: filteredByTime;
@@ -109,30 +147,91 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation)
.toList();
// Calculate center of all nodes, or default to (0, 0)
// Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0);
final hasMapContent = contactsWithLocation.isNotEmpty ||
double initialZoom = 10.0;
final hasMapContent =
contactsWithLocation.isNotEmpty ||
sharedMarkers.isNotEmpty ||
_isSelectingPoi ||
highlightPosition != null;
if (contactsWithLocation.isNotEmpty || sharedMarkers.isNotEmpty) {
double avgLat = contactsWithLocation
.map((c) => c.latitude!)
.fold<double>(0, (sum, lat) => sum + lat);
double avgLon = contactsWithLocation
.map((c) => c.longitude!)
.fold<double>(0, (sum, lon) => sum + lon);
for (final marker in sharedMarkers) {
avgLat += marker.position.latitude;
avgLon += marker.position.longitude;
}
final total = contactsWithLocation.length + sharedMarkers.length;
if (total > 0) {
center = LatLng(avgLat / total, avgLon / total);
final allPoints = [
...contactsWithLocation.map(
(c) => LatLng(c.latitude!, c.longitude!),
),
...sharedMarkers.map((m) => m.position),
];
if (allPoints.length >= 3) {
final latValues = allPoints.map((p) => p.latitude).toList();
final lonValues = allPoints.map((p) => p.longitude).toList();
final meanLat =
latValues.reduce((a, b) => a + b) / latValues.length;
final meanLon =
lonValues.reduce((a, b) => a + b) / lonValues.length;
final latStdDev = _standardDeviation(latValues);
final lonStdDev = _standardDeviation(lonValues);
final filteredPoints = allPoints
.where(
(p) =>
(p.latitude - meanLat).abs() <= latStdDev * 2 &&
(p.longitude - meanLon).abs() <= lonStdDev * 2,
)
.toList();
if (filteredPoints.isNotEmpty) {
final filteredLatValues = filteredPoints
.map((p) => p.latitude)
.toList();
final filteredLonValues = filteredPoints
.map((p) => p.longitude)
.toList();
final avgLat = filteredLatValues.reduce((a, b) => a + b);
final avgLon = filteredLonValues.reduce((a, b) => a + b);
center = LatLng(
avgLat / filteredPoints.length,
avgLon / filteredPoints.length,
);
// Use std deviation of filtered points for zoom
final filteredLatStdDev = _standardDeviation(filteredLatValues);
final filteredLonStdDev = _standardDeviation(filteredLonValues);
initialZoom = _zoomFromStdDev(
filteredLatStdDev,
filteredLonStdDev,
);
} else {
center = LatLng(meanLat, meanLon);
initialZoom = _zoomFromStdDev(latStdDev, lonStdDev);
}
} else {
double avgLat = 0.0;
double avgLon = 0.0;
for (final point in allPoints) {
avgLat += point.latitude;
avgLon += point.longitude;
}
center = LatLng(
avgLat / allPoints.length,
avgLon / allPoints.length,
);
initialZoom = 12.0;
}
}
if (highlightPosition != null) {
center = highlightPosition;
initialZoom = widget.highlightZoom;
}
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
_hasInitializedMap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
}
final allowBack = !connector.isConnected;
@@ -156,7 +255,9 @@ class _MapScreenState extends State<MapScreen> {
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
@@ -169,9 +270,12 @@ class _MapScreenState extends State<MapScreen> {
mapController: _mapController,
options: MapOptions(
initialCenter: center,
initialZoom: 13.0,
initialZoom: initialZoom,
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate
),
onTap: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
@@ -234,18 +338,23 @@ class _MapScreenState extends State<MapScreen> {
),
],
),
_buildLegend(contactsWithLocation.length, sharedMarkers.length),
_buildLegend(
contactsWithLocation.length,
sharedMarkers.length,
),
],
),
bottomNavigationBar: SafeArea(
top: false,
child: QuickSwitchBar(
selectedIndex: 2,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showFilterDialog(context, settingsService),
tooltip: context.l10n.map_filterNodes,
child: const Icon(Icons.filter_list),
),
),
@@ -259,27 +368,17 @@ class _MapScreenState extends State<MapScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_off,
size: 64,
color: Colors.grey[400],
),
Icon(Icons.location_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.map_noNodesWithLocation,
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
context.l10n.map_nodesNeedGps,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
@@ -293,7 +392,9 @@ class _MapScreenState extends State<MapScreen> {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) continue;
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
@@ -396,13 +497,37 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 8),
_buildLegendItem(Icons.person, context.l10n.map_chat, Colors.blue),
_buildLegendItem(Icons.router, context.l10n.map_repeater, Colors.green),
_buildLegendItem(Icons.meeting_room, context.l10n.map_room, Colors.purple),
_buildLegendItem(Icons.sensors, context.l10n.map_sensor, Colors.orange),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
Colors.blue,
),
_buildLegendItem(
Icons.router,
context.l10n.map_repeater,
Colors.green,
),
_buildLegendItem(
Icons.meeting_room,
context.l10n.map_room,
Colors.purple,
),
_buildLegendItem(
Icons.sensors,
context.l10n.map_sensor,
Colors.orange,
),
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
_buildLegendItem(Icons.flag, context.l10n.map_pinPrivate, Colors.purple),
_buildLegendItem(Icons.flag, context.l10n.map_pinPublic, Colors.orange),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPrivate,
Colors.purple,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPublic,
Colors.orange,
),
],
),
),
@@ -418,10 +543,7 @@ class _MapScreenState extends State<MapScreen> {
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(fontSize: 12),
),
Text(label, style: const TextStyle(fontSize: 12)),
],
),
);
@@ -475,7 +597,9 @@ class _MapScreenState extends State<MapScreen> {
label: payload.label,
flags: payload.flags,
fromName: message.senderName,
sourceLabel: channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name,
sourceLabel: channel.name.isEmpty
? 'Channel ${channel.index}'
: channel.name,
isChannel: true,
isPublicChannel: isPublic,
),
@@ -541,11 +665,7 @@ class _MapScreenState extends State<MapScreen> {
),
],
),
child: const Icon(
Icons.flag,
color: Colors.white,
size: 20,
),
child: const Icon(Icons.flag, color: Colors.white, size: 20),
),
],
),
@@ -563,10 +683,8 @@ class _MapScreenState extends State<MapScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
),
);
},
@@ -584,9 +702,7 @@ class _MapScreenState extends State<MapScreen> {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(contact: room),
),
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
);
},
),
@@ -613,9 +729,14 @@ class _MapScreenState extends State<MapScreen> {
children: [
_buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel),
_buildInfoRow('Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}'),
_buildInfoRow(context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen)),
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
),
_buildInfoRow('Public Key', contact.publicKeyHex),
],
),
@@ -624,7 +745,8 @@ class _MapScreenState extends State<MapScreen> {
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_close),
),
if (contact.type == advTypeChat) // Only show chat button for chat nodes
if (contact.type ==
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
@@ -637,22 +759,22 @@ class _MapScreenState extends State<MapScreen> {
},
child: Text(context.l10n.contacts_openChat),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
],
),
);
@@ -664,17 +786,13 @@ class _MapScreenState extends State<MapScreen> {
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ContactsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
}
@@ -722,7 +840,8 @@ class _MapScreenState extends State<MapScreen> {
'Location',
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
),
if (marker.flags.isNotEmpty) _buildInfoRow(context.l10n.map_flags, marker.flags),
if (marker.flags.isNotEmpty)
_buildInfoRow(context.l10n.map_flags, marker.flags),
],
),
actions: [
@@ -772,10 +891,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(fontSize: 14),
),
Text(value, style: const TextStyle(fontSize: 14)),
],
),
);
@@ -860,7 +976,10 @@ class _MapScreenState extends State<MapScreen> {
);
}
Future<String?> _promptForLabel(BuildContext context, String defaultLabel) async {
Future<String?> _promptForLabel(
BuildContext context,
String defaultLabel,
) async {
final controller = TextEditingController(text: defaultLabel);
return showDialog<String>(
context: context,
@@ -881,7 +1000,10 @@ class _MapScreenState extends State<MapScreen> {
TextButton(
onPressed: () {
final label = controller.text.trim().replaceAll('|', '/');
Navigator.pop(dialogContext, label.isEmpty ? defaultLabel : label);
Navigator.pop(
dialogContext,
label.isEmpty ? defaultLabel : label,
);
},
child: Text(context.l10n.common_continue),
),
@@ -913,8 +1035,11 @@ class _MapScreenState extends State<MapScreen> {
return Consumer<MeshCoreConnector>(
builder: (consumerContext, liveConnector, child) {
final allContacts = liveConnector.contacts
.where((contact) =>
contact.type != advTypeRepeater && contact.type != advTypeRoom)
.where(
(contact) =>
contact.type != advTypeRepeater &&
contact.type != advTypeRoom,
)
.toList();
return SafeArea(
child: SingleChildScrollView(
@@ -924,7 +1049,10 @@ class _MapScreenState extends State<MapScreen> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(context.l10n.map_sendToContact, style: const TextStyle(fontWeight: FontWeight.bold)),
child: Text(
context.l10n.map_sendToContact,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
@@ -935,7 +1063,10 @@ class _MapScreenState extends State<MapScreen> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setSheetState(() {
@@ -945,50 +1076,73 @@ class _MapScreenState extends State<MapScreen> {
),
),
...allContacts
.where((contact) =>
query.isEmpty || matchesContactQuery(contact, query))
.where(
(contact) =>
query.isEmpty ||
matchesContactQuery(contact, query),
)
.map((contact) {
return ListTile(
leading: const Icon(Icons.person),
title: Text(contact.name),
onTap: () {
Navigator.pop(sheetContext);
liveConnector.sendMessage(contact, markerText);
},
);
}),
return ListTile(
leading: const Icon(Icons.person),
title: Text(contact.name),
onTap: () {
Navigator.pop(sheetContext);
liveConnector.sendMessage(contact, markerText);
},
);
}),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(context.l10n.map_sendToChannel, style: const TextStyle(fontWeight: FontWeight.bold)),
child: Text(
context.l10n.map_sendToChannel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
if (liveConnector.isLoadingChannels)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: LinearProgressIndicator(),
)
else if (liveConnector.channels.where((c) => !c.isEmpty).isEmpty)
else if (liveConnector.channels
.where((c) => !c.isEmpty)
.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(context.l10n.map_noChannelsAvailable),
)
else
...liveConnector.channels.where((c) => !c.isEmpty).map((channel) {
...liveConnector.channels.where((c) => !c.isEmpty).map((
channel,
) {
final isPublic = _isPublicChannel(channel);
final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name;
final label = channel.name.isEmpty
? 'Channel ${channel.index}'
: channel.name;
return ListTile(
leading: Icon(
isPublic ? Icons.public : Icons.tag,
color: isPublic ? Colors.orange : Colors.blue,
),
title: Text(label),
subtitle: isPublic ? Text(context.l10n.channels_publicChannel) : null,
subtitle: isPublic
? Text(context.l10n.channels_publicChannel)
: null,
onTap: () async {
Navigator.pop(sheetContext);
final canSend = isPublic
? await _confirmPublicShare(context, label)
: true;
if (canSend) {
liveConnector.sendChannelMessage(channel, markerText);
liveConnector.sendChannelMessage(
channel,
markerText,
);
}
},
);
@@ -1008,12 +1162,17 @@ class _MapScreenState extends State<MapScreen> {
return channel.isPublicChannel;
}
Future<bool> _confirmPublicShare(BuildContext context, String channelLabel) async {
Future<bool> _confirmPublicShare(
BuildContext context,
String channelLabel,
) async {
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.map_publicLocationShare),
content: Text(context.l10n.map_publicLocationShareConfirm(channelLabel)),
content: Text(
context.l10n.map_publicLocationShareConfirm(channelLabel),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
@@ -1029,7 +1188,10 @@ class _MapScreenState extends State<MapScreen> {
return result ?? false;
}
void _showFilterDialog(BuildContext context, AppSettingsService settingsService) {
void _showFilterDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -1133,10 +1295,7 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 8),
Text(
_getTimeFilterLabel(settings.mapTimeFilterHours),
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
Slider(
value: _hoursToSliderValue(settings.mapTimeFilterHours),
@@ -1176,11 +1335,14 @@ class _MapScreenState extends State<MapScreen> {
if (hours <= 24) {
return (hours / 24) * 40;
} else if (hours <= 168) { // 7 days
} else if (hours <= 168) {
// 7 days
return 40 + ((hours - 24) / (168 - 24)) * 20;
} else if (hours <= 720) { // 30 days
} else if (hours <= 720) {
// 30 days
return 60 + ((hours - 168) / (720 - 168)) * 20;
} else if (hours <= 4380) { // 6 months
} else if (hours <= 4380) {
// 6 months
return 80 + ((hours - 720) / (4380 - 720)) * 19;
} else {
return 100;
+456
View File
@@ -0,0 +1,456 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighboursScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighboursScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighboursScreen> createState() => _NeighboursScreenState();
}
class _NeighboursScreenState extends State<NeighboursScreen> {
static const int _reqNeighboursKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighbourCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
bool _hasData = false;
Timer? _statusTimeout;
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbours;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbours();
_hasData = false;
}
void _setupMessageListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighboursResponse(connector, frame.sublist(6));
}
});
}
String fmtDuration(double seconds) {
if (seconds < 60) {
return '${seconds.toStringAsFixed(1)}s';
}
final int m = (seconds ~/ 60).toInt();
final double s = seconds - (60 * m);
if (m < 60) {
return '${m}m ${s.toStringAsFixed(0)}s';
}
final int h = m ~/ 60;
final int m2 = m % 60;
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighboursData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbours = {};
for (var i = 0; i < resultsCount; i++) {
final neighbourData = neighbours.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
neighbourData['lastHeard'] = buffer.readUInt32LE();
neighbourData['snr'] = buffer.readInt8() / 4.0;
}
return neighbours.values.toList();
}
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final neighbourCount = buffer.readUInt16LE();
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighbourData in parsedNeighbours) {
final publicKey = neighbourData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
publicKey,
)) {
neighbourData['contact'] = repeater;
}
}
});
setState(() {
_parsedNeighbours = parsedNeighbours;
_neighbourCount = neighbourCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbours() async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbours,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighboursKeyLen,
]),
);
await connector.sendFrame(frame);
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
final messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
),
);
_recordStatusResult(false);
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
void _recordStatusResult(bool success) {
final selection = _pendingStatusSelection;
if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
connector.recordRepeaterPathResult(repeater, selection, success, null);
_pendingStatusSelection = null;
}
@override
void dispose() {
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbours,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbours,
tooltip: l10n.repeater_refresh,
),
],
),
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbours,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbours == null ||
_parsedNeighbours!.isEmpty))
_buildNeighboursInfoCard(
"${l10n.repeater_neighbours} - $_neighbourCount",
),
],
),
),
),
);
}
Widget _buildNeighboursInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
for (final entry in _parsedNeighbours!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
: context.l10n.neighbors_unknownContact(
"<${pubKeyToHex(entry.value['publicKey'])}>",
),
context.l10n.neighbors_heardAgo(
fmtDuration(entry.value['lastHeard'] + 0.0),
),
entry.value['snr'],
connector.currentSf!,
),
],
),
),
);
}
Widget _buildInfoRow(
String label,
String value,
double snr,
int spreadingFactor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: SNRIcon(
snr: snr,
snrLevels: getSNRfromSF(spreadingFactor),
),
),
),
],
),
);
}
}
+163 -120
View File
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbours_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@@ -25,10 +27,17 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_management),
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -39,130 +48,167 @@ class RepeaterHubScreen extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white),
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
],
),
],
),
],
),
],
),
],
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TelemetryScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(repeater: repeater, password: password),
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbours,
subtitle: l10n.repeater_neighboursSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NeighboursScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
);
},
),
),
);
},
),
],
),
),
@@ -209,10 +255,7 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
+3 -3
View File
@@ -895,7 +895,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _bandwidth,
initialValue: _bandwidth,
decoration: InputDecoration(
labelText: l10n.repeater_bandwidth,
border: const OutlineInputBorder(),
@@ -917,7 +917,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _spreadingFactor,
initialValue: _spreadingFactor,
decoration: InputDecoration(
labelText: l10n.repeater_spreadingFactor,
border: const OutlineInputBorder(),
@@ -939,7 +939,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _codingRate,
initialValue: _codingRate,
decoration: InputDecoration(
labelText: l10n.repeater_codingRate,
border: const OutlineInputBorder(),
+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(
+227 -122
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -38,10 +39,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings_title),
centerTitle: true,
),
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
@@ -68,7 +66,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildDeviceInfoCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Padding(
@@ -83,21 +84,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected),
_buildInfoRow(
l10n.settings_infoStatus,
connector.isConnected
? l10n.common_connected
: l10n.common_disconnected,
),
_buildBatteryInfoRow(context, connector),
if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
if (connector.selfPublicKey != null)
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'),
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'),
_buildInfoRow(
l10n.settings_infoPublicKey,
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
),
_buildInfoRow(
l10n.settings_infoContactsCount,
'${connector.contacts.length}',
),
_buildInfoRow(
l10n.settings_infoChannelCount,
'${connector.channels.length}',
),
],
),
),
);
}
Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) {
Widget _buildBatteryInfoRow(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
@@ -167,7 +185,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildNodeSettingsCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Column(
@@ -298,7 +319,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const BleDebugLogScreen(),
),
);
},
),
@@ -311,7 +334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const AppDebugLogScreen(),
),
);
},
),
@@ -334,20 +359,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Row(
children: [
if (leading != null) ...[
leading,
const SizedBox(width: 8),
],
if (leading != null) ...[leading, const SizedBox(width: 8)],
Text(label, style: TextStyle(color: Colors.grey[600])),
],
),
Flexible(
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w500,
color: valueColor,
),
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
overflow: TextOverflow.ellipsis,
),
),
@@ -413,75 +432,154 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n;
final latController = TextEditingController();
final lonController = TextEditingController();
final intervalController = TextEditingController();
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
// Safe access to custom vars - may be null before device responds
final customVars = connector.currentCustomVars ?? {};
final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
if (hasGPS) ...[
const SizedBox(height: 16),
TextField(
controller: intervalController,
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: false,
),
),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_locationGPSEnable,
subtitle: l10n.settings_locationGPSEnableSubtitle,
value: isGPSEnabled,
onChanged: (value) async {
setDialogState(() => isGPSEnabled = value);
if (value) {
await connector.setCustomVar("gps:1");
} else {
await connector.setCustomVar("gps:0");
}
},
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
TextButton(
onPressed: () async {
Navigator.pop(context);
if (hasGPS) {
final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) {
return;
}
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.settings_locationIntervalInvalid),
),
);
return;
}
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
}
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty
? double.tryParse(latText)
: currentLat;
final lon = lonText.isNotEmpty
? double.tryParse(lonText)
: currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
);
}
@@ -530,17 +628,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_advertisementSent)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
}
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_timeSynchronized)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -560,7 +658,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context);
connector.rebootDevice();
},
child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)),
child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
),
),
],
),
@@ -572,7 +673,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
showAboutDialog(
context: context,
applicationName: l10n.appTitle,
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion,
applicationVersion: _appVersion.isEmpty
? l10n.common_loading
: _appVersion,
applicationLegalese: l10n.settings_aboutLegalese,
children: [
const SizedBox(height: 16),
@@ -604,7 +707,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
// Populate with current settings if available
if (widget.connector.currentFreqHz != null) {
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3);
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
.toStringAsFixed(3);
} else {
_frequencyController.text = '915.0';
}
@@ -670,26 +774,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_frequencyInvalid)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
return;
}
if (txPower == null || txPower < 0 || txPower > 22) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_txPowerInvalid)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
return;
}
final freqHz = (freqMHz * 1000).round();
final bwHz = _bandwidth.hz;
final sf = _spreadingFactor.value;
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr);
final cr = _toDeviceCodingRate(
_codingRate.value,
widget.connector.currentCr,
);
try {
await widget.connector.sendFrame(buildSetRadioParamsFrame(freqHz, bwHz, sf, cr));
await widget.connector.sendFrame(
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -727,7 +836,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.settings_presets, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
@@ -762,7 +874,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
const SizedBox(height: 16),
DropdownButtonFormField<LoRaBandwidth>(
@@ -772,10 +886,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaBandwidth.values
.map((bw) => DropdownMenuItem(
value: bw,
child: Text(bw.label),
))
.map(
(bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _bandwidth = value);
@@ -789,10 +902,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaSpreadingFactor.values
.map((sf) => DropdownMenuItem(
value: sf,
child: Text(sf.label),
))
.map(
(sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value);
@@ -806,10 +918,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaCodingRate.values
.map((cr) => DropdownMenuItem(
value: cr,
child: Text(cr.label),
))
.map(
(cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _codingRate = value);
@@ -833,10 +944,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: _saveSettings,
child: Text(l10n.common_save),
),
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
],
);
}
@@ -850,9 +958,6 @@ class _PresetChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ActionChip(
label: Text(label),
onPressed: onTap,
);
return ActionChip(label: Text(label), onPressed: onTap);
}
}
+4 -7
View File
@@ -1,7 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -34,7 +31,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _timeEstment = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -64,18 +60,19 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleStatusResponse(context, frame.sublist(6));
if (!mounted) return;
_handleStatusResponse(frame.sublist(6));
}
});
}
void _handleStatusResponse(BuildContext context, Uint8List frame) {
void _handleStatusResponse(Uint8List frame) {
if (!mounted) return;
setState(() {
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
});
+46 -11
View File
@@ -16,7 +16,9 @@ class BleDebugLogEntry {
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -26,14 +28,13 @@ class BleRawLogRxEntry {
final DateTime timestamp;
final Uint8List payload;
BleRawLogRxEntry({
required this.timestamp,
required this.payload,
});
BleRawLogRxEntry({required this.timestamp, required this.payload});
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier {
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
List<BleRawLogRxEntry> get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries);
List<BleRawLogRxEntry> get rawLogRxEntries =>
List.unmodifiable(_rawLogRxEntries);
void logFrame(Uint8List frame, {required bool outgoing, String? note}) {
if (frame.isEmpty) return;
@@ -85,15 +87,32 @@ class BleDebugLogService extends ChangeNotifier {
notifyListeners();
}
String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) {
final label = _codeLabel(code);
String _describeFrame(
int code,
Uint8List frame,
bool outgoing,
String? note,
) {
final label = _codeLabel(code, outgoing: outgoing);
final prefix = outgoing ? 'TX' : 'RX';
final extra = _frameDetail(code, frame);
final noteText = note != null ? '$note' : '';
return '$prefix $label$extra$noteText';
}
String _codeLabel(int code) {
String _codeLabel(int code, {required bool outgoing}) {
if (outgoing) {
return _commandLabel(code) ?? 'CODE_$code';
}
final pushLabel = _pushLabel(code);
if (pushLabel != null) return pushLabel;
final responseLabel = _responseLabel(code);
if (responseLabel != null) return responseLabel;
return 'CODE_$code';
}
String? _commandLabel(int code) {
switch (code) {
case cmdAppStart:
return 'CMD_APP_START';
@@ -135,6 +154,15 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_SET_CHANNEL';
case cmdGetRadioSettings:
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
default:
return null;
}
}
String? _responseLabel(int code) {
switch (code) {
case respCodeOk:
return 'RESP_CODE_OK';
case respCodeErr:
@@ -167,6 +195,13 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_INFO';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
default:
return null;
}
}
String? _pushLabel(int code) {
switch (code) {
case pushCodeAdvert:
return 'PUSH_CODE_ADVERT';
case pushCodePathUpdated:
@@ -184,7 +219,7 @@ class BleDebugLogService extends ChangeNotifier {
case pushCodeNewAdvert:
return 'PUSH_CODE_NEW_ADVERT';
default:
return 'CODE_$code';
return null;
}
}
+1 -3
View File
@@ -6,7 +6,6 @@ import 'package:crypto/crypto.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_selection.dart';
import 'storage_service.dart';
import 'app_settings_service.dart';
import 'app_debug_log_service.dart';
@@ -36,7 +35,6 @@ class MessageRetryService extends ChangeNotifier {
static const int maxRetries = 5;
static const int maxAckHistorySize = 100;
final StorageService _storage;
final Map<String, Timer> _timeoutTimers = {};
final Map<String, Message> _pendingMessages = {};
final Map<String, Contact> _pendingContacts = {};
@@ -59,7 +57,7 @@ class MessageRetryService extends ChangeNotifier {
AppDebugLogService? _debugLogService;
Function(String, PathSelection, bool, int?)? _recordPathResultCallback;
MessageRetryService(this._storage);
MessageRetryService();
void initialize({
required Function(Contact, String, int, int) sendMessageCallback,
+1 -1
View File
@@ -206,7 +206,7 @@ class NotificationService {
final preview = _truncateMessage(message, 30);
final body = preview.isEmpty
? 'Received new message'
: 'Received new message: $preview';
: preview;
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
+117
View File
@@ -0,0 +1,117 @@
import 'dart:convert';
import '../models/community.dart';
import 'prefs_manager.dart';
/// Persists communities to local storage using SharedPreferences.
///
/// Communities are stored as a JSON array under a single key.
/// Each community contains its secret K, so this data should
/// be considered sensitive (though device encryption handles security).
class CommunityStore {
static const String _communitiesKey = 'communities_v1';
/// Load all communities from storage
Future<List<Community>> loadCommunities() async {
final prefs = PrefsManager.instance;
final jsonString = prefs.getString(_communitiesKey);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((json) => Community.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
// If JSON is corrupted, return empty list
return [];
}
}
/// Save all communities to storage
Future<void> saveCommunities(List<Community> communities) async {
final prefs = PrefsManager.instance;
final jsonList = communities.map((c) => c.toJson()).toList();
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
}
/// Add a new community
Future<void> addCommunity(Community community) async {
final communities = await loadCommunities();
// Check if community with same ID already exists
final existingIndex = communities.indexWhere((c) => c.id == community.id);
if (existingIndex >= 0) {
// Replace existing
communities[existingIndex] = community;
} else {
communities.add(community);
}
await saveCommunities(communities);
}
/// Update an existing community
Future<void> updateCommunity(Community community) async {
final communities = await loadCommunities();
final index = communities.indexWhere((c) => c.id == community.id);
if (index >= 0) {
communities[index] = community;
await saveCommunities(communities);
}
}
/// Remove a community by ID
Future<void> removeCommunity(String communityId) async {
final communities = await loadCommunities();
communities.removeWhere((c) => c.id == communityId);
await saveCommunities(communities);
}
/// Get a community by ID
Future<Community?> getCommunity(String communityId) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.id == communityId);
} catch (_) {
return null;
}
}
/// Check if a community with the same secret already exists
/// (to prevent duplicate imports from QR scanning)
Future<Community?> findByCommunityId(String cid) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.communityId == cid);
} catch (_) {
return null;
}
}
/// Add a hashtag channel to a community
Future<void> addHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.addHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
/// Remove a hashtag channel from a community
Future<void> removeHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.removeHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class FeatureToggleRow extends StatefulWidget {
final String title;
final String subtitle;
final bool value;
final bool hasRefreshing;
final bool isRefreshing;
final ValueChanged<bool>? onChanged;
final VoidCallback? onRefresh;
final String? refreshTooltip;
const FeatureToggleRow({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.hasRefreshing = false,
this.isRefreshing = false,
this.onChanged,
this.onRefresh,
this.refreshTooltip,
});
@override
State<FeatureToggleRow> createState() => _FeatureToggleRow();
}
class _FeatureToggleRow extends State<FeatureToggleRow> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: SwitchListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
value: widget.value,
onChanged: widget.onChanged,
contentPadding: EdgeInsets.zero,
),
),
if (widget.hasRefreshing)
IconButton(
icon: widget.isRefreshing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh, size: 20),
onPressed: widget.isRefreshing ? null : widget.onRefresh,
tooltip: widget.refreshTooltip,
visualDensity: VisualDensity.compact,
),
],
);
}
}
+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),
),
);
},
);
}
}
+233
View File
@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
/// A reusable QR code display widget for sharing data.
///
/// Features:
/// - Configurable size and colors
/// - Optional logo/icon in center
/// - Automatic theming (light/dark mode aware)
/// - Title and instructions
class QrCodeDisplay extends StatelessWidget {
/// The data to encode in the QR code
final String data;
/// Size of the QR code (width and height)
final double size;
/// Optional widget to display in the center (e.g., app logo)
final Widget? embeddedImage;
/// Size of the embedded image (if provided)
final double embeddedImageSize;
/// Title displayed above the QR code
final String? title;
/// Instructions displayed below the QR code
final String? instructions;
/// Background color of the QR code (defaults to white)
final Color? backgroundColor;
/// Foreground color of the QR code modules (defaults to black)
final Color? foregroundColor;
/// Padding around the QR code
final EdgeInsets padding;
/// Error correction level
final int errorCorrectionLevel;
const QrCodeDisplay({
super.key,
required this.data,
this.size = 200,
this.embeddedImage,
this.embeddedImageSize = 50,
this.title,
this.instructions,
this.backgroundColor,
this.foregroundColor,
this.padding = const EdgeInsets.all(16),
this.errorCorrectionLevel = QrErrorCorrectLevel.M,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Default colors based on theme
final bgColor = backgroundColor ?? Colors.white;
final fgColor = foregroundColor ?? Colors.black;
return Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
// QR code container with rounded corners
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: embeddedImage != null
? _buildQrWithEmbeddedImage(fgColor, bgColor)
: _buildSimpleQr(fgColor, bgColor),
),
if (instructions != null) ...[
const SizedBox(height: 16),
Text(
instructions!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
Widget _buildSimpleQr(Color fgColor, Color bgColor) {
return QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
errorCorrectionLevel: errorCorrectionLevel,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
);
}
Widget _buildQrWithEmbeddedImage(Color fgColor, Color bgColor) {
return Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
// Use higher error correction when embedding image
errorCorrectionLevel: QrErrorCorrectLevel.H,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
),
Container(
width: embeddedImageSize,
height: embeddedImageSize,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: embeddedImage,
),
],
);
}
}
/// Dialog to display a QR code for sharing
class QrCodeShareDialog extends StatelessWidget {
final String data;
final String? title;
final String? instructions;
final Widget? embeddedImage;
const QrCodeShareDialog({
super.key,
required this.data,
this.title,
this.instructions,
this.embeddedImage,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrCodeDisplay(
data: data,
size: 250,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
padding: EdgeInsets.zero,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
),
],
),
),
);
}
/// Show the dialog
static Future<void> show({
required BuildContext context,
required String data,
String? title,
String? instructions,
Widget? embeddedImage,
}) {
return showDialog(
context: context,
builder: (context) => QrCodeShareDialog(
data: data,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
),
);
}
}
+391
View File
@@ -0,0 +1,391 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
/// A reusable QR code scanner widget that can be embedded anywhere.
///
/// Features:
/// - Configurable scan window overlay
/// - Flash toggle button
/// - Camera switch button (front/back)
/// - Customizable callbacks for scan results
/// - Optional validation function for QR data
/// - Automatic pause when not visible
/// - Debouncing to prevent duplicate scans
class QrScannerWidget extends StatefulWidget {
/// Called when a valid QR code is scanned
final void Function(String data) onScanned;
/// Optional validator - return true if the QR data is valid
final bool Function(String data)? validator;
/// Optional error callback when validation fails
final void Function(String data)? onValidationFailed;
/// Whether to show the flash toggle button
final bool showFlashButton;
/// Whether to show the camera switch button
final bool showCameraSwitchButton;
/// Custom overlay widget (defaults to scan window frame)
final Widget? overlay;
/// Instructions text shown below the scan window
final String? instructions;
/// Whether to continue scanning after first successful scan
final bool continuousScanning;
/// Debounce duration to prevent duplicate scans
final Duration debounceDuration;
const QrScannerWidget({
super.key,
required this.onScanned,
this.validator,
this.onValidationFailed,
this.showFlashButton = true,
this.showCameraSwitchButton = true,
this.overlay,
this.instructions,
this.continuousScanning = false,
this.debounceDuration = const Duration(milliseconds: 500),
});
@override
State<QrScannerWidget> createState() => _QrScannerWidgetState();
}
class _QrScannerWidgetState extends State<QrScannerWidget>
with WidgetsBindingObserver {
late MobileScannerController _controller;
bool _hasScanned = false;
String? _lastScannedData;
DateTime? _lastScanTime;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Handle app lifecycle changes - pause/resume scanner
if (!_controller.value.hasCameraPermission) return;
switch (state) {
case AppLifecycleState.resumed:
_controller.start();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
_controller.stop();
break;
}
}
void _handleDetection(BarcodeCapture capture) {
// Prevent duplicate scans
if (_hasScanned && !widget.continuousScanning) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
final String? rawValue = barcode.rawValue;
if (rawValue == null || rawValue.isEmpty) continue;
// Debounce - ignore if same data scanned too quickly
final now = DateTime.now();
if (_lastScannedData == rawValue &&
_lastScanTime != null &&
now.difference(_lastScanTime!) < widget.debounceDuration) {
continue;
}
_lastScannedData = rawValue;
_lastScanTime = now;
// Validate if validator provided
if (widget.validator != null && !widget.validator!(rawValue)) {
widget.onValidationFailed?.call(rawValue);
continue;
}
// Mark as scanned to prevent duplicates
if (!widget.continuousScanning) {
setState(() {
_hasScanned = true;
});
_controller.stop();
}
// Notify callback
widget.onScanned(rawValue);
return;
}
}
/// Reset the scanner to allow scanning again
void resetScanner() {
setState(() {
_hasScanned = false;
_lastScannedData = null;
_lastScanTime = null;
});
_controller.start();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Scanner view
MobileScanner(
controller: _controller,
onDetect: _handleDetection,
errorBuilder: (context, error, child) {
return _buildErrorWidget(context, error);
},
),
// Overlay
widget.overlay ?? _buildDefaultOverlay(context),
// Control buttons
Positioned(
bottom: 16,
left: 0,
right: 0,
child: _buildControls(context),
),
],
);
}
Widget _buildDefaultOverlay(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.5),
BlendMode.srcOut,
),
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
color: Colors.black,
backgroundBlendMode: BlendMode.dstOut,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 250,
width: 250,
decoration: BoxDecoration(
color: Colors.red, // This color is used for cutout
borderRadius: BorderRadius.circular(16),
),
),
if (widget.instructions != null) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.instructions!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
],
],
),
),
],
),
);
}
Widget _buildControls(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.showFlashButton)
ValueListenableBuilder(
valueListenable: _controller,
builder: (context, state, child) {
return IconButton.filled(
onPressed: () => _controller.toggleTorch(),
icon: Icon(
state.torchState == TorchState.on
? Icons.flash_on
: Icons.flash_off,
),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
);
},
),
if (widget.showFlashButton && widget.showCameraSwitchButton)
const SizedBox(width: 24),
if (widget.showCameraSwitchButton)
IconButton.filled(
onPressed: () => _controller.switchCamera(),
icon: const Icon(Icons.cameraswitch),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildErrorWidget(BuildContext context, MobileScannerException error) {
String message;
IconData icon;
switch (error.errorCode) {
case MobileScannerErrorCode.permissionDenied:
message = 'Camera permission denied.\nPlease enable camera access in settings.';
icon = Icons.no_photography;
break;
case MobileScannerErrorCode.unsupported:
message = 'Camera not supported on this device.';
icon = Icons.videocam_off;
break;
default:
message = 'Failed to start camera.\n${error.errorDetails?.message ?? ''}';
icon = Icons.error_outline;
}
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
);
}
}
/// A simpler scanner overlay with just corner brackets
class ScannerCornerOverlay extends StatelessWidget {
final double scanWindowSize;
final Color borderColor;
final double borderWidth;
final double cornerLength;
const ScannerCornerOverlay({
super.key,
this.scanWindowSize = 250,
this.borderColor = Colors.white,
this.borderWidth = 3,
this.cornerLength = 30,
});
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: scanWindowSize,
height: scanWindowSize,
child: CustomPaint(
painter: _CornerPainter(
color: borderColor,
strokeWidth: borderWidth,
cornerLength: cornerLength,
),
),
),
);
}
}
class _CornerPainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double cornerLength;
_CornerPainter({
required this.color,
required this.strokeWidth,
required this.cornerLength,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final path = Path();
// Top-left corner
path.moveTo(0, cornerLength);
path.lineTo(0, 0);
path.lineTo(cornerLength, 0);
// Top-right corner
path.moveTo(size.width - cornerLength, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, cornerLength);
// Bottom-right corner
path.moveTo(size.width, size.height - cornerLength);
path.lineTo(size.width, size.height);
path.lineTo(size.width - cornerLength, size.height);
// Bottom-left corner
path.moveTo(cornerLength, size.height);
path.lineTo(0, size.height);
path.lineTo(0, size.height - cornerLength);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
+43 -12
View File
@@ -31,6 +31,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
bool _savePassword = false;
bool _isLoading = true;
bool _obscurePassword = true;
String? _loginError;
late MeshCoreConnector _connector;
int _currentAttempt = 0;
static const int _maxAttempts = 5;
@@ -79,6 +80,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
setState(() {
_isLoggingIn = true;
_currentAttempt = 0;
_loginError = null;
});
try {
@@ -134,7 +136,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
'Login failed for ${repeater.name}',
tag: 'RepeaterLogin',
);
throw Exception('Wrong password or node is unreachable');
break;
}
appLogger.warn(
'Login attempt ${attempt + 1} timed out after ${timeoutSeconds}s',
@@ -156,7 +158,13 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
if (loginResult != true) {
throw Exception('Wrong password or node is unreachable');
if (mounted) {
setState(() {
_isLoggingIn = false;
_loginError = context.l10n.login_failedMessage;
});
}
return;
}
// If we got a response, login succeeded
@@ -182,13 +190,8 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
if (mounted) {
setState(() {
_isLoggingIn = false;
_loginError = context.l10n.login_failedMessage;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
@@ -261,15 +264,35 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
child: CircularProgressIndicator(),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.login_repeaterDescription,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
if (_loginError != null) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error, size: 18, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8),
Expanded(
child: Text(
_loginError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 13,
),
),
),
],
),
const SizedBox(height: 12),
],
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
@@ -291,6 +314,13 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
},
),
),
onChanged: (_) {
if (_loginError != null && mounted) {
setState(() {
_loginError = null;
});
}
},
onSubmitted: (_) => _handleLogin(),
autofocus: _passwordController.text.isEmpty,
),
@@ -382,6 +412,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
+62
View File
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
List<double> getSNRfromSF(int spreadingFactor) {
switch (spreadingFactor) {
case 7:
return [4.0, -2.0, -4.0, -6.0];
case 8:
return [4.0, -4.0, -6.0, -8.0];
case 9:
return [4.0, -6.0, -8.0, -10.0];
case 10:
return [4.0, -8.0, -10.0, -13.0];
case 11:
return [4.0, -10.0, -12.5, -15.0];
case 12:
return [4.0, -12.5, -15.0, -18.0];
default:
return []; // Or throw Exception('Invalid SF: $spreadingFactor');
}
}
class SNRIcon extends StatelessWidget {
final double snr;
final List<double> snrLevels;
const SNRIcon({
super.key,
required this.snr,
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
});
@override
Widget build(BuildContext context) {
IconData icon;
Color color;
if (snr >= snrLevels[0]) {
icon = Icons.signal_cellular_alt;
color = Colors.green;
} else if (snr >= snrLevels[1]) {
icon = Icons.signal_cellular_alt;
color = Colors.lightGreen;
} else if (snr >= snrLevels[2]) {
icon = Icons.signal_cellular_alt;
color = Colors.yellow;
} else if (snr >= snrLevels[3]) {
icon = Icons.signal_cellular_alt_2_bar;
color = Colors.orange;
} else {
icon = Icons.signal_cellular_alt_1_bar;
color = Colors.red;
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
],
);
}
}
@@ -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"
@@ -7,18 +7,22 @@ import Foundation
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) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
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>
+2
View File
@@ -12,5 +12,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+2
View File
@@ -30,5 +30,7 @@
<string>NSApplication</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>MeshCore needs Bluetooth to communicate with LoRa mesh devices</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
</dict>
</plist>
+2
View File
@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+106 -2
View File
@@ -262,6 +262,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:
@@ -397,6 +405,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:
@@ -453,6 +469,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173"
url: "https://pub.dev"
source: hosted
version: "6.0.11"
nested:
dependency: transitive
description:
@@ -605,6 +629,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: transitive
description:
@@ -794,6 +834,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:
@@ -883,5 +987,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"
+8 -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.3.0+3
version: 5.0.0+5
environment:
sdk: ^3.9.2
@@ -53,6 +53,10 @@ dependencies:
wakelock_plus: ^1.2.8
characters: ^1.4.0
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:
@@ -78,6 +82,9 @@ flutter:
# the material Icons class.
uses-material-design: true
assets:
- assets/images/
flutter_launcher_icons:
android: true
ios: true
+190 -19
View File
@@ -10,6 +10,7 @@ Translates ARB/JSON localization values using a local Ollama model, while:
- printing progress as it runs
Usage:
# Translate all strings:
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \
@@ -17,12 +18,28 @@ Usage:
--model ministral-3:latest \
--temperature 0 \
--concurrency 4
# Translate only missing/untranslated strings:
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \
--to-locale es \
--missing-only \
--model ministral-3:latest
# Translate all locales (missing strings only):
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--l10n-dir /home/zjs81/Desktop/meshcore-open/lib/l10n \
--missing-only \
--model ministral-3:latest
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
@@ -448,11 +465,48 @@ def fmt_duration(seconds: float) -> str:
return f"{h}h {m2}m"
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)."""
missing = []
for key in source_data:
if key == "@@locale":
continue
if key.startswith("@"):
continue
if key not in target_data:
missing.append(key)
return missing
def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]:
"""Find all locale .arb files in the directory, excluding the template.
Returns list of (locale_code, file_path) tuples.
"""
locales = []
template_basename = os.path.basename(template_file)
for filename in os.listdir(l10n_dir):
if not filename.endswith('.arb'):
continue
if filename == template_basename:
continue
# Extract locale from filename like app_es.arb -> es
if filename.startswith('app_') and filename.endswith('.arb'):
locale = filename[4:-4] # Remove 'app_' prefix and '.arb' suffix
filepath = os.path.join(l10n_dir, filename)
locales.append((locale, filepath))
return sorted(locales)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path")
ap.add_argument("--out", dest="out_path", required=True, help="Output .arb/.json file path")
ap.add_argument("--to-locale", required=True, help="Target locale code, e.g. es, fr, de")
ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path (source/template)")
ap.add_argument("--out", dest="out_path", default=None, help="Output .arb/.json file path (required unless using --l10n-dir)")
ap.add_argument("--to-locale", default=None, help="Target locale code, e.g. es, fr, de (required unless using --l10n-dir)")
ap.add_argument("--l10n-dir", default=None, help="Directory containing locale .arb files. When set, translates all locales.")
ap.add_argument("--missing-only", action="store_true", help="Only translate keys missing from target file")
ap.add_argument("--target-lang", default=None, help="Target language name for the model, e.g. Spanish (defaults from locale)")
ap.add_argument("--model", default="gemma3:4b", help="Ollama model name")
ap.add_argument("--fallback-model", default=None, help="Larger model to use for low-confidence translations")
@@ -504,19 +558,119 @@ def main() -> int:
"vi": "Vietnamese",
"id": "Indonesian",
}
target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale)
# Read source/template file
try:
with open(args.in_path, "r", encoding="utf-8") as f:
data = json.load(f)
source_data = json.load(f)
except Exception as e:
print(f"Failed to read input: {e}", file=sys.stderr)
return 2
if not isinstance(data, dict):
if not isinstance(source_data, dict):
print("Input JSON must be an object at top-level.", file=sys.stderr)
return 2
# If --l10n-dir is provided, process all locale files
if args.l10n_dir:
locales = get_all_locale_files(args.l10n_dir, args.in_path)
if not locales:
print(f"No locale files found in {args.l10n_dir}", file=sys.stderr)
return 1
print(f"Found {len(locales)} locale file(s) to process")
total_translated = 0
for locale_code, locale_path in locales:
target_lang = locale_map.get(locale_code, locale_code)
# Read existing target file
try:
with open(locale_path, "r", encoding="utf-8") as f:
target_data = json.load(f)
except Exception as e:
print(f" [{locale_code}] Failed to read {locale_path}: {e}")
continue
if args.missing_only:
missing_keys = find_missing_keys(source_data, target_data)
if not missing_keys:
print(f" [{locale_code}] No missing keys")
continue
print(f" [{locale_code}] {len(missing_keys)} missing key(s): {', '.join(missing_keys[:5])}{'...' if len(missing_keys) > 5 else ''}")
else:
missing_keys = None
# Run translation for this locale
result = translate_locale(
source_data=source_data,
target_data=target_data,
target_locale=locale_code,
target_lang=target_lang,
out_path=locale_path,
args=args,
locale_map=locale_map,
missing_keys=missing_keys,
)
total_translated += result
print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)")
return 0
# Single locale mode - validate required args
if not args.out_path:
print("--out is required when not using --l10n-dir", file=sys.stderr)
return 1
if not args.to_locale:
print("--to-locale is required when not using --l10n-dir", file=sys.stderr)
return 1
target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale)
# Read existing target file if --missing-only and file exists
target_data: Dict[str, Any] = {}
missing_keys: Optional[List[str]] = None
if args.missing_only:
if os.path.exists(args.out_path):
try:
with open(args.out_path, "r", encoding="utf-8") as f:
target_data = json.load(f)
missing_keys = find_missing_keys(source_data, target_data)
if not missing_keys:
print(f"No missing keys in {args.out_path}")
return 0
print(f"Found {len(missing_keys)} missing key(s) to translate")
except Exception as e:
print(f"Failed to read target file: {e}", file=sys.stderr)
return 2
else:
print(f"Target file {args.out_path} does not exist. Will translate all strings.")
result = translate_locale(
source_data=source_data,
target_data=target_data,
target_locale=args.to_locale,
target_lang=target_lang,
out_path=args.out_path,
args=args,
locale_map=locale_map,
missing_keys=missing_keys,
)
return 0 if result >= 0 else 1
def translate_locale(
source_data: Dict[str, Any],
target_data: Dict[str, Any],
target_locale: str,
target_lang: str,
out_path: str,
args,
locale_map: Dict[str, str],
missing_keys: Optional[List[str]] = None,
) -> int:
"""Translate a single locale. Returns number of strings translated."""
cfg = OllamaConfig(
host=args.host,
model=args.model,
@@ -540,17 +694,34 @@ def main() -> int:
top_p=args.top_p,
)
out_data: Dict[str, Any] = dict(data)
out_data["@@locale"] = args.to_locale
# Start with target data (preserves existing translations) or source data
if target_data:
out_data: Dict[str, Any] = dict(target_data)
else:
out_data: Dict[str, Any] = dict(source_data)
out_data["@@locale"] = target_locale
items: List[Tuple[str, str]] = [(k, v) for k, v in data.items() if is_translatable_entry(k, v)]
# Build list of items to translate
if missing_keys is not None:
# Only translate missing keys
items: List[Tuple[str, str]] = [
(k, source_data[k]) for k in missing_keys
if is_translatable_entry(k, source_data.get(k))
]
# Also copy over any metadata keys for missing items
for key in missing_keys:
meta_key = f"@{key}"
if meta_key in source_data:
out_data[meta_key] = source_data[meta_key]
else:
items: List[Tuple[str, str]] = [(k, v) for k, v in source_data.items() if is_translatable_entry(k, v)]
# Apply manual translations first
manual_count = 0
items_to_translate: List[Tuple[str, str]] = []
for k, v in items:
if k in MANUAL_TRANSLATIONS and args.to_locale in MANUAL_TRANSLATIONS[k]:
out_data[k] = MANUAL_TRANSLATIONS[k][args.to_locale]
if k in MANUAL_TRANSLATIONS and target_locale in MANUAL_TRANSLATIONS[k]:
out_data[k] = MANUAL_TRANSLATIONS[k][target_locale]
manual_count += 1
else:
items_to_translate.append((k, v))
@@ -560,8 +731,8 @@ def main() -> int:
total = len(items_to_translate)
if total == 0 and manual_count == 0:
print("No translatable string entries found (excluding @@locale and @metadata).", file=sys.stderr)
return 1
print("No translatable string entries found (excluding @@locale and @metadata).")
return 0
if total == 0:
print("All strings handled by manual translations.")
@@ -705,19 +876,19 @@ def main() -> int:
if args.dry_run:
print("Dry run: not writing output file.")
return 0
return translated_ok
try:
with open(args.out_path, "w", encoding="utf-8") as f:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(out_data, f, ensure_ascii=False, indent=2)
f.write("\n")
except Exception as e:
print(f"Failed to write output: {e}", file=sys.stderr)
return 2
return -1
print(f"Wrote: {args.out_path}")
return 0
print(f"Wrote: {out_path}")
return translated_ok
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+1
View File
@@ -0,0 +1 @@
{}
@@ -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