mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-21 09:55:27 +10:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4239fb11ed | |||
| 5fae2e5f73 | |||
| 947fafbbb7 | |||
| 72f0aa7208 | |||
| f87d4896ab | |||
| 9250dfec31 | |||
| 37db955ab2 | |||
| 739d9475c0 | |||
| b526175be4 | |||
| 73081862ad | |||
| fac062a100 | |||
| ef6bd78632 | |||
| 01c8390989 | |||
| c05f813d65 | |||
| c52b19b09f | |||
| 6a666839b6 | |||
| bc77f7e287 | |||
| 9332d8126f | |||
| 9ce00556ec | |||
| 4995f5f380 | |||
| 4e6e7b6061 | |||
| aa350aa4ae | |||
| dfd38b19e9 | |||
| 4afab3f629 | |||
| 67816130ac | |||
| d573f0c312 | |||
| 5b699cd624 | |||
| a4d3d248a5 | |||
| 2a3f2b3a24 | |||
| 675083fa01 | |||
| 5fc4b80b16 | |||
| 84a32c1e67 | |||
| 607583060a | |||
| 71cf556b61 | |||
| c26174ad18 | |||
| 04021a39a1 | |||
| fe23e9f7a0 | |||
| d7ec1876af | |||
| 87a2807f5b | |||
| daca42701c | |||
| ea43cf17eb | |||
| 8ef6e2c656 | |||
| 24de98d5ee | |||
| 0fd841b5b5 | |||
| c365b7889b | |||
| 2db30ace6a | |||
| 0d8801fa75 | |||
| bcae6ac19f | |||
| 2f4b230b31 | |||
| 98e0b05e73 | |||
| 2a909e6081 | |||
| d1009d3c20 | |||
| 91b1696bc5 | |||
| 978ea4790d | |||
| 8b1228bf8d | |||
| ddee76ced2 | |||
| 6a3c59fa2c | |||
| a54cc78691 | |||
| 05fb5a13fa | |||
| c320378be1 | |||
| b3645481c7 | |||
| 589707aa13 | |||
| 6070802213 | |||
| 2525b9425b | |||
| b786c90514 | |||
| a35590a407 | |||
| 8d15f7cef6 | |||
| e449f5e1d5 | |||
| b34d684e67 | |||
| 488a286701 | |||
| c742d98fbb | |||
| 1d4c9ad9bd | |||
| 818f514702 | |||
| be54419e5b | |||
| 00eb1a68a6 | |||
| 79ffc21bd6 | |||
| 0374f4f5da | |||
| 4650584f9b | |||
| 8d8b938878 | |||
| e3a0bd3b13 | |||
| 4f83d87f8c | |||
| 6d7d51f0a4 | |||
| 33680f0cb9 | |||
| 5115d8bbe3 | |||
| d30e7c4e2c | |||
| 8470171e88 | |||
| ede3142d40 | |||
| 6712088fcd | |||
| 7b519854d7 | |||
| 90ce46392a | |||
| d61ec217fc | |||
| 3ac81a5448 | |||
| 7004067839 | |||
| 935b7b07eb | |||
| cdacc54421 | |||
| bf8f002d55 | |||
| 998ff50495 | |||
| 92d2b224e7 | |||
| 34a6b5d895 | |||
| c953a1a798 | |||
| 42115bf200 | |||
| d0c8fab6fb | |||
| eeb8ff34e8 | |||
| 641307a316 | |||
| c37abb63e3 | |||
| 898ef1c11c | |||
| 749f9d4dfd | |||
| 9c1b5899fb | |||
| cacb9bc677 | |||
| 0ebd688787 | |||
| bb18038f60 | |||
| fcf741b20a | |||
| 88aa104ae5 | |||
| 90f90ad7cf | |||
| 2089613696 | |||
| 4003519deb |
@@ -2,6 +2,8 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
name: Flutter Analyze
|
name: Flutter and Dart
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@@ -19,5 +21,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Analyze
|
- name: Analyze code
|
||||||
run: flutter analyze --fatal-infos --fatal-warnings
|
run: flutter analyze --fatal-infos --fatal-warnings
|
||||||
|
|
||||||
|
- name: Verify formatting
|
||||||
|
run: dart format --output=none --set-exit-if-changed .
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test -r github
|
||||||
@@ -65,6 +65,7 @@ secrets.dart
|
|||||||
**/ios/Flutter/Flutter.podspec
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
|
||||||
# Android
|
# Android
|
||||||
|
.gradle/
|
||||||
**/android/.gradle/
|
**/android/.gradle/
|
||||||
**/android/captures/
|
**/android/captures/
|
||||||
**/android/local.properties
|
**/android/local.properties
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
|||||||
|
|
||||||
MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities.
|
MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities.
|
||||||
|
|
||||||
|
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
|
||||||
|
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
@@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core Functionality
|
### Core Functionality
|
||||||
|
|
||||||
- **Direct Messaging**: Private encrypted conversations with individual contacts
|
- **Direct Messaging**: Private encrypted conversations with individual contacts
|
||||||
- **Public Channels**: Broadcast messages to channel subscribers on the mesh network
|
- **Public Channels**: Broadcast messages to channel subscribers on the mesh network
|
||||||
- **Contact Management**: Organize contacts, track last seen times, and manage conversation history
|
- **Contact Management**: Organize contacts, track last seen times, and manage conversation history
|
||||||
@@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- **Message Replies**: Thread conversations with inline reply functionality
|
- **Message Replies**: Thread conversations with inline reply functionality
|
||||||
|
|
||||||
### Mesh Network
|
### Mesh Network
|
||||||
|
|
||||||
- **Path Visualization**: View routing paths and signal quality for each contact
|
- **Path Visualization**: View routing paths and signal quality for each contact
|
||||||
- **Route Management**: Manual path overriding and automatic route rotation
|
- **Route Management**: Manual path overriding and automatic route rotation
|
||||||
- **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking
|
- **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking
|
||||||
@@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- **Repeater Support**: Connect to and manage repeater nodes for extended range
|
- **Repeater Support**: Connect to and manage repeater nodes for extended range
|
||||||
|
|
||||||
### Map & Location
|
### Map & Location
|
||||||
|
|
||||||
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map
|
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map
|
||||||
- **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
|
- **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
|
||||||
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
||||||
@@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
||||||
|
|
||||||
### Device Management
|
### Device Management
|
||||||
|
|
||||||
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
|
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
|
||||||
- **Device Settings**: Configure radio parameters, power settings, and network options
|
- **Device Settings**: Configure radio parameters, power settings, and network options
|
||||||
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
||||||
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
||||||
|
|
||||||
### Repeater Hub
|
### Repeater Hub
|
||||||
|
|
||||||
- **CLI Access**: Full command-line interface to repeater nodes
|
- **CLI Access**: Full command-line interface to repeater nodes
|
||||||
- **Settings Management**: Configure repeater behavior, power limits, and network settings
|
- **Settings Management**: Configure repeater behavior, power limits, and network settings
|
||||||
- **Statistics Dashboard**: View repeater traffic, connected clients, and system health
|
- **Statistics Dashboard**: View repeater traffic, connected clients, and system health
|
||||||
@@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
- **Framework**: Flutter 3.38.5 / Dart 3.10.4
|
- **Framework**: Flutter 3.38.5 / Dart 3.10.4
|
||||||
- **State Management**: Provider pattern with ChangeNotifier
|
- **State Management**: Provider pattern with ChangeNotifier
|
||||||
- **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy
|
- **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy
|
||||||
@@ -64,11 +74,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- **Encryption**: End-to-end encryption for private messages using the MeshCore protocol
|
- **Encryption**: End-to-end encryption for private messages using the MeshCore protocol
|
||||||
|
|
||||||
### Platform Support
|
### Platform Support
|
||||||
|
|
||||||
- ✅ **Android**: Full support (API 21+)
|
- ✅ **Android**: Full support (API 21+)
|
||||||
- ✅ **iOS**: Full support (iOS 12+)
|
- ✅ **iOS**: Full support (iOS 12+)
|
||||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
| Package | Purpose |
|
| Package | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| flutter_blue_plus | Bluetooth Low Energy communication |
|
| flutter_blue_plus | Bluetooth Low Energy communication |
|
||||||
@@ -84,6 +96,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Flutter SDK 3.38.5 or later
|
- Flutter SDK 3.38.5 or later
|
||||||
- Android Studio / Xcode (for mobile development)
|
- Android Studio / Xcode (for mobile development)
|
||||||
- A MeshCore-compatible LoRa device
|
- A MeshCore-compatible LoRa device
|
||||||
@@ -91,17 +104,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/zjs81/meshcore-open.git
|
git clone https://github.com/zjs81/meshcore-open.git
|
||||||
cd meshcore-open
|
cd meshcore-open
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run the app**
|
3. **Run the app**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter run
|
flutter run
|
||||||
```
|
```
|
||||||
@@ -109,11 +125,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
### Building for Release
|
### Building for Release
|
||||||
|
|
||||||
**Android APK:**
|
**Android APK:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter build apk --release
|
flutter build apk --release
|
||||||
```
|
```
|
||||||
|
|
||||||
**iOS:**
|
**iOS:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter build ios --release
|
flutter build ios --release
|
||||||
```
|
```
|
||||||
@@ -152,25 +170,30 @@ lib/
|
|||||||
## BLE Protocol
|
## BLE Protocol
|
||||||
|
|
||||||
### Nordic UART Service (NUS)
|
### Nordic UART Service (NUS)
|
||||||
|
|
||||||
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
|
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
|
||||||
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
||||||
|
|
||||||
### Device Discovery
|
### Device Discovery
|
||||||
|
|
||||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
||||||
|
|
||||||
### Message Format
|
### Message Format
|
||||||
|
|
||||||
Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions.
|
Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### App Settings
|
### App Settings
|
||||||
|
|
||||||
- **Theme**: System default, light, or dark mode
|
- **Theme**: System default, light, or dark mode
|
||||||
- **Notifications**: Configurable for messages, channels, and node advertisements
|
- **Notifications**: Configurable for messages, channels, and node advertisements
|
||||||
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
||||||
- **Message Retry**: Automatic retry with configurable path clearing
|
- **Message Retry**: Automatic retry with configurable path clearing
|
||||||
|
|
||||||
### Device Settings
|
### Device Settings
|
||||||
|
|
||||||
- **Radio Power**: Transmit power adjustment (10-30 dBm)
|
- **Radio Power**: Transmit power adjustment (10-30 dBm)
|
||||||
- **Frequency**: LoRa frequency configuration
|
- **Frequency**: LoRa frequency configuration
|
||||||
- **Bandwidth**: Channel bandwidth selection
|
- **Bandwidth**: Channel bandwidth selection
|
||||||
@@ -182,22 +205,24 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
|||||||
This is an open-source project. Contributions are welcome!
|
This is an open-source project. Contributions are welcome!
|
||||||
|
|
||||||
### Development Guidelines
|
### Development Guidelines
|
||||||
|
|
||||||
- Follow the Flutter style guide
|
- Follow the Flutter style guide
|
||||||
- Use Material 3 design components
|
- Use Material 3 design components
|
||||||
- Write clear commit messages
|
- Write clear commit messages
|
||||||
- Test on both Android and iOS before submitting PRs
|
- Test on both Android and iOS before submitting PRs
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- Prefer `StatelessWidget` with `Consumer` for reactive UI
|
- Prefer `StatelessWidget` with `Consumer` for reactive UI
|
||||||
- Use `const` constructors where possible
|
- Use `const` constructors where possible
|
||||||
- Keep functions small and focused
|
- Keep functions small and focused
|
||||||
- Avoid premature abstractions
|
- Avoid premature abstractions
|
||||||
|
- Run dart format on all changes before submitting
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues, questions, or feature requests, please open an issue on GitHub:
|
For issues, questions, or feature requests, please open an issue on GitHub:
|
||||||
https://github.com/zjs81/meshcore-open/issues
|
<https://github.com/zjs81/meshcore-open/issues>
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# TestFlight and App Store Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com)
|
||||||
|
- [x] Xcode installed
|
||||||
|
- [x] Apple Transporter app installed
|
||||||
|
- [x] App icons ready (1024x1024px)
|
||||||
|
- [x] Bundle ID configured: `com.monitormx.meshcoreopen`
|
||||||
|
|
||||||
|
## Step 1: Register Bundle Identifier
|
||||||
|
|
||||||
|
1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list)
|
||||||
|
2. Click the **"+"** button
|
||||||
|
3. Select **"App IDs"** → Continue
|
||||||
|
4. Select **"App"** → Continue
|
||||||
|
5. Fill in:
|
||||||
|
- **Description**: Meshcore Open
|
||||||
|
- **Bundle ID**: Explicit - `com.monitormx.meshcoreopen`
|
||||||
|
- **Capabilities**: Leave defaults (or add as needed)
|
||||||
|
6. Click **Continue** → **Register**
|
||||||
|
|
||||||
|
## Step 2: Create App in App Store Connect
|
||||||
|
|
||||||
|
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
|
2. Sign in with your Apple ID
|
||||||
|
3. Click **"My Apps"**
|
||||||
|
4. Click the **"+"** button → **"New App"**
|
||||||
|
5. Fill in the form:
|
||||||
|
- **Platforms**: iOS
|
||||||
|
- **Name**: Meshcore Open
|
||||||
|
- **Primary Language**: English (U.S.)
|
||||||
|
- **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown
|
||||||
|
- **SKU**: `meshcore-open-001` (or any unique identifier)
|
||||||
|
- **User Access**: Full Access
|
||||||
|
6. Click **"Create"**
|
||||||
|
|
||||||
|
## Step 3: Build the IPA
|
||||||
|
|
||||||
|
Run these commands from the project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add CocoaPods to PATH
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
../flutter/bin/flutter clean
|
||||||
|
|
||||||
|
# Build IPA for App Store
|
||||||
|
../flutter/bin/flutter build ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
The IPA will be created at: `build/ios/ipa/meshcore_open.ipa`
|
||||||
|
|
||||||
|
## Step 4: Upload to App Store Connect via Transporter
|
||||||
|
|
||||||
|
1. **Open Apple Transporter**
|
||||||
|
- Launch from Applications folder
|
||||||
|
- Sign in with your Apple ID
|
||||||
|
|
||||||
|
2. **Upload the IPA**
|
||||||
|
- Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter
|
||||||
|
- Click **"Deliver"**
|
||||||
|
- Wait for upload to complete (usually 1-5 minutes)
|
||||||
|
|
||||||
|
3. **Processing**
|
||||||
|
- Apple will process your build (10-30 minutes)
|
||||||
|
- You'll receive an email when processing is complete
|
||||||
|
|
||||||
|
## Step 5: Configure App Store Connect Metadata
|
||||||
|
|
||||||
|
### App Information
|
||||||
|
1. In App Store Connect, go to your app
|
||||||
|
2. Fill in required information:
|
||||||
|
- **Subtitle**: Short description (30 chars max)
|
||||||
|
- **Privacy Policy URL**: Required for Bluetooth apps
|
||||||
|
- **Category**: Utilities or Productivity
|
||||||
|
- **Age Rating**: Complete questionnaire
|
||||||
|
|
||||||
|
### App Store Listing
|
||||||
|
1. Go to **App Store** tab
|
||||||
|
2. Upload **Screenshots** (required):
|
||||||
|
- iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot
|
||||||
|
- iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot
|
||||||
|
- Optional: iPad screenshots
|
||||||
|
|
||||||
|
3. Fill in **Description**:
|
||||||
|
```
|
||||||
|
Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- BLE connectivity to MeshCore devices
|
||||||
|
- Real-time mesh network communication
|
||||||
|
- Map visualization with OpenStreetMap
|
||||||
|
- Community management with QR code scanning
|
||||||
|
- Message tracking and retry system
|
||||||
|
|
||||||
|
Connect to your MeshCore LoRa device and start communicating over the mesh network.
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Keywords**: `lora,mesh,networking,bluetooth,communication`
|
||||||
|
5. **Support URL**: Your GitHub or website URL
|
||||||
|
6. **Marketing URL**: (Optional)
|
||||||
|
|
||||||
|
### Version Information
|
||||||
|
1. **What's New in This Version**:
|
||||||
|
```
|
||||||
|
Initial release of Meshcore Open
|
||||||
|
|
||||||
|
- BLE device connectivity
|
||||||
|
- Mesh network messaging
|
||||||
|
- Map integration
|
||||||
|
- Community features
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build**: Select the uploaded build once processing completes
|
||||||
|
|
||||||
|
## Step 6: TestFlight Setup
|
||||||
|
|
||||||
|
### Internal Testing (No Review Required)
|
||||||
|
1. Go to **TestFlight** tab in App Store Connect
|
||||||
|
2. Click **Internal Testing** → **"+"** to create a group
|
||||||
|
3. Name your group (e.g., "Internal Testers")
|
||||||
|
4. Add yourself as a tester using your email
|
||||||
|
5. Select the build you uploaded
|
||||||
|
6. Testers will receive an email with TestFlight invitation
|
||||||
|
|
||||||
|
### External Testing (Requires Beta Review)
|
||||||
|
1. Click **External Testing** → **"+"** to create a group
|
||||||
|
2. Add build and testers
|
||||||
|
3. Fill in **Test Information**:
|
||||||
|
- **What to Test**: Brief description of features
|
||||||
|
- **Feedback Email**: Your email address
|
||||||
|
4. Click **Submit for Review**
|
||||||
|
5. Beta review typically takes 24-48 hours
|
||||||
|
|
||||||
|
## Step 7: App Store Submission
|
||||||
|
|
||||||
|
Once you're ready for public release:
|
||||||
|
|
||||||
|
1. Go to **App Store** tab
|
||||||
|
2. Complete all required metadata (if not done)
|
||||||
|
3. Select your build
|
||||||
|
4. Fill in **App Review Information**:
|
||||||
|
- **Contact Information**: Your name, phone, email
|
||||||
|
- **Demo Account**: If app requires login
|
||||||
|
- **Notes**: Any special instructions for reviewers
|
||||||
|
5. Answer **Export Compliance** questions:
|
||||||
|
- Does your app use encryption? **Yes** (uses TLS/HTTPS)
|
||||||
|
- Is encryption registration required? **No** (standard encryption)
|
||||||
|
6. Click **Add for Review**
|
||||||
|
7. Review summary and click **Submit to App Review**
|
||||||
|
|
||||||
|
## Step 8: After Submission
|
||||||
|
|
||||||
|
- **App Review**: Typically 24-48 hours
|
||||||
|
- **Common Rejection Reasons**:
|
||||||
|
- Missing privacy policy
|
||||||
|
- Incomplete app information
|
||||||
|
- Crashes or bugs
|
||||||
|
- Misleading app description
|
||||||
|
|
||||||
|
- **If Approved**: You can release immediately or schedule a release date
|
||||||
|
- **If Rejected**: Address issues and resubmit
|
||||||
|
|
||||||
|
## Updating the App
|
||||||
|
|
||||||
|
When you need to release an update:
|
||||||
|
|
||||||
|
1. **Update version** in `pubspec.yaml`:
|
||||||
|
```yaml
|
||||||
|
version: 0.5.0+6 # Increment version (0.5.0) and build number (+6)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build new IPA**:
|
||||||
|
```bash
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
../flutter/bin/flutter clean
|
||||||
|
../flutter/bin/flutter build ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Upload via Transporter** (same process as above)
|
||||||
|
|
||||||
|
4. **Create new version** in App Store Connect:
|
||||||
|
- Click **"+"** next to versions
|
||||||
|
- Select version number
|
||||||
|
- Update "What's New" text
|
||||||
|
- Select new build
|
||||||
|
- Submit for review
|
||||||
|
|
||||||
|
## macOS Build (Bonus)
|
||||||
|
|
||||||
|
To build for macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
../flutter/bin/flutter build macos --release
|
||||||
|
cd build/macos/Build/Products/Release
|
||||||
|
zip -r meshcore_open-macos.zip meshcore_open.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Distribution:
|
||||||
|
- Share the zip file directly
|
||||||
|
- Users unzip and drag to Applications
|
||||||
|
- First run: Right-click → Open (to bypass Gatekeeper)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin`
|
||||||
|
- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities)
|
||||||
|
- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj`
|
||||||
|
|
||||||
|
### Upload Errors
|
||||||
|
- **No profiles found**: Create app in App Store Connect first
|
||||||
|
- **Bundle ID not registered**: Register in Apple Developer portal
|
||||||
|
- **Authentication failed**: Use Transporter app instead of CLI
|
||||||
|
|
||||||
|
### TestFlight Issues
|
||||||
|
- **Build not appearing**: Wait 10-30 minutes for processing
|
||||||
|
- **Can't add testers**: Check you have available slots (100 internal, 10,000 external)
|
||||||
|
- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa`
|
||||||
|
- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app`
|
||||||
|
- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj`
|
||||||
|
- **Version Info**: `pubspec.yaml`
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
|
- [Apple Developer Portal](https://developer.apple.com/account)
|
||||||
|
- [TestFlight Documentation](https://developer.apple.com/testflight/)
|
||||||
|
- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
||||||
|
- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with:
|
||||||
|
- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/)
|
||||||
|
- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues)
|
||||||
|
- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues)
|
||||||
@@ -83,5 +83,5 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Generated
+61
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770562336,
|
||||||
|
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
description = "MeshCore Flutter Application";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
# Flutter and Dart
|
||||||
|
flutter
|
||||||
|
dart
|
||||||
|
|
||||||
|
# Java (required for Android development)
|
||||||
|
jdk17
|
||||||
|
|
||||||
|
# Android development tools
|
||||||
|
android-tools
|
||||||
|
gradle
|
||||||
|
|
||||||
|
# For the shell hook to set up the environment for Flutter development
|
||||||
|
gtk3
|
||||||
|
glib
|
||||||
|
sysprof
|
||||||
|
libclang
|
||||||
|
cmake
|
||||||
|
ninja
|
||||||
|
pkg-config
|
||||||
|
libdatrie
|
||||||
|
|
||||||
|
# Additional tools for installing Android SDK if not present
|
||||||
|
curl
|
||||||
|
unzip
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "MeshCore Flutter Development Environment"
|
||||||
|
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||||
|
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
|
||||||
|
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
|
||||||
|
|
||||||
|
# Setup Android SDK in home directory (standard location)
|
||||||
|
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||||
|
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||||
|
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
|
||||||
|
|
||||||
|
echo "Android SDK: $ANDROID_HOME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if Android SDK exists and offer to download if not
|
||||||
|
if [ ! -d "$ANDROID_HOME" ]; then
|
||||||
|
echo "WARNING: Android SDK not found at $ANDROID_HOME"
|
||||||
|
echo ""
|
||||||
|
echo "To download and set up the Android SDK, run this command:"
|
||||||
|
echo ""
|
||||||
|
cat << 'EOF'
|
||||||
|
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
|
||||||
|
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
|
||||||
|
unzip -q cmdline-tools.zip && \
|
||||||
|
mkdir -p cmdline-tools/latest && \
|
||||||
|
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
|
||||||
|
rm cmdline-tools.zip && \
|
||||||
|
cd cmdline-tools/latest/bin && \
|
||||||
|
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
|
||||||
|
echo "Android SDK setup complete!"
|
||||||
|
EOF
|
||||||
|
echo ""
|
||||||
|
echo "Then run 'flutter doctor' again to verify."
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "Android SDK found at $ANDROID_HOME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -57,9 +57,6 @@ PODS:
|
|||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -79,7 +76,6 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/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`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
@@ -112,8 +108,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
:path: ".symlinks/plugins/mobile_scanner/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
@@ -140,7 +134,6 @@ SPEC CHECKSUMS:
|
|||||||
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto;
|
|||||||
import 'package:pointycastle/export.dart';
|
import 'package:pointycastle/export.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
@@ -24,6 +23,7 @@ import '../services/notification_service.dart';
|
|||||||
import '../storage/channel_message_store.dart';
|
import '../storage/channel_message_store.dart';
|
||||||
import '../storage/channel_order_store.dart';
|
import '../storage/channel_order_store.dart';
|
||||||
import '../storage/channel_settings_store.dart';
|
import '../storage/channel_settings_store.dart';
|
||||||
|
import '../storage/channel_store.dart';
|
||||||
import '../storage/contact_settings_store.dart';
|
import '../storage/contact_settings_store.dart';
|
||||||
import '../storage/contact_store.dart';
|
import '../storage/contact_store.dart';
|
||||||
import '../storage/message_store.dart';
|
import '../storage/message_store.dart';
|
||||||
@@ -67,9 +67,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||||
final Set<String> _loadedConversationKeys = {};
|
final Set<String> _loadedConversationKeys = {};
|
||||||
final Map<int, Set<String>> _processedChannelReactions =
|
final Map<int, Set<String>> _processedChannelReactions =
|
||||||
{}; // channelIndex -> Set of "reactionKey_emoji"
|
{}; // channelIndex -> Set of "targetHash_emoji"
|
||||||
final Map<String, Set<String>> _processedContactReactions =
|
final Map<String, Set<String>> _processedContactReactions =
|
||||||
{}; // contactPubKeyHex -> Set of "reactionKey_emoji"
|
{}; // contactPubKeyHex -> Set of "targetHash_emoji"
|
||||||
|
|
||||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||||
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
||||||
@@ -90,11 +90,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int? _currentBwHz;
|
int? _currentBwHz;
|
||||||
int? _currentSf;
|
int? _currentSf;
|
||||||
int? _currentCr;
|
int? _currentCr;
|
||||||
|
bool? _clientRepeat;
|
||||||
|
int? _firmwareVerCode;
|
||||||
int? _batteryMillivolts;
|
int? _batteryMillivolts;
|
||||||
double? _selfLatitude;
|
double? _selfLatitude;
|
||||||
double? _selfLongitude;
|
double? _selfLongitude;
|
||||||
bool _isLoadingContacts = false;
|
bool _isLoadingContacts = false;
|
||||||
bool _isLoadingChannels = false;
|
bool _isLoadingChannels = false;
|
||||||
|
bool _hasLoadedChannels = false;
|
||||||
bool _batteryRequested = false;
|
bool _batteryRequested = false;
|
||||||
bool _awaitingSelfInfo = false;
|
bool _awaitingSelfInfo = false;
|
||||||
bool _preserveContactsOnRefresh = false;
|
bool _preserveContactsOnRefresh = false;
|
||||||
@@ -122,7 +125,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
List<Channel> _previousChannelsCache = [];
|
List<Channel> _previousChannelsCache = [];
|
||||||
static const int _maxChannelSyncRetries = 3;
|
static const int _maxChannelSyncRetries = 3;
|
||||||
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
|
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
|
||||||
static const Duration _batteryPollInterval = Duration(seconds: 30);
|
static const Duration _batteryPollInterval = Duration(seconds: 120);
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
MessageRetryService? _retryService;
|
MessageRetryService? _retryService;
|
||||||
@@ -138,14 +141,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
||||||
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
||||||
final ContactStore _contactStore = ContactStore();
|
final ContactStore _contactStore = ContactStore();
|
||||||
|
final ChannelStore _channelStore = ChannelStore();
|
||||||
final UnreadStore _unreadStore = UnreadStore();
|
final UnreadStore _unreadStore = UnreadStore();
|
||||||
|
List<Channel> _cachedChannels = [];
|
||||||
final Map<int, bool> _channelSmazEnabled = {};
|
final Map<int, bool> _channelSmazEnabled = {};
|
||||||
bool _lastSentWasCliCommand =
|
bool _lastSentWasCliCommand =
|
||||||
false; // Track if last sent message was a CLI command
|
false; // Track if last sent message was a CLI command
|
||||||
final Map<String, bool> _contactSmazEnabled = {};
|
final Map<String, bool> _contactSmazEnabled = {};
|
||||||
final Set<String> _knownContactKeys = {};
|
final Set<String> _knownContactKeys = {};
|
||||||
final Map<String, int> _contactLastReadMs = {};
|
final Map<String, int> _contactUnreadCount = {};
|
||||||
final Map<int, int> _channelLastReadMs = {};
|
|
||||||
bool _unreadStateLoaded = false;
|
bool _unreadStateLoaded = false;
|
||||||
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
|
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
|
||||||
String? _activeContactKey;
|
String? _activeContactKey;
|
||||||
@@ -198,6 +202,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int? get currentBwHz => _currentBwHz;
|
int? get currentBwHz => _currentBwHz;
|
||||||
int? get currentSf => _currentSf;
|
int? get currentSf => _currentSf;
|
||||||
int? get currentCr => _currentCr;
|
int? get currentCr => _currentCr;
|
||||||
|
bool? get clientRepeat => _clientRepeat;
|
||||||
|
int? get firmwareVerCode => _firmwareVerCode;
|
||||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||||
int? get batteryMillivolts => _batteryMillivolts;
|
int? get batteryMillivolts => _batteryMillivolts;
|
||||||
int get maxContacts => _maxContacts;
|
int get maxContacts => _maxContacts;
|
||||||
@@ -320,17 +326,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int getUnreadCountForContactKey(String contactKeyHex) {
|
int getUnreadCountForContactKey(String contactKeyHex) {
|
||||||
if (!_unreadStateLoaded) return 0;
|
if (!_unreadStateLoaded) return 0;
|
||||||
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0;
|
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0;
|
||||||
final messages = _conversations[contactKeyHex];
|
return _contactUnreadCount[contactKeyHex] ?? 0;
|
||||||
if (messages == null || messages.isEmpty) return 0;
|
|
||||||
final lastReadMs = _contactLastReadMs[contactKeyHex] ?? 0;
|
|
||||||
var count = 0;
|
|
||||||
for (final message in messages) {
|
|
||||||
if (message.isOutgoing || message.isCli) continue;
|
|
||||||
if (message.timestamp.millisecondsSinceEpoch > lastReadMs) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int getUnreadCountForChannel(Channel channel) {
|
int getUnreadCountForChannel(Channel channel) {
|
||||||
@@ -339,17 +335,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
int getUnreadCountForChannelIndex(int channelIndex) {
|
int getUnreadCountForChannelIndex(int channelIndex) {
|
||||||
if (!_unreadStateLoaded) return 0;
|
if (!_unreadStateLoaded) return 0;
|
||||||
final messages = _channelMessages[channelIndex];
|
return _findChannelByIndex(channelIndex)?.unreadCount ?? 0;
|
||||||
if (messages == null || messages.isEmpty) return 0;
|
|
||||||
final lastReadMs = _channelLastReadMs[channelIndex] ?? 0;
|
|
||||||
var count = 0;
|
|
||||||
for (final message in messages) {
|
|
||||||
if (message.isOutgoing) continue;
|
|
||||||
if (message.timestamp.millisecondsSinceEpoch > lastReadMs) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int getTotalUnreadCount() {
|
int getTotalUnreadCount() {
|
||||||
@@ -379,16 +365,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadUnreadState() async {
|
Future<void> loadUnreadState() async {
|
||||||
_contactLastReadMs
|
_contactUnreadCount
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(await _unreadStore.loadContactLastRead());
|
..addAll(await _unreadStore.loadContactUnreadCount());
|
||||||
_channelLastReadMs
|
|
||||||
..clear()
|
|
||||||
..addAll(await _unreadStore.loadChannelLastRead());
|
|
||||||
_unreadStateLoaded = true;
|
_unreadStateLoaded = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadCachedChannels() async {
|
||||||
|
_cachedChannels = await _channelStore.loadChannels();
|
||||||
|
}
|
||||||
|
|
||||||
void setActiveContact(String? contactKeyHex) {
|
void setActiveContact(String? contactKeyHex) {
|
||||||
if (contactKeyHex != null &&
|
if (contactKeyHex != null &&
|
||||||
!_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
!_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
||||||
@@ -410,17 +397,36 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
void markContactRead(String contactKeyHex) {
|
void markContactRead(String contactKeyHex) {
|
||||||
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
||||||
final markMs = _calculateReadTimestampMs(
|
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
|
||||||
_conversations[contactKeyHex]?.map((m) => m.timestamp),
|
if (previousCount > 0) {
|
||||||
);
|
_contactUnreadCount[contactKeyHex] = 0;
|
||||||
_setContactLastReadMs(contactKeyHex, markMs);
|
_appDebugLogService?.info(
|
||||||
|
'Contact $contactKeyHex marked as read (was $previousCount unread)',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void markChannelRead(int channelIndex) {
|
void markChannelRead(int channelIndex) {
|
||||||
final markMs = _calculateReadTimestampMs(
|
final channel = _findChannelByIndex(channelIndex);
|
||||||
_channelMessages[channelIndex]?.map((m) => m.timestamp),
|
if (channel != null && channel.unreadCount > 0) {
|
||||||
);
|
final previousCount = channel.unreadCount;
|
||||||
_setChannelLastReadMs(channelIndex, markMs);
|
channel.unreadCount = 0;
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
_channelStore.saveChannels(
|
||||||
|
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setChannelSmazEnabled(int channelIndex, bool enabled) async {
|
Future<void> setChannelSmazEnabled(int channelIndex, bool enabled) async {
|
||||||
@@ -655,7 +661,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_scanResults.clear();
|
_scanResults.clear();
|
||||||
for (var result in results) {
|
for (var result in results) {
|
||||||
if (result.device.platformName.startsWith("MeshCore-") ||
|
if (result.device.platformName.startsWith("MeshCore-") ||
|
||||||
result.advertisementData.advName.startsWith("MeshCore-")) {
|
result.advertisementData.advName.startsWith("MeshCore-") ||
|
||||||
|
result.advertisementData.advName.startsWith("Whisper-")) {
|
||||||
_scanResults.add(result);
|
_scanResults.add(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +713,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_connectionSubscription = device.connectionState.listen((state) {
|
_connectionSubscription = device.connectionState.listen((state) {
|
||||||
if (state == BluetoothConnectionState.disconnected) {
|
if (state == BluetoothConnectionState.disconnected && isConnected) {
|
||||||
_handleDisconnection();
|
_handleDisconnection();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -772,9 +779,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
_setState(MeshCoreConnectionState.connected);
|
_setState(MeshCoreConnectionState.connected);
|
||||||
|
|
||||||
// Enable wake lock to prevent BLE disconnection when screen turns off
|
|
||||||
await WakelockPlus.enable();
|
|
||||||
|
|
||||||
await _requestDeviceInfo();
|
await _requestDeviceInfo();
|
||||||
_startBatteryPolling();
|
_startBatteryPolling();
|
||||||
final gotSelfInfo = await _waitForSelfInfo(
|
final gotSelfInfo = await _waitForSelfInfo(
|
||||||
@@ -787,6 +791,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Keep device clock aligned on every connection.
|
// Keep device clock aligned on every connection.
|
||||||
await syncTime();
|
await syncTime();
|
||||||
|
|
||||||
|
// Fetch channels so we can track unread counts for incoming messages
|
||||||
|
unawaited(getChannels());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Connection error: $e");
|
debugPrint("Connection error: $e");
|
||||||
await disconnect(manual: false);
|
await disconnect(manual: false);
|
||||||
@@ -880,9 +887,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_setState(MeshCoreConnectionState.disconnecting);
|
_setState(MeshCoreConnectionState.disconnecting);
|
||||||
_stopBatteryPolling();
|
_stopBatteryPolling();
|
||||||
|
|
||||||
// Disable wake lock when disconnecting
|
|
||||||
await WakelockPlus.disable();
|
|
||||||
|
|
||||||
await _notifySubscription?.cancel();
|
await _notifySubscription?.cancel();
|
||||||
_notifySubscription = null;
|
_notifySubscription = null;
|
||||||
|
|
||||||
@@ -916,6 +920,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_selfName = null;
|
_selfName = null;
|
||||||
_selfLatitude = null;
|
_selfLatitude = null;
|
||||||
_selfLongitude = null;
|
_selfLongitude = null;
|
||||||
|
_clientRepeat = null;
|
||||||
|
_firmwareVerCode = null;
|
||||||
_batteryMillivolts = null;
|
_batteryMillivolts = null;
|
||||||
_batteryRequested = false;
|
_batteryRequested = false;
|
||||||
_awaitingSelfInfo = false;
|
_awaitingSelfInfo = false;
|
||||||
@@ -927,6 +933,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_pendingQueueSync = false;
|
_pendingQueueSync = false;
|
||||||
_isSyncingChannels = false;
|
_isSyncingChannels = false;
|
||||||
_channelSyncInFlight = false;
|
_channelSyncInFlight = false;
|
||||||
|
_hasLoadedChannels = false;
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.disconnected);
|
_setState(MeshCoreConnectionState.disconnected);
|
||||||
if (!manual) {
|
if (!manual) {
|
||||||
@@ -990,6 +997,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestDeviceInfo() async {
|
Future<void> _requestDeviceInfo() async {
|
||||||
|
if (!isConnected || _awaitingSelfInfo) return;
|
||||||
_awaitingSelfInfo = true;
|
_awaitingSelfInfo = true;
|
||||||
await sendFrame(buildDeviceQueryFrame());
|
await sendFrame(buildDeviceQueryFrame());
|
||||||
await sendFrame(buildAppStartFrame());
|
await sendFrame(buildAppStartFrame());
|
||||||
@@ -1283,15 +1291,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (reactionInfo != null) {
|
if (reactionInfo != null) {
|
||||||
// Check if we've already processed this reaction
|
// Check if we've already processed this reaction
|
||||||
_processedChannelReactions.putIfAbsent(channel.index, () => {});
|
_processedChannelReactions.putIfAbsent(channel.index, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionIdentifier =
|
||||||
final reactionIdentifier = reactionKey != null
|
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
|
||||||
? '${reactionKey}_${reactionInfo.emoji}'
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (reactionIdentifier != null &&
|
if (_processedChannelReactions[channel.index]!.contains(
|
||||||
_processedChannelReactions[channel.index]!.contains(
|
reactionIdentifier,
|
||||||
reactionIdentifier,
|
)) {
|
||||||
)) {
|
|
||||||
// Already processed, don't process again
|
// Already processed, don't process again
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1305,9 +1310,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
await _channelMessageStore.saveChannelMessages(channel.index, messages);
|
await _channelMessageStore.saveChannelMessages(channel.index, messages);
|
||||||
|
|
||||||
// Mark this reaction as processed
|
// Mark this reaction as processed
|
||||||
if (reactionIdentifier != null) {
|
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
|
||||||
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -1343,8 +1346,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
unawaited(_persistContacts());
|
unawaited(_persistContacts());
|
||||||
_conversations.remove(contact.publicKeyHex);
|
_conversations.remove(contact.publicKeyHex);
|
||||||
_loadedConversationKeys.remove(contact.publicKeyHex);
|
_loadedConversationKeys.remove(contact.publicKeyHex);
|
||||||
_contactLastReadMs.remove(contact.publicKeyHex);
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
_messageStore.clearMessages(contact.publicKeyHex);
|
_messageStore.clearMessages(contact.publicKeyHex);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1500,13 +1505,21 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getChannels({int? maxChannels}) async {
|
Future<void> getChannels({int? maxChannels, bool force = false}) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
if (_isSyncingChannels) {
|
if (_isSyncingChannels) {
|
||||||
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
|
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip fetching if already loaded and not forced
|
||||||
|
if (_hasLoadedChannels && !force) {
|
||||||
|
debugPrint(
|
||||||
|
'[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isLoadingChannels = true;
|
_isLoadingChannels = true;
|
||||||
_isSyncingChannels = true;
|
_isSyncingChannels = true;
|
||||||
_previousChannelsCache = List<Channel>.from(_channels);
|
_previousChannelsCache = List<Channel>.from(_channels);
|
||||||
@@ -1611,6 +1624,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
_cleanupChannelSync(completed: true);
|
_cleanupChannelSync(completed: true);
|
||||||
|
|
||||||
|
// Cache channels for offline use
|
||||||
|
_cachedChannels = List<Channel>.from(_channels);
|
||||||
|
unawaited(_channelStore.saveChannels(_channels));
|
||||||
|
|
||||||
// Apply ordering and notify UI
|
// Apply ordering and notify UI
|
||||||
_applyChannelOrder();
|
_applyChannelOrder();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1626,6 +1643,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_totalChannelsToRequest = 0;
|
_totalChannelsToRequest = 0;
|
||||||
|
|
||||||
if (completed) {
|
if (completed) {
|
||||||
|
_hasLoadedChannels = true;
|
||||||
_previousChannelsCache.clear();
|
_previousChannelsCache.clear();
|
||||||
}
|
}
|
||||||
// Keep cache on failure/disconnection for future attempts
|
// Keep cache on failure/disconnection for future attempts
|
||||||
@@ -1636,7 +1654,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
await sendFrame(buildSetChannelFrame(index, name, psk));
|
await sendFrame(buildSetChannelFrame(index, name, psk));
|
||||||
// Refresh channels after setting
|
// Refresh channels after setting
|
||||||
await getChannels();
|
await getChannels(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteChannel(int index) async {
|
Future<void> deleteChannel(int index) async {
|
||||||
@@ -1644,14 +1662,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Delete by setting empty name and zero PSK
|
// Delete by setting empty name and zero PSK
|
||||||
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
|
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
|
||||||
_channelLastReadMs.remove(index);
|
|
||||||
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
|
||||||
// Clear stored messages for this channel
|
// Clear stored messages for this channel
|
||||||
await _channelMessageStore.clearChannelMessages(index);
|
await _channelMessageStore.clearChannelMessages(index);
|
||||||
// Clear in-memory messages for this channel
|
// Clear in-memory messages for this channel
|
||||||
_channelMessages.remove(index);
|
_channelMessages.remove(index);
|
||||||
// Refresh channels after deleting
|
// Refresh channels after deleting
|
||||||
await getChannels();
|
await getChannels(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleFrame(List<int> data) {
|
void _handleFrame(List<int> data) {
|
||||||
@@ -1810,6 +1826,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
void _handleDeviceInfo(Uint8List frame) {
|
void _handleDeviceInfo(Uint8List frame) {
|
||||||
if (frame.length < 4) return;
|
if (frame.length < 4) return;
|
||||||
|
_firmwareVerCode = frame[1];
|
||||||
|
|
||||||
|
// Parse client_repeat from firmware v9+ (byte 80)
|
||||||
|
if (frame.length >= 81) {
|
||||||
|
_clientRepeat = frame[80] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||||
final reportedContacts = frame[2];
|
final reportedContacts = frame[2];
|
||||||
final reportedChannels = frame[3];
|
final reportedChannels = frame[3];
|
||||||
@@ -1830,8 +1853,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleNoMoreMessages() {
|
void _handleNoMoreMessages() {
|
||||||
@@ -1923,9 +1946,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final contact = Contact.fromFrame(frame);
|
final contact = Contact.fromFrame(frame);
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
if (contact.type == advTypeRepeater) {
|
if (contact.type == advTypeRepeater) {
|
||||||
_contactLastReadMs.remove(contact.publicKeyHex);
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
_unreadStore.saveContactLastRead(
|
_unreadStore.saveContactUnreadCount(
|
||||||
Map<String, int>.from(_contactLastReadMs),
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Check if this is a new contact
|
// Check if this is a new contact
|
||||||
@@ -2112,6 +2135,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
|
// Ignore messages from self (device hearing its own broadcast)
|
||||||
|
// BUT allow repeated messages (pathLength indicates it went through repeater)
|
||||||
|
if (_selfPublicKey != null &&
|
||||||
|
message.senderKeyHex == pubKeyToHex(_selfPublicKey!) &&
|
||||||
|
(message.pathLength == null || message.pathLength == 0)) {
|
||||||
|
debugPrint('Ignoring direct message from self');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final contact = _contacts.cast<Contact?>().firstWhere(
|
final contact = _contacts.cast<Contact?>().firstWhere(
|
||||||
(c) => c?.publicKeyHex == message!.senderKeyHex,
|
(c) => c?.publicKeyHex == message!.senderKeyHex,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
@@ -2141,7 +2173,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_addMessage(message.senderKeyHex, message);
|
_addMessage(message.senderKeyHex, message);
|
||||||
_maybeMarkActiveContactRead(message);
|
_maybeIncrementContactUnread(message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Show notification for new incoming message
|
// Show notification for new incoming message
|
||||||
@@ -2332,7 +2364,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
pathBytes: message.pathBytes,
|
pathBytes: message.pathBytes,
|
||||||
);
|
);
|
||||||
final isNew = _addChannelMessage(message.channelIndex!, message);
|
final isNew = _addChannelMessage(message.channelIndex!, message);
|
||||||
_maybeMarkActiveChannelRead(message);
|
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
_maybeNotifyChannelMessage(message);
|
_maybeNotifyChannelMessage(message);
|
||||||
@@ -2354,7 +2386,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final channelHash = payload[0];
|
final channelHash = payload[0];
|
||||||
final encrypted = Uint8List.fromList(payload.sublist(1));
|
final encrypted = Uint8List.fromList(payload.sublist(1));
|
||||||
|
|
||||||
for (final channel in _channels) {
|
// Use cached channels as fallback if live channels not yet loaded
|
||||||
|
final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels;
|
||||||
|
for (final channel in channelsToSearch) {
|
||||||
if (channel.isEmpty) continue;
|
if (channel.isEmpty) continue;
|
||||||
final hash = _computeChannelHash(channel.psk);
|
final hash = _computeChannelHash(channel.psk);
|
||||||
if (hash != channelHash) continue;
|
if (hash != channelHash) continue;
|
||||||
@@ -2393,7 +2427,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
pathBytes: message.pathBytes,
|
pathBytes: message.pathBytes,
|
||||||
);
|
);
|
||||||
final isNew = _addChannelMessage(channel.index, message);
|
final isNew = _addChannelMessage(channel.index, message);
|
||||||
_maybeMarkActiveChannelRead(message);
|
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
final label = channel.name.isEmpty
|
final label = channel.name.isEmpty
|
||||||
@@ -2523,6 +2557,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
'[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}',
|
'[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Preserve unread count from cached channel
|
||||||
|
final cachedChannel = _cachedChannels.cast<Channel?>().firstWhere(
|
||||||
|
(c) => c?.index == channel.index,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if (cachedChannel != null) {
|
||||||
|
channel.unreadCount = cachedChannel.unreadCount;
|
||||||
|
}
|
||||||
|
|
||||||
// If we're syncing and this is the channel we're waiting for
|
// If we're syncing and this is the channel we're waiting for
|
||||||
if (_isSyncingChannels && _channelSyncInFlight) {
|
if (_isSyncingChannels && _channelSyncInFlight) {
|
||||||
if (channel.index == _nextChannelIndexToRequest) {
|
if (channel.index == _nextChannelIndexToRequest) {
|
||||||
@@ -2562,6 +2605,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
(c) => c.index == channel.index,
|
(c) => c.index == channel.index,
|
||||||
);
|
);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
|
// Preserve unread count from existing channel
|
||||||
|
channel.unreadCount = _channels[existingIndex].unreadCount;
|
||||||
_channels[existingIndex] = channel;
|
_channels[existingIndex] = channel;
|
||||||
} else {
|
} else {
|
||||||
_channels.add(channel);
|
_channels.add(channel);
|
||||||
@@ -2612,67 +2657,98 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return contact.type != advTypeRepeater;
|
return contact.type != advTypeRepeater;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _calculateReadTimestampMs(Iterable<DateTime>? timestamps) {
|
Channel? _findChannelByIndex(int index) {
|
||||||
var latestMs = 0;
|
return _channels.cast<Channel?>().firstWhere(
|
||||||
if (timestamps != null) {
|
(c) => c?.index == index,
|
||||||
for (final timestamp in timestamps) {
|
orElse: () => null,
|
||||||
final ms = timestamp.millisecondsSinceEpoch;
|
) ??
|
||||||
if (ms > latestMs) {
|
_cachedChannels.cast<Channel?>().firstWhere(
|
||||||
latestMs = ms;
|
(c) => c?.index == index,
|
||||||
}
|
orElse: () => null,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
return latestMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setContactLastReadMs(
|
void _maybeIncrementChannelUnread(
|
||||||
String contactKeyHex,
|
ChannelMessage message, {
|
||||||
int timestampMs, {
|
required bool isNew,
|
||||||
bool notify = true,
|
|
||||||
}) {
|
}) {
|
||||||
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
if (!isNew || message.isOutgoing) {
|
||||||
final existing = _contactLastReadMs[contactKeyHex] ?? 0;
|
_appDebugLogService?.info(
|
||||||
if (timestampMs <= existing) return;
|
'Skip unread increment: isNew=$isNew, isOutgoing=${message.isOutgoing}',
|
||||||
_contactLastReadMs[contactKeyHex] = timestampMs;
|
tag: 'Unread',
|
||||||
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
);
|
||||||
if (notify) {
|
return;
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _setChannelLastReadMs(
|
|
||||||
int channelIndex,
|
|
||||||
int timestampMs, {
|
|
||||||
bool notify = true,
|
|
||||||
}) {
|
|
||||||
final existing = _channelLastReadMs[channelIndex] ?? 0;
|
|
||||||
if (timestampMs <= existing) return;
|
|
||||||
_channelLastReadMs[channelIndex] = timestampMs;
|
|
||||||
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
|
||||||
if (notify) {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _maybeMarkActiveContactRead(Message message) {
|
|
||||||
if (message.isOutgoing || message.isCli) return;
|
|
||||||
if (_activeContactKey != message.senderKeyHex) return;
|
|
||||||
if (!_shouldTrackUnreadForContactKey(message.senderKeyHex)) return;
|
|
||||||
_setContactLastReadMs(
|
|
||||||
message.senderKeyHex,
|
|
||||||
message.timestamp.millisecondsSinceEpoch,
|
|
||||||
notify: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _maybeMarkActiveChannelRead(ChannelMessage message) {
|
|
||||||
if (message.isOutgoing) return;
|
|
||||||
final channelIndex = message.channelIndex;
|
final channelIndex = message.channelIndex;
|
||||||
if (channelIndex == null || _activeChannelIndex != channelIndex) return;
|
if (channelIndex == null) {
|
||||||
_setChannelLastReadMs(
|
_appDebugLogService?.info(
|
||||||
channelIndex,
|
'Skip unread increment: channelIndex is null',
|
||||||
message.timestamp.millisecondsSinceEpoch,
|
tag: 'Unread',
|
||||||
notify: false,
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't increment if user is viewing this channel
|
||||||
|
if (_activeChannelIndex == channelIndex) {
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Skip unread increment: channel $channelIndex is active',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final channel = _findChannelByIndex(channelIndex);
|
||||||
|
if (channel != null) {
|
||||||
|
channel.unreadCount++;
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
_channelStore.saveChannels(
|
||||||
|
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Channel $channelIndex not found in _channels (${_channels.length}) or _cachedChannels (${_cachedChannels.length})',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _maybeIncrementContactUnread(Message message) {
|
||||||
|
if (message.isOutgoing || message.isCli) {
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Skip contact unread increment: isOutgoing=${message.isOutgoing}, isCli=${message.isCli}',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final contactKey = message.senderKeyHex;
|
||||||
|
if (!_shouldTrackUnreadForContactKey(contactKey)) {
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Skip contact unread increment: should not track for $contactKey',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't increment if user is viewing this contact
|
||||||
|
if (_activeContactKey == contactKey) {
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Skip contact unread increment: contact $contactKey is active',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentCount = _contactUnreadCount[contactKey] ?? 0;
|
||||||
|
_contactUnreadCount[contactKey] = currentCount + 1;
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'Contact $contactKey unread count incremented to ${currentCount + 1}',
|
||||||
|
tag: 'Unread',
|
||||||
|
);
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2683,26 +2759,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Parse reaction info
|
// Parse reaction info
|
||||||
final reactionInfo = Message.parseReaction(message.text);
|
final reactionInfo = Message.parseReaction(message.text);
|
||||||
if (reactionInfo != null) {
|
if (reactionInfo != null) {
|
||||||
// Check if we've already processed this exact reaction using lightweight key
|
// Check if we've already processed this exact reaction
|
||||||
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
|
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionIdentifier =
|
||||||
final reactionIdentifier = reactionKey != null
|
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
|
||||||
? '${reactionKey}_${reactionInfo.emoji}'
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final isDuplicate =
|
final isDuplicate = _processedContactReactions[pubKeyHex]!.contains(
|
||||||
reactionIdentifier != null &&
|
reactionIdentifier,
|
||||||
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
|
);
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
// New reaction - process it
|
// New reaction - process it
|
||||||
_processContactReaction(messages, reactionInfo);
|
_processContactReaction(messages, reactionInfo, pubKeyHex);
|
||||||
_messageStore.saveMessages(pubKeyHex, messages);
|
_messageStore.saveMessages(pubKeyHex, messages);
|
||||||
|
|
||||||
// Mark as processed
|
// Mark as processed
|
||||||
if (reactionIdentifier != null) {
|
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
|
||||||
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -2717,15 +2789,53 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
void _processContactReaction(
|
void _processContactReaction(
|
||||||
List<Message> messages,
|
List<Message> messages,
|
||||||
ReactionInfo reactionInfo,
|
ReactionInfo reactionInfo,
|
||||||
|
String contactPubKeyHex,
|
||||||
) {
|
) {
|
||||||
// Find target message by messageId
|
// Find target message by computing hash and comparing
|
||||||
for (int i = 0; i < messages.length; i++) {
|
final targetHash = reactionInfo.targetHash;
|
||||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
final contact = _contacts.cast<Contact?>().firstWhere(
|
||||||
final currentReactions = Map<String, int>.from(messages[i].reactions);
|
(c) => c?.publicKeyHex == contactPubKeyHex,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
final isRoomServer = contact?.type == advTypeRoom;
|
||||||
|
|
||||||
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
|
final msg = messages[i];
|
||||||
|
|
||||||
|
// For 1:1 chats: contact reacts to my outgoing messages only
|
||||||
|
// For room servers: any message can be reacted to (multi-user)
|
||||||
|
if (!isRoomServer && !msg.isOutgoing) continue;
|
||||||
|
|
||||||
|
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
|
// For room servers, include sender name (resolve from fourByteRoomContactKey)
|
||||||
|
// For 1:1 chats, sender is implicit (null)
|
||||||
|
String? senderName;
|
||||||
|
if (isRoomServer && !msg.isOutgoing) {
|
||||||
|
// Resolve sender from the message's fourByteRoomContactKey
|
||||||
|
final senderContact = _contacts.cast<Contact?>().firstWhere(
|
||||||
|
(c) =>
|
||||||
|
c != null &&
|
||||||
|
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
senderName = senderContact?.name;
|
||||||
|
} else if (isRoomServer && msg.isOutgoing) {
|
||||||
|
senderName = selfName;
|
||||||
|
}
|
||||||
|
// For 1:1, senderName stays null
|
||||||
|
|
||||||
|
final msgHash = ReactionHelper.computeReactionHash(
|
||||||
|
timestampSecs,
|
||||||
|
senderName,
|
||||||
|
msg.text,
|
||||||
|
);
|
||||||
|
if (msgHash == targetHash) {
|
||||||
|
final currentReactions = Map<String, int>.from(msg.reactions);
|
||||||
currentReactions[reactionInfo.emoji] =
|
currentReactions[reactionInfo.emoji] =
|
||||||
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
||||||
|
|
||||||
messages[i] = messages[i].copyWith(reactions: currentReactions);
|
messages[i] = msg.copyWith(reactions: currentReactions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2876,18 +2986,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Parse reaction info
|
// Parse reaction info
|
||||||
final reactionInfo = ChannelMessage.parseReaction(message.text);
|
final reactionInfo = ChannelMessage.parseReaction(message.text);
|
||||||
if (reactionInfo != null) {
|
if (reactionInfo != null) {
|
||||||
// Check if we've already processed this exact reaction using lightweight key
|
// Check if we've already processed this exact reaction
|
||||||
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
|
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionIdentifier =
|
||||||
final reactionIdentifier = reactionKey != null
|
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
|
||||||
? '${reactionKey}_${reactionInfo.emoji}'
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final isDuplicate =
|
final isDuplicate = _processedChannelReactions[channelIndex]!.contains(
|
||||||
reactionIdentifier != null &&
|
reactionIdentifier,
|
||||||
_processedChannelReactions[channelIndex]!.contains(
|
);
|
||||||
reactionIdentifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
// New reaction - process it
|
// New reaction - process it
|
||||||
@@ -2896,9 +3002,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_channelMessageStore.saveChannelMessages(channelIndex, messages);
|
_channelMessageStore.saveChannelMessages(channelIndex, messages);
|
||||||
|
|
||||||
// Mark as processed
|
// Mark as processed
|
||||||
if (reactionIdentifier != null) {
|
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
|
||||||
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false; // Don't add reaction as a visible message
|
return false; // Don't add reaction as a visible message
|
||||||
}
|
}
|
||||||
@@ -2994,14 +3098,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
List<ChannelMessage> messages,
|
List<ChannelMessage> messages,
|
||||||
ReactionInfo reactionInfo,
|
ReactionInfo reactionInfo,
|
||||||
) {
|
) {
|
||||||
// Find target message by messageId
|
// Find target message by computing hash and comparing
|
||||||
for (int i = 0; i < messages.length; i++) {
|
final targetHash = reactionInfo.targetHash;
|
||||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
final currentReactions = Map<String, int>.from(messages[i].reactions);
|
final msg = messages[i];
|
||||||
|
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
final msgHash = ReactionHelper.computeReactionHash(
|
||||||
|
timestampSecs,
|
||||||
|
msg.senderName,
|
||||||
|
msg.text,
|
||||||
|
);
|
||||||
|
if (msgHash == targetHash) {
|
||||||
|
final currentReactions = Map<String, int>.from(msg.reactions);
|
||||||
currentReactions[reactionInfo.emoji] =
|
currentReactions[reactionInfo.emoji] =
|
||||||
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
||||||
|
|
||||||
messages[i] = messages[i].copyWith(reactions: currentReactions);
|
messages[i] = msg.copyWith(reactions: currentReactions);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -3043,28 +3155,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
|
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
|
||||||
final selfKey = _selfPublicKey;
|
|
||||||
if (selfKey == null) return false;
|
|
||||||
if (pathBytes.length < pathHashSize) return false;
|
|
||||||
final trimmed = senderName.trim();
|
final trimmed = senderName.trim();
|
||||||
if (trimmed.isEmpty) return false;
|
if (trimmed.isEmpty) return false;
|
||||||
|
|
||||||
final selfName = _selfName?.trim();
|
final selfName = _selfName?.trim();
|
||||||
if (selfName == null || selfName.isEmpty) return false;
|
if (selfName == null || selfName.isEmpty) return false;
|
||||||
|
|
||||||
|
// If sender name doesn't match, keep the message
|
||||||
if (trimmed != selfName) return false;
|
if (trimmed != selfName) return false;
|
||||||
final prefix = selfKey.sublist(0, pathHashSize);
|
|
||||||
for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
|
// Name matches - this is from self
|
||||||
var match = true;
|
// Drop only if pathBytes is empty (direct broadcast)
|
||||||
for (int j = 0; j < pathHashSize; j++) {
|
// Keep if pathBytes has data (repeated through another node)
|
||||||
if (pathBytes[i + j] != prefix[j]) {
|
return pathBytes.isEmpty;
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
|
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
|
||||||
@@ -3118,8 +3221,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDisconnection() {
|
void _handleDisconnection() {
|
||||||
// Disable wake lock when connection is lost
|
|
||||||
WakelockPlus.disable();
|
|
||||||
_stopBatteryPolling();
|
_stopBatteryPolling();
|
||||||
|
|
||||||
for (final entry in _pendingRepeaterAcks.values) {
|
for (final entry in _pendingRepeaterAcks.values) {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class BufferReader {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void skipBytes(int count) {
|
||||||
|
_pointer += count;
|
||||||
|
}
|
||||||
|
|
||||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||||
|
|
||||||
String readString() =>
|
String readString() =>
|
||||||
@@ -98,6 +102,25 @@ class BufferWriter {
|
|||||||
}
|
}
|
||||||
writeBytes(bytes);
|
writeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void writeHex(String hex) {
|
||||||
|
// Validate hex string length is even and not empty
|
||||||
|
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||||
|
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||||
|
}
|
||||||
|
List<int> result = [];
|
||||||
|
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||||
|
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||||
|
final byte = int.tryParse(hexByte, radix: 16);
|
||||||
|
if (byte == null) {
|
||||||
|
throw FormatException(
|
||||||
|
'Invalid hex characters at position $i: $hexByte',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.add(byte);
|
||||||
|
}
|
||||||
|
writeBytes(Uint8List.fromList(result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
@@ -127,6 +150,7 @@ const int cmdSendStatusReq = 27;
|
|||||||
const int cmdGetContactByKey = 30;
|
const int cmdGetContactByKey = 30;
|
||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
|
const int cmdSendTracePath = 36;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdGetRadioSettings = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdGetTelemetryReq = 39;
|
||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
@@ -159,6 +183,7 @@ const int respCodeContactMsgRecv = 7;
|
|||||||
const int respCodeChannelMsgRecv = 8;
|
const int respCodeChannelMsgRecv = 8;
|
||||||
const int respCodeCurrTime = 9;
|
const int respCodeCurrTime = 9;
|
||||||
const int respCodeNoMoreMessages = 10;
|
const int respCodeNoMoreMessages = 10;
|
||||||
|
const int respCodeExportContact = 11;
|
||||||
const int respCodeBattAndStorage = 12;
|
const int respCodeBattAndStorage = 12;
|
||||||
const int respCodeDeviceInfo = 13;
|
const int respCodeDeviceInfo = 13;
|
||||||
const int respCodeContactMsgRecvV3 = 16;
|
const int respCodeContactMsgRecvV3 = 16;
|
||||||
@@ -176,6 +201,7 @@ const int pushCodeLoginSuccess = 0x85;
|
|||||||
const int pushCodeLoginFail = 0x86;
|
const int pushCodeLoginFail = 0x86;
|
||||||
const int pushCodeStatusResponse = 0x87;
|
const int pushCodeStatusResponse = 0x87;
|
||||||
const int pushCodeLogRxData = 0x88;
|
const int pushCodeLogRxData = 0x88;
|
||||||
|
const int pushCodeTraceData = 0x89;
|
||||||
const int pushCodeNewAdvert = 0x8A;
|
const int pushCodeNewAdvert = 0x8A;
|
||||||
const int pushCodeTelemetryResponse = 0x8B;
|
const int pushCodeTelemetryResponse = 0x8B;
|
||||||
const int pushCodeBinaryResponse = 0x8C;
|
const int pushCodeBinaryResponse = 0x8C;
|
||||||
@@ -195,8 +221,10 @@ const int maxFrameSize = 172;
|
|||||||
const int appProtocolVersion = 3;
|
const int appProtocolVersion = 3;
|
||||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||||
const int maxTextPayloadBytes = 160;
|
const int maxTextPayloadBytes = 160;
|
||||||
const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
|
const int _sendTextMsgOverheadBytes =
|
||||||
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
|
1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
|
||||||
|
const int _sendChannelTextMsgOverheadBytes =
|
||||||
|
1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
|
||||||
|
|
||||||
int maxContactMessageBytes() {
|
int maxContactMessageBytes() {
|
||||||
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
|
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
|
||||||
@@ -522,18 +550,29 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_SET_RADIO_PARAMS frame
|
// Build CMD_SET_RADIO_PARAMS frame
|
||||||
// Format: [cmd][freq x4][bw x4][sf][cr]
|
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
|
||||||
|
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
|
||||||
// freq: frequency in Hz (300000-2500000)
|
// freq: frequency in Hz (300000-2500000)
|
||||||
// bw: bandwidth in Hz (7000-500000)
|
// bw: bandwidth in Hz (7000-500000)
|
||||||
// sf: spreading factor (5-12)
|
// sf: spreading factor (5-12)
|
||||||
// cr: coding rate (5-8)
|
// cr: coding rate (5-8)
|
||||||
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
|
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
|
||||||
|
Uint8List buildSetRadioParamsFrame(
|
||||||
|
int freqHz,
|
||||||
|
int bwHz,
|
||||||
|
int sf,
|
||||||
|
int cr, {
|
||||||
|
bool? clientRepeat,
|
||||||
|
}) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSetRadioParams);
|
writer.writeByte(cmdSetRadioParams);
|
||||||
writer.writeUInt32LE(freqHz);
|
writer.writeUInt32LE(freqHz);
|
||||||
writer.writeUInt32LE(bwHz);
|
writer.writeUInt32LE(bwHz);
|
||||||
writer.writeByte(sf);
|
writer.writeByte(sf);
|
||||||
writer.writeByte(cr);
|
writer.writeByte(cr);
|
||||||
|
if (clientRepeat != null) {
|
||||||
|
writer.writeByte(clientRepeat ? 1 : 0);
|
||||||
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,3 +747,44 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
|||||||
}
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build a trace request frame
|
||||||
|
//[cmd][tag x4][auth x4][flag][payload]
|
||||||
|
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendTracePath);
|
||||||
|
writer.writeUInt32LE(tag);
|
||||||
|
writer.writeUInt32LE(auth);
|
||||||
|
writer.writeByte(flag);
|
||||||
|
if (payload != null && payload.isNotEmpty) {
|
||||||
|
writer.writeBytes(payload);
|
||||||
|
}
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a export contact frame
|
||||||
|
// [cmd][pub_key x32 / if empty exports your contact info]
|
||||||
|
Uint8List buildExportContactFrame(Uint8List pubKey) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdExportContact);
|
||||||
|
writer.writeBytes(pubKey);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a import contact frame
|
||||||
|
// [cmd][contact_frame x98+]
|
||||||
|
Uint8List buildImportContactFrame(String contactFrame) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdImportContact);
|
||||||
|
writer.writeHex(contactFrame);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a export contact frame
|
||||||
|
// [cmd][pub_key x32]
|
||||||
|
Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdShareContact);
|
||||||
|
writer.writeBytes(pubKey);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ class CayenneLpp {
|
|||||||
static const int lppUnixTime = 133; // 4 bytes, unsigned
|
static const int lppUnixTime = 133; // 4 bytes, unsigned
|
||||||
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
|
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
|
||||||
static const int lppColour = 135; // 1 byte per RGB Color
|
static const int lppColour = 135; // 1 byte per RGB Color
|
||||||
static const int lppGps = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
static const int lppGps =
|
||||||
|
136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
||||||
static const int lppSwitch = 142; // 1 byte, 0/1
|
static const int lppSwitch = 142; // 1 byte, 0/1
|
||||||
static const int lppPolyline = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
static const int lppPolyline =
|
||||||
|
240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
||||||
|
|
||||||
final BufferWriter _writer = BufferWriter();
|
final BufferWriter _writer = BufferWriter();
|
||||||
|
|
||||||
@@ -201,10 +203,10 @@ class CayenneLpp {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final channelData = channels.putIfAbsent(channel, () => {
|
final channelData = channels.putIfAbsent(
|
||||||
'channel': channel,
|
channel,
|
||||||
'values': <String, dynamic>{},
|
() => {'channel': channel, 'values': <String, dynamic>{}},
|
||||||
});
|
);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case lppGenericSensor:
|
case lppGenericSensor:
|
||||||
@@ -254,8 +256,8 @@ class CayenneLpp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||||
return channelsOut;
|
return channelsOut;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ class LinkHandler {
|
|||||||
),
|
),
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
url,
|
url,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,53 +1,68 @@
|
|||||||
class ReactionInfo {
|
import '../widgets/emoji_picker.dart';
|
||||||
final String targetMessageId;
|
|
||||||
final String emoji;
|
|
||||||
final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix
|
|
||||||
|
|
||||||
ReactionInfo({
|
class ReactionInfo {
|
||||||
required this.targetMessageId,
|
final String targetHash;
|
||||||
required this.emoji,
|
final String emoji;
|
||||||
this.reactionKey,
|
|
||||||
});
|
ReactionInfo({required this.targetHash, required this.emoji});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReactionHelper {
|
class ReactionHelper {
|
||||||
/// Parse reaction format: r:[messageId]:[emoji]
|
static List<String>? _cachedEmojis;
|
||||||
/// Supports both old format (full messageId) and new format (timestamp_senderPrefix)
|
|
||||||
|
/// Combined list of all reaction emojis in fixed order.
|
||||||
|
/// Order must stay stable for index compatibility.
|
||||||
|
static List<String> get reactionEmojis {
|
||||||
|
return _cachedEmojis ??= [
|
||||||
|
...EmojiPicker.quickEmojis,
|
||||||
|
...EmojiPicker.smileys,
|
||||||
|
...EmojiPicker.gestures,
|
||||||
|
...EmojiPicker.hearts,
|
||||||
|
...EmojiPicker.objects,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert emoji to 2-char hex index. Returns null if emoji not in list.
|
||||||
|
static String? emojiToIndex(String emoji) {
|
||||||
|
final idx = reactionEmojis.indexOf(emoji);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
return idx.toRadixString(16).padLeft(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert 2-char hex index to emoji. Returns null if invalid index.
|
||||||
|
static String? indexToEmoji(String hexIndex) {
|
||||||
|
final idx = int.tryParse(hexIndex, radix: 16);
|
||||||
|
if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null;
|
||||||
|
return reactionEmojis[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a 4-char hex hash for a message reaction.
|
||||||
|
/// Hash input: timestampSeconds + [senderName] + first 5 chars of text
|
||||||
|
/// For 1:1 chats, senderName can be null (sender is implicit).
|
||||||
|
static String computeReactionHash(
|
||||||
|
int timestampSeconds,
|
||||||
|
String? senderName,
|
||||||
|
String text,
|
||||||
|
) {
|
||||||
|
final first5 = text.length >= 5 ? text.substring(0, 5) : text;
|
||||||
|
final input = senderName != null
|
||||||
|
? '$timestampSeconds$senderName$first5'
|
||||||
|
: '$timestampSeconds$first5';
|
||||||
|
// Use hashCode and take lower 16 bits, format as 4 hex chars
|
||||||
|
final hash = input.hashCode & 0xFFFF;
|
||||||
|
return hash.toRadixString(16).padLeft(4, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index)
|
||||||
|
/// Returns null if text is not a valid reaction format
|
||||||
static ReactionInfo? parseReaction(String text) {
|
static ReactionInfo? parseReaction(String text) {
|
||||||
final regex = RegExp(r'^r:([^:]+):(.+)$');
|
final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$');
|
||||||
final match = regex.firstMatch(text);
|
final match = regex.firstMatch(text);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
|
|
||||||
final targetId = match.group(1)!;
|
final emoji = indexToEmoji(match.group(2)!);
|
||||||
final emoji = match.group(2)!;
|
if (emoji == null) return null;
|
||||||
|
|
||||||
// Extract reaction key for deduplication
|
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||||
// If targetId is in new format (timestamp_senderPrefix), use it directly
|
|
||||||
// Otherwise, extract timestamp from old format (timestamp_nameHash_textHash)
|
|
||||||
String? reactionKey;
|
|
||||||
if (targetId.contains('_')) {
|
|
||||||
final parts = targetId.split('_');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
// New format: timestamp_senderPrefix, or old format with at least timestamp
|
|
||||||
reactionKey = '${parts[0]}_${parts[1]}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReactionInfo(
|
|
||||||
targetMessageId: targetId,
|
|
||||||
emoji: emoji,
|
|
||||||
reactionKey: reactionKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a lightweight reaction key for a message
|
|
||||||
/// Format: r:[timestamp]_[senderPrefix]:[emoji]
|
|
||||||
static String buildReactionText(String timestamp, String senderPrefix, String emoji) {
|
|
||||||
return 'r:${timestamp}_$senderPrefix:$emoji';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract sender prefix from public key hex (first 8 chars)
|
|
||||||
static String getSenderPrefix(String senderKeyHex) {
|
|
||||||
return senderKeyHex.substring(0, 8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -262,8 +262,9 @@ class Smaz {
|
|||||||
".com",
|
".com",
|
||||||
];
|
];
|
||||||
|
|
||||||
static final List<Uint8List> _rcbBytes =
|
static final List<Uint8List> _rcbBytes = _rcb
|
||||||
_rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false);
|
.map((s) => Uint8List.fromList(ascii.encode(s)))
|
||||||
|
.toList(growable: false);
|
||||||
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
|
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
|
||||||
return entry.length > maxLen ? entry.length : maxLen;
|
return entry.length > maxLen ? entry.length : maxLen;
|
||||||
});
|
});
|
||||||
@@ -358,24 +359,32 @@ class Smaz {
|
|||||||
final code = input[index];
|
final code = input[index];
|
||||||
if (code == _verbatimSingle) {
|
if (code == _verbatimSingle) {
|
||||||
if (index + 1 >= input.length) {
|
if (index + 1 >= input.length) {
|
||||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.');
|
throw const FormatException(
|
||||||
|
'Invalid SMAZ stream: truncated verbatim byte.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
out.addByte(input[index + 1]);
|
out.addByte(input[index + 1]);
|
||||||
index += 2;
|
index += 2;
|
||||||
} else if (code == _verbatimRun) {
|
} else if (code == _verbatimRun) {
|
||||||
if (index + 1 >= input.length) {
|
if (index + 1 >= input.length) {
|
||||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim length.');
|
throw const FormatException(
|
||||||
|
'Invalid SMAZ stream: truncated verbatim length.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final len = input[index + 1] + 1;
|
final len = input[index + 1] + 1;
|
||||||
final end = index + 2 + len;
|
final end = index + 2 + len;
|
||||||
if (end > input.length) {
|
if (end > input.length) {
|
||||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim run.');
|
throw const FormatException(
|
||||||
|
'Invalid SMAZ stream: truncated verbatim run.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
out.add(input.sublist(index + 2, end));
|
out.add(input.sublist(index + 2, end));
|
||||||
index = end;
|
index = end;
|
||||||
} else {
|
} else {
|
||||||
if (code >= _rcbBytes.length) {
|
if (code >= _rcbBytes.length) {
|
||||||
throw const FormatException('Invalid SMAZ stream: code out of range.');
|
throw const FormatException(
|
||||||
|
'Invalid SMAZ stream: code out of range.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
out.add(_rcbBytes[code]);
|
out.add(_rcbBytes[code]);
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
if (maxBytes <= 0) return oldValue;
|
if (maxBytes <= 0) return oldValue;
|
||||||
final bytes = utf8.encode(newValue.text);
|
final bytes = utf8.encode(newValue.text);
|
||||||
if (bytes.length <= maxBytes) return newValue;
|
if (bytes.length <= maxBytes) return newValue;
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Брой контакти",
|
"settings_infoContactsCount": "Брой контакти",
|
||||||
"settings_infoChannelCount": "Брой канали",
|
"settings_infoChannelCount": "Брой канали",
|
||||||
"settings_presets": "Предварителни настройки",
|
"settings_presets": "Предварителни настройки",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Честота (MHz)",
|
"settings_frequency": "Честота (MHz)",
|
||||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||||
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
|
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Мощност (dBm)",
|
"settings_txPower": "TX Мощност (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
|
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
|
||||||
"settings_longRange": "Дълъг обхват",
|
|
||||||
"settings_fastSpeed": "Бърза скорост",
|
|
||||||
"settings_error": "Грешка: {message}",
|
"settings_error": "Грешка: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerate": "Регенерация",
|
"community_regenerate": "Регенерация",
|
||||||
"community_updateSecret": "Актуализирай тайна",
|
"community_updateSecret": "Актуализирай тайна",
|
||||||
"community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
|
"community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
|
||||||
"community_secretUpdated": "Секретно обновено за \"{name}\""
|
"community_secretUpdated": "Секретно обновено за \"{name}\"",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Вие",
|
||||||
|
"pathTrace_notAvailable": "Пътека за проследяване не е достъпна.",
|
||||||
|
"contacts_pathTrace": "Пътен проследяване",
|
||||||
|
"pathTrace_refreshTooltip": "Обнови Path Trace.",
|
||||||
|
"pathTrace_failed": "Пътят за проследяване не успя.",
|
||||||
|
"contacts_repeaterPing": "Пингване на повторителя",
|
||||||
|
"contacts_repeaterPathTrace": "Трасировка до повторител",
|
||||||
|
"contacts_ping": "Пинг",
|
||||||
|
"contacts_chatTraceRoute": "Трасиране на път",
|
||||||
|
"contacts_roomPathTrace": "Трасиране на път до съ",
|
||||||
|
"contacts_roomPing": "Ping на сървъра на стаята",
|
||||||
|
"contacts_pathTraceTo": "Проследи маршрут към {name}",
|
||||||
|
"appSettings_languageUk": "Украински",
|
||||||
|
"contacts_clipboardEmpty": "Клипборда е празна.",
|
||||||
|
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
||||||
|
"appSettings_languageRu": "Руски",
|
||||||
|
"contacts_contactImported": "Контактът е импортиран.",
|
||||||
|
"contacts_zeroHopAdvert": "Реклама без скок",
|
||||||
|
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
||||||
|
"contacts_floodAdvert": "Потопна реклама",
|
||||||
|
"contacts_addContactFromClipboard": "Добави контакт от клипборда",
|
||||||
|
"contacts_copyAdvertToClipboard": "Копирай обявата в клипборда",
|
||||||
|
"contacts_ShareContact": "Копирай контакт в клипборда",
|
||||||
|
"contacts_ShareContactZeroHop": "Сподели контакт чрез обява",
|
||||||
|
"contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
|
||||||
|
"notification_activityTitle": "Активност на MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
|
||||||
|
"notification_newTypeDiscovered": "Открит нов {contactType}",
|
||||||
|
"notification_receivedNewMessage": "Получено ново съобщение",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
|
||||||
|
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.",
|
||||||
|
"settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX",
|
||||||
|
"settings_gpxExportContacts": "Експортирай спътници към GPX",
|
||||||
|
"settings_gpxExportSuccess": "Успешно изlexport на файл GPX.",
|
||||||
|
"settings_gpxExportNoContacts": "Няма контакти за изlexport.",
|
||||||
|
"settings_gpxExportChat": "Местоположения на спътници",
|
||||||
|
"settings_gpxExportError": "Възникна грешка при изнасяне.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята",
|
||||||
|
"settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС",
|
||||||
|
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
|
||||||
|
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
|
||||||
|
"map_pathTraceCancelled": "Отменен е следването на пътя.",
|
||||||
|
"pathTrace_clearTooltip": "Изчисти пътя",
|
||||||
|
"map_removeLast": "Премахни Последно",
|
||||||
|
"map_runTrace": "Изпълни Път на Следване",
|
||||||
|
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||||
|
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||||
|
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||||
|
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
||||||
|
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
||||||
|
"settings_clientRepeat": "Без електричество – повторение"
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-40
@@ -74,7 +74,7 @@
|
|||||||
"settings_title": "Einstellungen",
|
"settings_title": "Einstellungen",
|
||||||
"settings_deviceInfo": "Geräteinformationen",
|
"settings_deviceInfo": "Geräteinformationen",
|
||||||
"settings_appSettings": "App-Einstellungen",
|
"settings_appSettings": "App-Einstellungen",
|
||||||
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmungen",
|
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung",
|
||||||
"settings_nodeSettings": "Knoten-Einstellungen",
|
"settings_nodeSettings": "Knoten-Einstellungen",
|
||||||
"settings_nodeName": "Knotenname",
|
"settings_nodeName": "Knotenname",
|
||||||
"settings_nodeNameNotSet": "Nicht festgelegt",
|
"settings_nodeNameNotSet": "Nicht festgelegt",
|
||||||
@@ -96,14 +96,14 @@
|
|||||||
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
||||||
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
||||||
"settings_actions": "Aktionen",
|
"settings_actions": "Aktionen",
|
||||||
"settings_sendAdvertisement": "Sende eine Ankündigung",
|
"settings_sendAdvertisement": "Sende Ankündigung",
|
||||||
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
|
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
|
||||||
"settings_advertisementSent": "Ankündigung gesendet",
|
"settings_advertisementSent": "Ankündigung gesendet",
|
||||||
"settings_syncTime": "Zeitsynchronisierung",
|
"settings_syncTime": "Zeitsynchronisierung",
|
||||||
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
|
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
|
||||||
"settings_timeSynchronized": "Zeit synchronisiert",
|
"settings_timeSynchronized": "Zeit synchronisiert",
|
||||||
"settings_refreshContacts": "Kontakte aktualisieren",
|
"settings_refreshContacts": "Kontakte aktualisieren",
|
||||||
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
|
"settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden",
|
||||||
"settings_rebootDevice": "Gerät neu starten",
|
"settings_rebootDevice": "Gerät neu starten",
|
||||||
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
|
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
|
||||||
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
|
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
|
||||||
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Anzahl Kontakte",
|
"settings_infoContactsCount": "Anzahl Kontakte",
|
||||||
"settings_infoChannelCount": "Anzahl Kanäle",
|
"settings_infoChannelCount": "Anzahl Kanäle",
|
||||||
"settings_presets": "Voreinstellungen",
|
"settings_presets": "Voreinstellungen",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frequenz (MHz)",
|
"settings_frequency": "Frequenz (MHz)",
|
||||||
"settings_frequencyHelper": "300,00 - 2.500,00",
|
"settings_frequencyHelper": "300,00 - 2.500,00",
|
||||||
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
|
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX-Leistung (dBm)",
|
"settings_txPower": "TX-Leistung (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
|
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
|
||||||
"settings_longRange": "Grosse Reichweite",
|
|
||||||
"settings_fastSpeed": "Schnelle Geschwindigkeit",
|
|
||||||
"settings_error": "Fehler: {message}",
|
"settings_error": "Fehler: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -266,7 +261,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_manageRepeater": "Wiederholungen verwalten",
|
"contacts_manageRepeater": "Repeater verwalten",
|
||||||
"contacts_roomLogin": "Raum-Login",
|
"contacts_roomLogin": "Raum-Login",
|
||||||
"contacts_openChat": "Öffne Chat",
|
"contacts_openChat": "Öffne Chat",
|
||||||
"contacts_editGroup": "Gruppe bearbeiten",
|
"contacts_editGroup": "Gruppe bearbeiten",
|
||||||
@@ -360,7 +355,7 @@
|
|||||||
"channels_channelIndexLabel": "Kanalindex",
|
"channels_channelIndexLabel": "Kanalindex",
|
||||||
"channels_channelName": "Kanalname",
|
"channels_channelName": "Kanalname",
|
||||||
"channels_usePublicChannel": "Verwende öffentlichen Kanal",
|
"channels_usePublicChannel": "Verwende öffentlichen Kanal",
|
||||||
"channels_standardPublicPsk": "Standard-Öffentliche PSK",
|
"channels_standardPublicPsk": "Öffentliche Standard PSK",
|
||||||
"channels_pskHex": "PSK (Hex)",
|
"channels_pskHex": "PSK (Hex)",
|
||||||
"channels_generateRandomPsk": "Zufällige PSK generieren",
|
"channels_generateRandomPsk": "Zufällige PSK generieren",
|
||||||
"channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.",
|
"channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.",
|
||||||
@@ -489,8 +484,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debugFrame_textMessageHeader": "Textnachricht-Frame:",
|
"debugFrame_textMessageHeader": "Textnachrichten Frame:",
|
||||||
"debugFrame_destinationPubKey": "- Ziel-Pub-Schlüssel: {pubKey}",
|
"debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}",
|
||||||
"@debugFrame_destinationPubKey": {
|
"@debugFrame_destinationPubKey": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"pubKey": {
|
"pubKey": {
|
||||||
@@ -540,7 +535,7 @@
|
|||||||
"chat_routingMode": "Routenmodus",
|
"chat_routingMode": "Routenmodus",
|
||||||
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
|
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
|
||||||
"chat_forceFloodMode": "Flut-Modus erzwingen",
|
"chat_forceFloodMode": "Flut-Modus erzwingen",
|
||||||
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
|
"chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):",
|
||||||
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
|
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
|
||||||
"chat_hopSingular": "Sprung",
|
"chat_hopSingular": "Sprung",
|
||||||
"chat_hopPlural": "Sprünge",
|
"chat_hopPlural": "Sprünge",
|
||||||
@@ -554,7 +549,7 @@
|
|||||||
},
|
},
|
||||||
"chat_successes": "Erfolgreich",
|
"chat_successes": "Erfolgreich",
|
||||||
"chat_removePath": "Pfad entfernen",
|
"chat_removePath": "Pfad entfernen",
|
||||||
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
"chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
||||||
"chat_pathActions": "Pfadaktionen:",
|
"chat_pathActions": "Pfadaktionen:",
|
||||||
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
|
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
|
||||||
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
|
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
|
||||||
@@ -717,7 +712,7 @@
|
|||||||
"mapCache_cacheArea": "Zwischenspeicherbereich",
|
"mapCache_cacheArea": "Zwischenspeicherbereich",
|
||||||
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
|
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
|
||||||
"mapCache_zoomRange": "Zoom Bereich",
|
"mapCache_zoomRange": "Zoom Bereich",
|
||||||
"mapCache_estimatedTiles": "Geschätzte Fliesen: {count}",
|
"mapCache_estimatedTiles": "Geschätzte Kacheln: {count}",
|
||||||
"@mapCache_estimatedTiles": {
|
"@mapCache_estimatedTiles": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
@@ -854,7 +849,7 @@
|
|||||||
},
|
},
|
||||||
"path_enterCustomPath": "Gebe Pfad ein",
|
"path_enterCustomPath": "Gebe Pfad ein",
|
||||||
"path_currentPathLabel": "Aktueller Pfad",
|
"path_currentPathLabel": "Aktueller Pfad",
|
||||||
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
|
"path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.",
|
||||||
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
|
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
|
||||||
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
|
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
|
||||||
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
|
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
|
||||||
@@ -887,7 +882,7 @@
|
|||||||
"repeater_forceFloodMode": "Flut-Modus erzwingen",
|
"repeater_forceFloodMode": "Flut-Modus erzwingen",
|
||||||
"repeater_pathManagement": "Pfadverwaltung",
|
"repeater_pathManagement": "Pfadverwaltung",
|
||||||
"repeater_refresh": "Aktualisieren",
|
"repeater_refresh": "Aktualisieren",
|
||||||
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
|
"repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.",
|
||||||
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
|
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
|
||||||
"@repeater_errorLoadingStatus": {
|
"@repeater_errorLoadingStatus": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -957,7 +952,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}",
|
"repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}",
|
||||||
"@repeater_duplicatesFloodDirect": {
|
"@repeater_duplicatesFloodDirect": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"flood": {
|
"flood": {
|
||||||
@@ -983,7 +978,7 @@
|
|||||||
"repeater_adminPassword": "Admin-Passwort",
|
"repeater_adminPassword": "Admin-Passwort",
|
||||||
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
|
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
|
||||||
"repeater_guestPassword": "Gast-Passwort",
|
"repeater_guestPassword": "Gast-Passwort",
|
||||||
"repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort",
|
"repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort",
|
||||||
"repeater_radioSettings": "Funk Einstellungen",
|
"repeater_radioSettings": "Funk Einstellungen",
|
||||||
"repeater_frequencyMhz": "Frequenz (MHz)",
|
"repeater_frequencyMhz": "Frequenz (MHz)",
|
||||||
"repeater_frequencyHelper": "300-2500 MHz",
|
"repeater_frequencyHelper": "300-2500 MHz",
|
||||||
@@ -1026,7 +1021,7 @@
|
|||||||
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
|
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
|
||||||
"repeater_dangerZone": "Gefahrenzone",
|
"repeater_dangerZone": "Gefahrenzone",
|
||||||
"repeater_rebootRepeater": "Neustart Repeater",
|
"repeater_rebootRepeater": "Neustart Repeater",
|
||||||
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
|
"repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.",
|
||||||
"repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?",
|
"repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?",
|
||||||
"repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung",
|
"repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung",
|
||||||
"repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren",
|
"repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren",
|
||||||
@@ -1086,11 +1081,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_cliTitle": "Repeater CLI",
|
"repeater_cliTitle": "Repeater CLI",
|
||||||
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
|
"repeater_debugNextCommand": "Fehlersuche des nächsten Befehls",
|
||||||
"repeater_commandHelp": "Hilfe",
|
"repeater_commandHelp": "Hilfe",
|
||||||
"repeater_clearHistory": "Löschen der Historie",
|
"repeater_clearHistory": "Löschen der Historie",
|
||||||
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
|
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
|
||||||
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
|
"repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle",
|
||||||
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
|
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
|
||||||
"repeater_previousCommand": "Vorhergehende Aktion",
|
"repeater_previousCommand": "Vorhergehende Aktion",
|
||||||
"repeater_nextCommand": "Nächste Aktion",
|
"repeater_nextCommand": "Nächste Aktion",
|
||||||
@@ -1132,7 +1127,7 @@
|
|||||||
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung 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_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_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
|
||||||
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
"repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
||||||
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
|
"repeater_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_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_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
|
||||||
@@ -1143,14 +1138,14 @@
|
|||||||
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
|
"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_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
|
||||||
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
|
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
|
||||||
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.",
|
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.",
|
||||||
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
|
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
|
||||||
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
|
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
|
||||||
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
|
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
|
||||||
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
|
"repeater_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_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
|
||||||
"repeater_cliHelpRegion": "Listet alle definierten Regionen 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_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.",
|
||||||
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
|
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
|
||||||
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
|
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
|
||||||
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
|
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
|
||||||
@@ -1243,7 +1238,7 @@
|
|||||||
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
|
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
|
||||||
"channelPath_repeaterHops": "Repeater-Sprünge",
|
"channelPath_repeaterHops": "Repeater-Sprünge",
|
||||||
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
|
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
|
||||||
"channelPath_messageDetails": "Nachrichtsdetails",
|
"channelPath_messageDetails": "Nachrichtendetails",
|
||||||
"channelPath_senderLabel": "Sender",
|
"channelPath_senderLabel": "Sender",
|
||||||
"channelPath_timeLabel": "Zeit",
|
"channelPath_timeLabel": "Zeit",
|
||||||
"channelPath_repeatsLabel": "Wiederholungen",
|
"channelPath_repeatsLabel": "Wiederholungen",
|
||||||
@@ -1347,7 +1342,7 @@
|
|||||||
"listFilter_users": "Benutzer",
|
"listFilter_users": "Benutzer",
|
||||||
"listFilter_repeaters": "Repeater",
|
"listFilter_repeaters": "Repeater",
|
||||||
"listFilter_roomServers": "Raumserver",
|
"listFilter_roomServers": "Raumserver",
|
||||||
"listFilter_unreadOnly": "Nur nicht gelesen",
|
"listFilter_unreadOnly": "Nicht gelesen",
|
||||||
"listFilter_newGroup": "Neue Gruppe",
|
"listFilter_newGroup": "Neue Gruppe",
|
||||||
"@neighbors_errorLoading": {
|
"@neighbors_errorLoading": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1358,11 +1353,11 @@
|
|||||||
},
|
},
|
||||||
"repeater_neighbours": "Nachbarn",
|
"repeater_neighbours": "Nachbarn",
|
||||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||||
"neighbors_receivedData": "Empfangene Nachbarendaten",
|
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||||
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
|
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
|
"neighbors_repeatersNeighbours": "Nachbarn",
|
||||||
"neighbors_noData": "Keine Nachbardaten verfügbar.",
|
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||||
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
|
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
|
||||||
@@ -1389,8 +1384,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_heardAgo": "Hörte: {time} vor her.",
|
"neighbors_heardAgo": "Gehört vor: {time}",
|
||||||
"neighbors_unknownContact": "Unbekannte {pubkey}",
|
"neighbors_unknownContact": "Unbekannt {pubkey}",
|
||||||
"settings_locationGPSEnable": "GPS aktivieren",
|
"settings_locationGPSEnable": "GPS aktivieren",
|
||||||
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
|
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
|
||||||
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
|
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
|
||||||
@@ -1493,9 +1488,9 @@
|
|||||||
"community_deleted": "Community \"{name}\" verlassen",
|
"community_deleted": "Community \"{name}\" verlassen",
|
||||||
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
|
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
|
||||||
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
|
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
|
||||||
"community_selectCommunity": "Wählen Sie Community",
|
"community_selectCommunity": "Wählen Sie eine Community",
|
||||||
"community_regularHashtag": "Regulärer Hashtag",
|
"community_regularHashtag": "Regulärer Hashtag",
|
||||||
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
|
"community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)",
|
||||||
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
|
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
|
||||||
"community_forCommunity": "Für {name}",
|
"community_forCommunity": "Für {name}",
|
||||||
"community_communityHashtag": "Community Hashtag",
|
"community_communityHashtag": "Community Hashtag",
|
||||||
@@ -1528,10 +1523,106 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community_regenerate": "Neu generieren",
|
"community_regenerate": "Neu generieren",
|
||||||
"community_secretRegenerated": "Geheime Wiederherstellung für \"{name}\" erfolgreich",
|
"community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich",
|
||||||
"community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
|
"community_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_regenerateSecret": "Neugenerierung des Schlüssels",
|
||||||
"community_secretUpdated": "Geheime für \"{name}\" aktualisiert",
|
"community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert",
|
||||||
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
|
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
|
||||||
"community_updateSecret": "Aktualisieren Sie das Geheimnis"
|
"community_updateSecret": "Aktualisieren Sie den Schlüssel",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_refreshTooltip": "Path Trace aktualisieren.",
|
||||||
|
"pathTrace_you": "Du",
|
||||||
|
"pathTrace_failed": "Pfadverfolgung fehlgeschlagen.",
|
||||||
|
"pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.",
|
||||||
|
"contacts_pathTrace": "Pfadverfolgung",
|
||||||
|
"contacts_ping": "Pingen",
|
||||||
|
"contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater",
|
||||||
|
"contacts_repeaterPing": "Repeater pingen",
|
||||||
|
"contacts_roomPathTrace": "Pfadverfolgung zum Raumserver",
|
||||||
|
"contacts_roomPing": "Raumserver anpingen",
|
||||||
|
"contacts_pathTraceTo": "Route nach {name} verfolgen",
|
||||||
|
"contacts_chatTraceRoute": "Pfadverfolgungsroute",
|
||||||
|
"appSettings_languageRu": "Russisch",
|
||||||
|
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
||||||
|
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
||||||
|
"appSettings_languageUk": "Ukrainisch",
|
||||||
|
"contacts_contactImported": "Kontakt wurde importiert.",
|
||||||
|
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
||||||
|
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
||||||
|
"contacts_floodAdvert": "Flut-Ankündigung",
|
||||||
|
"contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen",
|
||||||
|
"contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen",
|
||||||
|
"contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren",
|
||||||
|
"contacts_ShareContact": "Kontakt in die Zwischenablage kopieren",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
|
||||||
|
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
|
||||||
|
"notification_activityTitle": "MeshCore Aktivität",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
|
||||||
|
"@notification_messagesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
|
||||||
|
"@notification_channelMessagesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
|
||||||
|
"@notification_newNodesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
|
||||||
|
"@notification_newTypeDiscovered": {
|
||||||
|
"placeholders": {
|
||||||
|
"contactType": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_receivedNewMessage": "Neue Nachricht empfangen",
|
||||||
|
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
|
||||||
|
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
|
||||||
|
"settings_gpxExportContacts": "Kontakte als GPX exportieren",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte",
|
||||||
|
"settings_gpxExportChat": "Kontaktstandorte",
|
||||||
|
"settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.",
|
||||||
|
"settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.",
|
||||||
|
"settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt",
|
||||||
|
"settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.",
|
||||||
|
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
|
||||||
|
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
|
||||||
|
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
|
||||||
|
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
|
||||||
|
"map_removeLast": "Letztes Entfernen",
|
||||||
|
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
|
||||||
|
"map_runTrace": "Pfadverlauf ausführen",
|
||||||
|
"pathTrace_clearTooltip": "Pfad löschen",
|
||||||
|
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||||
|
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||||
|
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||||
|
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
||||||
|
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
||||||
|
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen."
|
||||||
}
|
}
|
||||||
|
|||||||
+473
-157
File diff suppressed because it is too large
Load Diff
+97
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Número de contactos",
|
"settings_infoContactsCount": "Número de contactos",
|
||||||
"settings_infoChannelCount": "Número de canales",
|
"settings_infoChannelCount": "Número de canales",
|
||||||
"settings_presets": "Preajustes",
|
"settings_presets": "Preajustes",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frecuencia (MHz)",
|
"settings_frequency": "Frecuencia (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
|
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Potencia (dBm)",
|
"settings_txPower": "TX Potencia (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
|
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
|
||||||
"settings_longRange": "Largo Alcance",
|
|
||||||
"settings_fastSpeed": "Velocidad Rápida",
|
|
||||||
"settings_error": "Error: {message}",
|
"settings_error": "Error: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,101 @@
|
|||||||
"community_regenerate": "Regenerar",
|
"community_regenerate": "Regenerar",
|
||||||
"community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
|
"community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
|
||||||
"community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
|
"community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
|
||||||
"community_updateSecret": "Actualizar Contraseña"
|
"community_updateSecret": "Actualizar Contraseña",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Tú",
|
||||||
|
"pathTrace_failed": "El trazado de ruta falló.",
|
||||||
|
"pathTrace_refreshTooltip": "Actualizar Path Trace",
|
||||||
|
"contacts_pathTrace": "Rastreo de caminos",
|
||||||
|
"contacts_repeaterPathTrace": "Rastrear ruta al repetidor",
|
||||||
|
"contacts_repeaterPing": "Pingar repetidor",
|
||||||
|
"contacts_ping": "Ping",
|
||||||
|
"pathTrace_notAvailable": "El trazado de ruta no está disponible.",
|
||||||
|
"contacts_roomPing": "Pingar servidor de sala",
|
||||||
|
"contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación",
|
||||||
|
"contacts_pathTraceTo": "Rastrear ruta a {name}",
|
||||||
|
"contacts_chatTraceRoute": "Ruta de trazado",
|
||||||
|
"appSettings_languageUk": "Ucraniano",
|
||||||
|
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
||||||
|
"appSettings_languageRu": "Ruso",
|
||||||
|
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
||||||
|
"contacts_floodAdvert": "Anuncio de inundación",
|
||||||
|
"contacts_contactImported": "El contacto ha sido importado.",
|
||||||
|
"contacts_contactImportFailed": "Contacto no se importó correctamente.",
|
||||||
|
"contacts_zeroHopAdvert": "Anuncio de Zero Hop",
|
||||||
|
"contacts_ShareContactZeroHop": "Compartir contacto por anuncio",
|
||||||
|
"contacts_ShareContact": "Copiar contacto al Portapapeles",
|
||||||
|
"contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles",
|
||||||
|
"contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
|
||||||
|
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
|
||||||
|
"notification_activityTitle": "Actividad de MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
|
||||||
|
"@notification_messagesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
|
||||||
|
"@notification_channelMessagesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
|
||||||
|
"@notification_newNodesCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
|
||||||
|
"@notification_newTypeDiscovered": {
|
||||||
|
"placeholders": {
|
||||||
|
"contactType": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification_receivedNewMessage": "Nuevo mensaje recibido",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
|
||||||
|
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
|
||||||
|
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
|
||||||
|
"settings_gpxExportNoContacts": "No hay contactos para exportar.",
|
||||||
|
"settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO",
|
||||||
|
"settings_gpxExportError": "Hubo un error al exportar.",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.",
|
||||||
|
"settings_gpxExportAll": "Exportar todos los contactos a GPX",
|
||||||
|
"settings_gpxExportContacts": "Exportar compañeros a GPX",
|
||||||
|
"settings_gpxExportChat": "Ubicaciones de compañero",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala",
|
||||||
|
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
|
||||||
|
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
|
||||||
|
"pathTrace_clearTooltip": "Borrar ruta",
|
||||||
|
"map_runTrace": "Ejecutar Rastreo de Ruta",
|
||||||
|
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
|
||||||
|
"map_removeLast": "Eliminar último",
|
||||||
|
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||||
|
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||||
|
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||||
|
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
||||||
|
"settings_clientRepeat": "Repetir sin conexión",
|
||||||
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios."
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-38
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Nombre de contacts",
|
"settings_infoContactsCount": "Nombre de contacts",
|
||||||
"settings_infoChannelCount": "Nombre de canaux",
|
"settings_infoChannelCount": "Nombre de canaux",
|
||||||
"settings_presets": "Préréglages",
|
"settings_presets": "Préréglages",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Fréquence (MHz)",
|
"settings_frequency": "Fréquence (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Puissance (dBm)",
|
"settings_txPower": "TX Puissance (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||||
"settings_longRange": "Portée Longue",
|
|
||||||
"settings_fastSpeed": "Vitesse Rapide",
|
|
||||||
"settings_error": "Erreur : {message}",
|
"settings_error": "Erreur : {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -210,8 +205,8 @@
|
|||||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
|
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
|
||||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||||
"appSettings_mapDisplay": "Affichage de la carte",
|
"appSettings_mapDisplay": "Affichage de la carte",
|
||||||
"appSettings_showRepeaters": "Afficher les répétiteurs",
|
"appSettings_showRepeaters": "Afficher les répéteurs",
|
||||||
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte",
|
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte",
|
||||||
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
|
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
|
||||||
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
|
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
|
||||||
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
|
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
|
||||||
@@ -266,7 +261,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_manageRepeater": "Gérer le répétiteur",
|
"contacts_manageRepeater": "Gérer le répéteur",
|
||||||
"contacts_roomLogin": "Connexion Salle",
|
"contacts_roomLogin": "Connexion Salle",
|
||||||
"contacts_openChat": "Ouverture du Chat",
|
"contacts_openChat": "Ouverture du Chat",
|
||||||
"contacts_editGroup": "Modifier le groupe",
|
"contacts_editGroup": "Modifier le groupe",
|
||||||
@@ -542,9 +537,9 @@
|
|||||||
"chat_forceFloodMode": "Mode tout le réseau forcé",
|
"chat_forceFloodMode": "Mode tout le réseau forcé",
|
||||||
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
|
"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_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
|
||||||
"chat_hopSingular": "Sautez",
|
"chat_hopSingular": "saut",
|
||||||
"chat_hopPlural": "sautez",
|
"chat_hopPlural": "sauts",
|
||||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
|
||||||
"@chat_hopsCount": {
|
"@chat_hopsCount": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
@@ -636,7 +631,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map_chat": "Chat",
|
"map_chat": "Chat",
|
||||||
"map_repeater": "Répétiteur",
|
"map_repeater": "Répéteur",
|
||||||
"map_room": "Salle",
|
"map_room": "Salle",
|
||||||
"map_sensor": "Capteur",
|
"map_sensor": "Capteur",
|
||||||
"map_pinDm": "Clé (DM)",
|
"map_pinDm": "Clé (DM)",
|
||||||
@@ -677,7 +672,7 @@
|
|||||||
"map_lastSeenTime": "Dernière fois vu",
|
"map_lastSeenTime": "Dernière fois vu",
|
||||||
"map_sharedPin": "Clé partagée",
|
"map_sharedPin": "Clé partagée",
|
||||||
"map_joinRoom": "Rejoindre la salle",
|
"map_joinRoom": "Rejoindre la salle",
|
||||||
"map_manageRepeater": "Gérer le répétiteur",
|
"map_manageRepeater": "Gérer le répéteur",
|
||||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||||
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
|
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
|
||||||
@@ -800,13 +795,13 @@
|
|||||||
"time_allTime": "Tout le temps",
|
"time_allTime": "Tout le temps",
|
||||||
"dialog_disconnect": "Déconnecter",
|
"dialog_disconnect": "Déconnecter",
|
||||||
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
||||||
"login_repeaterLogin": "Connexion au répétiteur",
|
"login_repeaterLogin": "Connexion au répéteur",
|
||||||
"login_roomLogin": "Connexion Salle",
|
"login_roomLogin": "Connexion Salle",
|
||||||
"login_password": "Mot de passe",
|
"login_password": "Mot de passe",
|
||||||
"login_enterPassword": "Entrez votre mot de passe",
|
"login_enterPassword": "Entrez votre mot de passe",
|
||||||
"login_savePassword": "Sauvegarder le mot de passe",
|
"login_savePassword": "Sauvegarder le mot de passe",
|
||||||
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
|
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
|
||||||
"login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.",
|
"login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.",
|
||||||
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
|
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
|
||||||
"login_routing": "Redirection",
|
"login_routing": "Redirection",
|
||||||
"login_routingMode": "Mode de routage",
|
"login_routingMode": "Mode de routage",
|
||||||
@@ -871,17 +866,17 @@
|
|||||||
},
|
},
|
||||||
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
|
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
|
||||||
"path_setPath": "Définir le chemin",
|
"path_setPath": "Définir le chemin",
|
||||||
"repeater_management": "Gestion des répétiteurs",
|
"repeater_management": "Gestion des répéteurs",
|
||||||
"repeater_managementTools": "Outils de Gestion",
|
"repeater_managementTools": "Outils de Gestion",
|
||||||
"repeater_status": "État",
|
"repeater_status": "État",
|
||||||
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
|
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
|
||||||
"repeater_telemetry": "Télémetrie",
|
"repeater_telemetry": "Télémetrie",
|
||||||
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
|
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
|
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
|
||||||
"repeater_settings": "Paramètres",
|
"repeater_settings": "Paramètres",
|
||||||
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
|
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
|
||||||
"repeater_statusTitle": "État du répétiteur",
|
"repeater_statusTitle": "État du répéteur",
|
||||||
"repeater_routingMode": "Mode de routage",
|
"repeater_routingMode": "Mode de routage",
|
||||||
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
|
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
|
||||||
"repeater_forceFloodMode": "Mode tout le réseau forcé",
|
"repeater_forceFloodMode": "Mode tout le réseau forcé",
|
||||||
@@ -976,10 +971,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_settingsTitle": "Paramètres du répétiteur",
|
"repeater_settingsTitle": "Paramètres du répéteur",
|
||||||
"repeater_basicSettings": "Paramètres de base",
|
"repeater_basicSettings": "Paramètres de base",
|
||||||
"repeater_repeaterName": "Nom du répétiteur",
|
"repeater_repeaterName": "Nom du répéteur",
|
||||||
"repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur",
|
"repeater_repeaterNameHelper": "Afficher le nom de ce répéteur",
|
||||||
"repeater_adminPassword": "Mot de passe Administrateur",
|
"repeater_adminPassword": "Mot de passe Administrateur",
|
||||||
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
|
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
|
||||||
"repeater_guestPassword": "Mot de passe invité",
|
"repeater_guestPassword": "Mot de passe invité",
|
||||||
@@ -999,7 +994,7 @@
|
|||||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||||
"repeater_features": "Fonctionnalités",
|
"repeater_features": "Fonctionnalités",
|
||||||
"repeater_packetForwarding": "Transfert de paquets",
|
"repeater_packetForwarding": "Transfert de paquets",
|
||||||
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
|
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||||
"repeater_guestAccess": "Accès Invité",
|
"repeater_guestAccess": "Accès Invité",
|
||||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||||
"repeater_privacyMode": "Mode de confidentialité",
|
"repeater_privacyMode": "Mode de confidentialité",
|
||||||
@@ -1026,14 +1021,14 @@
|
|||||||
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
|
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
|
||||||
"repeater_dangerZone": "Zone dangereuse",
|
"repeater_dangerZone": "Zone dangereuse",
|
||||||
"repeater_rebootRepeater": "Redémarrer Répéteur",
|
"repeater_rebootRepeater": "Redémarrer Répéteur",
|
||||||
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
|
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur",
|
||||||
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
|
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?",
|
||||||
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
|
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
|
||||||
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
|
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
|
||||||
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?",
|
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?",
|
||||||
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
|
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
|
||||||
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur",
|
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur",
|
||||||
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !",
|
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !",
|
||||||
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
|
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
|
||||||
"repeater_commandSent": "Commande envoyée : {command}",
|
"repeater_commandSent": "Commande envoyée : {command}",
|
||||||
"@repeater_commandSent": {
|
"@repeater_commandSent": {
|
||||||
@@ -1085,7 +1080,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_cliTitle": "Répétiteur CLI",
|
"repeater_cliTitle": "Répéteur CLI",
|
||||||
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
|
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
|
||||||
"repeater_commandHelp": "Aide",
|
"repeater_commandHelp": "Aide",
|
||||||
"repeater_clearHistory": "Effacer l'historique",
|
"repeater_clearHistory": "Effacer l'historique",
|
||||||
@@ -1119,13 +1114,13 @@
|
|||||||
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
|
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
|
||||||
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
|
"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_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
|
||||||
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
|
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.",
|
||||||
"repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
|
"repeater_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_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_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.",
|
||||||
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
|
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
|
||||||
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
|
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
|
||||||
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
|
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.",
|
||||||
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
|
"repeater_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_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
|
||||||
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
|
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
|
||||||
@@ -1147,7 +1142,7 @@
|
|||||||
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
|
"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_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_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
|
||||||
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
||||||
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
|
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
|
||||||
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
|
"repeater_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_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
|
||||||
@@ -1171,8 +1166,8 @@
|
|||||||
"repeater_settingsCategory": "Paramètres",
|
"repeater_settingsCategory": "Paramètres",
|
||||||
"repeater_bridge": "Pont",
|
"repeater_bridge": "Pont",
|
||||||
"repeater_logging": "Journalisation",
|
"repeater_logging": "Journalisation",
|
||||||
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)",
|
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)",
|
||||||
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)",
|
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)",
|
||||||
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
|
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
|
||||||
"repeater_gpsManagement": "Gestion GPS",
|
"repeater_gpsManagement": "Gestion GPS",
|
||||||
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
|
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
|
||||||
@@ -1241,7 +1236,7 @@
|
|||||||
"channelPath_title": "Chemin de paquet",
|
"channelPath_title": "Chemin de paquet",
|
||||||
"channelPath_viewMap": "Afficher la carte",
|
"channelPath_viewMap": "Afficher la carte",
|
||||||
"channelPath_otherObservedPaths": "Autres chemins observés",
|
"channelPath_otherObservedPaths": "Autres chemins observés",
|
||||||
"channelPath_repeaterHops": "Sauts du répétiteur",
|
"channelPath_repeaterHops": "Sauts du répéteur",
|
||||||
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
|
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
|
||||||
"channelPath_messageDetails": "Détails du message",
|
"channelPath_messageDetails": "Détails du message",
|
||||||
"channelPath_senderLabel": "Expéditeur",
|
"channelPath_senderLabel": "Expéditeur",
|
||||||
@@ -1306,7 +1301,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channelPath_mapTitle": "Carte du chemin",
|
"channelPath_mapTitle": "Carte du chemin",
|
||||||
"channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.",
|
"channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.",
|
||||||
"channelPath_primaryPath": "Chemin {index} (Principal)",
|
"channelPath_primaryPath": "Chemin {index} (Principal)",
|
||||||
"@channelPath_primaryPath": {
|
"@channelPath_primaryPath": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
"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_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
|
||||||
"community_updateSecret": "Mettre à jour le secret",
|
"community_updateSecret": "Mettre à jour le secret",
|
||||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\""
|
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Vous",
|
||||||
|
"pathTrace_refreshTooltip": "Actualiser Path Trace",
|
||||||
|
"pathTrace_failed": "Traçage du chemin échoué.",
|
||||||
|
"pathTrace_notAvailable": "Tracé de chemin non disponible.",
|
||||||
|
"contacts_pathTrace": "Traçage de chemin",
|
||||||
|
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||||
|
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||||
|
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
||||||
|
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||||
|
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||||
|
"contacts_ping": "Ping",
|
||||||
|
"contacts_roomPing": "Pinguer le serveur de la salle",
|
||||||
|
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||||
|
"appSettings_languageUk": "Ukrainien",
|
||||||
|
"appSettings_languageRu": "Russe",
|
||||||
|
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
||||||
|
"contacts_contactImported": "Le contact a été importé.",
|
||||||
|
"contacts_floodAdvert": "Annonce à tout le réseau",
|
||||||
|
"contacts_contactImportFailed": "Échec de l'importation du contact.",
|
||||||
|
"contacts_zeroHopAdvert": "Annonce Zero saut",
|
||||||
|
"contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers",
|
||||||
|
"contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers",
|
||||||
|
"contacts_ShareContact": "Copier le contact dans le presse-papiers",
|
||||||
|
"contacts_ShareContactZeroHop": "Partager un contact par annonce",
|
||||||
|
"contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.",
|
||||||
|
"notification_activityTitle": "Activité MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||||
|
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||||
|
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||||
|
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||||
|
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||||
|
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
||||||
|
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
||||||
|
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
||||||
|
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.",
|
||||||
|
"settings_gpxExportChat": "Emplacements des compagnons",
|
||||||
|
"settings_gpxExportSuccess": "Fichier GPX exporté avec succès.",
|
||||||
|
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
|
||||||
|
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
|
||||||
|
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
|
||||||
|
"pathTrace_clearTooltip": "Effacer le chemin",
|
||||||
|
"map_pathTraceCancelled": "Traçage de chemin annulé",
|
||||||
|
"map_removeLast": "Supprimer le dernier",
|
||||||
|
"map_runTrace": "Exécuter la traçage de chemin",
|
||||||
|
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||||
|
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||||
|
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||||
|
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
||||||
|
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
||||||
|
"settings_clientRepeat": "Répétition hors réseau"
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Numero contatti",
|
"settings_infoContactsCount": "Numero contatti",
|
||||||
"settings_infoChannelCount": "Numero Canale",
|
"settings_infoChannelCount": "Numero Canale",
|
||||||
"settings_presets": "Preset",
|
"settings_presets": "Preset",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frequenza (MHz)",
|
"settings_frequency": "Frequenza (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
|
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Potenza (dBm)",
|
"settings_txPower": "TX Potenza (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
|
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
|
||||||
"settings_longRange": "Lungo Raggio",
|
|
||||||
"settings_fastSpeed": "Velocità Rapida",
|
|
||||||
"settings_error": "Errore: {message}",
|
"settings_error": "Errore: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
|
"community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
|
||||||
"community_updateSecret": "Aggiorna Segreto",
|
"community_updateSecret": "Aggiorna Segreto",
|
||||||
"community_secretUpdated": "Segreto aggiornato per \"{name}\"",
|
"community_secretUpdated": "Segreto aggiornato per \"{name}\"",
|
||||||
"community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\""
|
"community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_failed": "Tracciamento del percorso fallito.",
|
||||||
|
"pathTrace_you": "Tu",
|
||||||
|
"pathTrace_notAvailable": "Tracciamento del percorso non disponibile.",
|
||||||
|
"pathTrace_refreshTooltip": "Aggiorna Path Trace.",
|
||||||
|
"contacts_ping": "Ping",
|
||||||
|
"contacts_repeaterPathTrace": "Traccia percorso al ripetitore",
|
||||||
|
"contacts_roomPathTrace": "Traccia del percorso al server della stanza",
|
||||||
|
"contacts_pathTrace": "Traccia Percorso",
|
||||||
|
"contacts_repeaterPing": "Ripetitore ping",
|
||||||
|
"contacts_pathTraceTo": "Traccia percorso verso {name}",
|
||||||
|
"contacts_roomPing": "Ping al server della stanza",
|
||||||
|
"contacts_chatTraceRoute": "Traccia percorso path",
|
||||||
|
"appSettings_languageRu": "Russo",
|
||||||
|
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
||||||
|
"appSettings_languageUk": "Ucraino",
|
||||||
|
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
||||||
|
"contacts_floodAdvert": "Annuncio alluvionale",
|
||||||
|
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||||
|
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
|
||||||
|
"contacts_clipboardEmpty": "La clipboard è vuota.",
|
||||||
|
"contacts_ShareContact": "Copia contatto negli Appunti",
|
||||||
|
"contacts_contactImported": "Il contatto è stato importato.",
|
||||||
|
"contacts_contactImportFailed": "Contatto non importato con successo.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
|
||||||
|
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
|
||||||
|
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
|
||||||
|
"notification_activityTitle": "Attività MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
|
||||||
|
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
|
||||||
|
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
|
||||||
|
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
|
||||||
|
"settings_gpxExportContacts": "Esporta compagni in GPX",
|
||||||
|
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
|
||||||
|
"settings_gpxExportNoContacts": "Nessun contatto da esportare.",
|
||||||
|
"settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo",
|
||||||
|
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
|
||||||
|
"settings_gpxExportAll": "Esporta tutti i contatti in GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Esporta tutti i contatti con una posizione in un file GPX.",
|
||||||
|
"settings_gpxExportChat": "Posizioni dei compagni",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Posizioni del server ripetitore e della stanza",
|
||||||
|
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
|
||||||
|
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
|
||||||
|
"map_removeLast": "Rimuovi ultimo",
|
||||||
|
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
|
||||||
|
"pathTrace_clearTooltip": "Pulisci percorso",
|
||||||
|
"map_runTrace": "Esegui Path Trace",
|
||||||
|
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||||
|
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||||
|
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||||
|
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||||
|
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||||
|
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||||
|
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri."
|
||||||
}
|
}
|
||||||
|
|||||||
+360
-24
@@ -376,6 +376,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Scan'**
|
/// **'Scan'**
|
||||||
String get scanner_scan;
|
String get scanner_scan;
|
||||||
|
|
||||||
|
/// No description provided for @scanner_bluetoothOff.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Bluetooth is off'**
|
||||||
|
String get scanner_bluetoothOff;
|
||||||
|
|
||||||
|
/// No description provided for @scanner_bluetoothOffMessage.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Please turn on Bluetooth to scan for devices'**
|
||||||
|
String get scanner_bluetoothOffMessage;
|
||||||
|
|
||||||
|
/// No description provided for @scanner_enableBluetooth.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enable Bluetooth'**
|
||||||
|
String get scanner_enableBluetooth;
|
||||||
|
|
||||||
/// No description provided for @device_quickSwitch.
|
/// No description provided for @device_quickSwitch.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -730,24 +748,6 @@ abstract class AppLocalizations {
|
|||||||
/// **'Presets'**
|
/// **'Presets'**
|
||||||
String get settings_presets;
|
String get settings_presets;
|
||||||
|
|
||||||
/// No description provided for @settings_preset915Mhz.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'915 MHz'**
|
|
||||||
String get settings_preset915Mhz;
|
|
||||||
|
|
||||||
/// No description provided for @settings_preset868Mhz.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'868 MHz'**
|
|
||||||
String get settings_preset868Mhz;
|
|
||||||
|
|
||||||
/// No description provided for @settings_preset433Mhz.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'433 MHz'**
|
|
||||||
String get settings_preset433Mhz;
|
|
||||||
|
|
||||||
/// No description provided for @settings_frequency.
|
/// No description provided for @settings_frequency.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -802,17 +802,23 @@ abstract class AppLocalizations {
|
|||||||
/// **'Invalid TX power (0-22 dBm)'**
|
/// **'Invalid TX power (0-22 dBm)'**
|
||||||
String get settings_txPowerInvalid;
|
String get settings_txPowerInvalid;
|
||||||
|
|
||||||
/// No description provided for @settings_longRange.
|
/// No description provided for @settings_clientRepeat.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Long Range'**
|
/// **'Off-Grid Repeat'**
|
||||||
String get settings_longRange;
|
String get settings_clientRepeat;
|
||||||
|
|
||||||
/// No description provided for @settings_fastSpeed.
|
/// No description provided for @settings_clientRepeatSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Fast Speed'**
|
/// **'Allow this device to repeat mesh packets for others'**
|
||||||
String get settings_fastSpeed;
|
String get settings_clientRepeatSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_clientRepeatFreqWarning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'**
|
||||||
|
String get settings_clientRepeatFreqWarning;
|
||||||
|
|
||||||
/// No description provided for @settings_error.
|
/// No description provided for @settings_error.
|
||||||
///
|
///
|
||||||
@@ -946,6 +952,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Български'**
|
/// **'Български'**
|
||||||
String get appSettings_languageBg;
|
String get appSettings_languageBg;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_languageRu.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Русский'**
|
||||||
|
String get appSettings_languageRu;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_languageUk.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Українська'**
|
||||||
|
String get appSettings_languageUk;
|
||||||
|
|
||||||
/// No description provided for @appSettings_notifications.
|
/// No description provided for @appSettings_notifications.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2506,6 +2524,30 @@ abstract class AppLocalizations {
|
|||||||
/// **'Manage Repeater'**
|
/// **'Manage Repeater'**
|
||||||
String get map_manageRepeater;
|
String get map_manageRepeater;
|
||||||
|
|
||||||
|
/// No description provided for @map_tapToAdd.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tap on nodes to add them to the path.'**
|
||||||
|
String get map_tapToAdd;
|
||||||
|
|
||||||
|
/// No description provided for @map_runTrace.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run Path Trace'**
|
||||||
|
String get map_runTrace;
|
||||||
|
|
||||||
|
/// No description provided for @map_removeLast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove Last'**
|
||||||
|
String get map_removeLast;
|
||||||
|
|
||||||
|
/// No description provided for @map_pathTraceCancelled.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace cancelled.'**
|
||||||
|
String get map_pathTraceCancelled;
|
||||||
|
|
||||||
/// No description provided for @mapCache_title.
|
/// No description provided for @mapCache_title.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -4687,6 +4729,300 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'New group'**
|
/// **'New group'**
|
||||||
String get listFilter_newGroup;
|
String get listFilter_newGroup;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_you.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'You'**
|
||||||
|
String get pathTrace_you;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_failed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace failed.'**
|
||||||
|
String get pathTrace_failed;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_notAvailable.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace not available.'**
|
||||||
|
String get pathTrace_notAvailable;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_refreshTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh Path Trace.'**
|
||||||
|
String get pathTrace_refreshTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_someHopsNoLocation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'One or more of the hops is missing a location!'**
|
||||||
|
String get pathTrace_someHopsNoLocation;
|
||||||
|
|
||||||
|
/// No description provided for @pathTrace_clearTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear path.'**
|
||||||
|
String get pathTrace_clearTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_pathTrace.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path Trace'**
|
||||||
|
String get contacts_pathTrace;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_ping.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ping'**
|
||||||
|
String get contacts_ping;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_repeaterPathTrace.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace to repeater'**
|
||||||
|
String get contacts_repeaterPathTrace;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_repeaterPing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ping repeater'**
|
||||||
|
String get contacts_repeaterPing;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_roomPathTrace.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace to room server'**
|
||||||
|
String get contacts_roomPathTrace;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_roomPing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ping room server'**
|
||||||
|
String get contacts_roomPing;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_chatTraceRoute.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path trace route'**
|
||||||
|
String get contacts_chatTraceRoute;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_pathTraceTo.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Trace route to {name}'**
|
||||||
|
String contacts_pathTraceTo(String name);
|
||||||
|
|
||||||
|
/// No description provided for @contacts_clipboardEmpty.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clipboard is empty.'**
|
||||||
|
String get contacts_clipboardEmpty;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_invalidAdvertFormat.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Invalid contact data'**
|
||||||
|
String get contacts_invalidAdvertFormat;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_contactImported.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact has been imported.'**
|
||||||
|
String get contacts_contactImported;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_contactImportFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to import contact.'**
|
||||||
|
String get contacts_contactImportFailed;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_zeroHopAdvert.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Zero Hop Advert'**
|
||||||
|
String get contacts_zeroHopAdvert;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_floodAdvert.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flood Advert'**
|
||||||
|
String get contacts_floodAdvert;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_copyAdvertToClipboard.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copy Advert to Clipboard'**
|
||||||
|
String get contacts_copyAdvertToClipboard;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_addContactFromClipboard.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add Contact from Clipboard'**
|
||||||
|
String get contacts_addContactFromClipboard;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_ShareContact.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copy contact to Clipboard'**
|
||||||
|
String get contacts_ShareContact;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_ShareContactZeroHop.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Share contact by advert'**
|
||||||
|
String get contacts_ShareContactZeroHop;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_zeroHopContactAdvertSent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sent contact by advert.'**
|
||||||
|
String get contacts_zeroHopContactAdvertSent;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_zeroHopContactAdvertFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to send contact.'**
|
||||||
|
String get contacts_zeroHopContactAdvertFailed;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_contactAdvertCopied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Advert copied to Clipboard.'**
|
||||||
|
String get contacts_contactAdvertCopied;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_contactAdvertCopyFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copying advert to Clipboard failed.'**
|
||||||
|
String get contacts_contactAdvertCopyFailed;
|
||||||
|
|
||||||
|
/// No description provided for @notification_activityTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'MeshCore Activity'**
|
||||||
|
String get notification_activityTitle;
|
||||||
|
|
||||||
|
/// No description provided for @notification_messagesCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count} {count, plural, =1{message} other{messages}}'**
|
||||||
|
String notification_messagesCount(int count);
|
||||||
|
|
||||||
|
/// No description provided for @notification_channelMessagesCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count} {count, plural, =1{channel message} other{channel messages}}'**
|
||||||
|
String notification_channelMessagesCount(int count);
|
||||||
|
|
||||||
|
/// No description provided for @notification_newNodesCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count} {count, plural, =1{new node} other{new nodes}}'**
|
||||||
|
String notification_newNodesCount(int count);
|
||||||
|
|
||||||
|
/// No description provided for @notification_newTypeDiscovered.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New {contactType} discovered'**
|
||||||
|
String notification_newTypeDiscovered(String contactType);
|
||||||
|
|
||||||
|
/// No description provided for @notification_receivedNewMessage.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Received new message'**
|
||||||
|
String get notification_receivedNewMessage;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportRepeaters.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Export repeaters / room server to GPX'**
|
||||||
|
String get settings_gpxExportRepeaters;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportRepeatersSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Exports repeaters / roomserver with a location to GPX file.'**
|
||||||
|
String get settings_gpxExportRepeatersSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportContacts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Export companions to GPX'**
|
||||||
|
String get settings_gpxExportContacts;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportContactsSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Exports companions with a location to GPX file.'**
|
||||||
|
String get settings_gpxExportContactsSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Export all contacts to GPX'**
|
||||||
|
String get settings_gpxExportAll;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportAllSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Exports all contacts with a location to GPX file.'**
|
||||||
|
String get settings_gpxExportAllSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportSuccess.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Successfully exported GPX file.'**
|
||||||
|
String get settings_gpxExportSuccess;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportNoContacts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No contacts to export.'**
|
||||||
|
String get settings_gpxExportNoContacts;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportNotAvailable.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Not supported on your device/OS'**
|
||||||
|
String get settings_gpxExportNotAvailable;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'There was an error when exporting.'**
|
||||||
|
String get settings_gpxExportError;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportRepeatersRoom.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Repeater & room server locations'**
|
||||||
|
String get settings_gpxExportRepeatersRoom;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportChat.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Companion locations'**
|
||||||
|
String get settings_gpxExportChat;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportAllContacts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'All contacts locations'**
|
||||||
|
String get settings_gpxExportAllContacts;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportShareText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Map data exported from meshcore-open'**
|
||||||
|
String get settings_gpxExportShareText;
|
||||||
|
|
||||||
|
/// No description provided for @settings_gpxExportShareSubject.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'meshcore-open GPX map data export'**
|
||||||
|
String get settings_gpxExportShareSubject;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Сканирай';
|
String get scanner_scan => 'Сканирай';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Бързо превключване';
|
String get device_quickSwitch => 'Бързо превключване';
|
||||||
|
|
||||||
@@ -340,15 +350,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Предварителни настройки';
|
String get settings_presets => 'Предварителни настройки';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Честота (MHz)';
|
String get settings_frequency => 'Честота (MHz)';
|
||||||
|
|
||||||
@@ -377,10 +378,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Дълъг обхват';
|
String get settings_clientRepeat => 'Без електричество – повторение';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Бърза скорост';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -450,6 +456,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Руски';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Украински';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -1357,6 +1369,19 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
|
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd =>
|
||||||
|
'Натиснете върху възлите, за да ги добавите към пътя.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Изпълни Път на Следване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Премахни Последно';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Кеш на офлайн карти';
|
String get mapCache_title => 'Кеш на офлайн карти';
|
||||||
|
|
||||||
@@ -2676,4 +2701,193 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Нова група';
|
String get listFilter_newGroup => 'Нова група';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Вие';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Пътят за проследяване не успя.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Обнови Path Trace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Един или повече от хмелите липсва местоположение!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Пътен проследяване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Пинг';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Трасировка до повторител';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Пингване на повторителя';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Трасиране на път до съ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping на сървъра на стаята';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Трасиране на път';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Проследи маршрут към $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Клипборда е празна.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Контактът е импортиран.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Контактът не е успешно импортиран.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Реклама без скок';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Потопна реклама';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard => 'Добави контакт от клипборда';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Копирай контакт в клипборда';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Неуспешно изпращане на контакт.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Рекламата е копирана в клипборда.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Копирането на обявата в клипборда не успя.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Активност на MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'съобщения',
|
||||||
|
one: 'съобщение',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'съобщения в канали',
|
||||||
|
one: 'съобщение в канал',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'нови възли',
|
||||||
|
one: 'нов възел',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Открит нов $contactType';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Получено ново съобщение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Експортиране на повтарящи се устройства / сървър на стаята до GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Изпраща повторители / roomserver с местоположение в GPX файл.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Експортирай спътници към GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Експортира спътници с местоположение в GPX файл.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Експортирай всички контакти в GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Експортира всички контакти с местоположение в файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Не е поддържан на вашето устройство/ОС';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Възникна грешка при изнасяне.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Местоположения на повторител и сървър на стаята';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Местоположения на спътници';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts =>
|
||||||
|
'Местоположения на всички контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Картинни данни изнесени от meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open износ на данни за карта в формат GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Scannen';
|
String get scanner_scan => 'Scannen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Schnelles Umschalten';
|
String get device_quickSwitch => 'Schnelles Umschalten';
|
||||||
|
|
||||||
@@ -160,7 +170,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_appSettingsSubtitle =>
|
String get settings_appSettingsSubtitle =>
|
||||||
'Benachrichtigungen, Messaging und Kartenwahrnehmungen';
|
'Benachrichtigungen, Messaging und Kartenwahrnehmung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_nodeSettings => 'Knoten-Einstellungen';
|
String get settings_nodeSettings => 'Knoten-Einstellungen';
|
||||||
@@ -244,10 +254,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get settings_actions => 'Aktionen';
|
String get settings_actions => 'Aktionen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_sendAdvertisement => 'Sende eine Ankündigung';
|
String get settings_sendAdvertisement => 'Sende Ankündigung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung';
|
String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_advertisementSent => 'Ankündigung gesendet';
|
String get settings_advertisementSent => 'Ankündigung gesendet';
|
||||||
@@ -267,7 +277,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_refreshContactsSubtitle =>
|
String get settings_refreshContactsSubtitle =>
|
||||||
'Kontakte-Liste vom Gerät neu laden';
|
'Kontakt-Liste vom Gerät neu laden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_rebootDevice => 'Gerät neu starten';
|
String get settings_rebootDevice => 'Gerät neu starten';
|
||||||
@@ -334,15 +344,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Voreinstellungen';
|
String get settings_presets => 'Voreinstellungen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frequenz (MHz)';
|
String get settings_frequency => 'Frequenz (MHz)';
|
||||||
|
|
||||||
@@ -371,10 +372,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Grosse Reichweite';
|
String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Schnelle Geschwindigkeit';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -444,6 +450,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Russisch';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ukrainisch';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Benachrichtigungen';
|
String get appSettings_notifications => 'Benachrichtigungen';
|
||||||
|
|
||||||
@@ -662,7 +674,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_manageRepeater => 'Wiederholungen verwalten';
|
String get contacts_manageRepeater => 'Repeater verwalten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_manageRoom => 'Raum-Server verwalten';
|
String get contacts_manageRoom => 'Raum-Server verwalten';
|
||||||
@@ -796,7 +808,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get channels_usePublicChannel => 'Verwende öffentlichen Kanal';
|
String get channels_usePublicChannel => 'Verwende öffentlichen Kanal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_standardPublicPsk => 'Standard-Öffentliche PSK';
|
String get channels_standardPublicPsk => 'Öffentliche Standard PSK';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_pskHex => 'PSK (Hex)';
|
String get channels_pskHex => 'PSK (Hex)';
|
||||||
@@ -1029,11 +1041,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get debugFrame_textMessageHeader => 'Textnachricht-Frame:';
|
String get debugFrame_textMessageHeader => 'Textnachrichten Frame:';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String debugFrame_destinationPubKey(String pubKey) {
|
String debugFrame_destinationPubKey(String pubKey) {
|
||||||
return '- Ziel-Pub-Schlüssel: $pubKey';
|
return '- Ziel-Public-Schlüssel: $pubKey';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1080,7 +1092,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_recentAckPaths =>
|
String get chat_recentAckPaths =>
|
||||||
'Aktuelle ACK-Pfade (tasten, um zu verwenden):';
|
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_pathHistoryFull =>
|
String get chat_pathHistoryFull =>
|
||||||
@@ -1111,7 +1123,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_noPathHistoryYet =>
|
String get chat_noPathHistoryYet =>
|
||||||
'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
|
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_pathActions => 'Pfadaktionen:';
|
String get chat_pathActions => 'Pfadaktionen:';
|
||||||
@@ -1356,6 +1368,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Repeater verwalten';
|
String get map_manageRepeater => 'Repeater verwalten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd =>
|
||||||
|
'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Pfadverlauf ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Letztes Entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Offline-Karten-Cache';
|
String get mapCache_title => 'Offline-Karten-Cache';
|
||||||
|
|
||||||
@@ -1412,7 +1437,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String mapCache_estimatedTiles(int count) {
|
String mapCache_estimatedTiles(int count) {
|
||||||
return 'Geschätzte Fliesen: $count';
|
return 'Geschätzte Kacheln: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1586,7 +1611,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get path_hexPrefixInstructions =>
|
String get path_hexPrefixInstructions =>
|
||||||
'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.';
|
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get path_hexPrefixExample =>
|
String get path_hexPrefixExample =>
|
||||||
@@ -1683,7 +1708,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_statusRequestTimeout =>
|
String get repeater_statusRequestTimeout =>
|
||||||
'Statusanfrage zeitweise fehlgeschlagen.';
|
'Statusanfrage durch Timeout fehlgeschlagen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String repeater_errorLoadingStatus(String error) {
|
String repeater_errorLoadingStatus(String error) {
|
||||||
@@ -1760,7 +1785,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String repeater_duplicatesFloodDirect(String flood, String direct) {
|
String repeater_duplicatesFloodDirect(String flood, String direct) {
|
||||||
return 'Überflut: $flood, Direkt: $direct';
|
return 'Flut: $flood, Direkt: $direct';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1791,7 +1816,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_guestPasswordHelper =>
|
String get repeater_guestPasswordHelper =>
|
||||||
'Schreibgeschützter Zugriffspasswort';
|
'Schreibgeschütztes Zugriffspasswort';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_radioSettings => 'Funk Einstellungen';
|
String get repeater_radioSettings => 'Funk Einstellungen';
|
||||||
@@ -1888,8 +1913,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get repeater_rebootRepeater => 'Neustart Repeater';
|
String get repeater_rebootRepeater => 'Neustart Repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_rebootRepeaterSubtitle =>
|
String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.';
|
||||||
'Wiederholen Sie das Repeater-Gerät.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_rebootRepeaterConfirm =>
|
String get repeater_rebootRepeaterConfirm =>
|
||||||
@@ -1987,7 +2011,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get repeater_cliTitle => 'Repeater CLI';
|
String get repeater_cliTitle => 'Repeater CLI';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl';
|
String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_commandHelp => 'Hilfe';
|
String get repeater_commandHelp => 'Hilfe';
|
||||||
@@ -2000,7 +2024,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_typeCommandOrUseQuick =>
|
String get repeater_typeCommandOrUseQuick =>
|
||||||
'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle';
|
'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...';
|
String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...';
|
||||||
@@ -2126,7 +2150,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetRxDelay =>
|
String get repeater_cliHelpSetRxDelay =>
|
||||||
'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
|
'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetTxDelay =>
|
String get repeater_cliHelpSetTxDelay =>
|
||||||
@@ -2170,7 +2194,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpGetBridgeType =>
|
String get repeater_cliHelpGetBridgeType =>
|
||||||
'Ruft Brückentyp none, rs232, espnow ab.';
|
'Ruft Brückentyp: none, rs232, espnow ab.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpLogStart =>
|
String get repeater_cliHelpLogStart =>
|
||||||
@@ -2197,7 +2221,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpRegionLoad =>
|
String get repeater_cliHelpRegionLoad =>
|
||||||
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.';
|
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpRegionGet =>
|
String get repeater_cliHelpRegionGet =>
|
||||||
@@ -2346,10 +2370,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_receivedData => 'Empfangene Nachbarendaten';
|
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.';
|
String get neighbors_requestTimedOut =>
|
||||||
|
'Anfrage durch Timeout fehlgeschlagen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_errorLoading(String error) {
|
String neighbors_errorLoading(String error) {
|
||||||
@@ -2357,19 +2382,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn';
|
String get neighbors_repeatersNeighbours => 'Nachbarn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
|
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_unknownContact(String pubkey) {
|
String neighbors_unknownContact(String pubkey) {
|
||||||
return 'Unbekannte $pubkey';
|
return 'Unbekannt $pubkey';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_heardAgo(String time) {
|
String neighbors_heardAgo(String time) {
|
||||||
return 'Hörte: $time vor her.';
|
return 'Gehört vor: $time';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2389,7 +2414,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
'Die Detailangaben für dieses Paket sind nicht verfügbar.';
|
'Die Detailangaben für dieses Paket sind nicht verfügbar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelPath_messageDetails => 'Nachrichtsdetails';
|
String get channelPath_messageDetails => 'Nachrichtendetails';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelPath_senderLabel => 'Sender';
|
String get channelPath_senderLabel => 'Sender';
|
||||||
@@ -2588,7 +2613,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_regenerateSecret => 'Neu generieren Sie das Geheimnis';
|
String get community_regenerateSecret => 'Neugenerierung des Schlüssels';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String community_regenerateSecretConfirm(String name) {
|
String community_regenerateSecretConfirm(String name) {
|
||||||
@@ -2600,15 +2625,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String community_secretRegenerated(String name) {
|
String community_secretRegenerated(String name) {
|
||||||
return 'Geheime Wiederherstellung für \"$name\" erfolgreich';
|
return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_updateSecret => 'Aktualisieren Sie das Geheimnis';
|
String get community_updateSecret => 'Aktualisieren Sie den Schlüssel';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String community_secretUpdated(String name) {
|
String community_secretUpdated(String name) {
|
||||||
return 'Geheime für \"$name\" aktualisiert';
|
return 'Schlüssel für \"$name\" aktualisiert';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2625,14 +2650,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
'Füge einen Hashtag-Kanal für diese Community hinzu';
|
'Füge einen Hashtag-Kanal für diese Community hinzu';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_selectCommunity => 'Wählen Sie Community';
|
String get community_selectCommunity => 'Wählen Sie eine Community';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_regularHashtag => 'Regulärer Hashtag';
|
String get community_regularHashtag => 'Regulärer Hashtag';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_regularHashtagDesc =>
|
String get community_regularHashtagDesc =>
|
||||||
'Öffentliches Hashtag (jeder kann teilnehmen)';
|
'Öffentlicher Hashtag (jeder kann teilnehmen)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get community_communityHashtag => 'Community Hashtag';
|
String get community_communityHashtag => 'Community Hashtag';
|
||||||
@@ -2677,8 +2702,200 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get listFilter_roomServers => 'Raumserver';
|
String get listFilter_roomServers => 'Raumserver';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_unreadOnly => 'Nur nicht gelesen';
|
String get listFilter_unreadOnly => 'Nicht gelesen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Neue Gruppe';
|
String get listFilter_newGroup => 'Neue Gruppe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Du';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Bei einer oder mehreren Knoten fehlt der Standort!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Pfad löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Pfadverfolgung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Pingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Pfadverfolgung zum Repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Repeater pingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Pfadverfolgung zum Raumserver';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Raumserver anpingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Pfadverfolgungsroute';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Route nach $name verfolgen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Kontakt wurde importiert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Kontakt konnte nicht importiert werden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Flut-Ankündigung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard =>
|
||||||
|
'Ankündigung in die Zwischenablage kopieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Kontakt aus Zwischenablage hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Kontakt über Anzeige gesendet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Kontakt konnte nicht gesendet werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Anzeige in die Zwischenablage kopiert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'MeshCore Aktivität';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Nachrichten',
|
||||||
|
one: 'Nachricht',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Kanalnachrichten',
|
||||||
|
one: 'Kanalnachricht',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'neue Knoten',
|
||||||
|
one: 'neuer Knoten',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Neuer $contactType entdeckt';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Neue Nachricht empfangen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Repeater und Raumserver als GPX exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Kontakte als GPX exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exportiert Kontakte mit einem Ort in eine GPX-Datei.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Nicht auf Ihrem Gerät/Betriebssystem unterstützt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError =>
|
||||||
|
'Beim Export ist ein Fehler aufgetreten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Repeater- und Raumserver-Standorte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Kontaktstandorte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'GPX-Kartendaten aus meshcore-open exportiert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'GPX-Kartendaten aus meshcore-open exportieren';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Scan';
|
String get scanner_scan => 'Scan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth is off';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Please turn on Bluetooth to scan for devices';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Quick switch';
|
String get device_quickSwitch => 'Quick switch';
|
||||||
|
|
||||||
@@ -332,15 +342,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Presets';
|
String get settings_presets => 'Presets';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frequency (MHz)';
|
String get settings_frequency => 'Frequency (MHz)';
|
||||||
|
|
||||||
@@ -369,10 +370,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Long Range';
|
String get settings_clientRepeat => 'Off-Grid Repeat';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Fast Speed';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Allow this device to repeat mesh packets for others';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -442,6 +448,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Русский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifications';
|
String get appSettings_notifications => 'Notifications';
|
||||||
|
|
||||||
@@ -1336,6 +1348,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Manage Repeater';
|
String get map_manageRepeater => 'Manage Repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Run Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Remove Last';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Path trace cancelled.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Offline Map Cache';
|
String get mapCache_title => 'Offline Map Cache';
|
||||||
|
|
||||||
@@ -2636,4 +2660,189 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'New group';
|
String get listFilter_newGroup => 'New group';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'You';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Path trace failed.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Path trace not available.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Refresh Path Trace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'One or more of the hops is missing a location!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Clear path.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Path trace to repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Ping repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Path trace to room server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping room server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Path trace route';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Trace route to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Clipboard is empty.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Invalid contact data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Contact has been imported.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed => 'Failed to import contact.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Zero Hop Advert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Flood Advert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Copy Advert to Clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard => 'Add Contact from Clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Copy contact to Clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Share contact by advert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Sent contact by advert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed => 'Failed to send contact.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied => 'Advert copied to Clipboard.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Copying advert to Clipboard failed.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'MeshCore Activity';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'messages',
|
||||||
|
one: 'message',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'channel messages',
|
||||||
|
one: 'channel message',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'new nodes',
|
||||||
|
one: 'new node',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'New $contactType discovered';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Received new message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Export repeaters / room server to GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exports repeaters / roomserver with a location to GPX file.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Export companions to GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exports companions with a location to GPX file.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Export all contacts to GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exports all contacts with a location to GPX file.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Successfully exported GPX file.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'No contacts to export.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Not supported on your device/OS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'There was an error when exporting.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Repeater & room server locations';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Companion locations';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'All contacts locations';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Map data exported from meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open GPX map data export';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Escanea';
|
String get scanner_scan => 'Escanea';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Cambiar rápidamente';
|
String get device_quickSwitch => 'Cambiar rápidamente';
|
||||||
|
|
||||||
@@ -337,15 +347,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Preajustes';
|
String get settings_presets => 'Preajustes';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frecuencia (MHz)';
|
String get settings_frequency => 'Frecuencia (MHz)';
|
||||||
|
|
||||||
@@ -374,10 +375,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Largo Alcance';
|
String get settings_clientRepeat => 'Repetir sin conexión';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Velocidad Rápida';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -447,6 +453,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Ruso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ucraniano';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificaciones';
|
String get appSettings_notifications => 'Notificaciones';
|
||||||
|
|
||||||
@@ -1354,6 +1366,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Gestionar Repetidor';
|
String get map_manageRepeater => 'Gestionar Repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Eliminar último';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Caché de Mapa Offline';
|
String get mapCache_title => 'Caché de Mapa Offline';
|
||||||
|
|
||||||
@@ -2675,4 +2699,194 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nuevo grupo';
|
String get listFilter_newGroup => 'Nuevo grupo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Tú';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'El trazado de ruta falló.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Actualizar Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Uno o más de los lúpulos carecen de una ubicación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Borrar ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Rastreo de caminos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Pingar repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace =>
|
||||||
|
'Rastreo de ruta al servidor de la habitación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Pingar servidor de sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Ruta de trazado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Rastrear ruta a $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'El portapapeles está vacío.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'El contacto ha sido importado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Contacto no se importó correctamente.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Anuncio de inundación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Agregar contacto desde el portapapeles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Copiar contacto al Portapapeles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'No se pudo enviar el contacto.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Copiar anuncio al Portapapeles ha fallado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Actividad de MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'mensajes',
|
||||||
|
one: 'mensaje',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'mensajes de canal',
|
||||||
|
one: 'mensaje de canal',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nuevos nodos',
|
||||||
|
one: 'nuevo nodo',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nuevo $contactType descubierto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nuevo mensaje recibido';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exportar repetidores / servidor de sala a GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exporta repetidores o roomserver con una ubicación a un archivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Exportar compañeros a GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exporta compañeros con una ubicación a archivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Exportar todos los contactos a GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exporta todos los contactos con una ubicación a un archivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'No hay contactos para exportar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'No compatible con tu dispositivo/SO';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Hubo un error al exportar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Ubicaciones del servidor de repetidor y sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Ubicaciones de compañero';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts =>
|
||||||
|
'Todas las ubicaciones de contactos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Datos del mapa exportados desde meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open exportación de datos de mapa GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Scanner';
|
String get scanner_scan => 'Scanner';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Basculement rapide';
|
String get device_quickSwitch => 'Basculement rapide';
|
||||||
|
|
||||||
@@ -338,15 +348,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Préréglages';
|
String get settings_presets => 'Préréglages';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Fréquence (MHz)';
|
String get settings_frequency => 'Fréquence (MHz)';
|
||||||
|
|
||||||
@@ -375,10 +376,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Portée Longue';
|
String get settings_clientRepeat => 'Répétition hors réseau';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Vitesse Rapide';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Permettez à cet appareil de répéter les paquets de données pour les autres.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -448,6 +454,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Russe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ukrainien';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifications';
|
String get appSettings_notifications => 'Notifications';
|
||||||
|
|
||||||
@@ -554,11 +566,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get appSettings_mapDisplay => 'Affichage de la carte';
|
String get appSettings_mapDisplay => 'Affichage de la carte';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_showRepeaters => 'Afficher les répétiteurs';
|
String get appSettings_showRepeaters => 'Afficher les répéteurs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_showRepeatersSubtitle =>
|
String get appSettings_showRepeatersSubtitle =>
|
||||||
'Afficher les nœuds répétiteurs sur la carte';
|
'Afficher les nœuds répéteurs sur la carte';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_showChatNodes => 'Afficher les nœuds de discussion';
|
String get appSettings_showChatNodes => 'Afficher les nœuds de discussion';
|
||||||
@@ -665,7 +677,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_manageRepeater => 'Gérer le répétiteur';
|
String get contacts_manageRepeater => 'Gérer le répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_manageRoom => 'Gérer le Room Server';
|
String get contacts_manageRoom => 'Gérer le Room Server';
|
||||||
@@ -1088,18 +1100,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
|
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_hopSingular => 'Sautez';
|
String get chat_hopSingular => 'saut';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_hopPlural => 'sautez';
|
String get chat_hopPlural => 'sauts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chat_hopsCount(int count) {
|
String chat_hopsCount(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'hops',
|
other: 'sauts',
|
||||||
one: 'hop',
|
one: 'saut',
|
||||||
);
|
);
|
||||||
return '$count $_temp0';
|
return '$count $_temp0';
|
||||||
}
|
}
|
||||||
@@ -1253,7 +1265,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get map_chat => 'Chat';
|
String get map_chat => 'Chat';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_repeater => 'Répétiteur';
|
String get map_repeater => 'Répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_room => 'Salle';
|
String get map_room => 'Salle';
|
||||||
@@ -1359,7 +1371,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get map_joinRoom => 'Rejoindre la salle';
|
String get map_joinRoom => 'Rejoindre la salle';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Gérer le répétiteur';
|
String get map_manageRepeater => 'Gérer le répéteur';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd =>
|
||||||
|
'Appuyez sur les nœuds pour les ajouter au chemin.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Exécuter la traçage de chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Supprimer le dernier';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Traçage de chemin annulé';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Cache de Carte Hors Ligne';
|
String get mapCache_title => 'Cache de Carte Hors Ligne';
|
||||||
@@ -1503,7 +1528,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
|
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_repeaterLogin => 'Connexion au répétiteur';
|
String get login_repeaterLogin => 'Connexion au répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_roomLogin => 'Connexion Salle';
|
String get login_roomLogin => 'Connexion Salle';
|
||||||
@@ -1523,7 +1548,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_repeaterDescription =>
|
String get login_repeaterDescription =>
|
||||||
'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.';
|
'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_roomDescription =>
|
String get login_roomDescription =>
|
||||||
@@ -1628,7 +1653,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get path_setPath => 'Définir le chemin';
|
String get path_setPath => 'Définir le chemin';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_management => 'Gestion des répétiteurs';
|
String get repeater_management => 'Gestion des répéteurs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get room_management => 'Administración del Servidor de Habitación';
|
String get room_management => 'Administración del Servidor de Habitación';
|
||||||
@@ -1641,7 +1666,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_statusSubtitle =>
|
String get repeater_statusSubtitle =>
|
||||||
'Afficher l\'état, les statistiques et les voisins du répétiteur';
|
'Afficher l\'état, les statistiques et les voisins du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_telemetry => 'Télémetrie';
|
String get repeater_telemetry => 'Télémetrie';
|
||||||
@@ -1654,7 +1679,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get repeater_cli => 'CLI';
|
String get repeater_cli => 'CLI';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
|
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Voisins';
|
String get repeater_neighbours => 'Voisins';
|
||||||
@@ -1668,10 +1693,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settingsSubtitle =>
|
String get repeater_settingsSubtitle =>
|
||||||
'Configurer les paramètres du répétiteur';
|
'Configurer les paramètres du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_statusTitle => 'État du répétiteur';
|
String get repeater_statusTitle => 'État du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_routingMode => 'Mode de routage';
|
String get repeater_routingMode => 'Mode de routage';
|
||||||
@@ -1777,16 +1802,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settingsTitle => 'Paramètres du répétiteur';
|
String get repeater_settingsTitle => 'Paramètres du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_basicSettings => 'Paramètres de base';
|
String get repeater_basicSettings => 'Paramètres de base';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_repeaterName => 'Nom du répétiteur';
|
String get repeater_repeaterName => 'Nom du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur';
|
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_adminPassword => 'Mot de passe Administrateur';
|
String get repeater_adminPassword => 'Mot de passe Administrateur';
|
||||||
@@ -1850,7 +1875,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_packetForwardingSubtitle =>
|
String get repeater_packetForwardingSubtitle =>
|
||||||
'Activer le répétiteur pour transmettre des paquets';
|
'Activer le répéteur pour transmettre des paquets';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_guestAccess => 'Accès Invité';
|
String get repeater_guestAccess => 'Accès Invité';
|
||||||
@@ -1899,11 +1924,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_rebootRepeaterSubtitle =>
|
String get repeater_rebootRepeaterSubtitle =>
|
||||||
'Réinitialiser l\'appareil répétiteur';
|
'Réinitialiser l\'appareil répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_rebootRepeaterConfirm =>
|
String get repeater_rebootRepeaterConfirm =>
|
||||||
'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?';
|
'Êtes-vous sûr de vouloir redémarrer ce répéteur ?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité';
|
String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité';
|
||||||
@@ -1914,18 +1939,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_regenerateIdentityKeyConfirm =>
|
String get repeater_regenerateIdentityKeyConfirm =>
|
||||||
'Cela générera une nouvelle identité pour le répétiteur. Continuer ?';
|
'Cela générera une nouvelle identité pour le répéteur. Continuer ?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_eraseFileSystem => 'Supprimer le système de fichiers';
|
String get repeater_eraseFileSystem => 'Supprimer le système de fichiers';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_eraseFileSystemSubtitle =>
|
String get repeater_eraseFileSystemSubtitle =>
|
||||||
'Formater le système de fichiers du répétiteur';
|
'Formater le système de fichiers du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_eraseFileSystemConfirm =>
|
String get repeater_eraseFileSystemConfirm =>
|
||||||
'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !';
|
'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_eraseSerialOnly =>
|
String get repeater_eraseSerialOnly =>
|
||||||
@@ -1993,7 +2018,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliTitle => 'Répétiteur CLI';
|
String get repeater_cliTitle => 'Répéteur CLI';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_debugNextCommand => 'Déboguer Prochaine Commande';
|
String get repeater_debugNextCommand => 'Déboguer Prochaine Commande';
|
||||||
@@ -2085,7 +2110,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetRepeat =>
|
String get repeater_cliHelpSetRepeat =>
|
||||||
'Active ou désactive le rôle du répétiteur pour ce nœud.';
|
'Active ou désactive le rôle du répéteur pour ce nœud.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetAllowReadOnly =>
|
String get repeater_cliHelpSetAllowReadOnly =>
|
||||||
@@ -2109,7 +2134,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetAdvertInterval =>
|
String get repeater_cliHelpSetAdvertInterval =>
|
||||||
'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.';
|
'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpSetFloodAdvertInterval =>
|
String get repeater_cliHelpSetFloodAdvertInterval =>
|
||||||
@@ -2195,7 +2220,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpNeighbors =>
|
String get repeater_cliHelpNeighbors =>
|
||||||
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
|
'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpNeighborRemove =>
|
String get repeater_cliHelpNeighborRemove =>
|
||||||
@@ -2283,12 +2308,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get repeater_logging => 'Journalisation';
|
String get repeater_logging => 'Journalisation';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighborsRepeaterOnly =>
|
String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)';
|
||||||
'Voisins (Uniquement répétiteur)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_regionManagementRepeaterOnly =>
|
String get repeater_regionManagementRepeaterOnly =>
|
||||||
'Gestion des régions (uniquement pour le répétiteur)';
|
'Gestion des régions (uniquement pour le répéteur)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_regionNote =>
|
String get repeater_regionNote =>
|
||||||
@@ -2393,7 +2417,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get channelPath_otherObservedPaths => 'Autres chemins observés';
|
String get channelPath_otherObservedPaths => 'Autres chemins observés';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelPath_repeaterHops => 'Sauts du répétiteur';
|
String get channelPath_repeaterHops => 'Sauts du répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelPath_noHopDetails =>
|
String get channelPath_noHopDetails =>
|
||||||
@@ -2461,7 +2485,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelPath_noRepeaterLocations =>
|
String get channelPath_noRepeaterLocations =>
|
||||||
'Aucune position de répétiteur disponible pour ce chemin.';
|
'Aucune position de répéteur disponible pour ce chemin.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channelPath_primaryPath(int index) {
|
String channelPath_primaryPath(int index) {
|
||||||
@@ -2692,4 +2716,201 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nouveau groupe';
|
String get listFilter_newGroup => 'Nouveau groupe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Vous';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Traçage du chemin échoué.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Tracé de chemin non disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Actualiser Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Un ou plusieurs des sauts manquent d\'une localisation !';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Effacer le chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Traçage de chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Pinguer le répéteur';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace =>
|
||||||
|
'Traçage du chemin vers le serveur de la salle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Pinguer le serveur de la salle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Tracer le chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Tracer l\'itinéraire vers $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Le presse-papiers est vide.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Données de contact non valides';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Le contact a été importé.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Échec de l\'importation du contact.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Annonce Zero saut';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Annonce à tout le réseau';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard =>
|
||||||
|
'Copier l\'annonce dans le presse-papiers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Ajouter un contact depuis le presse-papiers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact =>
|
||||||
|
'Copier le contact dans le presse-papiers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Partager un contact par annonce';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Envoyer un contact par annonce.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Échec de l\'envoi du contact.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Annonce copiée dans le presse-papiers.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'La copie de l\'annonce vers le presse-papiers a échoué.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Activité MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'messages',
|
||||||
|
one: 'message',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'messages de canal',
|
||||||
|
one: 'message de canal',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nouveaux nœuds',
|
||||||
|
one: 'nouveau nœud',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nouveau $contactType découvert';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nouveau message reçu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exporter les répéteurs / serveur de salle au format GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts =>
|
||||||
|
'Exporter les compagnons au format GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exporte les compagnons avec un emplacement vers un fichier GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll =>
|
||||||
|
'Exporter tous les contacts au format GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exporte tous les contacts avec une localisation vers un fichier GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Aucun contact à exporter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Non pris en charge sur votre appareil/Système d\'exploitation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError =>
|
||||||
|
'Une erreur s\'est produite lors de l\'exportation.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Emplacements des serveurs de répéteur et de salle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Emplacements des compagnons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts =>
|
||||||
|
'Tous les emplacements des contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Données de carte exportées à partir de meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open exporter les données de carte GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Scansiona';
|
String get scanner_scan => 'Scansiona';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Passa velocemente';
|
String get device_quickSwitch => 'Passa velocemente';
|
||||||
|
|
||||||
@@ -336,15 +346,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Preset';
|
String get settings_presets => 'Preset';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frequenza (MHz)';
|
String get settings_frequency => 'Frequenza (MHz)';
|
||||||
|
|
||||||
@@ -373,10 +374,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Lungo Raggio';
|
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Velocità Rapida';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -446,6 +452,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Russo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ucraino';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifiche';
|
String get appSettings_notifications => 'Notifiche';
|
||||||
|
|
||||||
@@ -1353,6 +1365,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Gestisci Ripetitore';
|
String get map_manageRepeater => 'Gestisci Ripetitore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Esegui Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Rimuovi ultimo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Cache Mappa Offline';
|
String get mapCache_title => 'Cache Mappa Offline';
|
||||||
|
|
||||||
@@ -2675,4 +2699,198 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nuovo gruppo';
|
String get listFilter_newGroup => 'Nuovo gruppo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Tu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Tracciamento del percorso fallito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable =>
|
||||||
|
'Tracciamento del percorso non disponibile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Uno o più dei luppoli mancano di una posizione!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Traccia Percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Traccia percorso al ripetitore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Ripetitore ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace =>
|
||||||
|
'Traccia del percorso al server della stanza';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping al server della stanza';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Traccia percorso path';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Traccia percorso verso $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Il contatto è stato importato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Contatto non importato con successo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Annuncio Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Annuncio alluvionale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Copia Annuncio negli Appunti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Aggiungere contatto dalla clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Copia contatto negli Appunti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop =>
|
||||||
|
'Condividi contatto tramite annuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Inviato contatto tramite annuncio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Invio del contatto non riuscito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied => 'Annuncio copiato negli Appunti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Copia dell\'annuncio nella Clipboard non riuscita.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Attività MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'messaggi',
|
||||||
|
one: 'messaggio',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'messaggi del canale',
|
||||||
|
one: 'messaggio del canale',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nuovi nodi',
|
||||||
|
one: 'nuovo nodo',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nuovo $contactType scoperto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nuovo messaggio ricevuto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Esporta ripetitori / server di stanza in GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Esporta ripetitori / roomserver con una posizione in un file GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Esporta compagni in GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Esporta i compagni con una posizione in un file GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Esporta tutti i contatti in GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Esporta tutti i contatti con una posizione in un file GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess =>
|
||||||
|
'Esportazione del file GPX completata con successo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Nessun contatto da esportare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Non supportato sul tuo dispositivo/Sistema Operativo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError =>
|
||||||
|
'Si è verificato un errore durante l\'esportazione.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Posizioni del server ripetitore e della stanza';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Posizioni dei compagni';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Tutte le posizioni dei contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Dati mappa esportati da meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open esportazione dati mappa GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Scan';
|
String get scanner_scan => 'Scan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Snelle overschakeling';
|
String get device_quickSwitch => 'Snelle overschakeling';
|
||||||
|
|
||||||
@@ -334,15 +344,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Presets';
|
String get settings_presets => 'Presets';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frequentie (MHz)';
|
String get settings_frequency => 'Frequentie (MHz)';
|
||||||
|
|
||||||
@@ -371,10 +372,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Lange Afstand';
|
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Hoge Snelheid';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -444,6 +450,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Russisch';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Oekraïens';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificaties';
|
String get appSettings_notifications => 'Notificaties';
|
||||||
|
|
||||||
@@ -1349,6 +1361,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Beheer Repeater';
|
String get map_manageRepeater => 'Beheer Repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd =>
|
||||||
|
'Tik op knooppunten om ze toe te voegen aan het pad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Padeshulp traceren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Verwijder Laatste';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Offline Kaarten Cache';
|
String get mapCache_title => 'Offline Kaarten Cache';
|
||||||
|
|
||||||
@@ -2666,4 +2691,194 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nieuwe groep';
|
String get listFilter_newGroup => 'Nieuwe groep';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Jij';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Padtrace mislukt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Padtrace niet beschikbaar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Een of meer van de hops ontbreken een locatie!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Weg wissen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Pad Traceren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Pingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Pad traceren naar repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Ping repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Padtrace naar room server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping kamer server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Route traceren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Trace route to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Knipbord is leeg.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Contact is geïmporteerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Contact kon niet geïmporteerd worden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Overstromingsadvertentie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Contact uit klembord toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Contact verzonden via advertentie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Mislukt om contact te verzenden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Reclame gekopieerd naar Klembord.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Kopiëren van advertentie naar Clipboard is mislukt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'MeshCore Activiteit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'berichten',
|
||||||
|
one: 'bericht',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'kanaalberichten',
|
||||||
|
one: 'kanaalbericht',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nieuwe knooppunten',
|
||||||
|
one: 'nieuw knooppunt',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nieuw $contactType ontdekt';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nieuw bericht ontvangen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exporteer repeaters / roomserver naar GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exporteert repeaters / roomserver met een locatie naar GPX-bestand.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Companions exporteren naar GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exporteert metgezellen met een locatie naar een GPX-bestand.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Alle contacten exporteren naar GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exporteert alle contacten met een locatie naar een GPX-bestand.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Niet ondersteund op uw apparaat/besturingssysteem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Er was een fout bij het exporteren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Repeater- en kamer servers locaties';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Locaties van metgezellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Alle contactlocaties';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Kaartgegevens geëxporteerd uit meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open GPX kaartgegevens exporteren';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Przeskanuj';
|
String get scanner_scan => 'Przeskanuj';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Włącz Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Szybka zmiana';
|
String get device_quickSwitch => 'Szybka zmiana';
|
||||||
|
|
||||||
@@ -337,15 +347,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Preset';
|
String get settings_presets => 'Preset';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Częstotliwość (MHz)';
|
String get settings_frequency => 'Częstotliwość (MHz)';
|
||||||
|
|
||||||
@@ -375,10 +376,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Długi zasięg';
|
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Szybka prędkość';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -448,6 +454,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Rosyjski';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ukraińska';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Powiadomienia';
|
String get appSettings_notifications => 'Powiadomienia';
|
||||||
|
|
||||||
@@ -1355,6 +1367,18 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Zarządzaj Powtórzami';
|
String get map_manageRepeater => 'Zarządzaj Powtórzami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Uruchom ślad ścieżki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Usuń ostatni';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Bufor Map Offline';
|
String get mapCache_title => 'Bufor Map Offline';
|
||||||
|
|
||||||
@@ -2674,4 +2698,201 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nowa grupa';
|
String get listFilter_newGroup => 'Nowa grupa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Ty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Śledzenie Ścieżek';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Pingować';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Repeater pingowy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace =>
|
||||||
|
'Śledzenie ścieżki do serwera pokojowego';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Pinguj serwer pokoju';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Śledź trasę promienia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Śledź trasę do $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Schowek jest pusty.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Kontakt został zaimportowany.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Kontakt nie został zaimportowany.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Reklama Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Reklama powodziowa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Kopiuj kontakt do schowka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop =>
|
||||||
|
'Udostępnij kontakt przez ogłoszenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Wysłano kontakt przez ogłoszenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Nie udało się wysłać kontaktu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Kopiowanie ogłoszenia do schowka nie powiodło się.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Aktywność MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'wiadomości',
|
||||||
|
many: 'wiadomości',
|
||||||
|
few: 'wiadomości',
|
||||||
|
one: 'wiadomość',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'wiadomości kanału',
|
||||||
|
many: 'wiadomości kanału',
|
||||||
|
few: 'wiadomości kanału',
|
||||||
|
one: 'wiadomość kanału',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nowych węzłów',
|
||||||
|
many: 'nowych węzłów',
|
||||||
|
few: 'nowe węzły',
|
||||||
|
one: 'nowy węzeł',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nowy $contactType wykryty';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Otrzymano nową wiadomość';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Eksportuj powtórki / serwer pokojowy do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Eksportuje towarzyszy z lokalizacją do pliku GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts =>
|
||||||
|
'Brak kontaktów do wyeksportowania.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Lokalizacje serwerów powtarzających i pomieszczeń';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Dane mapy wyeksportowane z meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'Eksport danych mapy GPX meshcore-open';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Digitalizar';
|
String get scanner_scan => 'Digitalizar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth está desativado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Mudar rapidamente';
|
String get device_quickSwitch => 'Mudar rapidamente';
|
||||||
|
|
||||||
@@ -338,15 +348,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Presets';
|
String get settings_presets => 'Presets';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frequência (MHz)';
|
String get settings_frequency => 'Frequência (MHz)';
|
||||||
|
|
||||||
@@ -375,10 +376,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Alcance Longo';
|
String get settings_clientRepeat => 'Repetição sem rede';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Velocidade Rápida';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -448,6 +454,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Russo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ucraniano';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificações';
|
String get appSettings_notifications => 'Notificações';
|
||||||
|
|
||||||
@@ -1355,6 +1367,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Gerenciar Repetidor';
|
String get map_manageRepeater => 'Gerenciar Repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Executar Traçado de Caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Remover Último';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Cache de Mapa Offline';
|
String get mapCache_title => 'Cache de Mapa Offline';
|
||||||
|
|
||||||
@@ -2677,4 +2701,193 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Novo grupo';
|
String get listFilter_newGroup => 'Novo grupo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Você';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Falha no rastreamento de caminho.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Traçado de caminho não disponível.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Atualizar Path Trace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Um ou mais dos lúpulos estão sem localização!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Limpar caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Traçado de Caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Pingar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Pingar repetidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Pingar servidor da sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Rastrear rota do caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Rastrear rota para $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Contato foi importado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed => 'Contato falhou ao ser importado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Anúncio Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Anúncio de Inundação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard =>
|
||||||
|
'Copiar Anúncio para Área de Transferência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Adicionar Contato da Área de Transferência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact =>
|
||||||
|
'Copiar contato para Área de Transferência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Anúncio copiado para a Área de Transferência.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Cópia do anúncio para a Área de Transferência falhou.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Atividade MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'mensagens',
|
||||||
|
one: 'mensagem',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'mensagens de canal',
|
||||||
|
one: 'mensagem de canal',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'novos nós',
|
||||||
|
one: 'novo nó',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Novo $contactType descoberto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nova mensagem recebida';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exportar repetidores / servidor de sala para GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exporta repetidores / roomserver com localização para arquivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Exportar companheiros para GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exporta companheiros com uma localização para um arquivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Exportar todos os contatos para GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exporta todos os contatos com uma localização para um arquivo GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Nenhum contato para exportar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Não suportado no seu dispositivo/SO';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Ocorreu um erro ao exportar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Localizações do servidor de repetidor e sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Localizações de companheiros';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Todos os locais de contatos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Dados do mapa exportados do meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open exportação de dados de mapa GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Сканирование';
|
String get scanner_scan => 'Сканирование';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth выключен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Быстрое переключение';
|
String get device_quickSwitch => 'Быстрое переключение';
|
||||||
|
|
||||||
@@ -335,15 +345,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Пресеты';
|
String get settings_presets => 'Пресеты';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 МГц';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 МГц';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 МГц';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Частота (МГц)';
|
String get settings_frequency => 'Частота (МГц)';
|
||||||
|
|
||||||
@@ -373,10 +374,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
'Недопустимая мощность передачи (0–22 дБм)';
|
'Недопустимая мощность передачи (0–22 дБм)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Дальний радиус';
|
String get settings_clientRepeat => 'Повторение \"вне сети\"';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Высокая скорость';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Позвольте этому устройству повторять пакеты данных для других устройств.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -446,6 +452,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Болгарский';
|
String get appSettings_languageBg => 'Болгарский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Русский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -1356,6 +1368,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Управление репитером';
|
String get map_manageRepeater => 'Управление репитером';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Запустить трассировку пути';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Удалить последний';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Отмена трассировки пути';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Кэш офлайн-карты';
|
String get mapCache_title => 'Кэш офлайн-карты';
|
||||||
|
|
||||||
@@ -2679,4 +2703,202 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Новая группа';
|
String get listFilter_newGroup => 'Новая группа';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Вы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Путь трассировки не выполнен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Трассировка пути недоступна.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Обновить Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Одному или нескольким хмелям не указано местоположение!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Очистить путь';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Трассировка пути';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Пинговать';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Отследить путь к ретранслятору';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Пинговать повторитель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Трассировка пути к серверу комнаты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Пинговать сервер комнаты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Трассировка маршрута';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Показать маршрут к $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Буфер обмена пуст.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat =>
|
||||||
|
'Недействительные контактные данные';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Контакт был импортирован';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed => 'Контакт не удалось импортировать';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Реклама Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Рекламный поток';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard =>
|
||||||
|
'Копировать рекламу в буфер обмена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Добавить контакт из буфера обмена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Копировать контакт в буфер обмена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop =>
|
||||||
|
'Поделиться контактом по объявлению';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Отправлено сообщение по объявлению.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Не удалось отправить контакт.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Реклама скопирована в буфер обмена.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Копирование рекламы в буфер обмена не удалось.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Активность MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'сообщений',
|
||||||
|
many: 'сообщений',
|
||||||
|
few: 'сообщения',
|
||||||
|
one: 'сообщение',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'сообщений канала',
|
||||||
|
many: 'сообщений канала',
|
||||||
|
few: 'сообщения канала',
|
||||||
|
one: 'сообщение канала',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'новых узлов',
|
||||||
|
many: 'новых узлов',
|
||||||
|
few: 'новых узла',
|
||||||
|
one: 'новый узел',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Обнаружен новый $contactType';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Получено новое сообщение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Экспортировать рипитеры / сервер комнаты в GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Экспортировать спутников в GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Экспортирует спутников с местоположением в файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Экспортировать все контакты в GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Экспортирует все контакты с местоположением в файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Успешно экспортирован файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Нет контактов для экспорта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Не поддерживается на вашем устройстве/ОС';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Произошла ошибка при экспорте.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Местоположения повторителей и серверов комнат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Местоположения спутников';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Все местоположения контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Данные карты экспортированы из meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open экспорт данных карты GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Skončiť';
|
String get scanner_scan => 'Skončiť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Rýchle prepínač';
|
String get device_quickSwitch => 'Rýchle prepínač';
|
||||||
|
|
||||||
@@ -334,15 +344,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Prednastavenia';
|
String get settings_presets => 'Prednastavenia';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frekvencia (MHz)';
|
String get settings_frequency => 'Frekvencia (MHz)';
|
||||||
|
|
||||||
@@ -371,10 +372,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Dlhý dosah';
|
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Rýchla rýchlosť';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -444,6 +450,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Ruština';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ukrajinská';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Upozornenia';
|
String get appSettings_notifications => 'Upozornenia';
|
||||||
|
|
||||||
@@ -1350,6 +1362,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Spravovať Opakovanie';
|
String get map_manageRepeater => 'Spravovať Opakovanie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Spustiť trasovaním cesty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Odstrániť posledný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Offline Mapa Pamäť';
|
String get mapCache_title => 'Offline Mapa Pamäť';
|
||||||
|
|
||||||
@@ -2662,4 +2686,195 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Nová skupina';
|
String get listFilter_newGroup => 'Nová skupina';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Vy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Sledovanie cesty zlyhalo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Jedna alebo viac chmeľov chýba lokalita!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Sledovanie lúčov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Pingovať';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Pingovať opakovač';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping server miestnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Sledovať trasu lúča';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Sledovať trasu k $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Schránka je prázdna.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Kontakt bol importovaný.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed =>
|
||||||
|
'Kontakt sa nepodarilo importovať.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Inzerát Zero Hop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Inzerát povodní';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Kopírovať kontakt do schránky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Zlyhalo odoslanie kontaktu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Inzerát bol skopírovaný do schránky.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Kopírovanie inzerátu do schránky zlyhalo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Aktivita MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'správ',
|
||||||
|
few: 'správy',
|
||||||
|
one: 'správa',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'správ kanálu',
|
||||||
|
few: 'správy kanálu',
|
||||||
|
one: 'správa kanálu',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nových uzlov',
|
||||||
|
few: 'nové uzly',
|
||||||
|
one: 'nový uzol',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Nový $contactType objavený';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Prijatá nová správa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exportovať repeater / server miestnosti do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exportuje repeater / roomserver s lokalitou do súboru GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Export sprievodcov do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exportuje sprievodcov s umiestnením do súboru GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exportuje všetky kontakty s lokalitou do súboru GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Nie je podporované na vašom zariadení/operáciomnom systéme';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Umiestnenia opakovačov a serverov miestností';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Lokácie sprievodcov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Mapové údaje exportované z meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open export dát GPX mapových údajov';
|
||||||
}
|
}
|
||||||
|
|||||||
+369
-154
File diff suppressed because it is too large
Load Diff
@@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Skanna';
|
String get scanner_scan => 'Skanna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Snabb växling';
|
String get device_quickSwitch => 'Snabb växling';
|
||||||
|
|
||||||
@@ -331,15 +341,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Fördefinierade inställningar';
|
String get settings_presets => 'Fördefinierade inställningar';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 MHz';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 MHz';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Frekvens (MHz)';
|
String get settings_frequency => 'Frekvens (MHz)';
|
||||||
|
|
||||||
@@ -368,10 +369,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
|
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Lång räckvidd';
|
String get settings_clientRepeat => 'Upprepa utan elnät';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Snabb hastighet';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Låt enheten repetera nätpaket för andra användare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -441,6 +447,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Ryska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Ukrainska';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Meddelanden';
|
String get appSettings_notifications => 'Meddelanden';
|
||||||
|
|
||||||
@@ -1342,6 +1354,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Hantera Upprepare';
|
String get map_manageRepeater => 'Hantera Upprepare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Kör spårsökning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Ta bort sista';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Offline Kartcache';
|
String get mapCache_title => 'Offline Kartcache';
|
||||||
|
|
||||||
@@ -2650,4 +2674,192 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Ny grupp';
|
String get listFilter_newGroup => 'Ny grupp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Du';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Sökvägsföljning misslyckades.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Path trace ej tillgänglig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Uppdatera Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'En eller flera av humlen saknar en plats!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Rensa väg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Ping';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Vägspårning till repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Ping-repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Vägspårning till rumserver';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Ping rumsserver';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Spåra rutt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Spåra rutt till $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Urklipp är tomt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Kontakt har importerats.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed => 'Kontakt kunde inte importeras.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Reklam med nollhopp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Översvämningsannons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Lägg till kontakt från urklipp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Kopiera kontakt till Urklipp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop => 'Dela kontakt via annons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent => 'Skickat kontakt via annons.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Misslyckades med att skicka kontakt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied => 'Annons kopierad till Urklipp.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Kopiering av annons till Urklipp misslyckades.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'MeshCore Aktivitet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'meddelanden',
|
||||||
|
one: 'meddelande',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'kanalmeddelanden',
|
||||||
|
one: 'kanalmeddelande',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'nya noder',
|
||||||
|
one: 'ny nod',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Ny $contactType upptäckt';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Nytt meddelande mottaget';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Exportera repeater / rumsservrar till GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Exporterar repeater / roomserver med plats till GPX-fil.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Exportera följeslagare till GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Exporterar följeslagare med en plats till GPX-fil.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Exportera alla kontakter till GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Exporterar alla kontakter med en plats till GPX-fil.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Stöds inte på din enhet/operativsystem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError =>
|
||||||
|
'Det uppstod ett fel när data exporterades.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Repeater- och rumsserverplatser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Medhjälparplatser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Alla kontakters platser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Kartdata exporterad från meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'meshcore-open export av GPX-kartdata';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_scan => 'Сканувати';
|
String get scanner_scan => 'Сканувати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOff => 'Bluetooth вимкнено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_bluetoothOffMessage =>
|
||||||
|
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get device_quickSwitch => 'Швидке перемикання';
|
String get device_quickSwitch => 'Швидке перемикання';
|
||||||
|
|
||||||
@@ -336,15 +346,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_presets => 'Попередні налаштування';
|
String get settings_presets => 'Попередні налаштування';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset915Mhz => '915 МГц';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset868Mhz => '868 МГц';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settings_preset433Mhz => '433 МГц';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_frequency => 'Частота (МГц)';
|
String get settings_frequency => 'Частота (МГц)';
|
||||||
|
|
||||||
@@ -373,10 +374,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)';
|
String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_longRange => 'Дальній діапазон';
|
String get settings_clientRepeat => 'Автономна система';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_fastSpeed => 'Висока швидкість';
|
String get settings_clientRepeatSubtitle =>
|
||||||
|
'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_clientRepeatFreqWarning =>
|
||||||
|
'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settings_error(String message) {
|
String settings_error(String message) {
|
||||||
@@ -446,6 +452,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageBg => 'Български';
|
String get appSettings_languageBg => 'Български';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageRu => 'Російська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Сповіщення';
|
String get appSettings_notifications => 'Сповіщення';
|
||||||
|
|
||||||
@@ -1355,6 +1367,18 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_manageRepeater => 'Керувати ретранслятором';
|
String get map_manageRepeater => 'Керувати ретранслятором';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTrace => 'Виконати трасування шляху';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_removeLast => 'Видалити останній';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_pathTraceCancelled => 'Відмінується трасування шляху';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mapCache_title => 'Офлайн-кеш карти';
|
String get mapCache_title => 'Офлайн-кеш карти';
|
||||||
|
|
||||||
@@ -2686,4 +2710,201 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_newGroup => 'Нова група';
|
String get listFilter_newGroup => 'Нова група';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_you => 'Ви';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_failed => 'Відстеження шляху не вдалося.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_notAvailable => 'Трасування шляху недоступне.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_refreshTooltip => 'Оновити Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_someHopsNoLocation =>
|
||||||
|
'Одне або більше хмелів відсутнє місце розташування!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathTrace_clearTooltip => 'Очистити шлях';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_pathTrace => 'Трасування шляхів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ping => 'Пінгувати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPathTrace => 'Трасування шляху до повторювача';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_repeaterPing => 'Пінгувати повторювач';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPathTrace => 'Трасування шляху до серверу кімнати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_roomPing => 'Пінг сервера кімнати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_chatTraceRoute => 'Трасування шляху';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_pathTraceTo(String name) {
|
||||||
|
return 'Відстежити маршрут до $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_clipboardEmpty => 'Буфер обміну порожній';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_invalidAdvertFormat => 'Недійсні контактні дані';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImported => 'Контакт було імпортовано.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopAdvert => 'Реклама без перехоплення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_floodAdvert => 'Залив реклами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_copyAdvertToClipboard =>
|
||||||
|
'Копіювати оголошення в буфер обміну';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_addContactFromClipboard =>
|
||||||
|
'Додати контакт з буфера обміну';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContact => 'Копіювати контакт у буфер обміну';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_ShareContactZeroHop =>
|
||||||
|
'Поділитися контактом за оголошенням';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertSent =>
|
||||||
|
'Відправлено контакт за оголошенням';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_zeroHopContactAdvertFailed =>
|
||||||
|
'Не вдалося надіслати контакт.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopied =>
|
||||||
|
'Рекламу скопійовано до буфера обміну.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_contactAdvertCopyFailed =>
|
||||||
|
'Копіювання оголошення в буфер обміну завершилося невдало';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_activityTitle => 'Активність MeshCore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_messagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'повідомлень',
|
||||||
|
many: 'повідомлень',
|
||||||
|
few: 'повідомлення',
|
||||||
|
one: 'повідомлення',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_channelMessagesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'повідомлень каналу',
|
||||||
|
many: 'повідомлень каналу',
|
||||||
|
few: 'повідомлення каналу',
|
||||||
|
one: 'повідомлення каналу',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newNodesCount(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'нових вузлів',
|
||||||
|
many: 'нових вузлів',
|
||||||
|
few: 'нових вузли',
|
||||||
|
one: 'новий вузол',
|
||||||
|
);
|
||||||
|
return '$count $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String notification_newTypeDiscovered(String contactType) {
|
||||||
|
return 'Виявлено новий $contactType';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notification_receivedNewMessage => 'Отримано нове повідомлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeaters =>
|
||||||
|
'Експортувати ретранслятори / сервер кімнати до GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersSubtitle =>
|
||||||
|
'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContacts => 'Експортувати супутників до GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportContactsSubtitle =>
|
||||||
|
'Експортує супутників з місцезнаходженням у файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAll => 'Експортувати всі контакти до GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllSubtitle =>
|
||||||
|
'Експортує всі контакти з місцем розташування у файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNoContacts => 'Немає контактів для експорту.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportNotAvailable =>
|
||||||
|
'Не підтримується на вашому пристрої/операційній системі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportError => 'Сталася помилка під час експорту.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportRepeatersRoom =>
|
||||||
|
'Місцезнаходження повторювача та сервера кімнати';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportChat => 'Місця супутників';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportAllContacts => 'Усі місця контактів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareText =>
|
||||||
|
'Дані карти експортовані з meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_gpxExportShareSubject =>
|
||||||
|
'експорт даних карти meshcore-open у форматі GPX';
|
||||||
}
|
}
|
||||||
|
|||||||
+638
-455
File diff suppressed because it is too large
Load Diff
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Aantal Contacten",
|
"settings_infoContactsCount": "Aantal Contacten",
|
||||||
"settings_infoChannelCount": "Aantal Kanalen",
|
"settings_infoChannelCount": "Aantal Kanalen",
|
||||||
"settings_presets": "Presets",
|
"settings_presets": "Presets",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frequentie (MHz)",
|
"settings_frequency": "Frequentie (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
|
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Vermogen (dBm)",
|
"settings_txPower": "TX Vermogen (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||||
"settings_longRange": "Lange Afstand",
|
|
||||||
"settings_fastSpeed": "Hoge Snelheid",
|
|
||||||
"settings_error": "Fout: {message}",
|
"settings_error": "Fout: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerate": "Regeneer",
|
"community_regenerate": "Regeneer",
|
||||||
"community_updateSecret": "Bijwerken Geheime",
|
"community_updateSecret": "Bijwerken Geheime",
|
||||||
"community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
|
"community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
|
||||||
"community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken"
|
"community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Jij",
|
||||||
|
"pathTrace_failed": "Padtrace mislukt.",
|
||||||
|
"pathTrace_notAvailable": "Padtrace niet beschikbaar.",
|
||||||
|
"pathTrace_refreshTooltip": "Path Trace vernieuwen.",
|
||||||
|
"contacts_pathTrace": "Pad Traceren",
|
||||||
|
"contacts_ping": "Pingen",
|
||||||
|
"contacts_repeaterPathTrace": "Pad traceren naar repeater",
|
||||||
|
"contacts_repeaterPing": "Ping repeater",
|
||||||
|
"contacts_roomPathTrace": "Padtrace naar room server",
|
||||||
|
"contacts_roomPing": "Ping kamer server",
|
||||||
|
"contacts_chatTraceRoute": "Route traceren",
|
||||||
|
"contacts_pathTraceTo": "Trace route to {name}",
|
||||||
|
"appSettings_languageUk": "Oekraïens",
|
||||||
|
"contacts_invalidAdvertFormat": "Ongeldige contactgegevens",
|
||||||
|
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
|
||||||
|
"contacts_zeroHopAdvert": "Zero Hop Reclame",
|
||||||
|
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||||
|
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||||
|
"appSettings_languageRu": "Russisch",
|
||||||
|
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||||
|
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||||
|
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||||
|
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||||
|
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||||
|
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||||
|
"notification_activityTitle": "MeshCore Activiteit",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{bericht} other{berichten}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{kanaalbericht} other{kanaalberichten}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
|
||||||
|
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
|
||||||
|
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
|
||||||
|
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
|
||||||
|
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
|
||||||
|
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
|
||||||
|
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
|
||||||
|
"settings_gpxExportError": "Er was een fout bij het exporteren.",
|
||||||
|
"settings_gpxExportContacts": "Companions exporteren naar GPX",
|
||||||
|
"settings_gpxExportAll": "Alle contacten exporteren naar GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exporteert alle contacten met een locatie naar een GPX-bestand.",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exporteert metgezellen met een locatie naar een GPX-bestand.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
|
||||||
|
"settings_gpxExportChat": "Locaties van metgezellen",
|
||||||
|
"settings_gpxExportAllContacts": "Alle contactlocaties",
|
||||||
|
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
|
||||||
|
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
|
||||||
|
"map_removeLast": "Verwijder Laatste",
|
||||||
|
"pathTrace_clearTooltip": "Weg wissen",
|
||||||
|
"map_pathTraceCancelled": "Pad traceren geannuleerd",
|
||||||
|
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
|
||||||
|
"map_runTrace": "Padeshulp traceren",
|
||||||
|
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||||
|
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
||||||
|
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||||
|
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||||
|
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist."
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Liczba kontaktów",
|
"settings_infoContactsCount": "Liczba kontaktów",
|
||||||
"settings_infoChannelCount": "Liczba kanałów",
|
"settings_infoChannelCount": "Liczba kanałów",
|
||||||
"settings_presets": "Preset",
|
"settings_presets": "Preset",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Częstotliwość (MHz)",
|
"settings_frequency": "Częstotliwość (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
|
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Moc (dBm)",
|
"settings_txPower": "TX Moc (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
|
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
|
||||||
"settings_longRange": "Długi zasięg",
|
|
||||||
"settings_fastSpeed": "Szybka prędkość",
|
|
||||||
"settings_error": "Błąd: {message}",
|
"settings_error": "Błąd: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.",
|
"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_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"",
|
||||||
"community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
|
"community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
|
||||||
"community_updateSecret": "Zaktualizuj tajny klucz"
|
"community_updateSecret": "Zaktualizuj tajny klucz",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Ty",
|
||||||
|
"pathTrace_failed": "Śledzenie ścieżki nie powiodło się.",
|
||||||
|
"pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.",
|
||||||
|
"contacts_pathTrace": "Śledzenie Ścieżek",
|
||||||
|
"contacts_ping": "Pingować",
|
||||||
|
"contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera",
|
||||||
|
"contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego",
|
||||||
|
"contacts_roomPing": "Pinguj serwer pokoju",
|
||||||
|
"pathTrace_refreshTooltip": "Odśwież ścieżkę.",
|
||||||
|
"contacts_repeaterPing": "Repeater pingowy",
|
||||||
|
"contacts_pathTraceTo": "Śledź trasę do {name}",
|
||||||
|
"contacts_chatTraceRoute": "Śledź trasę promienia",
|
||||||
|
"appSettings_languageRu": "Rosyjski",
|
||||||
|
"appSettings_languageUk": "Ukraińska",
|
||||||
|
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
|
||||||
|
"contacts_zeroHopAdvert": "Reklama Zero Hop",
|
||||||
|
"contacts_floodAdvert": "Reklama powodziowa",
|
||||||
|
"contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka",
|
||||||
|
"contacts_clipboardEmpty": "Schowek jest pusty.",
|
||||||
|
"contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe",
|
||||||
|
"contacts_addContactFromClipboard": "Dodaj kontakt z schowka",
|
||||||
|
"contacts_contactImported": "Kontakt został zaimportowany.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.",
|
||||||
|
"contacts_contactAdvertCopied": "Reklama skopiowana do schowka.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.",
|
||||||
|
"contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie",
|
||||||
|
"contacts_ShareContact": "Kopiuj kontakt do schowka",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
|
||||||
|
"notification_activityTitle": "Aktywność MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
|
||||||
|
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
|
||||||
|
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
|
||||||
|
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
|
||||||
|
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
|
||||||
|
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
|
||||||
|
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
|
||||||
|
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
|
||||||
|
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
|
||||||
|
"settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów",
|
||||||
|
"settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.",
|
||||||
|
"settings_gpxExportChat": "Lokalizacje towarzyszy",
|
||||||
|
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
|
||||||
|
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
|
||||||
|
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
|
||||||
|
"map_runTrace": "Uruchom ślad ścieżki",
|
||||||
|
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
|
||||||
|
"map_removeLast": "Usuń ostatni",
|
||||||
|
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
||||||
|
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||||
|
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||||
|
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||||
|
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||||
|
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz."
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Número de Contatos",
|
"settings_infoContactsCount": "Número de Contatos",
|
||||||
"settings_infoChannelCount": "Número do Canal",
|
"settings_infoChannelCount": "Número do Canal",
|
||||||
"settings_presets": "Presets",
|
"settings_presets": "Presets",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frequência (MHz)",
|
"settings_frequency": "Frequência (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
|
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Potência (dBm)",
|
"settings_txPower": "TX Potência (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
|
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
|
||||||
"settings_longRange": "Alcance Longo",
|
|
||||||
"settings_fastSpeed": "Velocidade Rápida",
|
|
||||||
"settings_error": "Erro: {message}",
|
"settings_error": "Erro: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerate": "Regenerar",
|
"community_regenerate": "Regenerar",
|
||||||
"community_secretUpdated": "Segredo atualizado para \"{name}\"",
|
"community_secretUpdated": "Segredo atualizado para \"{name}\"",
|
||||||
"community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
|
"community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
|
||||||
"community_updateSecret": "Atualizar Segredo"
|
"community_updateSecret": "Atualizar Segredo",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Você",
|
||||||
|
"pathTrace_failed": "Falha no rastreamento de caminho.",
|
||||||
|
"pathTrace_notAvailable": "Traçado de caminho não disponível.",
|
||||||
|
"pathTrace_refreshTooltip": "Atualizar Path Trace.",
|
||||||
|
"contacts_pathTrace": "Traçado de Caminho",
|
||||||
|
"contacts_ping": "Pingar",
|
||||||
|
"contacts_repeaterPathTrace": "Traçar caminho para repetidor",
|
||||||
|
"contacts_repeaterPing": "Pingar repetidor",
|
||||||
|
"contacts_roomPathTrace": "Traçar caminho para o servidor da sala",
|
||||||
|
"contacts_roomPing": "Pingar servidor da sala",
|
||||||
|
"contacts_chatTraceRoute": "Rastrear rota do caminho",
|
||||||
|
"contacts_pathTraceTo": "Rastrear rota para {name}",
|
||||||
|
"contacts_invalidAdvertFormat": "Dados de Contato Inválidos",
|
||||||
|
"contacts_clipboardEmpty": "Área de Transferência Está Vazia.",
|
||||||
|
"appSettings_languageUk": "Ucraniano",
|
||||||
|
"contacts_contactImported": "Contato foi importado.",
|
||||||
|
"contacts_zeroHopAdvert": "Anúncio Zero Hop",
|
||||||
|
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
||||||
|
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
||||||
|
"appSettings_languageRu": "Russo",
|
||||||
|
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
||||||
|
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
||||||
|
"contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.",
|
||||||
|
"contacts_floodAdvert": "Anúncio de Inundação",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.",
|
||||||
|
"contacts_ShareContactZeroHop": "Compartilhar contato por anúncio",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
|
||||||
|
"notification_activityTitle": "Atividade MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
|
||||||
|
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
|
||||||
|
"notification_receivedNewMessage": "Nova mensagem recebida",
|
||||||
|
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
|
||||||
|
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.",
|
||||||
|
"settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO",
|
||||||
|
"settings_gpxExportError": "Ocorreu um erro ao exportar.",
|
||||||
|
"settings_gpxExportAll": "Exportar todos os contatos para GPX",
|
||||||
|
"settings_gpxExportContacts": "Exportar companheiros para GPX",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala",
|
||||||
|
"settings_gpxExportChat": "Localizações de companheiros",
|
||||||
|
"settings_gpxExportNoContacts": "Nenhum contato para exportar.",
|
||||||
|
"settings_gpxExportAllContacts": "Todos os locais de contatos",
|
||||||
|
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
|
||||||
|
"map_runTrace": "Executar Traçado de Caminho",
|
||||||
|
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
|
||||||
|
"pathTrace_clearTooltip": "Limpar caminho",
|
||||||
|
"map_removeLast": "Remover Último",
|
||||||
|
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
|
||||||
|
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||||
|
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||||
|
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
||||||
|
"settings_clientRepeat": "Repetição sem rede",
|
||||||
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos."
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-6
@@ -101,9 +101,6 @@
|
|||||||
"settings_infoContactsCount": "Количество контактов",
|
"settings_infoContactsCount": "Количество контактов",
|
||||||
"settings_infoChannelCount": "Количество каналов",
|
"settings_infoChannelCount": "Количество каналов",
|
||||||
"settings_presets": "Пресеты",
|
"settings_presets": "Пресеты",
|
||||||
"settings_preset915Mhz": "915 МГц",
|
|
||||||
"settings_preset868Mhz": "868 МГц",
|
|
||||||
"settings_preset433Mhz": "433 МГц",
|
|
||||||
"settings_frequency": "Частота (МГц)",
|
"settings_frequency": "Частота (МГц)",
|
||||||
"settings_frequencyHelper": "300.0 – 2500.0",
|
"settings_frequencyHelper": "300.0 – 2500.0",
|
||||||
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
|
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
|
||||||
@@ -113,8 +110,6 @@
|
|||||||
"settings_txPower": "Мощность передачи (дБм)",
|
"settings_txPower": "Мощность передачи (дБм)",
|
||||||
"settings_txPowerHelper": "0 – 22",
|
"settings_txPowerHelper": "0 – 22",
|
||||||
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
|
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
|
||||||
"settings_longRange": "Дальний радиус",
|
|
||||||
"settings_fastSpeed": "Высокая скорость",
|
|
||||||
"settings_error": "Ошибка: {message}",
|
"settings_error": "Ошибка: {message}",
|
||||||
"appSettings_title": "Настройки приложения",
|
"appSettings_title": "Настройки приложения",
|
||||||
"appSettings_appearance": "Внешний вид",
|
"appSettings_appearance": "Внешний вид",
|
||||||
@@ -774,5 +769,72 @@
|
|||||||
"chat_openLink": "Открыть ссылку?",
|
"chat_openLink": "Открыть ссылку?",
|
||||||
"chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?",
|
"chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?",
|
||||||
"neighbors_heardAgo": "Слушал(а): {time} назад",
|
"neighbors_heardAgo": "Слушал(а): {time} назад",
|
||||||
"chat_invalidLink": "Неправильный формат ссылки"
|
"chat_invalidLink": "Неправильный формат ссылки",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Вы",
|
||||||
|
"pathTrace_failed": "Путь трассировки не выполнен.",
|
||||||
|
"pathTrace_notAvailable": "Трассировка пути недоступна.",
|
||||||
|
"pathTrace_refreshTooltip": "Обновить Path Trace",
|
||||||
|
"contacts_pathTrace": "Трассировка пути",
|
||||||
|
"contacts_ping": "Пинговать",
|
||||||
|
"contacts_repeaterPathTrace": "Отследить путь к ретранслятору",
|
||||||
|
"contacts_repeaterPing": "Пинговать повторитель",
|
||||||
|
"contacts_roomPathTrace": "Трассировка пути к серверу комнаты",
|
||||||
|
"contacts_roomPing": "Пинговать сервер комнаты",
|
||||||
|
"contacts_chatTraceRoute": "Трассировка маршрута",
|
||||||
|
"contacts_pathTraceTo": "Показать маршрут к {name}",
|
||||||
|
"contacts_contactImported": "Контакт был импортирован",
|
||||||
|
"contacts_contactImportFailed": "Контакт не удалось импортировать",
|
||||||
|
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
||||||
|
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
||||||
|
"appSettings_languageUk": "Українська",
|
||||||
|
"contacts_floodAdvert": "Рекламный поток",
|
||||||
|
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
||||||
|
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
||||||
|
"contacts_ShareContact": "Копировать контакт в буфер обмена",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.",
|
||||||
|
"contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.",
|
||||||
|
"contacts_addContactFromClipboard": "Добавить контакт из буфера обмена",
|
||||||
|
"contacts_ShareContactZeroHop": "Поделиться контактом по объявлению",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.",
|
||||||
|
"notification_activityTitle": "Активность MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}",
|
||||||
|
"notification_newTypeDiscovered": "Обнаружен новый {contactType}",
|
||||||
|
"notification_receivedNewMessage": "Получено новое сообщение",
|
||||||
|
"settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.",
|
||||||
|
"settings_gpxExportContacts": "Экспортировать спутников в GPX",
|
||||||
|
"settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС",
|
||||||
|
"settings_gpxExportError": "Произошла ошибка при экспорте.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат",
|
||||||
|
"settings_gpxExportChat": "Местоположения спутников",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.",
|
||||||
|
"settings_gpxExportAll": "Экспортировать все контакты в GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.",
|
||||||
|
"settings_gpxExportAllContacts": "Все местоположения контактов",
|
||||||
|
"settings_gpxExportSuccess": "Успешно экспортирован файл GPX.",
|
||||||
|
"settings_gpxExportNoContacts": "Нет контактов для экспорта.",
|
||||||
|
"settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!",
|
||||||
|
"map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.",
|
||||||
|
"map_removeLast": "Удалить последний",
|
||||||
|
"map_pathTraceCancelled": "Отмена трассировки пути",
|
||||||
|
"pathTrace_clearTooltip": "Очистить путь",
|
||||||
|
"map_runTrace": "Запустить трассировку пути",
|
||||||
|
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||||
|
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||||
|
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
||||||
|
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
||||||
|
"settings_clientRepeat": "Повторение \"вне сети\""
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Počet kontaktov",
|
"settings_infoContactsCount": "Počet kontaktov",
|
||||||
"settings_infoChannelCount": "Počet kanálov",
|
"settings_infoChannelCount": "Počet kanálov",
|
||||||
"settings_presets": "Prednastavenia",
|
"settings_presets": "Prednastavenia",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frekvencia (MHz)",
|
"settings_frequency": "Frekvencia (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 – 2500,0",
|
"settings_frequencyHelper": "300,0 – 2500,0",
|
||||||
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
|
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX Výkon (dBm)",
|
"settings_txPower": "TX Výkon (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
|
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
|
||||||
"settings_longRange": "Dlhý dosah",
|
|
||||||
"settings_fastSpeed": "Rýchla rýchlosť",
|
|
||||||
"settings_error": "Chyba: {message}",
|
"settings_error": "Chyba: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerateSecret": "Zobraziť nový tajný kód",
|
"community_regenerateSecret": "Zobraziť nový tajný kód",
|
||||||
"community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
|
"community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
|
||||||
"community_updateSecret": "Aktualizovať tajné heslo",
|
"community_updateSecret": "Aktualizovať tajné heslo",
|
||||||
"community_secretUpdated": "Zmena tajnej slova pre \"{name}\""
|
"community_secretUpdated": "Zmena tajnej slova pre \"{name}\"",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Vy",
|
||||||
|
"pathTrace_failed": "Sledovanie cesty zlyhalo.",
|
||||||
|
"pathTrace_notAvailable": "Path trace nie je k dispozícii.",
|
||||||
|
"pathTrace_refreshTooltip": "Obnoviť Path Trace.",
|
||||||
|
"contacts_pathTrace": "Sledovanie lúčov",
|
||||||
|
"contacts_ping": "Pingovať",
|
||||||
|
"contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču",
|
||||||
|
"contacts_repeaterPing": "Pingovať opakovač",
|
||||||
|
"contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti",
|
||||||
|
"contacts_roomPing": "Ping server miestnosti",
|
||||||
|
"contacts_chatTraceRoute": "Sledovať trasu lúča",
|
||||||
|
"contacts_pathTraceTo": "Sledovať trasu k {name}",
|
||||||
|
"contacts_clipboardEmpty": "Schránka je prázdna.",
|
||||||
|
"appSettings_languageUk": "Ukrajinská",
|
||||||
|
"contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.",
|
||||||
|
"contacts_zeroHopAdvert": "Inzerát Zero Hop",
|
||||||
|
"contacts_floodAdvert": "Inzerát povodní",
|
||||||
|
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
||||||
|
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
||||||
|
"appSettings_languageRu": "Ruština",
|
||||||
|
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
||||||
|
"contacts_contactImported": "Kontakt bol importovaný.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
||||||
|
"contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.",
|
||||||
|
"contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát",
|
||||||
|
"contacts_ShareContact": "Kopírovať kontakt do schránky",
|
||||||
|
"notification_activityTitle": "Aktivita MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
|
||||||
|
"notification_newTypeDiscovered": "Nový {contactType} objavený",
|
||||||
|
"notification_receivedNewMessage": "Prijatá nová správa",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
|
||||||
|
"settings_gpxExportContacts": "Export sprievodcov do GPX",
|
||||||
|
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
|
||||||
|
"settings_gpxExportNoContacts": "Žiadne kontakty na export.",
|
||||||
|
"settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností",
|
||||||
|
"settings_gpxExportError": "Vyskytol sa chyba počas exportu.",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.",
|
||||||
|
"settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX",
|
||||||
|
"settings_gpxExportAll": "Exportovať všetky kontakty do GPX",
|
||||||
|
"settings_gpxExportAllContacts": "Všetky kontaktné lokality",
|
||||||
|
"settings_gpxExportChat": "Lokácie sprievodcov",
|
||||||
|
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
|
||||||
|
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
|
||||||
|
"pathTrace_clearTooltip": "Zmazať cestu",
|
||||||
|
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
|
||||||
|
"map_removeLast": "Odstrániť posledný",
|
||||||
|
"map_runTrace": "Spustiť trasovaním cesty",
|
||||||
|
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||||
|
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||||
|
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||||
|
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
||||||
|
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
||||||
|
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných."
|
||||||
}
|
}
|
||||||
|
|||||||
+209
-146
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "sl",
|
"@@locale": "sl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakti",
|
"nav_contacts": "Stiki",
|
||||||
"nav_channels": "Kanali",
|
"nav_channels": "Kanali",
|
||||||
"nav_map": "Karta",
|
"nav_map": "Karta",
|
||||||
"common_cancel": "Prekliči",
|
"common_cancel": "Prekliči",
|
||||||
@@ -69,49 +69,49 @@
|
|||||||
},
|
},
|
||||||
"scanner_stop": "Prekliči",
|
"scanner_stop": "Prekliči",
|
||||||
"scanner_scan": "Skeniraj",
|
"scanner_scan": "Skeniraj",
|
||||||
"device_quickSwitch": "Hitro preklopiti",
|
"device_quickSwitch": "Hitro preklop",
|
||||||
"device_meshcore": "MeshCore",
|
"device_meshcore": "MeshCore",
|
||||||
"settings_title": "Nastavitve",
|
"settings_title": "Nastavitve",
|
||||||
"settings_deviceInfo": "Informacije o napravei",
|
"settings_deviceInfo": "Informacije o napravei",
|
||||||
"settings_appSettings": "Nastavitve aplikacije",
|
"settings_appSettings": "Nastavitve aplikacije",
|
||||||
"settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.",
|
"settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.",
|
||||||
"settings_nodeSettings": "Nastavitve časa",
|
"settings_nodeSettings": "Nastavitev časa",
|
||||||
"settings_nodeName": "Ime omrežno mesto",
|
"settings_nodeName": "Ime node-a",
|
||||||
"settings_nodeNameNotSet": "Nezavedeno",
|
"settings_nodeNameNotSet": "Ni nastavljeno",
|
||||||
"settings_nodeNameHint": "Vnesite ime časa",
|
"settings_nodeNameHint": "Vnesite ime node-a",
|
||||||
"settings_nodeNameUpdated": "Ime posodobljeno",
|
"settings_nodeNameUpdated": "Ime posodobljeno",
|
||||||
"settings_radioSettings": "Nastavitve radija",
|
"settings_radioSettings": "Nastavitve radija",
|
||||||
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširni faktor",
|
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor",
|
||||||
"settings_radioSettingsUpdated": "Radio nastavitve posodobljene",
|
"settings_radioSettingsUpdated": "Radio nastavitve posodobljene",
|
||||||
"settings_location": "Lokacija",
|
"settings_location": "Lokacija",
|
||||||
"settings_locationSubtitle": "GPS koordinate",
|
"settings_locationSubtitle": "GPS koordinate",
|
||||||
"settings_locationUpdated": "Lokacija posodobljena",
|
"settings_locationUpdated": "Lokacija posodobljena",
|
||||||
"settings_locationBothRequired": "Vnesite širino in dolžino.",
|
"settings_locationBothRequired": "Vnesite širino in dolžino.",
|
||||||
"settings_locationInvalid": "Neveljna zemeljska širina ali dolžina.",
|
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
|
||||||
"settings_latitude": "Širina",
|
"settings_latitude": "Širina",
|
||||||
"settings_longitude": "Dolžina",
|
"settings_longitude": "Dolžina",
|
||||||
"settings_privacyMode": "Mod podjetja",
|
"settings_privacyMode": "Zasebnost",
|
||||||
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
|
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
|
||||||
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
|
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
|
||||||
"settings_privacyModeEnabled": "Privatni režim je omogočen.",
|
"settings_privacyModeEnabled": "Privatni način je omogočen.",
|
||||||
"settings_privacyModeDisabled": "Privatni režim je onemogočen.",
|
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
|
||||||
"settings_actions": "Akcije",
|
"settings_actions": "Akcije",
|
||||||
"settings_sendAdvertisement": "Pošlji Oglas",
|
"settings_sendAdvertisement": "Pošlji Oglas",
|
||||||
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
||||||
"settings_advertisementSent": "Oglas poslan",
|
"settings_advertisementSent": "Oglas poslan",
|
||||||
"settings_syncTime": "Ugasniti čas",
|
"settings_syncTime": "Nastavi uro",
|
||||||
"settings_syncTimeSubtitle": "Nastavi uro naprave v čas telefona",
|
"settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona",
|
||||||
"settings_timeSynchronized": "Sinhronizirano po času",
|
"settings_timeSynchronized": "Ura sinhronizirana",
|
||||||
"settings_refreshContacts": "Ponovno obišči kontakte",
|
"settings_refreshContacts": "Ponovno obišči kontakte",
|
||||||
"settings_refreshContactsSubtitle": "Ponovno naloži seznam kontaktov iz naprave",
|
"settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi",
|
||||||
"settings_rebootDevice": "Restart Naprave",
|
"settings_rebootDevice": "Ponovni zagon naprave",
|
||||||
"settings_rebootDeviceSubtitle": "Ponovite zažetek naprave MeshCore",
|
"settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo",
|
||||||
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.",
|
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.",
|
||||||
"settings_debug": "Napravi popravek",
|
"settings_debug": "Debug",
|
||||||
"settings_bleDebugLog": "Logarjev zapis BLE",
|
"settings_bleDebugLog": "BLE debug log (razhroščevanje)",
|
||||||
"settings_bleDebugLogSubtitle": "Navodila BLE, odgovori in surovo podatkovno",
|
"settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki",
|
||||||
"settings_appDebugLog": "Log zapiske aplikacije",
|
"settings_appDebugLog": "Logi aplikacije",
|
||||||
"settings_appDebugLogSubtitle": "Prijavni sporočila aplikacije",
|
"settings_appDebugLogSubtitle": "Debug sporočila aplikacije",
|
||||||
"settings_about": "Oglejte si",
|
"settings_about": "Oglejte si",
|
||||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||||
"@settings_aboutVersion": {
|
"@settings_aboutVersion": {
|
||||||
@@ -121,30 +121,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings_aboutLegalese": "MeshCore Odprtokodni Projekt 2024",
|
"settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024",
|
||||||
"settings_aboutDescription": "Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.",
|
"settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.",
|
||||||
"settings_infoName": "Ime",
|
"settings_infoName": "Ime",
|
||||||
"settings_infoId": "ID",
|
"settings_infoId": "ID",
|
||||||
"settings_infoStatus": "Status",
|
"settings_infoStatus": "Status",
|
||||||
"settings_infoBattery": "Baterija",
|
"settings_infoBattery": "Baterija",
|
||||||
"settings_infoPublicKey": "Ključ javnega tipa",
|
"settings_infoPublicKey": "Javni ključ",
|
||||||
"settings_infoContactsCount": "Število kontaktov",
|
"settings_infoContactsCount": "Število stikov",
|
||||||
"settings_infoChannelCount": "Število kanalov",
|
"settings_infoChannelCount": "Število kanalov",
|
||||||
"settings_presets": "Prednastavitve",
|
"settings_presets": "Prednastavitve",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frekvenca (MHz)",
|
"settings_frequency": "Frekvenca (MHz)",
|
||||||
"settings_frequencyHelper": "300,00 - 2500,00",
|
"settings_frequencyHelper": "300,00 - 2500,00",
|
||||||
"settings_frequencyInvalid": "Neveljčna frekvenca (300-2500 MHz)",
|
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
|
||||||
"settings_bandwidth": "Pasovna širina",
|
"settings_bandwidth": "Pasovna širina",
|
||||||
"settings_spreadingFactor": "Razširitveni faktor",
|
"settings_spreadingFactor": "Razširitveni faktor",
|
||||||
"settings_codingRate": "Programska hitrost",
|
"settings_codingRate": "Programska hitrost",
|
||||||
"settings_txPower": "TX Moč (dBm)",
|
"settings_txPower": "TX Moč (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Neveljaven TX moč (0-22 dBm)",
|
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
|
||||||
"settings_longRange": "Dolenje območje",
|
|
||||||
"settings_fastSpeed": "Hitra hitrost",
|
|
||||||
"settings_error": "Napaka: {message}",
|
"settings_error": "Napaka: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -156,8 +151,8 @@
|
|||||||
"appSettings_title": "Nastavitve aplikacije",
|
"appSettings_title": "Nastavitve aplikacije",
|
||||||
"appSettings_appearance": "Prikaži",
|
"appSettings_appearance": "Prikaži",
|
||||||
"appSettings_theme": "Tema",
|
"appSettings_theme": "Tema",
|
||||||
"appSettings_themeSystem": "Predpomnilnik sistema",
|
"appSettings_themeSystem": "Sistemska tema",
|
||||||
"appSettings_themeLight": "Luč",
|
"appSettings_themeLight": "Svetlo",
|
||||||
"appSettings_themeDark": "Temno",
|
"appSettings_themeDark": "Temno",
|
||||||
"appSettings_language": "Jezik",
|
"appSettings_language": "Jezik",
|
||||||
"appSettings_languageSystem": "Sistemska privzeta vrednost",
|
"appSettings_languageSystem": "Sistemska privzeta vrednost",
|
||||||
@@ -174,8 +169,8 @@
|
|||||||
"appSettings_languageNl": "Nederlands",
|
"appSettings_languageNl": "Nederlands",
|
||||||
"appSettings_languageSk": "Slovenčina",
|
"appSettings_languageSk": "Slovenčina",
|
||||||
"appSettings_languageBg": "Български",
|
"appSettings_languageBg": "Български",
|
||||||
"appSettings_notifications": "Obveščanja",
|
"appSettings_notifications": "Obvestila",
|
||||||
"appSettings_enableNotifications": "Omogoči obveščanje",
|
"appSettings_enableNotifications": "Omogoči obvestila",
|
||||||
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
|
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
|
||||||
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
|
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
|
||||||
"appSettings_notificationsEnabled": "Obvestila omogočena",
|
"appSettings_notificationsEnabled": "Obvestila omogočena",
|
||||||
@@ -185,19 +180,19 @@
|
|||||||
"appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala",
|
"appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala",
|
||||||
"appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala",
|
"appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala",
|
||||||
"appSettings_advertisementNotifications": "Opozorila o oglasih",
|
"appSettings_advertisementNotifications": "Opozorila o oglasih",
|
||||||
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so novi vozlišči odkrivljeni.",
|
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.",
|
||||||
"appSettings_messaging": "Komuniciranje",
|
"appSettings_messaging": "Komuniciranje",
|
||||||
"appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju",
|
"appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju",
|
||||||
"appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja",
|
"appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja",
|
||||||
"appSettings_pathsWillBeCleared": "Potnice bodo očiščene po 5 neuspešnih poskusih.",
|
"appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.",
|
||||||
"appSettings_pathsWillNotBeCleared": "Potniški poti ne bodo samodejno čiščeni.",
|
"appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.",
|
||||||
"appSettings_autoRouteRotation": "Avtomatsko Občutke in Rotacije",
|
"appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti",
|
||||||
"appSettings_autoRouteRotationSubtitle": "Med spreminjanjem med najboljšimi potmi in plovilnim načinom",
|
"appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom",
|
||||||
"appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno",
|
"appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno",
|
||||||
"appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno",
|
"appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno",
|
||||||
"appSettings_battery": "Baterija",
|
"appSettings_battery": "Baterija",
|
||||||
"appSettings_batteryChemistry": "Razem z možnostmi",
|
"appSettings_batteryChemistry": "Kemija baterije",
|
||||||
"appSettings_batteryChemistryPerDevice": "Nastavitve za naprave ({deviceName})",
|
"appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})",
|
||||||
"@appSettings_batteryChemistryPerDevice": {
|
"@appSettings_batteryChemistryPerDevice": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
@@ -205,20 +200,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appSettings_batteryChemistryConnectFirst": "Povežite se z napravo za izbiro",
|
"appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo",
|
||||||
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
|
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
|
||||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)",
|
"appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)",
|
||||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||||
"appSettings_mapDisplay": "Prikaz zemljevide",
|
"appSettings_mapDisplay": "Prikaz zemljevida",
|
||||||
"appSettings_showRepeaters": "Prikaži ponovitve",
|
"appSettings_showRepeaters": "Prikaži repetitorje",
|
||||||
"appSettings_showRepeatersSubtitle": "Prikaži ponovljalne notranjosti na zemljeploscu",
|
"appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi",
|
||||||
"appSettings_showChatNodes": "Prikaži čakalne notranjosti",
|
"appSettings_showChatNodes": "Prikaži naprave za klepet",
|
||||||
"appSettings_showChatNodesSubtitle": "Prikaži pogovorni pike na zemljeploscu",
|
"appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu",
|
||||||
"appSettings_showOtherNodes": "Pokaži druge vozlišča",
|
"appSettings_showOtherNodes": "Pokaži druge naprave",
|
||||||
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste notranjih elementov na zemljevalu.",
|
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.",
|
||||||
"appSettings_timeFilter": "Filtri po času",
|
"appSettings_timeFilter": "Filter po času",
|
||||||
"appSettings_timeFilterShowAll": "Pokaži vse notranje elemente",
|
"appSettings_timeFilterShowAll": "Pokaži vse naprave",
|
||||||
"appSettings_timeFilterShowLast": "Pokaži notranjosti iz zadnjih {hours} ur",
|
"appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah",
|
||||||
"@appSettings_timeFilterShowLast": {
|
"@appSettings_timeFilterShowLast": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -226,15 +221,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appSettings_mapTimeFilter": "Filtri časa zemljevida",
|
"appSettings_mapTimeFilter": "Filter časa na zemljevidu",
|
||||||
"appSettings_showNodesDiscoveredWithin": "Pokaži notranje čepke, odkrivene v:",
|
"appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:",
|
||||||
"appSettings_allTime": "Vse čase",
|
"appSettings_allTime": "Brez omejitev",
|
||||||
"appSettings_lastHour": "Minuto nazaj",
|
"appSettings_lastHour": "V zadnji uri",
|
||||||
"appSettings_last6Hours": "Zadnjih 6 ur",
|
"appSettings_last6Hours": "Zadnjih 6 ur",
|
||||||
"appSettings_last24Hours": "Zadnjih 24 ur",
|
"appSettings_last24Hours": "Zadnjih 24 ur",
|
||||||
"appSettings_lastWeek": "Lepošno",
|
"appSettings_lastWeek": "Prejšnji teden",
|
||||||
"appSettings_offlineMapCache": "Omrezni Poudni Arhiv",
|
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
|
||||||
"appSettings_noAreaSelected": "Nizkana označena površina",
|
"appSettings_noAreaSelected": "Območje ni izbrano",
|
||||||
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
|
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
|
||||||
"@appSettings_areaSelectedZoom": {
|
"@appSettings_areaSelectedZoom": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -246,19 +241,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appSettings_debugCard": "Napravi popravek",
|
"appSettings_debugCard": "Razhroščevanje",
|
||||||
"appSettings_appDebugLogging": "Programski Log",
|
"appSettings_appDebugLogging": "Programski dnevnik",
|
||||||
"appSettings_appDebugLoggingSubtitle": "Log aplikacijske debug sporočila za odpravljanje težav",
|
"appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav",
|
||||||
"appSettings_appDebugLoggingEnabled": "Omogočeno zaznamovanje napak v aplikaciji",
|
"appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno",
|
||||||
"appSettings_appDebugLoggingDisabled": "Programski logi aplikacije so onemogočeni.",
|
"appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.",
|
||||||
"contacts_title": "Kontakti",
|
"contacts_title": "Stiki",
|
||||||
"contacts_noContacts": "Še ni kontaktov.",
|
"contacts_noContacts": "Ni stikov.",
|
||||||
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
|
"contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.",
|
||||||
"contacts_searchContacts": "Iskanje kontaktov...",
|
"contacts_searchContacts": "Iskanje stikov...",
|
||||||
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
|
"contacts_noUnreadContacts": "Ne prebrani stiki.",
|
||||||
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
|
"contacts_noContactsFound": "Stiki niso najdeni.",
|
||||||
"contacts_deleteContact": "Izbrisati Kontakt",
|
"contacts_deleteContact": "Izbriši stik",
|
||||||
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
|
"contacts_removeConfirm": "Izbrišem {contactName} iz stikov?",
|
||||||
"@contacts_removeConfirm": {
|
"@contacts_removeConfirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"contactName": {
|
"contactName": {
|
||||||
@@ -266,12 +261,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_manageRepeater": "Upravljajte Ponovitve",
|
"contacts_manageRepeater": "Upravljaj Ponovitve",
|
||||||
"contacts_roomLogin": "Vnos v sobo",
|
"contacts_roomLogin": "Prijava v sobo",
|
||||||
"contacts_openChat": "Odprta kleta",
|
"contacts_openChat": "Odpri klepet",
|
||||||
"contacts_editGroup": "Uredi Skupino",
|
"contacts_editGroup": "Uredi skupino",
|
||||||
"contacts_deleteGroup": "Izbrisati Skupino",
|
"contacts_deleteGroup": "Izbriši skupino",
|
||||||
"contacts_deleteGroupConfirm": "Odpovedati {groupName}?",
|
"contacts_deleteGroupConfirm": "Izbriši {groupName}?",
|
||||||
"@contacts_deleteGroupConfirm": {
|
"@contacts_deleteGroupConfirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"groupName": {
|
"groupName": {
|
||||||
@@ -279,8 +274,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_newGroup": "Novo skupino",
|
"contacts_newGroup": "Nova skupina",
|
||||||
"contacts_groupName": "Skupina imena",
|
"contacts_groupName": "Ime skupine",
|
||||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
@@ -290,11 +285,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_filterContacts": "Filtri kontakt\\,...",
|
"contacts_filterContacts": "Filtriraj stik\\,...",
|
||||||
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
|
"contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.",
|
||||||
"contacts_noMembers": "Nič članov.",
|
"contacts_noMembers": "Ni članov.",
|
||||||
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
|
"contacts_lastSeenNow": "Nazadnje viden zdaj",
|
||||||
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
|
"contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -302,8 +297,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Zadnjič ogledan pred 1 uro.",
|
"contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.",
|
||||||
"contacts_lastSeenHoursAgo": "Zadnjič videti {hours} ur nazaj",
|
"contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -311,8 +306,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenDayAgo": "Zadnjič ogledan pred 1 dnem",
|
"contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem",
|
||||||
"contacts_lastSeenDaysAgo": "Zadnjič videti {days} dni nazaj",
|
"contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem",
|
||||||
"@contacts_lastSeenDaysAgo": {
|
"@contacts_lastSeenDaysAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -321,10 +316,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels_title": "Kanali",
|
"channels_title": "Kanali",
|
||||||
"channels_noChannelsConfigured": "Nekonfigurirane kanale",
|
"channels_noChannelsConfigured": "Kanali še niso konfigurirani",
|
||||||
"channels_addPublicChannel": "Dodaj Objavni Kanal",
|
"channels_addPublicChannel": "Dodaj javni kanal",
|
||||||
"channels_searchChannels": "Poišči kanale...",
|
"channels_searchChannels": "Poišči kanale...",
|
||||||
"channels_noChannelsFound": "Niti kanalov najti ni.",
|
"channels_noChannelsFound": "Ne najdem kanalov.",
|
||||||
"channels_channelIndex": "Kanal {index}",
|
"channels_channelIndex": "Kanal {index}",
|
||||||
"@channels_channelIndex": {
|
"@channels_channelIndex": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -334,13 +329,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels_hashtagChannel": "Hashtag kanal",
|
"channels_hashtagChannel": "Hashtag kanal",
|
||||||
"channels_public": "javno",
|
"channels_public": "Javni",
|
||||||
"channels_private": "Zasebno",
|
"channels_private": "Zasebni",
|
||||||
"channels_publicChannel": "Ogljišna skupina",
|
"channels_publicChannel": "Javni kanal",
|
||||||
"channels_privateChannel": "Zatemniščen kanal",
|
"channels_privateChannel": "Zasebni kanal",
|
||||||
"channels_editChannel": "Uredi kanal",
|
"channels_editChannel": "Uredi kanal",
|
||||||
"channels_deleteChannel": "Pošlji kanal",
|
"channels_deleteChannel": "Pošlji kanal",
|
||||||
"channels_deleteChannelConfirm": "Izbrisati \"{name}\"? To se ne da povrniti.",
|
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -424,8 +419,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chat_typeMessage": "Vnesite sporočilo...",
|
"chat_typeMessage": "Vnesi sporočilo...",
|
||||||
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} bajt).",
|
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).",
|
||||||
"@chat_messageTooLong": {
|
"@chat_messageTooLong": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"maxBytes": {
|
"maxBytes": {
|
||||||
@@ -433,9 +428,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chat_messageCopied": "Pošljeno sporočilo",
|
"chat_messageCopied": "Sporočilo poslano",
|
||||||
"chat_messageDeleted": "Pošiljanje sporočila izbrisano",
|
"chat_messageDeleted": "Sporočilo izbrisano",
|
||||||
"chat_retryingMessage": "Ponovna poskus.",
|
"chat_retryingMessage": "Ponovni poskus.",
|
||||||
"chat_retryCount": "Ponovit {current}/{max}",
|
"chat_retryCount": "Ponovit {current}/{max}",
|
||||||
"@chat_retryCount": {
|
"@chat_retryCount": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -448,31 +443,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chat_sendGif": "Pošlji GIF",
|
"chat_sendGif": "Pošlji GIF",
|
||||||
"chat_reply": "Odpošlji",
|
"chat_reply": "Odgovori",
|
||||||
"chat_addReaction": "Dodaj Reakcijo",
|
"chat_addReaction": "Dodaj reakcijo",
|
||||||
"chat_me": "jaz",
|
"chat_me": "jaz",
|
||||||
"emojiCategorySmileys": "Emoji",
|
"emojiCategorySmileys": "Emoji",
|
||||||
"emojiCategoryGestures": "Gestikulacije",
|
"emojiCategoryGestures": "Gestikulacije",
|
||||||
"emojiCategoryHearts": "Srce",
|
"emojiCategoryHearts": "Srce",
|
||||||
"emojiCategoryObjects": "Predmeti",
|
"emojiCategoryObjects": "Predmeti",
|
||||||
"gifPicker_title": "Izberi GIF",
|
"gifPicker_title": "Izberi GIF",
|
||||||
"gifPicker_searchHint": "Iskalite GIF-e...",
|
"gifPicker_searchHint": "Išči GIF-e...",
|
||||||
"gifPicker_poweredBy": "Naprodno z GIPHY",
|
"gifPicker_poweredBy": "Napredno z GIPHY",
|
||||||
"gifPicker_noGifsFound": "Niti GIF-jev najti ni.",
|
"gifPicker_noGifsFound": "Ne najdem GIF-ov.",
|
||||||
"gifPicker_failedLoad": "Neuspešno je naložilo GIF-e",
|
"gifPicker_failedLoad": "Neuspešno nalaganje GIF-a",
|
||||||
"gifPicker_failedSearch": "Posodobit neuspešno.",
|
"gifPicker_failedSearch": "Iskanje neuspešno.",
|
||||||
"gifPicker_noInternet": "Ni internetne povezave",
|
"gifPicker_noInternet": "Ni internetne povezave",
|
||||||
"debugLog_appTitle": "Log zapiske aplikacije",
|
"debugLog_appTitle": "Log zapiske aplikacije",
|
||||||
"debugLog_bleTitle": "Logarjev zapis BLE",
|
"debugLog_bleTitle": "Log zapis BLE",
|
||||||
"debugLog_copyLog": "Kopiraj zapiske",
|
"debugLog_copyLog": "Kopiraj dnevnik",
|
||||||
"debugLog_clearLog": "Pasters log",
|
"debugLog_clearLog": "Briši log",
|
||||||
"debugLog_copied": "Kopirana belež poteka.",
|
"debugLog_copied": "Beležka kopirana.",
|
||||||
"debugLog_bleCopied": "Kopirana beležke iz BLE",
|
"debugLog_bleCopied": "Kopirana beležka iz BLE",
|
||||||
"debugLog_noEntries": "Še ni ustvarjenih debug zapisov.",
|
"debugLog_noEntries": "Ni ustvarjenih debug zapisov.",
|
||||||
"debugLog_enableInSettings": "Omogoči beleženje napak v aplikaciji v nastavitvah",
|
"debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije",
|
||||||
"debugLog_frames": "Okna",
|
"debugLog_frames": "Okvirji",
|
||||||
"debugLog_rawLogRx": "Svež Log-RX",
|
"debugLog_rawLogRx": "Svež Log-RX",
|
||||||
"debugLog_noBleActivity": "Šele začnite z aktivnostjo BLE.",
|
"debugLog_noBleActivity": "Ni BLE aktivnosti.",
|
||||||
"debugFrame_length": "Izhodni rob: {count} bajtov",
|
"debugFrame_length": "Izhodni rob: {count} bajtov",
|
||||||
"@debugFrame_length": {
|
"@debugFrame_length": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -542,8 +537,8 @@
|
|||||||
"chat_forceFloodMode": "Nasilje obvezati v način",
|
"chat_forceFloodMode": "Nasilje obvezati v način",
|
||||||
"chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):",
|
"chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):",
|
||||||
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
|
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
|
||||||
"chat_hopSingular": "skoč",
|
"chat_hopSingular": "skok",
|
||||||
"chat_hopPlural": "škrabec",
|
"chat_hopPlural": "skokov",
|
||||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||||
"@chat_hopsCount": {
|
"@chat_hopsCount": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -554,16 +549,16 @@
|
|||||||
},
|
},
|
||||||
"chat_successes": "Uspešni",
|
"chat_successes": "Uspešni",
|
||||||
"chat_removePath": "Izbriši pot",
|
"chat_removePath": "Izbriši pot",
|
||||||
"chat_noPathHistoryYet": "Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
|
"chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
|
||||||
"chat_pathActions": "Potni ukazi:",
|
"chat_pathActions": "Potni ukazi:",
|
||||||
"chat_setCustomPath": "Nastavi Prilozeno Pot",
|
"chat_setCustomPath": "Nastavi Prilozeno Pot",
|
||||||
"chat_setCustomPathSubtitle": "Ročno določite potniško pot.",
|
"chat_setCustomPathSubtitle": "Ročno določite potniško pot.",
|
||||||
"chat_clearPath": "Čista pot",
|
"chat_clearPath": "Počisti pot",
|
||||||
"chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.",
|
"chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.",
|
||||||
"chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.",
|
"chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.",
|
||||||
"chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.",
|
"chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.",
|
||||||
"chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.",
|
"chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.",
|
||||||
"chat_fullPath": "Polni pot",
|
"chat_fullPath": "Polna pot",
|
||||||
"chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.",
|
"chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.",
|
||||||
"chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
|
"chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
|
||||||
"@chat_pathSetHops": {
|
"@chat_pathSetHops": {
|
||||||
@@ -1104,13 +1099,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_cliQuickGetName": "Dobiti ime",
|
"repeater_cliQuickGetName": "Pridobi ime",
|
||||||
"repeater_cliQuickGetRadio": "Dobiti Radiopravo",
|
"repeater_cliQuickGetRadio": "Dobiti Radiopravo",
|
||||||
"repeater_cliQuickGetTx": "Dobiti TX",
|
"repeater_cliQuickGetTx": "Pridobi TX",
|
||||||
"repeater_cliQuickNeighbors": "Sosedi",
|
"repeater_cliQuickNeighbors": "Sosedi",
|
||||||
"repeater_cliQuickVersion": "Različica",
|
"repeater_cliQuickVersion": "Različica",
|
||||||
"repeater_cliQuickAdvertise": "Oglasite",
|
"repeater_cliQuickAdvertise": "Oglasite",
|
||||||
"repeater_cliQuickClock": "Urnik",
|
"repeater_cliQuickClock": "Ura",
|
||||||
"repeater_cliHelpAdvert": "Pošlje paket oglasov",
|
"repeater_cliHelpAdvert": "Pošlje paket oglasov",
|
||||||
"repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)",
|
"repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)",
|
||||||
"repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.",
|
"repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.",
|
||||||
@@ -1142,7 +1137,7 @@
|
|||||||
"repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.",
|
"repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.",
|
||||||
"repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).",
|
"repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).",
|
||||||
"repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).",
|
"repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).",
|
||||||
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
|
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
|
||||||
"repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow",
|
"repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow",
|
||||||
"repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.",
|
"repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.",
|
||||||
"repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.",
|
"repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.",
|
||||||
@@ -1171,8 +1166,8 @@
|
|||||||
"repeater_settingsCategory": "Nastavitve",
|
"repeater_settingsCategory": "Nastavitve",
|
||||||
"repeater_bridge": "Most",
|
"repeater_bridge": "Most",
|
||||||
"repeater_logging": "Logiranje",
|
"repeater_logging": "Logiranje",
|
||||||
"repeater_neighborsRepeaterOnly": "Sosedi (le za ponovitelja)",
|
"repeater_neighborsRepeaterOnly": "Sosedi (le za repetitorje)",
|
||||||
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za ponovitve)",
|
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)",
|
||||||
"repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.",
|
"repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.",
|
||||||
"repeater_gpsManagement": "Upravljanje GPS",
|
"repeater_gpsManagement": "Upravljanje GPS",
|
||||||
"repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.",
|
"repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.",
|
||||||
@@ -1244,9 +1239,9 @@
|
|||||||
"channelPath_repeaterHops": "Skoki ponovitelja",
|
"channelPath_repeaterHops": "Skoki ponovitelja",
|
||||||
"channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.",
|
"channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.",
|
||||||
"channelPath_messageDetails": "Podrobnosti sporočila",
|
"channelPath_messageDetails": "Podrobnosti sporočila",
|
||||||
"channelPath_senderLabel": "Pošiljalec",
|
"channelPath_senderLabel": "Pošiljatelj",
|
||||||
"channelPath_timeLabel": "Čas",
|
"channelPath_timeLabel": "Ura",
|
||||||
"channelPath_repeatsLabel": "Ponovi",
|
"channelPath_repeatsLabel": "Ponovitve",
|
||||||
"channelPath_pathLabel": "Pot {index}",
|
"channelPath_pathLabel": "Pot {index}",
|
||||||
"channelPath_observedLabel": "Opazovani",
|
"channelPath_observedLabel": "Opazovani",
|
||||||
"channelPath_observedPathTitle": "Opazovana pot {index} • {hops}",
|
"channelPath_observedPathTitle": "Opazovana pot {index} • {hops}",
|
||||||
@@ -1478,10 +1473,10 @@
|
|||||||
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
|
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
|
||||||
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
|
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
|
||||||
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
|
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
|
||||||
"community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
|
"community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.",
|
||||||
"community_manageCommunities": "Upravljajte skupnosti",
|
"community_manageCommunities": "Upravljanje skupnosti",
|
||||||
"community_delete": "Opusti skupnost",
|
"community_delete": "Opusti skupnost",
|
||||||
"community_deleteConfirm": "Zapustiti \"{name}\"?",
|
"community_deleteConfirm": "Zapusti \"{name}\"?",
|
||||||
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
|
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
|
||||||
"@community_deleteChannelsWarning": {
|
"@community_deleteChannelsWarning": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1491,11 +1486,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community_deleted": "Zapustil skupnost \"{name}\"",
|
"community_deleted": "Zapustil skupnost \"{name}\"",
|
||||||
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
|
"community_addHashtagChannel": "Dodaj hashtag kanal",
|
||||||
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
|
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
|
||||||
"community_selectCommunity": "Izberi skupnost",
|
"community_selectCommunity": "Izberi skupnost",
|
||||||
"community_regularHashtag": "Oznaka s hashtagom",
|
"community_regularHashtag": "Oznaka s hashtagom",
|
||||||
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
|
"community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)",
|
||||||
"community_communityHashtag": "Skupnostni hashtag",
|
"community_communityHashtag": "Skupnostni hashtag",
|
||||||
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
|
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
|
||||||
"community_forCommunity": "Za {name}",
|
"community_forCommunity": "Za {name}",
|
||||||
@@ -1527,11 +1522,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community_secretRegenerated": "Tajna za \"{name}\" ponovno ustvarjena",
|
"community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno",
|
||||||
"community_regenerateSecret": "Preberi nov tajni kôd",
|
"community_regenerateSecret": "Ponovno ustvari geslo",
|
||||||
"community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
|
"community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
|
||||||
"community_regenerate": "Preberi znova",
|
"community_regenerate": "Preberi znova",
|
||||||
"community_scanToUpdateSecret": "Skeniraj nov kôd QR za posodabljanje tajne za {name}",
|
"community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}",
|
||||||
"community_updateSecret": "Ažurniraj tajno",
|
"community_updateSecret": "Ažuriraj ključ",
|
||||||
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\""
|
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\"",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Ti",
|
||||||
|
"pathTrace_failed": "Sledenje poti ni uspelo.",
|
||||||
|
"pathTrace_notAvailable": "Potni sled ni na voljo.",
|
||||||
|
"pathTrace_refreshTooltip": "Osveži Path Trace.",
|
||||||
|
"contacts_pathTrace": "Sledenje poti",
|
||||||
|
"contacts_ping": "Pingati",
|
||||||
|
"contacts_repeaterPathTrace": "Sledi poti do ponavljalnika",
|
||||||
|
"contacts_repeaterPing": "Pinguj ponavljalnik",
|
||||||
|
"contacts_roomPathTrace": "Sledenje poti do strežnika sobe",
|
||||||
|
"contacts_roomPing": "Ping strežnik sobe",
|
||||||
|
"contacts_chatTraceRoute": "Slediti poti žarkov",
|
||||||
|
"contacts_pathTraceTo": "Trace route to {name}",
|
||||||
|
"appSettings_languageRu": "Ruščina",
|
||||||
|
"appSettings_languageUk": "Ukrajinsko",
|
||||||
|
"contacts_contactImported": "Kontakt je bil uvožen.",
|
||||||
|
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
||||||
|
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
||||||
|
"contacts_floodAdvert": "Poplavna oglás",
|
||||||
|
"contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke",
|
||||||
|
"contacts_clipboardEmpty": "Odložišče je prazno.",
|
||||||
|
"contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče",
|
||||||
|
"contacts_addContactFromClipboard": "Dodaj stik iz odložišča",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Poslano po oglasu.",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.",
|
||||||
|
"contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.",
|
||||||
|
"contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa",
|
||||||
|
"contacts_ShareContact": "Kopiraj stik v Odložišče",
|
||||||
|
"notification_activityTitle": "Aktivnost MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
|
||||||
|
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
|
||||||
|
"notification_receivedNewMessage": "Prejeto novo sporočilo",
|
||||||
|
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
|
||||||
|
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
|
||||||
|
"settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX",
|
||||||
|
"settings_gpxExportError": "Pri izvozu je prišlo do napake.",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe",
|
||||||
|
"settings_gpxExportChat": "Lokacije spremljevalcev",
|
||||||
|
"settings_gpxExportAllContacts": "Lokacije vseh stikov",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.",
|
||||||
|
"settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.",
|
||||||
|
"settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.",
|
||||||
|
"settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open",
|
||||||
|
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
|
||||||
|
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
|
||||||
|
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
|
||||||
|
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
|
||||||
|
"map_removeLast": "Odstrani Zadnji",
|
||||||
|
"map_runTrace": "Zaženi sledenje poti",
|
||||||
|
"pathTrace_clearTooltip": "Počisti pot",
|
||||||
|
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||||
|
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||||
|
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||||
|
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
||||||
|
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
||||||
|
"settings_clientRepeat": "Neovadno ponavljanje"
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Kontakterantal",
|
"settings_infoContactsCount": "Kontakterantal",
|
||||||
"settings_infoChannelCount": "Kanalantal",
|
"settings_infoChannelCount": "Kanalantal",
|
||||||
"settings_presets": "Fördefinierade inställningar",
|
"settings_presets": "Fördefinierade inställningar",
|
||||||
"settings_preset915Mhz": "915 MHz",
|
|
||||||
"settings_preset868Mhz": "868 MHz",
|
|
||||||
"settings_preset433Mhz": "433 MHz",
|
|
||||||
"settings_frequency": "Frekvens (MHz)",
|
"settings_frequency": "Frekvens (MHz)",
|
||||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||||
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
|
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "TX-effekt (dBm)",
|
"settings_txPower": "TX-effekt (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
|
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
|
||||||
"settings_longRange": "Lång räckvidd",
|
|
||||||
"settings_fastSpeed": "Snabb hastighet",
|
|
||||||
"settings_error": "Fel: {message}",
|
"settings_error": "Fel: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1533,5 +1528,73 @@
|
|||||||
"community_regenerateSecret": "Regenerera hemlig kod",
|
"community_regenerateSecret": "Regenerera hemlig kod",
|
||||||
"community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
|
"community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
|
||||||
"community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
|
"community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
|
||||||
"community_updateSecret": "Uppdatera hemlighet"
|
"community_updateSecret": "Uppdatera hemlighet",
|
||||||
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Du",
|
||||||
|
"pathTrace_failed": "Sökvägsföljning misslyckades.",
|
||||||
|
"pathTrace_notAvailable": "Path trace ej tillgänglig.",
|
||||||
|
"pathTrace_refreshTooltip": "Uppdatera Path Trace",
|
||||||
|
"contacts_pathTrace": "Path Trace",
|
||||||
|
"contacts_ping": "Ping",
|
||||||
|
"contacts_repeaterPathTrace": "Vägspårning till repeater",
|
||||||
|
"contacts_repeaterPing": "Ping-repeater",
|
||||||
|
"contacts_roomPathTrace": "Vägspårning till rumserver",
|
||||||
|
"contacts_roomPing": "Ping rumsserver",
|
||||||
|
"contacts_chatTraceRoute": "Spåra rutt",
|
||||||
|
"contacts_pathTraceTo": "Spåra rutt till {name}",
|
||||||
|
"contacts_clipboardEmpty": "Urklipp är tomt.",
|
||||||
|
"appSettings_languageRu": "Ryska",
|
||||||
|
"contacts_contactImportFailed": "Kontakt kunde inte importeras.",
|
||||||
|
"contacts_zeroHopAdvert": "Reklam med nollhopp",
|
||||||
|
"contacts_floodAdvert": "Översvämningsannons",
|
||||||
|
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
||||||
|
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
||||||
|
"appSettings_languageUk": "Ukrainska",
|
||||||
|
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
||||||
|
"contacts_contactImported": "Kontakt har importerats.",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
||||||
|
"contacts_contactAdvertCopied": "Annons kopierad till Urklipp.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.",
|
||||||
|
"contacts_ShareContact": "Kopiera kontakt till Urklipp",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.",
|
||||||
|
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
|
||||||
|
"notification_activityTitle": "MeshCore Aktivitet",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
|
||||||
|
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
|
||||||
|
"notification_receivedNewMessage": "Nytt meddelande mottaget",
|
||||||
|
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
|
||||||
|
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
|
||||||
|
"settings_gpxExportNoContacts": "Inga kontakter att exportera.",
|
||||||
|
"settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser",
|
||||||
|
"settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX",
|
||||||
|
"settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.",
|
||||||
|
"settings_gpxExportContacts": "Exportera följeslagare till GPX",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.",
|
||||||
|
"settings_gpxExportChat": "Medhjälparplatser",
|
||||||
|
"settings_gpxExportError": "Det uppstod ett fel när data exporterades.",
|
||||||
|
"settings_gpxExportAllContacts": "Alla kontakters platser",
|
||||||
|
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
|
||||||
|
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
|
||||||
|
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
|
||||||
|
"pathTrace_clearTooltip": "Rensa väg",
|
||||||
|
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
|
||||||
|
"map_runTrace": "Kör spårsökning",
|
||||||
|
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
|
||||||
|
"map_removeLast": "Ta bort sista",
|
||||||
|
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||||
|
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||||
|
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
||||||
|
"settings_clientRepeat": "Upprepa utan elnät",
|
||||||
|
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz."
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-7
@@ -131,9 +131,6 @@
|
|||||||
"settings_infoContactsCount": "Кількість контактів",
|
"settings_infoContactsCount": "Кількість контактів",
|
||||||
"settings_infoChannelCount": "Кількість каналів",
|
"settings_infoChannelCount": "Кількість каналів",
|
||||||
"settings_presets": "Попередні налаштування",
|
"settings_presets": "Попередні налаштування",
|
||||||
"settings_preset915Mhz": "915 МГц",
|
|
||||||
"settings_preset868Mhz": "868 МГц",
|
|
||||||
"settings_preset433Mhz": "433 МГц",
|
|
||||||
"settings_frequency": "Частота (МГц)",
|
"settings_frequency": "Частота (МГц)",
|
||||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||||
"settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)",
|
"settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)",
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"settings_txPower": "Потужність TX (дБм)",
|
"settings_txPower": "Потужність TX (дБм)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)",
|
"settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)",
|
||||||
"settings_longRange": "Дальній діапазон",
|
|
||||||
"settings_fastSpeed": "Висока швидкість",
|
|
||||||
"settings_error": "Помилка: {message}",
|
"settings_error": "Помилка: {message}",
|
||||||
"@settings_error": {
|
"@settings_error": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1534,5 +1529,72 @@
|
|||||||
"community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано",
|
"community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано",
|
||||||
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»",
|
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»",
|
||||||
"community_updateSecret": "Оновити секрет",
|
"community_updateSecret": "Оновити секрет",
|
||||||
"community_secretUpdated": "Зміну секрету для «{name}» оновлено"
|
"community_secretUpdated": "Зміну секрету для «{name}» оновлено",
|
||||||
}
|
"@contacts_pathTraceTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathTrace_you": "Ви",
|
||||||
|
"pathTrace_failed": "Відстеження шляху не вдалося.",
|
||||||
|
"pathTrace_notAvailable": "Трасування шляху недоступне.",
|
||||||
|
"pathTrace_refreshTooltip": "Оновити Path Trace",
|
||||||
|
"contacts_pathTrace": "Трасування шляхів",
|
||||||
|
"contacts_ping": "Пінгувати",
|
||||||
|
"contacts_repeaterPathTrace": "Трасування шляху до повторювача",
|
||||||
|
"contacts_repeaterPing": "Пінгувати повторювач",
|
||||||
|
"contacts_roomPathTrace": "Трасування шляху до серверу кімнати",
|
||||||
|
"contacts_roomPing": "Пінг сервера кімнати",
|
||||||
|
"contacts_chatTraceRoute": "Трасування шляху",
|
||||||
|
"contacts_pathTraceTo": "Відстежити маршрут до {name}",
|
||||||
|
"contacts_invalidAdvertFormat": "Недійсні контактні дані",
|
||||||
|
"contacts_contactImported": "Контакт було імпортовано.",
|
||||||
|
"contacts_contactImportFailed": "Контакт не вдалося імпортувати",
|
||||||
|
"contacts_zeroHopAdvert": "Реклама без перехоплення",
|
||||||
|
"contacts_floodAdvert": "Залив реклами",
|
||||||
|
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||||
|
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||||
|
"appSettings_languageRu": "Російська",
|
||||||
|
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||||
|
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||||
|
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||||
|
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало",
|
||||||
|
"contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням",
|
||||||
|
"contacts_addContactFromClipboard": "Додати контакт з буфера обміну",
|
||||||
|
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
|
||||||
|
"notification_activityTitle": "Активність MeshCore",
|
||||||
|
"notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}",
|
||||||
|
"notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}",
|
||||||
|
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
|
||||||
|
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
|
||||||
|
"notification_receivedNewMessage": "Отримано нове повідомлення",
|
||||||
|
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
|
||||||
|
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
|
||||||
|
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
|
||||||
|
"settings_gpxExportNoContacts": "Немає контактів для експорту.",
|
||||||
|
"settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі",
|
||||||
|
"settings_gpxExportError": "Сталася помилка під час експорту.",
|
||||||
|
"settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.",
|
||||||
|
"settings_gpxExportAll": "Експортувати всі контакти до GPX",
|
||||||
|
"settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.",
|
||||||
|
"settings_gpxExportContacts": "Експортувати супутників до GPX",
|
||||||
|
"settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати",
|
||||||
|
"settings_gpxExportChat": "Місця супутників",
|
||||||
|
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
|
||||||
|
"settings_gpxExportAllContacts": "Усі місця контактів",
|
||||||
|
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
|
||||||
|
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
|
||||||
|
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
|
||||||
|
"map_runTrace": "Виконати трасування шляху",
|
||||||
|
"pathTrace_clearTooltip": "Очистити шлях",
|
||||||
|
"map_removeLast": "Видалити останній",
|
||||||
|
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||||
|
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||||
|
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||||
|
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||||
|
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||||
|
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||||
|
"settings_clientRepeat": "Автономна система"
|
||||||
|
}
|
||||||
|
|||||||
+576
-513
File diff suppressed because it is too large
Load Diff
+31
-12
@@ -60,21 +60,24 @@ void main() async {
|
|||||||
|
|
||||||
await connector.loadContactCache();
|
await connector.loadContactCache();
|
||||||
await connector.loadChannelSettings();
|
await connector.loadChannelSettings();
|
||||||
|
await connector.loadCachedChannels();
|
||||||
|
|
||||||
// Load persisted channel messages
|
// Load persisted channel messages
|
||||||
await connector.loadAllChannelMessages();
|
await connector.loadAllChannelMessages();
|
||||||
await connector.loadUnreadState();
|
await connector.loadUnreadState();
|
||||||
|
|
||||||
runApp(MeshCoreApp(
|
runApp(
|
||||||
connector: connector,
|
MeshCoreApp(
|
||||||
retryService: retryService,
|
connector: connector,
|
||||||
pathHistoryService: pathHistoryService,
|
retryService: retryService,
|
||||||
storage: storage,
|
pathHistoryService: pathHistoryService,
|
||||||
appSettingsService: appSettingsService,
|
storage: storage,
|
||||||
bleDebugLogService: bleDebugLogService,
|
appSettingsService: appSettingsService,
|
||||||
appDebugLogService: appDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
appDebugLogService: appDebugLogService,
|
||||||
));
|
mapTileCacheService: mapTileCacheService,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MeshCoreApp extends StatelessWidget {
|
class MeshCoreApp extends StatelessWidget {
|
||||||
@@ -124,10 +127,15 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
locale: _localeFromSetting(settingsService.settings.languageOverride),
|
locale: _localeFromSetting(
|
||||||
|
settingsService.settings.languageOverride,
|
||||||
|
),
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
@@ -135,8 +143,19 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
|
themeMode: _themeModeFromSetting(
|
||||||
|
settingsService.settings.themeMode,
|
||||||
|
),
|
||||||
|
builder: (context, child) {
|
||||||
|
// Update notification service with resolved locale
|
||||||
|
final locale = Localizations.localeOf(context);
|
||||||
|
NotificationService().setLocale(locale);
|
||||||
|
return child ?? const SizedBox.shrink();
|
||||||
|
},
|
||||||
home: const ScannerScreen(),
|
home: const ScannerScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,13 +76,14 @@ class AppSettings {
|
|||||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||||
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
||||||
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
||||||
mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
mapTimeFilterHours:
|
||||||
|
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||||
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
||||||
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
||||||
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
||||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||||
),
|
),
|
||||||
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
||||||
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
||||||
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
||||||
@@ -90,11 +91,13 @@ class AppSettings {
|
|||||||
notifyOnNewChannelMessage:
|
notifyOnNewChannelMessage:
|
||||||
json['notify_on_new_channel_message'] as bool? ?? true,
|
json['notify_on_new_channel_message'] as bool? ?? true,
|
||||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||||
autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false,
|
autoRouteRotationEnabled:
|
||||||
|
json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||||
themeMode: json['theme_mode'] as String? ?? 'system',
|
themeMode: json['theme_mode'] as String? ?? 'system',
|
||||||
languageOverride: json['language_override'] as String?,
|
languageOverride: json['language_override'] as String?,
|
||||||
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
||||||
batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map(
|
batteryChemistryByDeviceId:
|
||||||
|
(json['battery_chemistry_by_device_id'] as Map?)?.map(
|
||||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
) ??
|
) ??
|
||||||
{},
|
{},
|
||||||
@@ -132,8 +135,9 @@ class AppSettings {
|
|||||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||||
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
||||||
mapCacheBounds:
|
mapCacheBounds: mapCacheBounds == _unset
|
||||||
mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map<String, double>?,
|
? this.mapCacheBounds
|
||||||
|
: mapCacheBounds as Map<String, double>?,
|
||||||
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
||||||
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
||||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||||
@@ -141,12 +145,15 @@ class AppSettings {
|
|||||||
notifyOnNewChannelMessage:
|
notifyOnNewChannelMessage:
|
||||||
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
|
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
|
||||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||||
autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
autoRouteRotationEnabled:
|
||||||
|
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
languageOverride:
|
languageOverride: languageOverride == _unset
|
||||||
languageOverride == _unset ? this.languageOverride : languageOverride as String?,
|
? this.languageOverride
|
||||||
|
: languageOverride as String?,
|
||||||
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
||||||
batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
batteryChemistryByDeviceId:
|
||||||
|
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ class Channel {
|
|||||||
final int index;
|
final int index;
|
||||||
final String name;
|
final String name;
|
||||||
final Uint8List psk; // 16 bytes
|
final Uint8List psk; // 16 bytes
|
||||||
|
int unreadCount;
|
||||||
|
|
||||||
Channel({
|
Channel({
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.psk,
|
required this.psk,
|
||||||
|
this.unreadCount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get pskHex => _bytesToHex(psk);
|
String get pskHex => _bytesToHex(psk);
|
||||||
@@ -39,11 +41,7 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Channel empty(int index) {
|
static Channel empty(int index) {
|
||||||
return Channel(
|
return Channel(index: index, name: '', psk: Uint8List(16));
|
||||||
index: index,
|
|
||||||
name: '',
|
|
||||||
psk: Uint8List(16),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Channel fromHex(int index, String name, String pskHex) {
|
static Channel fromHex(int index, String name, String pskHex) {
|
||||||
|
|||||||
@@ -59,15 +59,18 @@ class ChannelMessage {
|
|||||||
this.replyToSenderName,
|
this.replyToSenderName,
|
||||||
this.replyToText,
|
this.replyToText,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
}) : messageId = messageId ?? '${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
|
}) : messageId =
|
||||||
reactions = reactions ?? {},
|
messageId ??
|
||||||
pathBytes = pathBytes ?? Uint8List(0),
|
'${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
|
||||||
pathVariants = _mergePathVariants(
|
reactions = reactions ?? {},
|
||||||
pathBytes ?? Uint8List(0),
|
pathBytes = pathBytes ?? Uint8List(0),
|
||||||
pathVariants,
|
pathVariants = _mergePathVariants(
|
||||||
);
|
pathBytes ?? Uint8List(0),
|
||||||
|
pathVariants,
|
||||||
|
);
|
||||||
|
|
||||||
String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null;
|
String? get senderKeyHex =>
|
||||||
|
senderKey != null ? pubKeyToHex(senderKey!) : null;
|
||||||
|
|
||||||
ChannelMessage copyWith({
|
ChannelMessage copyWith({
|
||||||
ChannelMessageStatus? status,
|
ChannelMessageStatus? status,
|
||||||
@@ -125,8 +128,10 @@ class ChannelMessage {
|
|||||||
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
||||||
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
||||||
final hasValidTxtType =
|
final hasValidTxtType =
|
||||||
cursor < data.length && (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
cursor < data.length &&
|
||||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && canFitPath) {
|
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
||||||
|
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
||||||
|
canFitPath) {
|
||||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
||||||
cursor += pathLen;
|
cursor += pathLen;
|
||||||
}
|
}
|
||||||
@@ -162,7 +167,8 @@ class ChannelMessage {
|
|||||||
final potentialSender = text.substring(0, colonIndex);
|
final potentialSender = text.substring(0, colonIndex);
|
||||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||||
senderName = potentialSender;
|
senderName = potentialSender;
|
||||||
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
final offset =
|
||||||
|
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||||
? colonIndex + 2
|
? colonIndex + 2
|
||||||
: colonIndex + 1;
|
: colonIndex + 1;
|
||||||
actualText = text.substring(offset);
|
actualText = text.substring(offset);
|
||||||
@@ -184,7 +190,11 @@ class ChannelMessage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ChannelMessage outgoing(String text, String senderName, int channelIndex) {
|
static ChannelMessage outgoing(
|
||||||
|
String text,
|
||||||
|
String senderName,
|
||||||
|
int channelIndex,
|
||||||
|
) {
|
||||||
return ChannelMessage(
|
return ChannelMessage(
|
||||||
senderKey: null,
|
senderKey: null,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
@@ -249,8 +259,5 @@ class ReplyInfo {
|
|||||||
final String mentionedNode;
|
final String mentionedNode;
|
||||||
final String actualMessage;
|
final String actualMessage;
|
||||||
|
|
||||||
ReplyInfo({
|
ReplyInfo({required this.mentionedNode, required this.actualMessage});
|
||||||
required this.mentionedNode,
|
|
||||||
required this.actualMessage,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ class Community {
|
|||||||
}) : hashtagChannels = hashtagChannels ?? [];
|
}) : hashtagChannels = hashtagChannels ?? [];
|
||||||
|
|
||||||
/// Generate a new community with a random 32-byte secret
|
/// Generate a new community with a random 32-byte secret
|
||||||
factory Community.create({
|
factory Community.create({required String id, required String name}) {
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
}) {
|
|
||||||
final random = Random.secure();
|
final random = Random.secure();
|
||||||
final secret = Uint8List(32);
|
final secret = Uint8List(32);
|
||||||
for (int i = 0; i < 32; i++) {
|
for (int i = 0; i < 32; i++) {
|
||||||
@@ -84,7 +81,8 @@ class Community {
|
|||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
secret: base64Decode(json['secret'] as String),
|
secret: base64Decode(json['secret'] as String),
|
||||||
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
|
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
|
||||||
hashtagChannels: (json['hashtag_channels'] as List<dynamic>?)
|
hashtagChannels:
|
||||||
|
(json['hashtag_channels'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
@@ -234,9 +232,7 @@ class Community {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is Community &&
|
other is Community && runtimeType == other.runtimeType && id == other.id;
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
id == other.id;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => id.hashCode;
|
||||||
|
|||||||
+54
-5
@@ -7,7 +7,8 @@ class Contact {
|
|||||||
final int type;
|
final int type;
|
||||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||||
final Uint8List path; // Path bytes from device
|
final Uint8List path; // Path bytes from device
|
||||||
final int? pathOverride; // User's path override: -1 = force flood, null = auto
|
final int?
|
||||||
|
pathOverride; // User's path override: -1 = force flood, null = auto
|
||||||
final Uint8List? pathOverrideBytes; // User's path override bytes
|
final Uint8List? pathOverrideBytes; // User's path override bytes
|
||||||
final double? latitude;
|
final double? latitude;
|
||||||
final double? longitude;
|
final double? longitude;
|
||||||
@@ -78,8 +79,12 @@ class Contact {
|
|||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
pathLength: pathLength ?? this.pathLength,
|
pathLength: pathLength ?? this.pathLength,
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
pathOverride: clearPathOverride ? null : (pathOverride ?? this.pathOverride),
|
pathOverride: clearPathOverride
|
||||||
pathOverrideBytes: clearPathOverride ? null : (pathOverrideBytes ?? this.pathOverrideBytes),
|
? null
|
||||||
|
: (pathOverride ?? this.pathOverride),
|
||||||
|
pathOverrideBytes: clearPathOverride
|
||||||
|
? null
|
||||||
|
: (pathOverrideBytes ?? this.pathOverrideBytes),
|
||||||
latitude: latitude ?? this.latitude,
|
latitude: latitude ?? this.latitude,
|
||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
lastSeen: lastSeen ?? this.lastSeen,
|
lastSeen: lastSeen ?? this.lastSeen,
|
||||||
@@ -93,15 +98,59 @@ class Contact {
|
|||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
final groupSize = pathHashSize;
|
final groupSize = pathHashSize;
|
||||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||||
final end = (i + groupSize) <= pathBytes.length ? (i + groupSize) : pathBytes.length;
|
final end = (i + groupSize) <= pathBytes.length
|
||||||
|
? (i + groupSize)
|
||||||
|
: pathBytes.length;
|
||||||
final chunk = pathBytes.sublist(i, end);
|
final chunk = pathBytes.sublist(i, end);
|
||||||
parts.add(
|
parts.add(
|
||||||
chunk.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(),
|
chunk
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return parts.join(',');
|
return parts.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get shortPubKeyHex {
|
||||||
|
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? get traceRouteBytes {
|
||||||
|
final pathBytes = _pathBytesForDisplay;
|
||||||
|
Uint8List? traceBytes;
|
||||||
|
|
||||||
|
if (pathLength <= 0) {
|
||||||
|
traceBytes = Uint8List(1);
|
||||||
|
traceBytes[0] = publicKey[0];
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == advTypeRepeater || type == advTypeRoom) {
|
||||||
|
final len = (pathBytes.length + pathBytes.length + 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
traceBytes[pathBytes.length] = publicKey[0];
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pathBytes.length < 2) {
|
||||||
|
return pathBytes[0] == 0 ? null : pathBytes;
|
||||||
|
}
|
||||||
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length - 1) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
Uint8List get _pathBytesForDisplay {
|
Uint8List get _pathBytesForDisplay {
|
||||||
if (pathOverride != null) {
|
if (pathOverride != null) {
|
||||||
if (pathOverride! < 0) return Uint8List(0);
|
if (pathOverride! < 0) return Uint8List(0);
|
||||||
|
|||||||
@@ -2,15 +2,9 @@ class ContactGroup {
|
|||||||
final String name;
|
final String name;
|
||||||
final List<String> memberKeys;
|
final List<String> memberKeys;
|
||||||
|
|
||||||
const ContactGroup({
|
const ContactGroup({required this.name, required this.memberKeys});
|
||||||
required this.name,
|
|
||||||
required this.memberKeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
ContactGroup copyWith({
|
ContactGroup copyWith({String? name, List<String>? memberKeys}) {
|
||||||
String? name,
|
|
||||||
List<String>? memberKeys,
|
|
||||||
}) {
|
|
||||||
return ContactGroup(
|
return ContactGroup(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
memberKeys: memberKeys ?? List<String>.from(this.memberKeys),
|
memberKeys: memberKeys ?? List<String>.from(this.memberKeys),
|
||||||
@@ -18,16 +12,12 @@ class ContactGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'name': name, 'members': memberKeys};
|
||||||
'name': name,
|
|
||||||
'members': memberKeys,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ContactGroup.fromJson(Map<String, dynamic> json) {
|
factory ContactGroup.fromJson(Map<String, dynamic> json) {
|
||||||
final members = (json['members'] as List?)
|
final members =
|
||||||
?.map((value) => value.toString())
|
(json['members'] as List?)?.map((value) => value.toString()).toList() ??
|
||||||
.toList() ??
|
|
||||||
<String>[];
|
<String>[];
|
||||||
return ContactGroup(
|
return ContactGroup(
|
||||||
name: json['name'] as String? ?? '',
|
name: json['name'] as String? ?? '',
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ class Message {
|
|||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
Uint8List? fourByteRoomContactKey,
|
Uint8List? fourByteRoomContactKey,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||||
reactions = reactions ?? {};
|
reactions = reactions ?? {};
|
||||||
|
|
||||||
String get senderKeyHex => pubKeyToHex(senderKey);
|
String get senderKeyHex => pubKeyToHex(senderKey);
|
||||||
|
|
||||||
@@ -80,7 +80,8 @@ class Message {
|
|||||||
pathLength: pathLength ?? this.pathLength,
|
pathLength: pathLength ?? this.pathLength,
|
||||||
pathBytes: pathBytes ?? this.pathBytes,
|
pathBytes: pathBytes ?? this.pathBytes,
|
||||||
reactions: reactions ?? this.reactions,
|
reactions: reactions ?? this.reactions,
|
||||||
fourByteRoomContactKey: fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
fourByteRoomContactKey:
|
||||||
|
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class PathRecord {
|
|||||||
tripTimeMs: json['trip_time_ms'] as int,
|
tripTimeMs: json['trip_time_ms'] as int,
|
||||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||||
wasFloodDiscovery: json['was_flood'] as bool,
|
wasFloodDiscovery: json['was_flood'] as bool,
|
||||||
pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
pathBytes:
|
||||||
|
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
||||||
successCount: json['success_count'] as int? ?? 0,
|
successCount: json['success_count'] as int? ?? 0,
|
||||||
failureCount: json['failure_count'] as int? ?? 0,
|
failureCount: json['failure_count'] as int? ?? 0,
|
||||||
);
|
);
|
||||||
@@ -65,14 +66,15 @@ class ContactPathHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'recent_paths': recentPaths.map((p) => p.toJson()).toList()};
|
||||||
'recent_paths': recentPaths.map((p) => p.toJson()).toList(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ContactPathHistory.fromJson(
|
factory ContactPathHistory.fromJson(
|
||||||
String contactPubKeyHex, Map<String, dynamic> json) {
|
String contactPubKeyHex,
|
||||||
final pathsList = (json['recent_paths'] as List?)
|
Map<String, dynamic> json,
|
||||||
|
) {
|
||||||
|
final pathsList =
|
||||||
|
(json['recent_paths'] as List?)
|
||||||
?.map((p) => PathRecord.fromJson(p as Map<String, dynamic>))
|
?.map((p) => PathRecord.fromJson(p as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|||||||
+183
-29
@@ -59,46 +59,200 @@ class RadioSettings {
|
|||||||
required this.txPowerDbm,
|
required this.txPowerDbm,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preset configurations
|
// Regional preset configurations
|
||||||
static RadioSettings get preset915MHz => RadioSettings(
|
static final List<(String, RadioSettings)> presets = [
|
||||||
frequencyMHz: 915.0,
|
(
|
||||||
bandwidth: LoRaBandwidth.bw125,
|
'Australia',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 915.8,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Australia (Narrow)',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 916.575,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||||
codingRate: LoRaCodingRate.cr4_5,
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
txPowerDbm: 20,
|
txPowerDbm: 20,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
static RadioSettings get preset868MHz => RadioSettings(
|
(
|
||||||
frequencyMHz: 868.0,
|
'Australia SA, WA, QLD',
|
||||||
bandwidth: LoRaBandwidth.bw125,
|
RadioSettings(
|
||||||
|
frequencyMHz: 923.125,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Czech Republic',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.432,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||||
codingRate: LoRaCodingRate.cr4_5,
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
txPowerDbm: 14,
|
txPowerDbm: 14,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
static RadioSettings get preset433MHz => RadioSettings(
|
(
|
||||||
|
'EU 433MHz',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 433.650,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'EU/UK (Long Range)',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.525,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'EU/UK (Medium Range)',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.525,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'EU/UK (Narrow)',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.618,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'New Zealand',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 917.375,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'New Zealand (Narrow)',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 917.375,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Portugal 433',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 433.375,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Portugal 869',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.618,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Switzerland',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 869.618,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'USA Arizona',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 908.205,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'USA/Canada',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 910.525,
|
||||||
|
bandwidth: LoRaBandwidth.bw62_5,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Vietnam',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 920.250,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Off-grid repeat presets (valid client_repeat frequencies)
|
||||||
|
(
|
||||||
|
'Off-Grid 433',
|
||||||
|
RadioSettings(
|
||||||
frequencyMHz: 433.0,
|
frequencyMHz: 433.0,
|
||||||
bandwidth: LoRaBandwidth.bw125,
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
codingRate: LoRaCodingRate.cr4_5,
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
txPowerDbm: 20,
|
txPowerDbm: 20,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
static RadioSettings get presetLongRange => RadioSettings(
|
(
|
||||||
frequencyMHz: 915.0,
|
'Off-Grid 869',
|
||||||
bandwidth: LoRaBandwidth.bw125,
|
RadioSettings(
|
||||||
spreadingFactor: LoRaSpreadingFactor.sf12,
|
frequencyMHz: 869.0,
|
||||||
codingRate: LoRaCodingRate.cr4_8,
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
txPowerDbm: 20,
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
);
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
|
txPowerDbm: 14,
|
||||||
static RadioSettings get presetFastSpeed => RadioSettings(
|
),
|
||||||
frequencyMHz: 915.0,
|
),
|
||||||
bandwidth: LoRaBandwidth.bw500,
|
(
|
||||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
'Off-Grid 918',
|
||||||
|
RadioSettings(
|
||||||
|
frequencyMHz: 918.0,
|
||||||
|
bandwidth: LoRaBandwidth.bw250,
|
||||||
|
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||||
codingRate: LoRaCodingRate.cr4_5,
|
codingRate: LoRaCodingRate.cr4_5,
|
||||||
txPowerDbm: 20,
|
txPowerDbm: 20,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
int get frequencyHz => (frequencyMHz * 1000).round();
|
int get frequencyHz => (frequencyMHz * 1000).round();
|
||||||
int get bandwidthHz => bandwidth.hz;
|
int get bandwidthHz => bandwidth.hz;
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
onPressed: hasEntries
|
onPressed: hasEntries
|
||||||
? () async {
|
? () async {
|
||||||
final text = entries
|
final text = entries
|
||||||
.map((entry) =>
|
.map(
|
||||||
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}')
|
(entry) =>
|
||||||
|
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}',
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
await Clipboard.setData(ClipboardData(text: text));
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -53,7 +55,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
child: hasEntries
|
child: hasEntries
|
||||||
? ListView.separated(
|
? ListView.separated(
|
||||||
itemCount: entries.length,
|
itemCount: entries.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -61,11 +63,17 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
leading: _buildLevelIcon(entry.level),
|
leading: _buildLevelIcon(entry.level),
|
||||||
title: Text(
|
title: Text(
|
||||||
'[${entry.tag}] ${entry.message}',
|
'[${entry.tag}] ${entry.message}',
|
||||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
entry.formattedTime,
|
entry.formattedTime,
|
||||||
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -74,16 +82,26 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.bug_report_outlined, size: 64, color: Colors.grey[400]),
|
Icon(
|
||||||
|
Icons.bug_report_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.debugLog_noEntries,
|
context.l10n.debugLog_noEntries,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.debugLog_enableInSettings,
|
context.l10n.debugLog_enableInSettings,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -99,7 +117,11 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
case AppDebugLogLevel.info:
|
case AppDebugLogLevel.info:
|
||||||
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
|
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
|
||||||
case AppDebugLogLevel.warning:
|
case AppDebugLogLevel.warning:
|
||||||
return const Icon(Icons.warning_amber_outlined, size: 18, color: Colors.orange);
|
return const Icon(
|
||||||
|
Icons.warning_amber_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.orange,
|
||||||
|
);
|
||||||
case AppDebugLogLevel.error:
|
case AppDebugLogLevel.error:
|
||||||
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
|
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppearanceCard(BuildContext context, AppSettingsService settingsService) {
|
Widget _buildAppearanceCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -58,7 +61,9 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.brightness_6_outlined),
|
leading: const Icon(Icons.brightness_6_outlined),
|
||||||
title: Text(context.l10n.appSettings_theme),
|
title: Text(context.l10n.appSettings_theme),
|
||||||
subtitle: Text(_themeModeLabel(context, settingsService.settings.themeMode)),
|
subtitle: Text(
|
||||||
|
_themeModeLabel(context, settingsService.settings.themeMode),
|
||||||
|
),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showThemeModeDialog(context, settingsService),
|
onTap: () => _showThemeModeDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
@@ -66,7 +71,12 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.language_outlined),
|
leading: const Icon(Icons.language_outlined),
|
||||||
title: Text(context.l10n.appSettings_language),
|
title: Text(context.l10n.appSettings_language),
|
||||||
subtitle: Text(_languageLabel(context, settingsService.settings.languageOverride)),
|
subtitle: Text(
|
||||||
|
_languageLabel(
|
||||||
|
context,
|
||||||
|
settingsService.settings.languageOverride,
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showLanguageDialog(context, settingsService),
|
onTap: () => _showLanguageDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
@@ -75,7 +85,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNotificationsCard(BuildContext context, AppSettingsService settingsService) {
|
Widget _buildNotificationsCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -90,17 +103,22 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(Icons.notifications_outlined),
|
secondary: const Icon(Icons.notifications_outlined),
|
||||||
title: Text(context.l10n.appSettings_enableNotifications),
|
title: Text(context.l10n.appSettings_enableNotifications),
|
||||||
subtitle: Text(context.l10n.appSettings_enableNotificationsSubtitle),
|
subtitle: Text(
|
||||||
|
context.l10n.appSettings_enableNotificationsSubtitle,
|
||||||
|
),
|
||||||
value: settingsService.settings.notificationsEnabled,
|
value: settingsService.settings.notificationsEnabled,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value) {
|
if (value) {
|
||||||
// Request permission when enabling
|
// Request permission when enabling
|
||||||
final granted = await NotificationService().requestPermissions();
|
final granted = await NotificationService()
|
||||||
|
.requestPermissions();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.appSettings_notificationPermissionDenied),
|
content: Text(
|
||||||
|
context.l10n.appSettings_notificationPermissionDenied,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -113,9 +131,11 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(value
|
content: Text(
|
||||||
? context.l10n.appSettings_notificationsEnabled
|
value
|
||||||
: context.l10n.appSettings_notificationsDisabled),
|
? context.l10n.appSettings_notificationsEnabled
|
||||||
|
: context.l10n.appSettings_notificationsDisabled,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -126,18 +146,24 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
Icons.message_outlined,
|
Icons.message_outlined,
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
context.l10n.appSettings_messageNotifications,
|
context.l10n.appSettings_messageNotifications,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context.l10n.appSettings_messageNotificationsSubtitle,
|
context.l10n.appSettings_messageNotificationsSubtitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
value: settingsService.settings.notifyOnNewMessage,
|
value: settingsService.settings.notifyOnNewMessage,
|
||||||
@@ -151,18 +177,24 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
Icons.forum_outlined,
|
Icons.forum_outlined,
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
context.l10n.appSettings_channelMessageNotifications,
|
context.l10n.appSettings_channelMessageNotifications,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context.l10n.appSettings_channelMessageNotificationsSubtitle,
|
context.l10n.appSettings_channelMessageNotificationsSubtitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
value: settingsService.settings.notifyOnNewChannelMessage,
|
value: settingsService.settings.notifyOnNewChannelMessage,
|
||||||
@@ -176,18 +208,24 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
Icons.cell_tower,
|
Icons.cell_tower,
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
context.l10n.appSettings_advertisementNotifications,
|
context.l10n.appSettings_advertisementNotifications,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context.l10n.appSettings_advertisementNotificationsSubtitle,
|
context.l10n.appSettings_advertisementNotificationsSubtitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
color: settingsService.settings.notificationsEnabled
|
||||||
|
? null
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
value: settingsService.settings.notifyOnNewAdvert,
|
value: settingsService.settings.notifyOnNewAdvert,
|
||||||
@@ -202,7 +240,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessagingCard(BuildContext context, AppSettingsService settingsService) {
|
Widget _buildMessagingCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -217,15 +258,19 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(Icons.refresh_outlined),
|
secondary: const Icon(Icons.refresh_outlined),
|
||||||
title: Text(context.l10n.appSettings_clearPathOnMaxRetry),
|
title: Text(context.l10n.appSettings_clearPathOnMaxRetry),
|
||||||
subtitle: Text(context.l10n.appSettings_clearPathOnMaxRetrySubtitle),
|
subtitle: Text(
|
||||||
|
context.l10n.appSettings_clearPathOnMaxRetrySubtitle,
|
||||||
|
),
|
||||||
value: settingsService.settings.clearPathOnMaxRetry,
|
value: settingsService.settings.clearPathOnMaxRetry,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
settingsService.setClearPathOnMaxRetry(value);
|
settingsService.setClearPathOnMaxRetry(value);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(value
|
content: Text(
|
||||||
? context.l10n.appSettings_pathsWillBeCleared
|
value
|
||||||
: context.l10n.appSettings_pathsWillNotBeCleared),
|
? context.l10n.appSettings_pathsWillBeCleared
|
||||||
|
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -241,9 +286,11 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
settingsService.setAutoRouteRotationEnabled(value);
|
settingsService.setAutoRouteRotationEnabled(value);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(value
|
content: Text(
|
||||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
value
|
||||||
: context.l10n.appSettings_autoRouteRotationDisabled),
|
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||||
|
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -254,7 +301,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMapSettingsCard(BuildContext context, AppSettingsService settingsService) {
|
Widget _buildMapSettingsCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -302,7 +352,9 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
settingsService.settings.mapTimeFilterHours == 0
|
settingsService.settings.mapTimeFilterHours == 0
|
||||||
? context.l10n.appSettings_timeFilterShowAll
|
? context.l10n.appSettings_timeFilterShowAll
|
||||||
: context.l10n.appSettings_timeFilterShowLast(settingsService.settings.mapTimeFilterHours.toInt()),
|
: context.l10n.appSettings_timeFilterShowLast(
|
||||||
|
settingsService.settings.mapTimeFilterHours.toInt(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showTimeFilterDialog(context, settingsService),
|
onTap: () => _showTimeFilterDialog(context, settingsService),
|
||||||
@@ -332,6 +384,7 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixed rendering issues
|
||||||
Widget _buildBatteryCard(
|
Widget _buildBatteryCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AppSettingsService settingsService,
|
AppSettingsService settingsService,
|
||||||
@@ -339,13 +392,15 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
) {
|
) {
|
||||||
final deviceId = connector.deviceId;
|
final deviceId = connector.deviceId;
|
||||||
final isConnected = connector.isConnected && deviceId != null;
|
final isConnected = connector.isConnected && deviceId != null;
|
||||||
final selection =
|
final selection = isConnected
|
||||||
isConnected ? settingsService.batteryChemistryForDevice(deviceId) : 'nmc';
|
? settingsService.batteryChemistryForDevice(deviceId)
|
||||||
|
: 'nmc';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -353,20 +408,38 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Main tile (icon + text only)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.battery_full),
|
leading: const Icon(Icons.battery_full),
|
||||||
title: Text(context.l10n.appSettings_batteryChemistry),
|
title: Text(context.l10n.appSettings_batteryChemistry),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
isConnected
|
isConnected
|
||||||
? context.l10n.appSettings_batteryChemistryPerDevice(connector.deviceDisplayName)
|
? context.l10n.appSettings_batteryChemistryPerDevice(
|
||||||
|
connector.deviceDisplayName,
|
||||||
|
)
|
||||||
: context.l10n.appSettings_batteryChemistryConnectFirst,
|
: context.l10n.appSettings_batteryChemistryConnectFirst,
|
||||||
),
|
),
|
||||||
trailing: DropdownButton<String>(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
value: selection,
|
),
|
||||||
|
|
||||||
|
// Dropdown (separate full-width row)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
initialValue: selection,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: UnderlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
onChanged: isConnected
|
onChanged: isConnected
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
settingsService.setBatteryChemistryForDevice(deviceId, value);
|
settingsService.setBatteryChemistryForDevice(
|
||||||
|
deviceId,
|
||||||
|
value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -391,7 +464,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showThemeModeDialog(BuildContext context, AppSettingsService settingsService) {
|
void _showThemeModeDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -471,12 +547,19 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
return context.l10n.appSettings_languageSk;
|
return context.l10n.appSettings_languageSk;
|
||||||
case 'bg':
|
case 'bg':
|
||||||
return context.l10n.appSettings_languageBg;
|
return context.l10n.appSettings_languageBg;
|
||||||
|
case 'ru':
|
||||||
|
return context.l10n.appSettings_languageRu;
|
||||||
|
case 'uk':
|
||||||
|
return context.l10n.appSettings_languageUk;
|
||||||
default:
|
default:
|
||||||
return context.l10n.appSettings_languageSystem;
|
return context.l10n.appSettings_languageSystem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showLanguageDialog(BuildContext context, AppSettingsService settingsService) {
|
void _showLanguageDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -547,6 +630,14 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
title: Text(context.l10n.appSettings_languageBg),
|
title: Text(context.l10n.appSettings_languageBg),
|
||||||
value: 'bg',
|
value: 'bg',
|
||||||
),
|
),
|
||||||
|
RadioListTile<String?>(
|
||||||
|
title: Text(context.l10n.appSettings_languageRu),
|
||||||
|
value: 'ru',
|
||||||
|
),
|
||||||
|
RadioListTile<String?>(
|
||||||
|
title: Text(context.l10n.appSettings_languageUk),
|
||||||
|
value: 'uk',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -561,7 +652,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showTimeFilterDialog(BuildContext context, AppSettingsService settingsService) {
|
void _showTimeFilterDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -581,33 +675,23 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.appSettings_allTime),
|
title: Text(context.l10n.appSettings_allTime),
|
||||||
leading: Radio<double>(
|
leading: Radio<double>(value: 0),
|
||||||
value: 0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.appSettings_lastHour),
|
title: Text(context.l10n.appSettings_lastHour),
|
||||||
leading: Radio<double>(
|
leading: Radio<double>(value: 1),
|
||||||
value: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.appSettings_last6Hours),
|
title: Text(context.l10n.appSettings_last6Hours),
|
||||||
leading: Radio<double>(
|
leading: Radio<double>(value: 6),
|
||||||
value: 6,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.appSettings_last24Hours),
|
title: Text(context.l10n.appSettings_last24Hours),
|
||||||
leading: Radio<double>(
|
leading: Radio<double>(value: 24),
|
||||||
value: 24,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.appSettings_lastWeek),
|
title: Text(context.l10n.appSettings_lastWeek),
|
||||||
leading: Radio<double>(
|
leading: Radio<double>(value: 168),
|
||||||
value: 168,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -622,7 +706,10 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDebugCard(BuildContext context, AppSettingsService settingsService) {
|
Widget _buildDebugCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -644,9 +731,11 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(value
|
content: Text(
|
||||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
value
|
||||||
: context.l10n.appSettings_appDebugLoggingDisabled),
|
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||||
|
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
final entries = logService.entries.reversed.toList();
|
final entries = logService.entries.reversed.toList();
|
||||||
final rawEntries = logService.rawLogRxEntries.reversed.toList();
|
final rawEntries = logService.rawLogRxEntries.reversed.toList();
|
||||||
final showingFrames = _view == _BleLogView.frames;
|
final showingFrames = _view == _BleLogView.frames;
|
||||||
final hasEntries = showingFrames ? entries.isNotEmpty : rawEntries.isNotEmpty;
|
final hasEntries = showingFrames
|
||||||
|
? entries.isNotEmpty
|
||||||
|
: rawEntries.isNotEmpty;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.debugLog_bleTitle),
|
title: Text(context.l10n.debugLog_bleTitle),
|
||||||
@@ -36,15 +38,23 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
? () async {
|
? () async {
|
||||||
final text = showingFrames
|
final text = showingFrames
|
||||||
? entries
|
? entries
|
||||||
.map((entry) => '${entry.description}\n${entry.hexPreview}\n')
|
.map(
|
||||||
.join('\n')
|
(entry) =>
|
||||||
|
'${entry.description}\n${entry.hexPreview}\n',
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
: rawEntries
|
: rawEntries
|
||||||
.map((entry) => 'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n')
|
.map(
|
||||||
.join('\n');
|
(entry) =>
|
||||||
|
'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n',
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
await Clipboard.setData(ClipboardData(text: text));
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(context.l10n.debugLog_bleCopied)),
|
SnackBar(
|
||||||
|
content: Text(context.l10n.debugLog_bleCopied),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -68,8 +78,14 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: SegmentedButton<_BleLogView>(
|
child: SegmentedButton<_BleLogView>(
|
||||||
segments: [
|
segments: [
|
||||||
ButtonSegment(value: _BleLogView.frames, label: Text(context.l10n.debugLog_frames)),
|
ButtonSegment(
|
||||||
ButtonSegment(value: _BleLogView.rawLogRx, label: Text(context.l10n.debugLog_rawLogRx)),
|
value: _BleLogView.frames,
|
||||||
|
label: Text(context.l10n.debugLog_frames),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: _BleLogView.rawLogRx,
|
||||||
|
label: Text(context.l10n.debugLog_rawLogRx),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selected: {_view},
|
selected: {_view},
|
||||||
onSelectionChanged: (selection) {
|
onSelectionChanged: (selection) {
|
||||||
@@ -81,8 +97,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: hasEntries
|
child: hasEntries
|
||||||
? ListView.separated(
|
? ListView.separated(
|
||||||
itemCount: showingFrames ? entries.length : rawEntries.length,
|
itemCount: showingFrames
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
? entries.length
|
||||||
|
: rawEntries.length,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (showingFrames) {
|
if (showingFrames) {
|
||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
@@ -94,7 +112,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
subtitle: Text('${entry.hexPreview}\n$time'),
|
subtitle: Text('${entry.hexPreview}\n$time'),
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
entry.outgoing ? Icons.upload : Icons.download,
|
entry.outgoing
|
||||||
|
? Icons.upload
|
||||||
|
: Icons.download,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -131,9 +151,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(info.title),
|
title: Text(info.title),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(child: SelectableText(info.rawHex)),
|
||||||
child: SelectableText(info.rawHex),
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
@@ -195,11 +213,18 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
}
|
}
|
||||||
final payload = raw.sublist(index);
|
final payload = raw.sublist(index);
|
||||||
|
|
||||||
final title = 'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
final title =
|
||||||
|
'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
||||||
final summary = _decodePayloadSummary(payloadType, payload);
|
final summary = _decodePayloadSummary(payloadType, payload);
|
||||||
final pathSummary = pathLen > 0 ? 'Path=${_bytesToHex(pathBytes)}' : 'Path=none';
|
final pathSummary = pathLen > 0
|
||||||
|
? 'Path=${_bytesToHex(pathBytes)}'
|
||||||
|
: 'Path=none';
|
||||||
final detail = '$summary • $pathSummary • len=${raw.length}';
|
final detail = '$summary • $pathSummary • len=${raw.length}';
|
||||||
return _RawPacketInfo(title: title, summary: detail, rawHex: _bytesToHex(raw));
|
return _RawPacketInfo(
|
||||||
|
title: title,
|
||||||
|
summary: detail,
|
||||||
|
rawHex: _bytesToHex(raw),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _decodePayloadSummary(int payloadType, Uint8List payload) {
|
String _decodePayloadSummary(int payloadType, Uint8List payload) {
|
||||||
@@ -245,7 +270,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
return 'ADVERT (short)';
|
return 'ADVERT (short)';
|
||||||
}
|
}
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
final pubKey = _bytesToHex(payload.sublist(offset, offset + 32), spaced: false);
|
final pubKey = _bytesToHex(
|
||||||
|
payload.sublist(offset, offset + 32),
|
||||||
|
spaced: false,
|
||||||
|
);
|
||||||
offset += 32;
|
offset += 32;
|
||||||
final timestamp = readUint32LE(payload, offset);
|
final timestamp = readUint32LE(payload, offset);
|
||||||
offset += 4;
|
offset += 4;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../helpers/link_handler.dart';
|
import '../helpers/link_handler.dart';
|
||||||
|
import '../helpers/reaction_helper.dart';
|
||||||
import '../helpers/utf8_length_limiter.dart';
|
import '../helpers/utf8_length_limiter.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
@@ -26,10 +27,7 @@ import 'map_screen.dart';
|
|||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
const ChannelChatScreen({
|
const ChannelChatScreen({super.key, required this.channel});
|
||||||
super.key,
|
|
||||||
required this.channel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||||
@@ -43,6 +41,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final Map<String, GlobalKey> _messageKeys = {};
|
final Map<String, GlobalKey> _messageKeys = {};
|
||||||
bool _isLoadingOlder = false;
|
bool _isLoadingOlder = false;
|
||||||
|
|
||||||
|
MeshCoreConnector? _connector;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -50,7 +50,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.read<MeshCoreConnector>().setActiveChannel(widget.channel.index);
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
_connector?.setActiveChannel(widget.channel.index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
context.read<MeshCoreConnector>().setActiveChannel(null);
|
_connector?.setActiveChannel(null);
|
||||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||||
_textFieldFocusNode.dispose();
|
_textFieldFocusNode.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
@@ -134,15 +135,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.channel.name.isEmpty
|
widget.channel.name.isEmpty
|
||||||
? context.l10n.channels_channelIndex(widget.channel.index)
|
? context.l10n.channels_channelIndex(
|
||||||
|
widget.channel.index,
|
||||||
|
)
|
||||||
: widget.channel.name,
|
: widget.channel.name,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
Consumer<MeshCoreConnector>(
|
Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final unreadCount =
|
final unreadCount = connector
|
||||||
connector.getUnreadCountForChannelIndex(widget.channel.index);
|
.getUnreadCountForChannelIndex(widget.channel.index);
|
||||||
final privacy = widget.channel.isPublicChannel ? context.l10n.channels_public : context.l10n.channels_private;
|
final privacy = widget.channel.isPublicChannel
|
||||||
|
? context.l10n.channels_public
|
||||||
|
: context.l10n.channels_private;
|
||||||
return Text(
|
return Text(
|
||||||
'$privacy • ${context.l10n.chat_unread(unreadCount)}',
|
'$privacy • ${context.l10n.chat_unread(unreadCount)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -201,7 +206,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
// Reverse messages so newest appear at bottom with reverse: true
|
// Reverse messages so newest appear at bottom with reverse: true
|
||||||
final reversedMessages = messages.reversed.toList();
|
final reversedMessages = messages.reversed.toList();
|
||||||
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
|
final itemCount =
|
||||||
|
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
|
||||||
|
|
||||||
// Auto-scroll to bottom if user is already at bottom
|
// Auto-scroll to bottom if user is already at bottom
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -224,7 +230,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -240,9 +248,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
JumpToBottomButton(
|
JumpToBottomButton(scrollController: _scrollController),
|
||||||
scrollController: _scrollController,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -261,15 +267,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final poi = _parsePoiMessage(message.text);
|
final poi = _parsePoiMessage(message.text);
|
||||||
final displayPath = message.pathBytes.isNotEmpty
|
final displayPath = message.pathBytes.isNotEmpty
|
||||||
? message.pathBytes
|
? message.pathBytes
|
||||||
: (message.pathVariants.isNotEmpty ? message.pathVariants.first : Uint8List(0));
|
: (message.pathVariants.isNotEmpty
|
||||||
|
? message.pathVariants.first
|
||||||
|
: Uint8List(0));
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
crossAxisAlignment: isOutgoing
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start,
|
mainAxisAlignment: isOutgoing
|
||||||
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isOutgoing) ...[
|
if (!isOutgoing) ...[
|
||||||
@@ -281,128 +293,160 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
onTap: () => _showMessagePathInfo(message),
|
onTap: () => _showMessagePathInfo(message),
|
||||||
onLongPress: () => _showMessageActions(message),
|
onLongPress: () => _showMessageActions(message),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: gifId != null
|
padding: gifId != null
|
||||||
? const EdgeInsets.all(4)
|
? const EdgeInsets.all(4)
|
||||||
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
: const EdgeInsets.symmetric(
|
||||||
constraints: BoxConstraints(
|
horizontal: 12,
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
vertical: 8,
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isOutgoing
|
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
|
||||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (!isOutgoing) ...[
|
|
||||||
Padding(
|
|
||||||
padding: gifId != null
|
|
||||||
? const EdgeInsets.only(left: 8, top: 4, bottom: 4)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: Text(
|
|
||||||
message.senderName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
constraints: BoxConstraints(
|
||||||
),
|
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||||
if (gifId == null) const SizedBox(height: 4),
|
),
|
||||||
],
|
decoration: BoxDecoration(
|
||||||
if (message.replyToMessageId != null) ...[
|
color: isOutgoing
|
||||||
_buildReplyPreview(message),
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
const SizedBox(height: 8),
|
: Theme.of(
|
||||||
],
|
context,
|
||||||
if (poi != null)
|
).colorScheme.surfaceContainerHighest,
|
||||||
_buildPoiMessage(context, poi, isOutgoing)
|
borderRadius: BorderRadius.circular(12),
|
||||||
else if (gifId != null)
|
),
|
||||||
ClipRRect(
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(8),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: GifMessage(
|
children: [
|
||||||
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
|
if (!isOutgoing) ...[
|
||||||
backgroundColor: Colors.transparent,
|
Padding(
|
||||||
fallbackTextColor: isOutgoing
|
padding: gifId != null
|
||||||
? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7)
|
? const EdgeInsets.only(
|
||||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
left: 8,
|
||||||
),
|
top: 4,
|
||||||
)
|
bottom: 4,
|
||||||
else
|
)
|
||||||
Linkify(
|
: EdgeInsets.zero,
|
||||||
text: message.text,
|
child: Text(
|
||||||
style: const TextStyle(fontSize: 14),
|
message.senderName,
|
||||||
linkStyle: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: Colors.green,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: TextDecoration.underline,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
options: const LinkifyOptions(
|
|
||||||
humanize: false,
|
|
||||||
defaultToHttps: false,
|
|
||||||
),
|
|
||||||
linkifiers: const [UrlLinkifier()],
|
|
||||||
onOpen: (link) => LinkHandler.handleLinkTap(context, link.url),
|
|
||||||
),
|
|
||||||
if (displayPath.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
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),
|
|
||||||
Padding(
|
|
||||||
padding: gifId != null
|
|
||||||
? const EdgeInsets.only(left: 8, right: 8, bottom: 4)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_formatTime(message.timestamp),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.repeatCount > 0) ...[
|
if (gifId == null) const SizedBox(height: 4),
|
||||||
const SizedBox(width: 6),
|
],
|
||||||
Icon(Icons.repeat, size: 12, color: Colors.grey[600]),
|
if (message.replyToMessageId != null) ...[
|
||||||
const SizedBox(width: 2),
|
_buildReplyPreview(message),
|
||||||
Text(
|
const SizedBox(height: 8),
|
||||||
'${message.repeatCount}',
|
],
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
if (poi != null)
|
||||||
|
_buildPoiMessage(context, poi, isOutgoing)
|
||||||
|
else if (gifId != null)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: GifMessage(
|
||||||
|
url:
|
||||||
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
fallbackTextColor: isOutgoing
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer
|
||||||
|
.withValues(alpha: 0.7)
|
||||||
|
: Theme.of(context).colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
if (isOutgoing) ...[
|
else
|
||||||
const SizedBox(width: 4),
|
Linkify(
|
||||||
Icon(
|
text: message.text,
|
||||||
message.status == ChannelMessageStatus.sent
|
style: const TextStyle(fontSize: 14),
|
||||||
? Icons.check
|
linkStyle: const TextStyle(
|
||||||
: message.status == ChannelMessageStatus.pending
|
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),
|
||||||
|
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),
|
||||||
|
Padding(
|
||||||
|
padding: gifId != null
|
||||||
|
? const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 4,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: 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),
|
||||||
|
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.schedule
|
||||||
: Icons.error_outline,
|
: Icons.error_outline,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: message.status == ChannelMessageStatus.failed
|
color:
|
||||||
? Colors.red
|
message.status ==
|
||||||
: Colors.grey[600],
|
ChannelMessageStatus.failed
|
||||||
),
|
? Colors.red
|
||||||
],
|
: Colors.grey[600],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (message.reactions.isNotEmpty) ...[
|
if (message.reactions.isNotEmpty) ...[
|
||||||
@@ -443,7 +487,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(Icons.location_on_outlined, size: 14, color: previewTextColor),
|
Icon(Icons.location_on_outlined, size: 14, color: previewTextColor),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(context.l10n.chat_location, style: TextStyle(fontSize: 12, color: previewTextColor)),
|
Text(
|
||||||
|
context.l10n.chat_location,
|
||||||
|
style: TextStyle(fontSize: 12, color: previewTextColor),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -467,10 +514,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(
|
left: BorderSide(color: colorScheme.primary, width: 3),
|
||||||
color: colorScheme.primary,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -508,17 +552,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.outline.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(emoji, style: const TextStyle(fontSize: 16)),
|
||||||
emoji,
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
if (count > 1) ...[
|
if (count > 1) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -545,7 +588,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
_PoiInfo? _parsePoiMessage(String text) {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final match = RegExp(r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|').firstMatch(trimmed);
|
final match = RegExp(
|
||||||
|
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|',
|
||||||
|
).firstMatch(trimmed);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
final lat = double.tryParse(match.group(1) ?? '');
|
final lat = double.tryParse(match.group(1) ?? '');
|
||||||
final lon = double.tryParse(match.group(2) ?? '');
|
final lon = double.tryParse(match.group(2) ?? '');
|
||||||
@@ -556,10 +601,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
|
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final textColor =
|
final textColor = isOutgoing
|
||||||
isOutgoing ? colorScheme.onPrimaryContainer : colorScheme.onSurface;
|
? colorScheme.onPrimaryContainer
|
||||||
|
: colorScheme.onSurface;
|
||||||
final metaColor = textColor.withValues(alpha: 0.7);
|
final metaColor = textColor.withValues(alpha: 0.7);
|
||||||
final channelColor = widget.channel.isPublicChannel ? Colors.orange : Colors.blue;
|
final channelColor = widget.channel.isPublicChannel
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.blue;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -587,18 +635,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_poiShared,
|
context.l10n.chat_poiShared,
|
||||||
style: TextStyle(
|
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||||
color: textColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (poi.label.isNotEmpty)
|
if (poi.label.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
poi.label,
|
poi.label,
|
||||||
style: TextStyle(
|
style: TextStyle(color: metaColor, fontSize: 12),
|
||||||
color: metaColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -675,10 +717,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -707,7 +746,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -745,73 +786,76 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.gif_box),
|
icon: const Icon(Icons.gif_box),
|
||||||
onPressed: () => _showGifPicker(context),
|
onPressed: () => _showGifPicker(context),
|
||||||
tooltip: context.l10n.chat_sendGif,
|
tooltip: context.l10n.chat_sendGif,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<TextEditingValue>(
|
child: ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: _textController,
|
valueListenable: _textController,
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
final gifId = _parseGifId(value.text);
|
final gifId = _parseGifId(value.text);
|
||||||
if (gifId != null) {
|
if (gifId != null) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: GifMessage(
|
child: GifMessage(
|
||||||
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
|
url:
|
||||||
backgroundColor:
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
backgroundColor: Theme.of(
|
||||||
fallbackTextColor:
|
context,
|
||||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
).colorScheme.surfaceContainerHighest,
|
||||||
maxSize: 160,
|
fallbackTextColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
maxSize: 160,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => _textController.clear(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextField(
|
||||||
|
controller: _textController,
|
||||||
|
focusNode: _textFieldFocusNode,
|
||||||
|
inputFormatters: [
|
||||||
|
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||||
|
],
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.l10n.chat_typeMessage,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
maxLines: null,
|
||||||
IconButton(
|
textInputAction: TextInputAction.send,
|
||||||
icon: const Icon(Icons.close),
|
onSubmitted: (_) => _sendMessage(),
|
||||||
onPressed: () => _textController.clear(),
|
);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
);
|
),
|
||||||
}
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
return TextField(
|
icon: const Icon(Icons.send),
|
||||||
controller: _textController,
|
onPressed: _sendMessage,
|
||||||
focusNode: _textFieldFocusNode,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
inputFormatters: [
|
),
|
||||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
],
|
||||||
],
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.l10n.chat_typeMessage,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
textInputAction: TextInputAction.send,
|
|
||||||
onSubmitted: (_) => _sendMessage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.send),
|
|
||||||
onPressed: _sendMessage,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -877,14 +921,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
_setReplyingTo(message);
|
_setReplyingTo(message);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
// Can't react to your own messages
|
||||||
leading: const Icon(Icons.add_reaction_outlined),
|
if (!message.isOutgoing)
|
||||||
title: Text(context.l10n.chat_addReaction),
|
ListTile(
|
||||||
onTap: () {
|
leading: const Icon(Icons.add_reaction_outlined),
|
||||||
Navigator.pop(sheetContext);
|
title: Text(context.l10n.chat_addReaction),
|
||||||
_showEmojiPicker(message);
|
onTap: () {
|
||||||
},
|
Navigator.pop(sheetContext);
|
||||||
),
|
_showEmojiPicker(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
title: Text(context.l10n.common_copy),
|
title: Text(context.l10n.common_copy),
|
||||||
@@ -926,25 +972,31 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
void _sendReaction(ChannelMessage message, String emoji) {
|
void _sendReaction(ChannelMessage message, String emoji) {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
// Send reaction with full messageId to find target, but parser will extract
|
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
|
||||||
// lightweight reactionKey (timestamp_senderPrefix) for deduplication
|
if (emojiIndex == null) return; // Unknown emoji, skip
|
||||||
final reactionText = 'r:${message.messageId}:$emoji';
|
final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
final hash = ReactionHelper.computeReactionHash(
|
||||||
|
timestampSecs,
|
||||||
|
message.senderName,
|
||||||
|
message.text,
|
||||||
|
);
|
||||||
|
final reactionText = 'r:$hash:$emojiIndex';
|
||||||
connector.sendChannelMessage(widget.channel, reactionText);
|
connector.sendChannelMessage(widget.channel, reactionText);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _copyMessageText(String text) {
|
void _copyMessageText(String text) {
|
||||||
Clipboard.setData(ClipboardData(text: text));
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(context.l10n.chat_messageCopied)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteMessage(ChannelMessage message) async {
|
Future<void> _deleteMessage(ChannelMessage message) async {
|
||||||
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
|
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(context.l10n.chat_messageDeleted)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
@@ -959,9 +1011,5 @@ class _PoiInfo {
|
|||||||
final double lon;
|
final double lon;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
const _PoiInfo({
|
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||||
required this.lat,
|
|
||||||
required this.lon,
|
|
||||||
required this.label,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -17,17 +18,17 @@ import '../models/contact.dart';
|
|||||||
class ChannelMessagePathScreen extends StatelessWidget {
|
class ChannelMessagePathScreen extends StatelessWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
|
|
||||||
const ChannelMessagePathScreen({
|
const ChannelMessagePathScreen({super.key, required this.message});
|
||||||
super.key,
|
|
||||||
required this.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants);
|
final primaryPath = _selectPrimaryPath(
|
||||||
|
message.pathBytes,
|
||||||
|
message.pathVariants,
|
||||||
|
);
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
@@ -41,6 +42,21 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.channelPath_title),
|
title: Text(l10n.channelPath_title),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.radar_outlined),
|
||||||
|
tooltip: l10n.channelPath_viewMap,
|
||||||
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
|
path: Uint8List.fromList(primaryPath),
|
||||||
|
flipPathRound: true,
|
||||||
|
reversePathRound: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.map_outlined),
|
icon: const Icon(Icons.map_outlined),
|
||||||
tooltip: l10n.channelPath_viewMap,
|
tooltip: l10n.channelPath_viewMap,
|
||||||
@@ -88,10 +104,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSummaryCard(
|
Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) {
|
||||||
BuildContext context, {
|
|
||||||
String? observedLabel,
|
|
||||||
}) {
|
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -105,21 +118,28 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
|
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
|
||||||
_buildDetailRow(l10n.channelPath_timeLabel, _formatTime(message.timestamp, l10n)),
|
_buildDetailRow(
|
||||||
|
l10n.channelPath_timeLabel,
|
||||||
|
_formatTime(message.timestamp, l10n),
|
||||||
|
),
|
||||||
if (message.repeatCount > 0)
|
if (message.repeatCount > 0)
|
||||||
_buildDetailRow(l10n.channelPath_repeatsLabel, message.repeatCount.toString()),
|
_buildDetailRow(
|
||||||
_buildDetailRow(l10n.channelPath_pathLabelTitle, _formatPathLabel(message.pathLength, l10n)),
|
l10n.channelPath_repeatsLabel,
|
||||||
if (observedLabel != null) _buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
|
message.repeatCount.toString(),
|
||||||
|
),
|
||||||
|
_buildDetailRow(
|
||||||
|
l10n.channelPath_pathLabelTitle,
|
||||||
|
_formatPathLabel(message.pathLength, l10n),
|
||||||
|
),
|
||||||
|
if (observedLabel != null)
|
||||||
|
_buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPathVariants(
|
Widget _buildPathVariants(BuildContext context, List<Uint8List> variants) {
|
||||||
BuildContext context,
|
|
||||||
List<Uint8List> variants,
|
|
||||||
) {
|
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -163,7 +183,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
hop.hasLocation
|
hop.hasLocation
|
||||||
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
||||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||||
: l10n.channelPath_noLocationData,
|
: l10n.channelPath_noLocationData,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -239,7 +259,6 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||||
@@ -257,8 +276,10 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
|||||||
_ChannelMessagePathMapScreenState();
|
_ChannelMessagePathMapScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScreen> {
|
class _ChannelMessagePathMapScreenState
|
||||||
|
extends State<ChannelMessagePathMapScreen> {
|
||||||
Uint8List? _selectedPath;
|
Uint8List? _selectedPath;
|
||||||
|
double _pathDistance = 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -270,32 +291,58 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
void didUpdateWidget(ChannelMessagePathMapScreen oldWidget) {
|
void didUpdateWidget(ChannelMessagePathMapScreen oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.message != widget.message ||
|
if (oldWidget.message != widget.message ||
|
||||||
!_pathsEqual(oldWidget.initialPath ?? Uint8List(0),
|
!_pathsEqual(
|
||||||
widget.initialPath ?? Uint8List(0))) {
|
oldWidget.initialPath ?? Uint8List(0),
|
||||||
|
widget.initialPath ?? Uint8List(0),
|
||||||
|
)) {
|
||||||
_selectedPath = widget.initialPath;
|
_selectedPath = widget.initialPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _getPathDistance(List<LatLng> points) {
|
||||||
|
double totalDistance = 0.0;
|
||||||
|
final distanceCalculator = Distance();
|
||||||
|
|
||||||
|
for (int i = 0; i < points.length - 1; i++) {
|
||||||
|
totalDistance += distanceCalculator(points[i], points[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDistance;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final primaryPath =
|
final primaryPath = _selectPrimaryPath(
|
||||||
_selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants);
|
widget.message.pathBytes,
|
||||||
final observedPaths =
|
widget.message.pathVariants,
|
||||||
_buildObservedPaths(primaryPath, widget.message.pathVariants);
|
);
|
||||||
|
final observedPaths = _buildObservedPaths(
|
||||||
|
primaryPath,
|
||||||
|
widget.message.pathVariants,
|
||||||
|
);
|
||||||
final selectedPath = _resolveSelectedPath(
|
final selectedPath = _resolveSelectedPath(
|
||||||
_selectedPath,
|
_selectedPath,
|
||||||
observedPaths,
|
observedPaths,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
);
|
);
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(selectedPath, connector.contacts, context.l10n);
|
final hops = _buildPathHops(
|
||||||
final points = hops
|
selectedPath,
|
||||||
.where((hop) => hop.hasLocation)
|
connector.contacts,
|
||||||
.map((hop) => hop.position!)
|
context.l10n,
|
||||||
.toList();
|
);
|
||||||
|
|
||||||
|
final points = <LatLng>[];
|
||||||
|
for (final hop in hops) {
|
||||||
|
if (hop.hasLocation) {
|
||||||
|
points.add(hop.position!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
|
||||||
final polylines = points.length > 1
|
final polylines = points.length > 1
|
||||||
? [
|
? [
|
||||||
Polyline(
|
Polyline(
|
||||||
@@ -306,16 +353,20 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
]
|
]
|
||||||
: <Polyline>[];
|
: <Polyline>[];
|
||||||
|
|
||||||
final initialCenter =
|
final initialCenter = points.isNotEmpty
|
||||||
points.isNotEmpty ? points.first : const LatLng(0, 0);
|
? points.first
|
||||||
|
: const LatLng(0, 0);
|
||||||
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
||||||
final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null;
|
final bounds = points.length > 1
|
||||||
final mapKey = ValueKey(_formatPathPrefixes(selectedPath));
|
? LatLngBounds.fromPoints(points)
|
||||||
|
: null;
|
||||||
|
final mapKey = ValueKey(
|
||||||
|
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}',
|
||||||
|
);
|
||||||
|
_pathDistance = _getPathDistance(points);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
|
||||||
title: Text(context.l10n.channelPath_mapTitle),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -334,6 +385,9 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
),
|
),
|
||||||
minZoom: 2.0,
|
minZoom: 2.0,
|
||||||
maxZoom: 18.0,
|
maxZoom: 18.0,
|
||||||
|
interactionOptions: InteractionOptions(
|
||||||
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -343,30 +397,28 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
MapTileCacheService.userAgentPackageName,
|
MapTileCacheService.userAgentPackageName,
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
),
|
),
|
||||||
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
|
if (polylines.isNotEmpty)
|
||||||
MarkerLayer(
|
PolylineLayer(polylines: polylines),
|
||||||
markers: _buildHopMarkers(hops),
|
MarkerLayer(markers: _buildHopMarkers(hops)),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (observedPaths.length > 1)
|
if (observedPaths.length > 1)
|
||||||
_buildPathSelector(
|
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||||
context,
|
index,
|
||||||
observedPaths,
|
) {
|
||||||
selectedIndex,
|
setState(() {
|
||||||
(index) {
|
_selectedPath = observedPaths[index].pathBytes;
|
||||||
setState(() {
|
});
|
||||||
_selectedPath = observedPaths[index].pathBytes;
|
}),
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (points.isEmpty)
|
if (points.isEmpty)
|
||||||
Center(
|
Center(
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(12),
|
padding: EdgeInsets.all(12),
|
||||||
child: Text(context.l10n.channelPath_noRepeaterLocations),
|
child: Text(
|
||||||
|
context.l10n.channelPath_noRepeaterLocations,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -448,8 +500,8 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
if (hop.hasLocation)
|
if (hop.hasLocation)
|
||||||
Marker(
|
Marker(
|
||||||
point: hop.position!,
|
point: hop.position!,
|
||||||
width: 40,
|
width: 35,
|
||||||
height: 40,
|
height: 35,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
@@ -474,6 +526,39 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
||||||
|
context.read<MeshCoreConnector>().selfLongitude != null)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
context.read<MeshCoreConnector>().selfLatitude!,
|
||||||
|
context.read<MeshCoreConnector>().selfLongitude!,
|
||||||
|
),
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.teal,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
context.l10n.pathTrace_you,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +581,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.channelPath_repeaterHops,
|
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -509,7 +594,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
itemCount: hops.length,
|
itemCount: hops.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final hop = hops[index];
|
final hop = hops[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -525,7 +610,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
|||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
hop.hasLocation
|
hop.hasLocation
|
||||||
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
||||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||||
: l10n.channelPath_noLocationData,
|
: l10n.channelPath_noLocationData,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -567,10 +652,7 @@ class _ObservedPath {
|
|||||||
final Uint8List pathBytes;
|
final Uint8List pathBytes;
|
||||||
final bool isPrimary;
|
final bool isPrimary;
|
||||||
|
|
||||||
const _ObservedPath({
|
const _ObservedPath({required this.pathBytes, required this.isPrimary});
|
||||||
required this.pathBytes,
|
|
||||||
required this.isPrimary,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_PathHop> _buildPathHops(
|
List<_PathHop> _buildPathHops(
|
||||||
@@ -597,10 +679,12 @@ List<_PathHop> _buildPathHops(
|
|||||||
|
|
||||||
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
|
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
|
||||||
final matches = contacts
|
final matches = contacts
|
||||||
.where((contact) =>
|
.where(
|
||||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
(contact) =>
|
||||||
contact.publicKey.isNotEmpty &&
|
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||||
contact.publicKey[0] == prefix)
|
contact.publicKey.isNotEmpty &&
|
||||||
|
contact.publicKey[0] == prefix,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (matches.isEmpty) return null;
|
if (matches.isEmpty) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -121,30 +121,52 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (_communities.isNotEmpty)
|
PopupMenuButton(
|
||||||
IconButton(
|
itemBuilder: (context) => [
|
||||||
icon: const Icon(Icons.groups),
|
PopupMenuItem(
|
||||||
tooltip: context.l10n.community_manageCommunities,
|
child: Row(
|
||||||
onPressed: () => _showManageCommunitiesDialog(context),
|
children: [
|
||||||
),
|
const Icon(Icons.logout, color: Colors.red),
|
||||||
IconButton(
|
const SizedBox(width: 8),
|
||||||
icon: const Icon(Icons.bluetooth_disabled),
|
Text(context.l10n.common_disconnect),
|
||||||
tooltip: context.l10n.common_disconnect,
|
],
|
||||||
onPressed: () => _disconnect(context),
|
),
|
||||||
),
|
onTap: () => _disconnect(context),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.tune),
|
if (_communities.isNotEmpty)
|
||||||
tooltip: context.l10n.common_settings,
|
PopupMenuItem(
|
||||||
onPressed: () => Navigator.push(
|
child: Row(
|
||||||
context,
|
children: [
|
||||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
const Icon(Icons.groups),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.community_manageCommunities),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _showManageCommunitiesDialog(context),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.settings_title),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await context.read<MeshCoreConnector>().getChannels();
|
await context.read<MeshCoreConnector>().getChannels(force: true);
|
||||||
},
|
},
|
||||||
child: () {
|
child: () {
|
||||||
if (connector.isLoadingChannels) {
|
if (connector.isLoadingChannels) {
|
||||||
@@ -931,7 +953,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
dialogContext.l10n.community_communityHashtag,
|
dialogContext.l10n.community_communityHashtag,
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
dialogContext.l10n.community_communityHashtagDesc,
|
dialogContext
|
||||||
|
.l10n
|
||||||
|
.community_communityHashtagDesc,
|
||||||
),
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
@@ -1026,10 +1050,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
if (hashtag.startsWith('#')) {
|
if (hashtag.startsWith('#')) {
|
||||||
hashtag = hashtag.substring(1);
|
hashtag = hashtag.substring(1);
|
||||||
}
|
}
|
||||||
final channelName = '#$hashtag';
|
final String channelName;
|
||||||
|
|
||||||
final Uint8List psk;
|
final Uint8List psk;
|
||||||
if (isRegularHashtag) {
|
if (isRegularHashtag) {
|
||||||
|
channelName = '#$hashtag';
|
||||||
// Regular hashtag - public derivation using SHA256
|
// Regular hashtag - public derivation using SHA256
|
||||||
psk = Channel.derivePskFromHashtag(hashtag);
|
psk = Channel.derivePskFromHashtag(hashtag);
|
||||||
} else {
|
} else {
|
||||||
@@ -1048,6 +1073,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
channelName =
|
||||||
|
'${selectedCommunity!.name} #$hashtag';
|
||||||
psk = selectedCommunity!
|
psk = selectedCommunity!
|
||||||
.deriveCommunityHashtagPsk(hashtag);
|
.deriveCommunityHashtagPsk(hashtag);
|
||||||
// Track in community's hashtag list
|
// Track in community's hashtag list
|
||||||
|
|||||||
+376
-237
@@ -6,11 +6,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../helpers/reaction_helper.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../helpers/link_handler.dart';
|
import '../helpers/link_handler.dart';
|
||||||
import '../helpers/utf8_length_limiter.dart';
|
import '../helpers/utf8_length_limiter.dart';
|
||||||
@@ -43,6 +45,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final _scrollController = ChatScrollController();
|
final _scrollController = ChatScrollController();
|
||||||
final _textFieldFocusNode = FocusNode();
|
final _textFieldFocusNode = FocusNode();
|
||||||
bool _isLoadingOlder = false;
|
bool _isLoadingOlder = false;
|
||||||
|
MeshCoreConnector? _connector;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -51,7 +54,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.read<MeshCoreConnector>().setActiveContact(widget.contact.publicKeyHex);
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
_connector?.setActiveContact(widget.contact.publicKeyHex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +79,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
context.read<MeshCoreConnector>().setActiveContact(null);
|
_connector?.setActiveContact(null);
|
||||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||||
_textFieldFocusNode.dispose();
|
_textFieldFocusNode.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
@@ -90,12 +94,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
title: Consumer2<PathHistoryService, MeshCoreConnector>(
|
title: Consumer2<PathHistoryService, MeshCoreConnector>(
|
||||||
builder: (context, pathService, connector, _) {
|
builder: (context, pathService, connector, _) {
|
||||||
final contact = _resolveContact(connector);
|
final contact = _resolveContact(connector);
|
||||||
final unreadCount = connector.getUnreadCountForContactKey(widget.contact.publicKeyHex);
|
final unreadCount = connector.getUnreadCountForContactKey(
|
||||||
|
widget.contact.publicKeyHex,
|
||||||
|
);
|
||||||
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
||||||
final pathLabel = _currentPathLabel(contact);
|
final pathLabel = _currentPathLabel(contact);
|
||||||
|
|
||||||
// Show path details if we have path data (from device or override)
|
// Show path details if we have path data (from device or override)
|
||||||
final hasPathData = contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
final hasPathData =
|
||||||
|
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
||||||
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -105,7 +112,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Text(contact.name),
|
Text(contact.name),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: hasPathData ? () => _showFullPathDialog(context, effectivePath) : null,
|
onTap: hasPathData
|
||||||
|
? () => _showFullPathDialog(context, effectivePath)
|
||||||
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$pathLabel • $unreadLabel',
|
'$pathLabel • $unreadLabel',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -143,12 +152,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
value: 'auto',
|
value: 'auto',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.auto_mode,
|
||||||
|
size: 20,
|
||||||
|
color: !isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_autoUseSavedPath,
|
context.l10n.chat_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: !isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -158,12 +175,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
value: 'flood',
|
value: 'flood',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.waves,
|
||||||
|
size: 20,
|
||||||
|
color: isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_forceFloodMode,
|
context.l10n.chat_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -195,9 +220,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
messages.isEmpty
|
messages.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: _buildMessageList(messages, connector),
|
: _buildMessageList(messages, connector),
|
||||||
JumpToBottomButton(
|
JumpToBottomButton(scrollController: _scrollController),
|
||||||
scrollController: _scrollController,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -230,7 +253,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageList(List<Message> messages, MeshCoreConnector connector) {
|
Widget _buildMessageList(
|
||||||
|
List<Message> messages,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) {
|
||||||
// Reverse messages so newest appear at bottom with reverse: true
|
// Reverse messages so newest appear at bottom with reverse: true
|
||||||
final reversedMessages = messages.reversed.toList();
|
final reversedMessages = messages.reversed.toList();
|
||||||
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
|
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
|
||||||
@@ -266,14 +292,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (widget.contact.type == advTypeRoom) {
|
if (widget.contact.type == advTypeRoom) {
|
||||||
contact = _resolveContactFrom4Bytes(
|
contact = _resolveContactFrom4Bytes(
|
||||||
connector,
|
connector,
|
||||||
message.fourByteRoomContactKey.isEmpty ? Uint8List.fromList([0, 0, 0, 0]) : message.fourByteRoomContactKey,
|
message.fourByteRoomContactKey.isEmpty
|
||||||
|
? Uint8List.fromList([0, 0, 0, 0])
|
||||||
|
: message.fourByteRoomContactKey,
|
||||||
);
|
);
|
||||||
fourByteHex = message.fourByteRoomContactKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase();
|
fourByteHex = message.fourByteRoomContactKey
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join()
|
||||||
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _MessageBubble(
|
return _MessageBubble(
|
||||||
message: message,
|
message: message,
|
||||||
senderName: widget.contact.type == advTypeRoom ? "${contact.name} [$fourByteHex]" : contact.name,
|
senderName: widget.contact.type == advTypeRoom
|
||||||
|
? "${contact.name} [$fourByteHex]"
|
||||||
|
: contact.name,
|
||||||
isRoomServer: widget.contact.type == advTypeRoom,
|
isRoomServer: widget.contact.type == advTypeRoom,
|
||||||
onTap: () => _openMessagePath(message, contact),
|
onTap: () => _openMessagePath(message, contact),
|
||||||
onLongPress: () => _showMessageActions(message, contact),
|
onLongPress: () => _showMessageActions(message, contact),
|
||||||
@@ -289,9 +322,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.surface,
|
color: colorScheme.surface,
|
||||||
border: Border(
|
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
|
||||||
top: BorderSide(color: Theme.of(context).dividerColor),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -313,10 +344,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: GifMessage(
|
child: GifMessage(
|
||||||
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
|
url:
|
||||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
fallbackTextColor:
|
backgroundColor:
|
||||||
colorScheme.onSurface.withValues(alpha: 0.6),
|
colorScheme.surfaceContainerHighest,
|
||||||
|
fallbackTextColor: colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
maxSize: 160,
|
maxSize: 160,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -340,7 +373,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.l10n.chat_typeMessage,
|
hintText: context.l10n.chat_typeMessage,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onSubmitted: (_) => _sendMessage(connector),
|
onSubmitted: (_) => _sendMessage(connector),
|
||||||
@@ -389,14 +425,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connector.sendMessage(
|
connector.sendMessage(widget.contact, text);
|
||||||
widget.contact,
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _showPathHistory(BuildContext context) {
|
void _showPathHistory(BuildContext context) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
@@ -421,13 +453,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (paths.isNotEmpty) ...[
|
if (paths.isNotEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_recentAckPaths,
|
context.l10n.chat_recentAckPaths,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (paths.length >= 100) ...[
|
if (paths.length >= 100) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber[100],
|
color: Colors.amber[100],
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -446,7 +484,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
dense: true,
|
dense: true,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: path.wasFloodDiscovery ? Colors.blue : Colors.green,
|
backgroundColor: path.wasFloodDiscovery
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.green,
|
||||||
child: Text(
|
child: Text(
|
||||||
'${path.hopCount}',
|
'${path.hopCount}',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
@@ -474,23 +514,36 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
path.wasFloodDiscovery
|
path.wasFloodDiscovery
|
||||||
? const Icon(Icons.waves, size: 16, color: Colors.grey)
|
? const Icon(
|
||||||
: const Icon(Icons.route, size: 16, color: Colors.grey),
|
Icons.waves,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.route,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onLongPress: () => _showFullPathDialog(context, path.pathBytes),
|
onLongPress: () =>
|
||||||
|
_showFullPathDialog(context, path.pathBytes),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (path.pathBytes.isEmpty) {
|
if (path.pathBytes.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.chat_pathDetailsNotAvailable),
|
content: Text(
|
||||||
|
context.l10n.chat_pathDetailsNotAvailable,
|
||||||
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pathBytes = Uint8List.fromList(path.pathBytes);
|
final pathBytes = Uint8List.fromList(
|
||||||
|
path.pathBytes,
|
||||||
|
);
|
||||||
final pathLength = path.pathBytes.length;
|
final pathLength = path.pathBytes.length;
|
||||||
|
|
||||||
// Set the path override to persist user's choice
|
// Set the path override to persist user's choice
|
||||||
@@ -520,7 +573,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_pathActions,
|
context.l10n.chat_pathActions,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -530,8 +586,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
backgroundColor: Colors.purple,
|
backgroundColor: Colors.purple,
|
||||||
child: Icon(Icons.edit_road, size: 16),
|
child: Icon(Icons.edit_road, size: 16),
|
||||||
),
|
),
|
||||||
title: Text(context.l10n.chat_setCustomPath, style: const TextStyle(fontSize: 14)),
|
title: Text(
|
||||||
subtitle: Text(context.l10n.chat_setCustomPathSubtitle, style: const TextStyle(fontSize: 11)),
|
context.l10n.chat_setCustomPath,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.chat_setCustomPathSubtitle,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showCustomPathDialog(context);
|
_showCustomPathDialog(context);
|
||||||
@@ -544,8 +606,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
child: Icon(Icons.clear_all, size: 16),
|
child: Icon(Icons.clear_all, size: 16),
|
||||||
),
|
),
|
||||||
title: Text(context.l10n.chat_clearPath, style: const TextStyle(fontSize: 14)),
|
title: Text(
|
||||||
subtitle: Text(context.l10n.chat_clearPathSubtitle, style: const TextStyle(fontSize: 11)),
|
context.l10n.chat_clearPath,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.chat_clearPathSubtitle,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.clearContactPath(widget.contact);
|
await connector.clearContactPath(widget.contact);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -565,10 +633,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
child: Icon(Icons.waves, size: 16),
|
child: Icon(Icons.waves, size: 16),
|
||||||
),
|
),
|
||||||
title: Text(context.l10n.chat_forceFloodMode, style: const TextStyle(fontSize: 14)),
|
title: Text(
|
||||||
subtitle: Text(context.l10n.chat_floodModeSubtitle, style: const TextStyle(fontSize: 11)),
|
context.l10n.chat_forceFloodMode,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.chat_floodModeSubtitle,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
await connector.setPathOverride(
|
||||||
|
widget.contact,
|
||||||
|
pathLen: -1,
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -597,7 +674,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
String _formatRelativeTime(DateTime time) {
|
String _formatRelativeTime(DateTime time) {
|
||||||
final diff = DateTime.now().difference(time);
|
final diff = DateTime.now().difference(time);
|
||||||
if (diff.inSeconds < 60) return context.l10n.time_justNow;
|
if (diff.inSeconds < 60) return context.l10n.time_justNow;
|
||||||
if (diff.inMinutes < 60) return context.l10n.time_minutesAgo(diff.inMinutes);
|
if (diff.inMinutes < 60) {
|
||||||
|
return context.l10n.time_minutesAgo(diff.inMinutes);
|
||||||
|
}
|
||||||
if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours);
|
if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours);
|
||||||
return context.l10n.time_daysAgo(diff.inDays);
|
return context.l10n.time_daysAgo(diff.inDays);
|
||||||
}
|
}
|
||||||
@@ -623,6 +702,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
title: Text(context.l10n.chat_fullPath),
|
title: Text(context.l10n.chat_fullPath),
|
||||||
content: SelectableText(formattedPath),
|
content: SelectableText(formattedPath),
|
||||||
actions: [
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
|
path: Uint8List.fromList(pathBytes),
|
||||||
|
flipPathRound: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(context.l10n.contacts_pathTrace),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(context.l10n.common_close),
|
child: Text(context.l10n.common_close),
|
||||||
@@ -639,7 +731,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveContactFrom4Bytes(MeshCoreConnector connector, Uint8List key4Bytes) {
|
Contact _resolveContactFrom4Bytes(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
Uint8List key4Bytes,
|
||||||
|
) {
|
||||||
return connector.contacts.firstWhere(
|
return connector.contacts.firstWhere(
|
||||||
(c) => listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)),
|
(c) => listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)),
|
||||||
orElse: () => widget.contact,
|
orElse: () => widget.contact,
|
||||||
@@ -673,12 +768,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
final status = !connector.isConnected
|
final status = !connector.isConnected
|
||||||
? context.l10n.chat_pathSavedLocally
|
? context.l10n.chat_pathSavedLocally
|
||||||
: (verified ? context.l10n.chat_pathDeviceConfirmed : context.l10n.chat_pathDeviceNotConfirmed);
|
: (verified
|
||||||
|
? context.l10n.chat_pathDeviceConfirmed
|
||||||
|
: context.l10n.chat_pathDeviceNotConfirmed);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
|
||||||
context.l10n.chat_pathSetHops(hopCount, status),
|
|
||||||
),
|
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -693,7 +788,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
builder: (context) => Consumer<MeshCoreConnector>(
|
builder: (context) => Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final contact = _resolveContact(connector);
|
final contact = _resolveContact(connector);
|
||||||
final smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
|
final smazEnabled = connector.isContactSmazEnabled(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(contact.name),
|
title: Text(contact.name),
|
||||||
@@ -709,7 +806,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
context.l10n.chat_location,
|
context.l10n.chat_location,
|
||||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||||
),
|
),
|
||||||
_buildInfoRow(context.l10n.chat_publicKey, '${contact.publicKeyHex.substring(0, 16)}...'),
|
_buildInfoRow(
|
||||||
|
context.l10n.chat_publicKey,
|
||||||
|
'${contact.publicKeyHex.substring(0, 16)}...',
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -717,7 +817,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
||||||
value: smazEnabled,
|
value: smazEnabled,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
connector.setContactSmazEnabled(contact.publicKeyHex, value);
|
connector.setContactSmazEnabled(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -764,7 +867,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
final currentContact = _resolveContact(connector);
|
final currentContact = _resolveContact(connector);
|
||||||
if (currentContact.pathLength > 0 && currentContact.path.isEmpty && connector.isConnected) {
|
if (currentContact.pathLength > 0 &&
|
||||||
|
currentContact.path.isEmpty &&
|
||||||
|
connector.isConnected) {
|
||||||
connector.getContacts();
|
connector.getContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,19 +890,31 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
onRefresh: connector.isConnected ? connector.getContacts : null,
|
onRefresh: connector.isConnected ? connector.getContacts : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
appLogger.info('PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted', tag: 'ChatScreen');
|
appLogger.info(
|
||||||
|
'PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted',
|
||||||
|
tag: 'ChatScreen',
|
||||||
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
appLogger.info('PathSelectionDialog was cancelled or returned null', tag: 'ChatScreen');
|
appLogger.info(
|
||||||
|
'PathSelectionDialog was cancelled or returned null',
|
||||||
|
tag: 'ChatScreen',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
appLogger.warn('Widget not mounted after dialog, cannot set path', tag: 'ChatScreen');
|
appLogger.warn(
|
||||||
|
'Widget not mounted after dialog, cannot set path',
|
||||||
|
tag: 'ChatScreen',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.info('Calling setPathOverride for ${widget.contact.name}', tag: 'ChatScreen');
|
appLogger.info(
|
||||||
|
'Calling setPathOverride for ${widget.contact.name}',
|
||||||
|
tag: 'ChatScreen',
|
||||||
|
);
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
widget.contact,
|
widget.contact,
|
||||||
pathLen: result.length,
|
pathLen: result.length,
|
||||||
@@ -809,7 +926,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
await _notifyPathSet(connector, widget.contact, result, result.length);
|
await _notifyPathSet(connector, widget.contact, result, result.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _openMessagePath(Message message, Contact contact) {
|
void _openMessagePath(Message message, Contact contact) {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final fourByteHex = message.fourByteRoomContactKey
|
final fourByteHex = message.fourByteRoomContactKey
|
||||||
@@ -850,14 +966,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
// Can't react to your own messages
|
||||||
leading: const Icon(Icons.add_reaction_outlined),
|
if (!message.isOutgoing)
|
||||||
title: Text(context.l10n.chat_addReaction),
|
ListTile(
|
||||||
onTap: () {
|
leading: const Icon(Icons.add_reaction_outlined),
|
||||||
Navigator.pop(sheetContext);
|
title: Text(context.l10n.chat_addReaction),
|
||||||
_showEmojiPicker(message);
|
onTap: () {
|
||||||
},
|
Navigator.pop(sheetContext);
|
||||||
),
|
_showEmojiPicker(message, contact);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
title: Text(context.l10n.common_copy),
|
title: Text(context.l10n.common_copy),
|
||||||
@@ -874,8 +992,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
await _deleteMessage(message);
|
await _deleteMessage(message);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (message.isOutgoing &&
|
if (message.isOutgoing && message.status == MessageStatus.failed)
|
||||||
message.status == MessageStatus.failed)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.refresh),
|
leading: const Icon(Icons.refresh),
|
||||||
title: Text(context.l10n.common_retry),
|
title: Text(context.l10n.common_retry),
|
||||||
@@ -906,50 +1023,57 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
void _copyMessageText(String text) {
|
void _copyMessageText(String text) {
|
||||||
Clipboard.setData(ClipboardData(text: text));
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(context.l10n.chat_messageCopied)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteMessage(Message message) async {
|
Future<void> _deleteMessage(Message message) async {
|
||||||
await context.read<MeshCoreConnector>().deleteMessage(message);
|
await context.read<MeshCoreConnector>().deleteMessage(message);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(context.l10n.chat_messageDeleted)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _retryMessage(Message message) {
|
void _retryMessage(Message message) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
// Retry using the contact's current path override setting
|
// Retry using the contact's current path override setting
|
||||||
connector.sendMessage(
|
connector.sendMessage(widget.contact, message.text);
|
||||||
widget.contact,
|
ScaffoldMessenger.of(
|
||||||
message.text,
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(context.l10n.chat_retryingMessage)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEmojiPicker(Message message) {
|
void _showEmojiPicker(Message message, Contact senderContact) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => EmojiPicker(
|
builder: (context) => EmojiPicker(
|
||||||
onEmojiSelected: (emoji) {
|
onEmojiSelected: (emoji) {
|
||||||
_sendReaction(message, emoji);
|
_sendReaction(message, senderContact, emoji);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendReaction(Message message, String emoji) {
|
void _sendReaction(Message message, Contact senderContact, String emoji) {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
// Send reaction with messageId if available, otherwise use lightweight format
|
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
|
||||||
// Parser will extract reactionKey (timestamp_senderPrefix) for deduplication
|
if (emojiIndex == null) return; // Unknown emoji, skip
|
||||||
final messageId = message.messageId ??
|
final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
'${message.timestamp.millisecondsSinceEpoch}_${message.senderKeyHex.substring(0, 8)}';
|
|
||||||
final reactionText = 'r:$messageId:$emoji';
|
// For room servers, include sender name (like channels) since multiple users
|
||||||
|
// For 1:1 chats, sender is implicit (null)
|
||||||
|
final senderName = widget.contact.type == advTypeRoom
|
||||||
|
? senderContact.name
|
||||||
|
: null;
|
||||||
|
final hash = ReactionHelper.computeReactionHash(
|
||||||
|
timestampSecs,
|
||||||
|
senderName,
|
||||||
|
message.text,
|
||||||
|
);
|
||||||
|
final reactionText = 'r:$hash:$emojiIndex';
|
||||||
connector.sendMessage(widget.contact, reactionText);
|
connector.sendMessage(widget.contact, reactionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -978,7 +1102,9 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
final isFailed = message.status == MessageStatus.failed;
|
final isFailed = message.status == MessageStatus.failed;
|
||||||
final bubbleColor = isFailed
|
final bubbleColor = isFailed
|
||||||
? colorScheme.errorContainer
|
? colorScheme.errorContainer
|
||||||
: (isOutgoing ? colorScheme.primary : colorScheme.surfaceContainerHighest);
|
: (isOutgoing
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.surfaceContainerHighest);
|
||||||
final textColor = isFailed
|
final textColor = isFailed
|
||||||
? colorScheme.onErrorContainer
|
? colorScheme.onErrorContainer
|
||||||
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
|
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
|
||||||
@@ -990,13 +1116,17 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
crossAxisAlignment: isOutgoing
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start,
|
mainAxisAlignment: isOutgoing
|
||||||
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isOutgoing) ...[
|
if (!isOutgoing) ...[
|
||||||
@@ -1005,133 +1135,154 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: gifId != null
|
padding: gifId != null
|
||||||
? const EdgeInsets.all(4)
|
? const EdgeInsets.all(4)
|
||||||
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
: const EdgeInsets.symmetric(
|
||||||
constraints: BoxConstraints(
|
horizontal: 12,
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
vertical: 8,
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bubbleColor,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (!isOutgoing) ...[
|
|
||||||
Padding(
|
|
||||||
padding: gifId != null
|
|
||||||
? const EdgeInsets.only(left: 8, top: 4, bottom: 4)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: Text(
|
|
||||||
senderName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
constraints: BoxConstraints(
|
||||||
),
|
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||||
if (gifId == null) const SizedBox(height: 4),
|
),
|
||||||
],
|
decoration: BoxDecoration(
|
||||||
if (poi != null)
|
color: bubbleColor,
|
||||||
_buildPoiMessage(context, poi, textColor, metaColor)
|
borderRadius: BorderRadius.circular(16),
|
||||||
else if (gifId != null)
|
),
|
||||||
ClipRRect(
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(12),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: GifMessage(
|
children: [
|
||||||
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
|
if (!isOutgoing) ...[
|
||||||
backgroundColor: Colors.transparent,
|
Padding(
|
||||||
fallbackTextColor: textColor.withValues(alpha: 0.7),
|
padding: gifId != null
|
||||||
),
|
? const EdgeInsets.only(
|
||||||
)
|
left: 8,
|
||||||
else
|
top: 4,
|
||||||
Linkify(
|
bottom: 4,
|
||||||
text: messageText,
|
)
|
||||||
style: TextStyle(
|
: EdgeInsets.zero,
|
||||||
color: textColor,
|
child: Text(
|
||||||
),
|
senderName,
|
||||||
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),
|
|
||||||
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),
|
|
||||||
Padding(
|
|
||||||
padding: gifId != null
|
|
||||||
? const EdgeInsets.only(left: 8, right: 8, bottom: 4)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: 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],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 12,
|
||||||
color: isOutgoing ? metaColor : Colors.green[700],
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
if (gifId == null) const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
),
|
if (poi != null)
|
||||||
|
_buildPoiMessage(context, poi, textColor, metaColor)
|
||||||
|
else if (gifId != null)
|
||||||
|
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
|
||||||
|
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),
|
||||||
|
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),
|
||||||
|
Padding(
|
||||||
|
padding: gifId != null
|
||||||
|
? const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 4,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: 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],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
color: isOutgoing
|
||||||
|
? metaColor
|
||||||
|
: Colors.green[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (message.reactions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||||
|
child: _buildReactionsDisplay(context, message, colorScheme),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
|
||||||
if (message.reactions.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
|
||||||
child: _buildReactionsDisplay(context, message, colorScheme),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1144,8 +1295,9 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
_PoiInfo? _parsePoiMessage(String text) {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final match = RegExp(r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$')
|
final match = RegExp(
|
||||||
.firstMatch(trimmed);
|
r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$',
|
||||||
|
).firstMatch(trimmed);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
final lat = double.tryParse(match.group(1) ?? '');
|
final lat = double.tryParse(match.group(1) ?? '');
|
||||||
final lon = double.tryParse(match.group(2) ?? '');
|
final lon = double.tryParse(match.group(2) ?? '');
|
||||||
@@ -1186,18 +1338,12 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_poiShared,
|
context.l10n.chat_poiShared,
|
||||||
style: TextStyle(
|
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||||
color: textColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (poi.label.isNotEmpty)
|
if (poi.label.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
poi.label,
|
poi.label,
|
||||||
style: TextStyle(
|
style: TextStyle(color: metaColor, fontSize: 12),
|
||||||
color: metaColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1206,7 +1352,11 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReactionsDisplay(BuildContext context, Message message, ColorScheme colorScheme) {
|
Widget _buildReactionsDisplay(
|
||||||
|
BuildContext context,
|
||||||
|
Message message,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
@@ -1227,10 +1377,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(emoji, style: const TextStyle(fontSize: 16)),
|
||||||
emoji,
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
if (count > 1) ...[
|
if (count > 1) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -1314,11 +1461,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Icon(
|
return Icon(icon, size: 12, color: color);
|
||||||
icon,
|
|
||||||
size: 12,
|
|
||||||
color: color,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatTime(DateTime time) {
|
String _formatTime(DateTime time) {
|
||||||
@@ -1333,9 +1476,5 @@ class _PoiInfo {
|
|||||||
final double lon;
|
final double lon;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
const _PoiInfo({
|
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||||
required this.lat,
|
|
||||||
required this.lon,
|
|
||||||
required this.label,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -27,10 +29,9 @@ import 'map_screen.dart';
|
|||||||
import 'repeater_hub_screen.dart';
|
import 'repeater_hub_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
enum RoomLoginDestination {
|
enum RoomLoginDestination { chat, management }
|
||||||
chat,
|
|
||||||
management,
|
enum ContactOperationType { import, export, zeroHopShare }
|
||||||
}
|
|
||||||
|
|
||||||
class ContactsScreen extends StatefulWidget {
|
class ContactsScreen extends StatefulWidget {
|
||||||
final bool hideBackButton;
|
final bool hideBackButton;
|
||||||
@@ -52,16 +53,22 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
List<ContactGroup> _groups = [];
|
List<ContactGroup> _groups = [];
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
|
final Set<ContactOperationType> _pendingOperations = {};
|
||||||
|
|
||||||
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadGroups();
|
_loadGroups();
|
||||||
|
_setupFrameListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchDebounce?.cancel();
|
_searchDebounce?.cancel();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_frameSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +84,137 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
await _groupStore.saveGroups(_groups);
|
await _groupStore.saveGroups(_groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupFrameListener() {
|
||||||
|
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;
|
||||||
|
final frameBuffer = BufferReader(frame);
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
|
if (code == respCodeExportContact) {
|
||||||
|
final advertPacket = frameBuffer.readRemainingBytes();
|
||||||
|
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||||
|
if (advertPacket.length < 98) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_pendingOperations.remove(ContactOperationType.export);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hexString = pubKeyToHex(advertPacket);
|
||||||
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeOk) {
|
||||||
|
// Show a snackbar indicating success
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingOperations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeErr) {
|
||||||
|
// Show a snackbar indicating failure
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingOperations.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _contactExport(Uint8List pubKey) async {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||||
|
_pendingOperations.add(ContactOperationType.export);
|
||||||
|
await connector.sendFrame(exportContactFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
||||||
|
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
||||||
|
await connector.sendFrame(exportContactZeroHopFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _contactImport() async {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final clipboardData = await Clipboard.getData('text/plain');
|
||||||
|
if (clipboardData == null || clipboardData.text == null) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final text = clipboardData.text!.trim();
|
||||||
|
if (!text.startsWith('meshcore://')) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hexString = text.substring('meshcore://'.length);
|
||||||
|
try {
|
||||||
|
final importContactFrame = buildImportContactFrame(hexString);
|
||||||
|
_pendingOperations.add(ContactOperationType.import);
|
||||||
|
await connector.sendFrame(importContactFrame);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
@@ -96,18 +234,94 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.bluetooth_disabled),
|
itemBuilder: (context) => [
|
||||||
tooltip: context.l10n.common_disconnect,
|
PopupMenuItem(
|
||||||
onPressed: () => _disconnect(context, connector),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.connect_without_contact),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_zeroHopAdvert),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => {
|
||||||
|
connector.sendSelfAdvert(flood: false),
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.settings_advertisementSent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.cell_tower),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_floodAdvert),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => {
|
||||||
|
connector.sendSelfAdvert(flood: true),
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.settings_advertisementSent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.copy),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_copyAdvertToClipboard),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _contactExport(Uint8List.fromList([])),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.paste),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_addContactFromClipboard),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _contactImport(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.connect_without_contact),
|
||||||
),
|
),
|
||||||
IconButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.tune),
|
itemBuilder: (context) => [
|
||||||
tooltip: context.l10n.common_settings,
|
PopupMenuItem(
|
||||||
onPressed: () => Navigator.push(
|
child: Row(
|
||||||
context,
|
children: [
|
||||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
const Icon(Icons.logout, color: Colors.red),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.common_disconnect),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _disconnect(context, connector),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.settings_title),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -497,7 +711,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => destination == RoomLoginDestination.management
|
builder: (context) =>
|
||||||
|
destination == RoomLoginDestination.management
|
||||||
? RepeaterHubScreen(repeater: room, password: password)
|
? RepeaterHubScreen(repeater: room, password: password)
|
||||||
: ChatScreen(contact: room),
|
: ChatScreen(contact: room),
|
||||||
),
|
),
|
||||||
@@ -760,7 +975,26 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (isRepeater)
|
if (isRepeater) ...[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.radar, color: Colors.green),
|
||||||
|
title: contact.pathLength > 0
|
||||||
|
? Text(context.l10n.contacts_pathTrace)
|
||||||
|
: Text(context.l10n.contacts_ping),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: contact.pathLength > 0
|
||||||
|
? context.l10n.contacts_repeaterPathTrace
|
||||||
|
: context.l10n.contacts_repeaterPing,
|
||||||
|
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.cell_tower, color: Colors.orange),
|
leading: const Icon(Icons.cell_tower, color: Colors.orange),
|
||||||
title: Text(context.l10n.contacts_manageRepeater),
|
title: Text(context.l10n.contacts_manageRepeater),
|
||||||
@@ -768,8 +1002,27 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
_showRepeaterLogin(context, contact);
|
_showRepeaterLogin(context, contact);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
else if (isRoom) ...[
|
] else if (isRoom) ...[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.radar, color: Colors.green),
|
||||||
|
title: contact.pathLength > 0
|
||||||
|
? Text(context.l10n.contacts_pathTrace)
|
||||||
|
: Text(context.l10n.contacts_ping),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: contact.pathLength > 0
|
||||||
|
? context.l10n.contacts_roomPathTrace
|
||||||
|
: context.l10n.contacts_roomPing,
|
||||||
|
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.room, color: Colors.blue),
|
leading: const Icon(Icons.room, color: Colors.blue),
|
||||||
title: Text(context.l10n.contacts_roomLogin),
|
title: Text(context.l10n.contacts_roomLogin),
|
||||||
@@ -779,14 +1032,39 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.room_preferences, color: Colors.orange),
|
leading: const Icon(
|
||||||
|
Icons.room_preferences,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
title: Text(context.l10n.room_management),
|
title: Text(context.l10n.room_management),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
_showRoomLogin(context, contact, RoomLoginDestination.management);
|
_showRoomLogin(
|
||||||
|
context,
|
||||||
|
contact,
|
||||||
|
RoomLoginDestination.management,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
] else
|
] else ...[
|
||||||
|
if (contact.pathLength > 0)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.radar, color: Colors.green),
|
||||||
|
title: Text(context.l10n.contacts_chatTraceRoute),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: context.l10n.contacts_pathTraceTo(
|
||||||
|
contact.name,
|
||||||
|
),
|
||||||
|
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.chat),
|
leading: const Icon(Icons.chat),
|
||||||
title: Text(context.l10n.contacts_openChat),
|
title: Text(context.l10n.contacts_openChat),
|
||||||
@@ -795,6 +1073,23 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
_openChat(context, contact);
|
_openChat(context, contact);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.copy),
|
||||||
|
title: Text(context.l10n.contacts_ShareContact),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_contactExport(contact.publicKey);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.connect_without_contact),
|
||||||
|
title: Text(context.l10n.contacts_ShareContactZeroHop),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_contactZeroHop(contact.publicKey);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete, color: Colors.red),
|
leading: const Icon(Icons.delete, color: Colors.red),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -860,16 +1155,18 @@ class _ContactTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final shotPublicKey =
|
|
||||||
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: _getTypeColor(contact.type),
|
backgroundColor: _getTypeColor(contact.type),
|
||||||
child: _buildContactAvatar(contact),
|
child: _buildContactAvatar(contact),
|
||||||
),
|
),
|
||||||
title: Text(contact.name),
|
title: Text(contact.name),
|
||||||
subtitle: Text(
|
subtitle: Column(
|
||||||
'${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(contact.pathLabel),
|
||||||
|
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// Clamp text scaling in trailing section to prevent overflow while
|
// Clamp text scaling in trailing section to prevent overflow while
|
||||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||||
@@ -891,8 +1188,13 @@ class _ContactTile extends StatelessWidget {
|
|||||||
_formatLastSeen(context, lastSeen),
|
_formatLastSeen(context, lastSeen),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
if (contact.hasLocation)
|
Row(
|
||||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.hasLocation)
|
||||||
|
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -127,9 +127,7 @@ class _DeviceScreenState extends State<DeviceScreen>
|
|||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -207,7 +205,6 @@ class _DeviceScreenState extends State<DeviceScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildBatteryIndicator(
|
Widget _buildBatteryIndicator(
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -224,11 +221,7 @@ class _DeviceScreenState extends State<DeviceScreen>
|
|||||||
final icon = _batteryIcon(percent);
|
final icon = _batteryIcon(percent);
|
||||||
|
|
||||||
return ActionChip(
|
return ActionChip(
|
||||||
avatar: Icon(
|
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
|
||||||
icon,
|
|
||||||
size: 16,
|
|
||||||
color: colorScheme.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
label: Text(displayLabel),
|
label: Text(displayLabel),
|
||||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
||||||
color: colorScheme.onSecondaryContainer,
|
color: colorScheme.onSecondaryContainer,
|
||||||
@@ -260,25 +253,19 @@ class _DeviceScreenState extends State<DeviceScreen>
|
|||||||
case 0:
|
case 0:
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
buildQuickSwitchRoute(
|
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
|
||||||
const ContactsScreen(hideBackButton: true),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
buildQuickSwitchRoute(
|
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
||||||
const ChannelsScreen(hideBackButton: true),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
buildQuickSwitchRoute(
|
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
||||||
const MapScreen(hideBackButton: true),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
_updateEstimate();
|
_updateEstimate();
|
||||||
if (bounds != null) {
|
if (bounds != null) {
|
||||||
_mapController.fitCamera(
|
_mapController.fitCamera(
|
||||||
CameraFit.bounds(
|
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||||
bounds: bounds,
|
|
||||||
padding: const EdgeInsets.all(48),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,8 +69,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final cacheService = context.read<MapTileCacheService>();
|
final cacheService = context.read<MapTileCacheService>();
|
||||||
final count =
|
final count = cacheService.estimateTileCount(
|
||||||
cacheService.estimateTileCount(_selectedBounds!, _minZoom, _maxZoom);
|
_selectedBounds!,
|
||||||
|
_minZoom,
|
||||||
|
_maxZoom,
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_estimatedTiles = count;
|
_estimatedTiles = count;
|
||||||
});
|
});
|
||||||
@@ -181,9 +181,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
result.failed,
|
result.failed,
|
||||||
)
|
)
|
||||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(message)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(message)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _clearCache() async {
|
Future<void> _clearCache() async {
|
||||||
@@ -224,10 +224,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
|
||||||
title: Text(l10n.mapCache_title),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -290,7 +287,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.mapCache_cacheArea,
|
l10n.mapCache_cacheArea,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
@@ -304,8 +304,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: _isDownloading || selectedBounds == null
|
||||||
_isDownloading || selectedBounds == null ? null : _clearBounds,
|
? null
|
||||||
|
: _clearBounds,
|
||||||
child: Text(l10n.common_clear),
|
child: Text(l10n.common_clear),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -313,11 +314,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
l10n.mapCache_zoomRange,
|
l10n.mapCache_zoomRange,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
RangeSlider(
|
RangeSlider(
|
||||||
values:
|
values: RangeValues(
|
||||||
RangeValues(_minZoom.toDouble(), _maxZoom.toDouble()),
|
_minZoom.toDouble(),
|
||||||
|
_maxZoom.toDouble(),
|
||||||
|
),
|
||||||
min: 3,
|
min: 3,
|
||||||
max: 18,
|
max: 18,
|
||||||
divisions: 15,
|
divisions: 15,
|
||||||
@@ -341,10 +347,12 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
LinearProgressIndicator(value: progressValue),
|
LinearProgressIndicator(value: progressValue),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(l10n.mapCache_downloadedTiles(
|
Text(
|
||||||
_completedTiles,
|
l10n.mapCache_downloadedTiles(
|
||||||
_estimatedTiles,
|
_completedTiles,
|
||||||
)),
|
_estimatedTiles,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
+392
-178
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -48,9 +50,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
final Set<String> _hiddenMarkerIds = {};
|
final Set<String> _hiddenMarkerIds = {};
|
||||||
Set<String> _removedMarkerIds = {};
|
Set<String> _removedMarkerIds = {};
|
||||||
|
bool _isBuildingPathTrace = false;
|
||||||
bool _isSelectingPoi = false;
|
bool _isSelectingPoi = false;
|
||||||
bool _hasInitializedMap = false;
|
bool _hasInitializedMap = false;
|
||||||
bool _removedMarkersLoaded = false;
|
bool _removedMarkersLoaded = false;
|
||||||
|
final List<int> _pathTrace = [];
|
||||||
|
final List<LatLng> _points = [];
|
||||||
|
final List<Polyline> _polylines = [];
|
||||||
|
bool _legendExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -147,6 +154,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
.where((c) => c.hasLocation)
|
.where((c) => c.hasLocation)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
_polylines.clear();
|
||||||
|
_polylines.addAll(
|
||||||
|
_points.length > 1
|
||||||
|
? [
|
||||||
|
Polyline(
|
||||||
|
points: _points,
|
||||||
|
strokeWidth: 4,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Polyline>[],
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate center and zoom of all nodes, or default to (0, 0)
|
// Calculate center and zoom of all nodes, or default to (0, 0)
|
||||||
LatLng center = const LatLng(0, 0);
|
LatLng center = const LatLng(0, 0);
|
||||||
double initialZoom = 10.0;
|
double initialZoom = 10.0;
|
||||||
@@ -225,13 +245,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re center map after removed markers have loaded
|
// Re center map after removed markers have loaded
|
||||||
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
|
if (!_hasInitializedMap && _removedMarkersLoaded) {
|
||||||
_hasInitializedMap = true;
|
_hasInitializedMap = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
if (hasMapContent) {
|
||||||
if (mounted) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_mapController.move(center, initialZoom);
|
if (mounted) {
|
||||||
}
|
_mapController.move(center, initialZoom);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowBack = !connector.isConnected;
|
final allowBack = !connector.isConnected;
|
||||||
@@ -245,105 +267,165 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
if (!_isBuildingPathTrace)
|
||||||
icon: const Icon(Icons.bluetooth_disabled),
|
IconButton(
|
||||||
tooltip: context.l10n.common_disconnect,
|
icon: const Icon(Icons.radar),
|
||||||
onPressed: () => _disconnect(context, connector),
|
onPressed: () => _startPath(),
|
||||||
),
|
tooltip: context.l10n.contacts_pathTrace,
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.tune),
|
|
||||||
tooltip: context.l10n.common_settings,
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const SettingsScreen(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.logout, color: Colors.red),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.common_disconnect),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _disconnect(context, connector),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.settings_title),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: !hasMapContent
|
body: Stack(
|
||||||
? _buildEmptyState()
|
children: [
|
||||||
: Stack(
|
FlutterMap(
|
||||||
children: [
|
mapController: _mapController,
|
||||||
FlutterMap(
|
options: MapOptions(
|
||||||
mapController: _mapController,
|
initialCenter: center,
|
||||||
options: MapOptions(
|
initialZoom: initialZoom,
|
||||||
initialCenter: center,
|
minZoom: 2.0,
|
||||||
initialZoom: initialZoom,
|
maxZoom: 18.0,
|
||||||
minZoom: 2.0,
|
interactionOptions: InteractionOptions(
|
||||||
maxZoom: 18.0,
|
flags: ~InteractiveFlag.rotate,
|
||||||
interactionOptions: InteractionOptions(
|
),
|
||||||
flags: ~InteractiveFlag.rotate
|
onTap: (_, latLng) {
|
||||||
),
|
if (_isSelectingPoi) {
|
||||||
onTap: (_, latLng) {
|
setState(() {
|
||||||
if (_isSelectingPoi) {
|
_isSelectingPoi = false;
|
||||||
setState(() {
|
});
|
||||||
_isSelectingPoi = false;
|
_shareMarker(
|
||||||
});
|
context: context,
|
||||||
_shareMarker(
|
connector: connector,
|
||||||
context: context,
|
position: latLng,
|
||||||
connector: connector,
|
defaultLabel: context.l10n.map_pointOfInterest,
|
||||||
position: latLng,
|
flags: 'poi',
|
||||||
defaultLabel: context.l10n.map_pointOfInterest,
|
);
|
||||||
flags: 'poi',
|
}
|
||||||
);
|
},
|
||||||
}
|
onLongPress: (_, latLng) {
|
||||||
},
|
if (_isSelectingPoi) {
|
||||||
onLongPress: (_, latLng) {
|
setState(() {
|
||||||
if (_isSelectingPoi) {
|
_isSelectingPoi = false;
|
||||||
setState(() {
|
});
|
||||||
_isSelectingPoi = false;
|
_shareMarker(
|
||||||
});
|
context: context,
|
||||||
_shareMarker(
|
connector: connector,
|
||||||
context: context,
|
position: latLng,
|
||||||
connector: connector,
|
defaultLabel: context.l10n.map_pointOfInterest,
|
||||||
position: latLng,
|
flags: 'poi',
|
||||||
defaultLabel: context.l10n.map_pointOfInterest,
|
);
|
||||||
flags: 'poi',
|
return;
|
||||||
);
|
}
|
||||||
return;
|
_showShareMarkerAtPositionSheet(
|
||||||
}
|
context: context,
|
||||||
_showShareMarkerAtPositionSheet(
|
connector: connector,
|
||||||
context: context,
|
position: latLng,
|
||||||
connector: connector,
|
);
|
||||||
position: latLng,
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TileLayer(
|
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
|
||||||
tileProvider: tileCache.tileProvider,
|
|
||||||
userAgentPackageName:
|
|
||||||
MapTileCacheService.userAgentPackageName,
|
|
||||||
maxZoom: 19,
|
|
||||||
),
|
|
||||||
MarkerLayer(
|
|
||||||
markers: [
|
|
||||||
if (highlightPosition != null)
|
|
||||||
Marker(
|
|
||||||
point: highlightPosition,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
child: Icon(
|
|
||||||
Icons.location_on_outlined,
|
|
||||||
color: Colors.red[600],
|
|
||||||
size: 34,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
..._buildMarkers(contactsWithLocation, settings),
|
|
||||||
...sharedMarkers.map(_buildSharedMarker),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_buildLegend(
|
|
||||||
contactsWithLocation.length,
|
|
||||||
sharedMarkers.length,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: kMapTileUrlTemplate,
|
||||||
|
tileProvider: tileCache.tileProvider,
|
||||||
|
userAgentPackageName:
|
||||||
|
MapTileCacheService.userAgentPackageName,
|
||||||
|
maxZoom: 19,
|
||||||
|
),
|
||||||
|
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||||
|
PolylineLayer(polylines: _polylines),
|
||||||
|
MarkerLayer(
|
||||||
|
markers: [
|
||||||
|
if (highlightPosition != null)
|
||||||
|
Marker(
|
||||||
|
point: highlightPosition,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(
|
||||||
|
Icons.location_on_outlined,
|
||||||
|
color: Colors.red[600],
|
||||||
|
size: 34,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
..._buildMarkers(contactsWithLocation, settings),
|
||||||
|
...sharedMarkers.map(_buildSharedMarker),
|
||||||
|
if (connector.selfLatitude != null &&
|
||||||
|
connector.selfLongitude != null)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
connector.selfLatitude!,
|
||||||
|
connector.selfLongitude!,
|
||||||
|
),
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.teal,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
context.l10n.pathTrace_you,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!_isBuildingPathTrace)
|
||||||
|
_buildLegend(
|
||||||
|
contactsWithLocation.length,
|
||||||
|
sharedMarkers.length,
|
||||||
|
),
|
||||||
|
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||||
|
],
|
||||||
|
),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: QuickSwitchBar(
|
child: QuickSwitchBar(
|
||||||
@@ -363,28 +445,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
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]),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.l10n.map_nodesNeedGps,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
|
|
||||||
@@ -404,14 +464,18 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
final marker = Marker(
|
final marker = Marker(
|
||||||
point: LatLng(contact.latitude!, contact.longitude!),
|
point: LatLng(contact.latitude!, contact.longitude!),
|
||||||
width: 80,
|
width: 35,
|
||||||
height: 80,
|
height: 35,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showNodeInfo(context, contact),
|
onLongPress: () =>
|
||||||
|
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
||||||
|
onTap: () => _isBuildingPathTrace
|
||||||
|
? _addToPath(context, contact)
|
||||||
|
: _showNodeInfo(context, contact),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getNodeColor(contact.type),
|
color: _getNodeColor(contact.type),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
@@ -427,7 +491,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
_getNodeIcon(contact.type),
|
_getNodeIcon(contact.type),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 24,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -476,60 +540,102 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(12.0),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
InkWell(
|
||||||
mainAxisSize: MainAxisSize.min,
|
borderRadius: BorderRadius.circular(12),
|
||||||
children: [
|
onTap: () {
|
||||||
Text(
|
setState(() {
|
||||||
context.l10n.map_nodesCount(nodeCount),
|
_legendExpanded = !_legendExpanded;
|
||||||
style: const TextStyle(
|
});
|
||||||
fontWeight: FontWeight.bold,
|
},
|
||||||
fontSize: 14,
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.map_nodesCount(nodeCount),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.l10n.map_pinsCount(markerCount),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
AnimatedRotation(
|
||||||
|
turns: _legendExpanded ? 0.5 : 0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: const Icon(Icons.expand_more, size: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
context.l10n.map_pinsCount(markerCount),
|
AnimatedCrossFade(
|
||||||
style: const TextStyle(
|
firstChild: const SizedBox.shrink(),
|
||||||
fontWeight: FontWeight.bold,
|
secondChild: Padding(
|
||||||
fontSize: 12,
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.person,
|
||||||
|
context.l10n.map_chat,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.router,
|
||||||
|
context.l10n.map_repeater,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.meeting_room,
|
||||||
|
context.l10n.map_room,
|
||||||
|
Colors.purple,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.sensors,
|
||||||
|
context.l10n.map_sensor,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.flag,
|
||||||
|
context.l10n.map_pinDm,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.flag,
|
||||||
|
context.l10n.map_pinPrivate,
|
||||||
|
Colors.purple,
|
||||||
|
),
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.flag,
|
||||||
|
context.l10n.map_pinPublic,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
crossFadeState: _legendExpanded
|
||||||
_buildLegendItem(
|
? CrossFadeState.showSecond
|
||||||
Icons.person,
|
: CrossFadeState.showFirst,
|
||||||
context.l10n.map_chat,
|
duration: const Duration(milliseconds: 200),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -537,7 +643,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
Widget _buildLegendItem(IconData icon, String label, Color color) {
|
Widget _buildLegendItem(IconData icon, String label, Color color) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 1.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -1385,6 +1491,114 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
return context.l10n.time_allTime;
|
return context.l10n.time_allTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addToPath(BuildContext context, Contact contact) {
|
||||||
|
setState(() {
|
||||||
|
_pathTrace.add(
|
||||||
|
contact.publicKey[0],
|
||||||
|
); // Add first 16 bytes of public key to path trace
|
||||||
|
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startPath() {
|
||||||
|
setState(() {
|
||||||
|
_isBuildingPathTrace = true;
|
||||||
|
_pathTrace.clear();
|
||||||
|
_points.clear();
|
||||||
|
_polylines.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removePath() {
|
||||||
|
setState(() {
|
||||||
|
_pathTrace.removeLast(); // Remove last node from path trace
|
||||||
|
_points.removeLast(); // Remove last point from points list
|
||||||
|
_polylines.clear(); // Clear polylines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPathTraceOverlay() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Positioned(
|
||||||
|
top: 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.contacts_pathTrace,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
if (_pathTrace.isEmpty) const SizedBox(height: 8),
|
||||||
|
if (_pathTrace.isEmpty)
|
||||||
|
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
if (_pathTrace.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
_pathTrace
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join(','),
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_pathTrace.isNotEmpty)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: l10n.contacts_pathTrace,
|
||||||
|
path: Uint8List.fromList(_pathTrace),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isBuildingPathTrace = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(l10n.map_runTrace),
|
||||||
|
),
|
||||||
|
if (_pathTrace.isNotEmpty)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _removePath,
|
||||||
|
child: Text(l10n.map_removeLast),
|
||||||
|
),
|
||||||
|
if (_pathTrace.isEmpty)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isBuildingPathTrace = false;
|
||||||
|
_pathTrace.clear();
|
||||||
|
_points.clear();
|
||||||
|
_polylines.clear();
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MarkerPayload {
|
class _MarkerPayload {
|
||||||
|
|||||||
@@ -0,0 +1,576 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
|
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
||||||
|
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
double getPathDistanceMeters(List<LatLng> points) {
|
||||||
|
if (points.length <= 1) return 0.0;
|
||||||
|
|
||||||
|
double distanceMeters = 0.0;
|
||||||
|
final distanceCalculator = Distance();
|
||||||
|
|
||||||
|
for (int i = 0; i < points.length - 1; i++) {
|
||||||
|
distanceMeters += distanceCalculator(points[i], points[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return distanceMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatDistance(double distanceMeters) {
|
||||||
|
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathTraceData {
|
||||||
|
final Uint8List pathData;
|
||||||
|
final Uint8List snrData;
|
||||||
|
final Map<int, Contact> pathContacts;
|
||||||
|
|
||||||
|
PathTraceData({
|
||||||
|
required this.pathData,
|
||||||
|
required this.snrData,
|
||||||
|
required this.pathContacts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathTraceMapScreen extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final Uint8List path;
|
||||||
|
final bool flipPathRound;
|
||||||
|
final bool reversePathRound;
|
||||||
|
|
||||||
|
const PathTraceMapScreen({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.path,
|
||||||
|
this.flipPathRound = false,
|
||||||
|
this.reversePathRound = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PathTraceMapScreen> createState() => _PathTraceMapScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||||
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
|
Timer? _timeoutTimer;
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _failed2Loaded = false;
|
||||||
|
bool _hasData = false;
|
||||||
|
PathTraceData? _traceData;
|
||||||
|
List<LatLng> _points = <LatLng>[];
|
||||||
|
List<Polyline> _polylines = [];
|
||||||
|
LatLng? _initialCenter = LatLng(0, 0);
|
||||||
|
double _initialZoom = 2.0;
|
||||||
|
LatLngBounds? _bounds;
|
||||||
|
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||||
|
double _pathDistanceMeters = 0.0;
|
||||||
|
|
||||||
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
|
return pathBytes
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_setupFrameListener();
|
||||||
|
_doPathTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_frameSubscription?.cancel();
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List addReturnpath(Uint8List pathBytes) {
|
||||||
|
Uint8List? traceBytes;
|
||||||
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length - 1) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _doPathTrace() async {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_failed2Loaded = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uint8List path;
|
||||||
|
|
||||||
|
Uint8List pathTmp = widget.reversePathRound
|
||||||
|
? Uint8List.fromList(widget.path.reversed.toList())
|
||||||
|
: widget.path;
|
||||||
|
|
||||||
|
if (widget.flipPathRound) {
|
||||||
|
path = addReturnpath(pathTmp);
|
||||||
|
} else {
|
||||||
|
path = pathTmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final frame = buildTraceReq(
|
||||||
|
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
0, //flags
|
||||||
|
0, //auth
|
||||||
|
payload: path,
|
||||||
|
);
|
||||||
|
connector.sendFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupFrameListener() {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
Uint8List tagData = Uint8List(4);
|
||||||
|
// Listen for incoming text messages from the repeater
|
||||||
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty) return;
|
||||||
|
final frameBuffer = BufferReader(frame);
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
|
if (code == respCodeSent) {
|
||||||
|
frameBuffer.skipBytes(1); //reserved
|
||||||
|
tagData = frameBuffer.readBytes(4);
|
||||||
|
final timeoutSeconds = frameBuffer.readUInt32LE();
|
||||||
|
|
||||||
|
// Start timeout timer for trace response
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeErr) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if it's a binary response
|
||||||
|
if (frame.length > 8 &&
|
||||||
|
code == pushCodeTraceData &&
|
||||||
|
listEquals(frame.sublist(4, 8), tagData)) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||||
|
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||||
|
_handleTraceResponse(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTraceResponse(Uint8List frame) async {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
|
final buffer = BufferReader(frame);
|
||||||
|
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||||
|
int pathLength = buffer.readUInt8();
|
||||||
|
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||||
|
buffer.skipBytes(4); // Skip auth code
|
||||||
|
Uint8List pathData = buffer.readBytes(pathLength);
|
||||||
|
Uint8List snrData = buffer.readRemainingBytes();
|
||||||
|
|
||||||
|
Map<int, Contact> pathContacts = {};
|
||||||
|
|
||||||
|
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
|
for (var repeaterData in pathData) {
|
||||||
|
if (listEquals(
|
||||||
|
repeater.publicKey.sublist(0, 1),
|
||||||
|
Uint8List.fromList([repeaterData]),
|
||||||
|
)) {
|
||||||
|
pathContacts[repeaterData] = repeater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_hasData = true;
|
||||||
|
_traceData = PathTraceData(
|
||||||
|
pathData: pathData,
|
||||||
|
snrData: snrData,
|
||||||
|
pathContacts: pathContacts,
|
||||||
|
);
|
||||||
|
_points = <LatLng>[];
|
||||||
|
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
for (final hop in _traceData!.pathData) {
|
||||||
|
final contact = _traceData!.pathContacts[hop];
|
||||||
|
if (contact != null &&
|
||||||
|
contact.hasLocation &&
|
||||||
|
contact.latitude != null &&
|
||||||
|
contact.longitude != null) {
|
||||||
|
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_polylines = _points.length > 1
|
||||||
|
? [
|
||||||
|
Polyline(
|
||||||
|
points: _points,
|
||||||
|
strokeWidth: 4,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Polyline>[];
|
||||||
|
|
||||||
|
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
|
||||||
|
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||||
|
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||||
|
_mapKey = ValueKey(
|
||||||
|
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||||
|
);
|
||||||
|
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, _) {
|
||||||
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
onPressed: _isLoading ? null : _doPathTrace,
|
||||||
|
tooltip: context.l10n.pathTrace_refreshTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (!_hasData)
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_isLoading) const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (!_isLoading && _failed2Loaded)
|
||||||
|
Text(context.l10n.pathTrace_notAvailable),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||||
|
if (_points.isEmpty &&
|
||||||
|
!_hasData &&
|
||||||
|
!_isLoading &&
|
||||||
|
!_failed2Loaded)
|
||||||
|
Center(
|
||||||
|
child: Card(
|
||||||
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.channelPath_noRepeaterLocations,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_hasData) _buildLegendCard(context, _traceData!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Marker> _buildHopMarkers(List<int> pathData) {
|
||||||
|
return [
|
||||||
|
for (final hop in pathData)
|
||||||
|
if (_traceData!.pathContacts[hop] != null &&
|
||||||
|
_traceData!.pathContacts[hop]!.hasLocation)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
_traceData!.pathContacts[hop]!.latitude!,
|
||||||
|
_traceData!.pathContacts[hop]!.longitude!,
|
||||||
|
),
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
_traceData!.pathContacts[hop]!.publicKey
|
||||||
|
.sublist(0, 1)
|
||||||
|
.map(
|
||||||
|
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
|
||||||
|
)
|
||||||
|
.join(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
||||||
|
context.read<MeshCoreConnector>().selfLongitude != null)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
context.read<MeshCoreConnector>().selfLatitude!,
|
||||||
|
context.read<MeshCoreConnector>().selfLongitude!,
|
||||||
|
),
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
context.l10n.pathTrace_you,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||||
|
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||||
|
if (index == 0) {
|
||||||
|
return context.l10n.pathTrace_you;
|
||||||
|
} else {
|
||||||
|
final contactName = pathTraceData
|
||||||
|
.pathContacts[pathTraceData.pathData[pathTraceData.pathData.length -
|
||||||
|
1]]
|
||||||
|
?.name;
|
||||||
|
final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1]
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
return contactName != null
|
||||||
|
? "$hex: $contactName"
|
||||||
|
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final contactName =
|
||||||
|
pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name;
|
||||||
|
final hex = pathTraceData.pathData[index - 1]
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
return contactName != null
|
||||||
|
? "$hex: $contactName"
|
||||||
|
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatDirectionSubText(PathTraceData pathTraceData, int index) {
|
||||||
|
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||||
|
if (index == 0) {
|
||||||
|
final contactName =
|
||||||
|
pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name;
|
||||||
|
final hex = pathTraceData.pathData[0]
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
return contactName != null
|
||||||
|
? "$hex: $contactName"
|
||||||
|
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||||
|
} else {
|
||||||
|
return context.l10n.pathTrace_you;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final contactName =
|
||||||
|
pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name;
|
||||||
|
final hex = pathTraceData.pathData[index]
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
return contactName != null
|
||||||
|
? "$hex: $contactName"
|
||||||
|
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMapPathTrace(
|
||||||
|
BuildContext context,
|
||||||
|
MapTileCacheService tileCache,
|
||||||
|
) {
|
||||||
|
return FlutterMap(
|
||||||
|
key: _mapKey,
|
||||||
|
options: MapOptions(
|
||||||
|
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||||
|
initialCenter: _initialCenter!,
|
||||||
|
initialZoom: _initialZoom,
|
||||||
|
initialCameraFit: _bounds == null
|
||||||
|
? null
|
||||||
|
: CameraFit.bounds(
|
||||||
|
bounds: _bounds!,
|
||||||
|
padding: const EdgeInsets.all(64),
|
||||||
|
maxZoom: 16,
|
||||||
|
),
|
||||||
|
minZoom: 2.0,
|
||||||
|
maxZoom: 18.0,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: kMapTileUrlTemplate,
|
||||||
|
tileProvider: tileCache.tileProvider,
|
||||||
|
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
||||||
|
maxZoom: 19,
|
||||||
|
),
|
||||||
|
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
||||||
|
if (_traceData!.pathData.isNotEmpty)
|
||||||
|
MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||||
|
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
||||||
|
final cardHeight = max(96.0, min(maxHeight, estimatedHeight));
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 16,
|
||||||
|
child: SizedBox(
|
||||||
|
height: cardHeight,
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: pathTraceData.pathData.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(l10n.channelPath_noHopDetailsAvailable),
|
||||||
|
)
|
||||||
|
: Scrollbar(
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
itemCount: pathTraceData.pathData.length + 1,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading:
|
||||||
|
index >= pathTraceData.snrData.length / 2
|
||||||
|
? Icon(Icons.call_received)
|
||||||
|
: Icon(Icons.call_made),
|
||||||
|
title: Text(
|
||||||
|
formatDirectionText(pathTraceData, index),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
formatDirectionSubText(
|
||||||
|
pathTraceData,
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
trailing: SNRIcon(
|
||||||
|
snr:
|
||||||
|
pathTraceData.snrData[index].toSigned(
|
||||||
|
8,
|
||||||
|
) /
|
||||||
|
4.0,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// Handle item tap
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,14 +119,24 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
|
|
||||||
// Show debug info if requested
|
// Show debug info if requested
|
||||||
if (showDebug && mounted) {
|
if (showDebug && mounted) {
|
||||||
final frame = buildSendCliCommandFrame(widget.repeater.publicKey, command);
|
final frame = buildSendCliCommandFrame(
|
||||||
DebugFrameViewer.showFrameDebug(context, frame, context.l10n.repeater_cliCommandFrameTitle);
|
widget.repeater.publicKey,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
DebugFrameViewer.showFrameDebug(
|
||||||
|
context,
|
||||||
|
frame,
|
||||||
|
context.l10n.repeater_cliCommandFrameTitle,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send CLI command to repeater with retry
|
// Send CLI command to repeater with retry
|
||||||
try {
|
try {
|
||||||
if (_commandService != null) {
|
if (_commandService != null) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
final repeater = _resolveRepeater(connector);
|
final repeater = _resolveRepeater(connector);
|
||||||
final response = await _commandService!.sendCommand(
|
final response = await _commandService!.sendCommand(
|
||||||
repeater,
|
repeater,
|
||||||
@@ -230,7 +240,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
Text(l10n.repeater_cliTitle),
|
Text(l10n.repeater_cliTitle),
|
||||||
Text(
|
Text(
|
||||||
repeater.name,
|
repeater.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -251,12 +264,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
value: 'auto',
|
value: 'auto',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.auto_mode,
|
||||||
|
size: 20,
|
||||||
|
color: !isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_autoUseSavedPath,
|
l10n.repeater_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: !isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -266,12 +287,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
value: 'flood',
|
value: 'flood',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.waves,
|
||||||
|
size: 20,
|
||||||
|
color: isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_forceFloodMode,
|
l10n.repeater_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -282,7 +311,8 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.timeline),
|
icon: const Icon(Icons.timeline),
|
||||||
tooltip: l10n.repeater_pathManagement,
|
tooltip: l10n.repeater_pathManagement,
|
||||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
onPressed: () =>
|
||||||
|
PathManagementDialog.show(context, contact: repeater),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.bug_report),
|
icon: const Icon(Icons.bug_report),
|
||||||
@@ -473,7 +503,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: l10n.repeater_enterCommandHint,
|
hintText: l10n.repeater_enterCommandHint,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
prefixText: '> ',
|
prefixText: '> ',
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
@@ -718,10 +751,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
final gpsCommands = [
|
final gpsCommands = [
|
||||||
_CommandHelpEntry(
|
_CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps),
|
||||||
command: 'gps',
|
|
||||||
description: l10n.repeater_cliHelpGps,
|
|
||||||
),
|
|
||||||
_CommandHelpEntry(
|
_CommandHelpEntry(
|
||||||
command: 'gps {on|off}',
|
command: 'gps {on|off}',
|
||||||
description: l10n.repeater_cliHelpGpsOnOff,
|
description: l10n.repeater_cliHelpGpsOnOff,
|
||||||
@@ -758,13 +788,25 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildHelpSection(context, l10n.repeater_general, generalCommands),
|
_buildHelpSection(
|
||||||
|
context,
|
||||||
|
l10n.repeater_general,
|
||||||
|
generalCommands,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands),
|
_buildHelpSection(
|
||||||
|
context,
|
||||||
|
l10n.repeater_settingsCategory,
|
||||||
|
settingsCommands,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildHelpSection(context, l10n.repeater_bridge, bridgeCommands),
|
_buildHelpSection(context, l10n.repeater_bridge, bridgeCommands),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildHelpSection(context, l10n.repeater_logging, loggingCommands),
|
_buildHelpSection(
|
||||||
|
context,
|
||||||
|
l10n.repeater_logging,
|
||||||
|
loggingCommands,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildHelpSection(
|
_buildHelpSection(
|
||||||
context,
|
context,
|
||||||
@@ -813,10 +855,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
),
|
),
|
||||||
if (note != null) ...[
|
if (note != null) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(note, style: const TextStyle(fontSize: 12)),
|
||||||
note,
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...commands.map((entry) => _buildHelpCommandCard(context, entry)),
|
...commands.map((entry) => _buildHelpCommandCard(context, entry)),
|
||||||
@@ -871,8 +910,5 @@ class _CommandHelpEntry {
|
|||||||
final String command;
|
final String command;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
const _CommandHelpEntry({
|
const _CommandHelpEntry({required this.command, required this.description});
|
||||||
required this.command,
|
|
||||||
required this.description,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
|
repeater.shortPubKeyHex,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
// Basic settings
|
// Basic settings
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _passwordController = TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
final TextEditingController _guestPasswordController = TextEditingController();
|
final TextEditingController _guestPasswordController =
|
||||||
|
TextEditingController();
|
||||||
|
|
||||||
// Radio settings
|
// Radio settings
|
||||||
final TextEditingController _freqController = TextEditingController();
|
final TextEditingController _freqController = TextEditingController();
|
||||||
@@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
bool _privacyMode = false;
|
bool _privacyMode = false;
|
||||||
|
|
||||||
// Advertisement settings
|
// Advertisement settings
|
||||||
|
bool _advertEnable = true;
|
||||||
int _advertInterval = 120; // minutes/2
|
int _advertInterval = 120; // minutes/2
|
||||||
|
bool _floodAdvertEnable = true;
|
||||||
int _floodAdvertInterval = 12; // hours
|
int _floodAdvertInterval = 12; // hours
|
||||||
int _privAdvertInterval = 60; // minutes
|
int _privAdvertInterval = 60; // minutes
|
||||||
|
|
||||||
@@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
if (_fetchedSettings.isEmpty) return;
|
if (_fetchedSettings.isEmpty) return;
|
||||||
|
|
||||||
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
||||||
appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Updating UI with keys: ${_fetchedSettings.keys.toList()}',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// Update name
|
// Update name
|
||||||
@@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
final radioStr = _fetchedSettings['radio']!;
|
final radioStr = _fetchedSettings['radio']!;
|
||||||
appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings');
|
appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings');
|
||||||
final parts = radioStr.split(',');
|
final parts = radioStr.split(',');
|
||||||
appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Split into ${parts.length} parts: $parts',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
|
|
||||||
if (parts.isNotEmpty) {
|
if (parts.isNotEmpty) {
|
||||||
final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim();
|
final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim();
|
||||||
@@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
appLog.info('CR text: "$crText"', tag: 'RadioSettings');
|
appLog.info('CR text: "$crText"', tag: 'RadioSettings');
|
||||||
_codingRate = int.tryParse(crText) ?? _codingRate;
|
_codingRate = int.tryParse(crText) ?? _codingRate;
|
||||||
}
|
}
|
||||||
appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fetchedSettings.containsKey('tx')) {
|
if (_fetchedSettings.containsKey('tx')) {
|
||||||
@@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_fetchedSettings.containsKey('lat')) {
|
if (_fetchedSettings.containsKey('lat')) {
|
||||||
appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Setting lat to: "${_fetchedSettings['lat']}"',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
_latController.text = _fetchedSettings['lat']!;
|
_latController.text = _fetchedSettings['lat']!;
|
||||||
}
|
}
|
||||||
if (_fetchedSettings.containsKey('lon')) {
|
if (_fetchedSettings.containsKey('lon')) {
|
||||||
appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Setting lon to: "${_fetchedSettings['lon']}"',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
_lonController.text = _fetchedSettings['lon']!;
|
_lonController.text = _fetchedSettings['lon']!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,12 +248,14 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
_fetchedSettings['advert.interval']!,
|
_fetchedSettings['advert.interval']!,
|
||||||
_advertInterval,
|
_advertInterval,
|
||||||
);
|
);
|
||||||
|
_advertEnable = _advertInterval > 0;
|
||||||
}
|
}
|
||||||
if (_fetchedSettings.containsKey('flood.advert.interval')) {
|
if (_fetchedSettings.containsKey('flood.advert.interval')) {
|
||||||
_floodAdvertInterval = _parseIntWithFallback(
|
_floodAdvertInterval = _parseIntWithFallback(
|
||||||
_fetchedSettings['flood.advert.interval']!,
|
_fetchedSettings['flood.advert.interval']!,
|
||||||
_floodAdvertInterval,
|
_floodAdvertInterval,
|
||||||
);
|
);
|
||||||
|
_floodAdvertEnable = _floodAdvertInterval > 0;
|
||||||
}
|
}
|
||||||
if (_fetchedSettings.containsKey('priv.advert.interval')) {
|
if (_fetchedSettings.containsKey('priv.advert.interval')) {
|
||||||
_privAdvertInterval = _parseIntWithFallback(
|
_privAdvertInterval = _parseIntWithFallback(
|
||||||
@@ -268,7 +288,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
|
|
||||||
void _applySettingResponse(String command, String response) {
|
void _applySettingResponse(String command, String response) {
|
||||||
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
||||||
appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings');
|
appLog.info(
|
||||||
|
'Command: "$command", Raw response: "$response"',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
final value = _extractCliValue(response);
|
final value = _extractCliValue(response);
|
||||||
appLog.info('Extracted value: "$value"', tag: 'RadioSettings');
|
appLog.info('Extracted value: "$value"', tag: 'RadioSettings');
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
@@ -280,7 +303,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
// Validate response content matches expected format for the command
|
// Validate response content matches expected format for the command
|
||||||
// This prevents mismatched responses over LoRa where order isn't guaranteed
|
// This prevents mismatched responses over LoRa where order isn't guaranteed
|
||||||
if (!_validateResponseForCommand(key, value)) {
|
if (!_validateResponseForCommand(key, value)) {
|
||||||
appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings');
|
appLog.warn(
|
||||||
|
'Response "$value" does not match expected format for "$key", ignoring',
|
||||||
|
tag: 'RadioSettings',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +337,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
// Must have at least 3 commas and start with a frequency-like number
|
// Must have at least 3 commas and start with a frequency-like number
|
||||||
final parts = value.split(',');
|
final parts = value.split(',');
|
||||||
if (parts.length < 4) return false;
|
if (parts.length < 4) return false;
|
||||||
final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), ''));
|
final freq = double.tryParse(
|
||||||
|
parts[0].replaceAll(RegExp(r'[^0-9.]'), ''),
|
||||||
|
);
|
||||||
// Frequency should be in reasonable LoRa range (300-2500 MHz)
|
// Frequency should be in reasonable LoRa range (300-2500 MHz)
|
||||||
return freq != null && freq >= 300 && freq <= 2500;
|
return freq != null && freq >= 300 && freq <= 2500;
|
||||||
|
|
||||||
@@ -339,22 +367,33 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
case 'privacy':
|
case 'privacy':
|
||||||
// Boolean values: on/off/true/false/1/0/enabled/disabled
|
// Boolean values: on/off/true/false/1/0/enabled/disabled
|
||||||
final lower = value.toLowerCase().trim();
|
final lower = value.toLowerCase().trim();
|
||||||
return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower);
|
return [
|
||||||
|
'on',
|
||||||
|
'off',
|
||||||
|
'true',
|
||||||
|
'false',
|
||||||
|
'1',
|
||||||
|
'0',
|
||||||
|
'enabled',
|
||||||
|
'disabled',
|
||||||
|
].contains(lower);
|
||||||
|
|
||||||
case 'advert.interval':
|
case 'advert.interval':
|
||||||
case 'flood.advert.interval':
|
case 'flood.advert.interval':
|
||||||
case 'priv.advert.interval':
|
case 'priv.advert.interval':
|
||||||
// Interval: positive integer
|
// Interval: non-negative integer (0 means disabled)
|
||||||
if (value.contains(',')) return false;
|
if (value.contains(',')) return false;
|
||||||
final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), ''));
|
final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), ''));
|
||||||
return interval != null && interval > 0;
|
return interval != null && interval >= 0;
|
||||||
|
|
||||||
case 'name':
|
case 'name':
|
||||||
// Name: any non-empty string, but should NOT look like radio settings
|
// Name: any non-empty string, but should NOT look like radio settings
|
||||||
if (value.isEmpty) return false;
|
if (value.isEmpty) return false;
|
||||||
// If it has 3+ commas and looks like numbers, probably radio data
|
// If it has 3+ commas and looks like numbers, probably radio data
|
||||||
final commaCount = ','.allMatches(value).length;
|
final commaCount = ','.allMatches(value).length;
|
||||||
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false;
|
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -551,7 +590,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
final freqMHz = double.tryParse(_freqController.text);
|
final freqMHz = double.tryParse(_freqController.text);
|
||||||
if (freqMHz != null) {
|
if (freqMHz != null) {
|
||||||
final bwKHz = _bandwidth! / 1000;
|
final bwKHz = _bandwidth! / 1000;
|
||||||
commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate');
|
commands.add(
|
||||||
|
'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +631,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
timestampSeconds: timestampSeconds,
|
timestampSeconds: timestampSeconds,
|
||||||
);
|
);
|
||||||
await connector.sendFrame(frame);
|
await connector.sendFrame(frame);
|
||||||
await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands
|
await Future.delayed(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
); // Delay between commands
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -614,7 +657,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.repeater_errorSavingSettings(e.toString())),
|
content: Text(
|
||||||
|
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||||
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -699,7 +744,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
Text(l10n.repeater_settingsTitle),
|
Text(l10n.repeater_settingsTitle),
|
||||||
Text(
|
Text(
|
||||||
repeater.name,
|
repeater.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -723,12 +771,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
value: 'auto',
|
value: 'auto',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.auto_mode,
|
||||||
|
size: 20,
|
||||||
|
color: !isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_autoUseSavedPath,
|
l10n.repeater_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: !isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -738,12 +794,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
value: 'flood',
|
value: 'flood',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.waves,
|
||||||
|
size: 20,
|
||||||
|
color: isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_forceFloodMode,
|
l10n.repeater_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -754,7 +818,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.timeline),
|
icon: const Icon(Icons.timeline),
|
||||||
tooltip: l10n.repeater_pathManagement,
|
tooltip: l10n.repeater_pathManagement,
|
||||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
onPressed: () =>
|
||||||
|
PathManagementDialog.show(context, contact: repeater),
|
||||||
),
|
),
|
||||||
if (_hasChanges)
|
if (_hasChanges)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
@@ -865,7 +930,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixText: 'MHz',
|
suffixText: 'MHz',
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
onChanged: (_) => _markChanged(),
|
onChanged: (_) => _markChanged(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -923,10 +990,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: _spreadingFactorOptions.map((sf) {
|
items: _spreadingFactorOptions.map((sf) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(value: sf, child: Text('SF$sf'));
|
||||||
value: sf,
|
|
||||||
child: Text('SF$sf'),
|
|
||||||
);
|
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@@ -945,10 +1009,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: _codingRateOptions.map((cr) {
|
items: _codingRateOptions.map((cr) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(value: cr, child: Text('4/$cr'));
|
||||||
value: cr,
|
|
||||||
child: Text('4/$cr'),
|
|
||||||
);
|
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@@ -988,7 +1049,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
helperText: l10n.repeater_latitudeHelper,
|
helperText: l10n.repeater_latitudeHelper,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
signed: true,
|
||||||
|
),
|
||||||
onChanged: (_) => _markChanged(),
|
onChanged: (_) => _markChanged(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -999,7 +1063,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
helperText: l10n.repeater_longitudeHelper,
|
helperText: l10n.repeater_longitudeHelper,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
signed: true,
|
||||||
|
),
|
||||||
onChanged: (_) => _markChanged(),
|
onChanged: (_) => _markChanged(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1018,11 +1085,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color),
|
Icon(
|
||||||
|
Icons.toggle_on,
|
||||||
|
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_features,
|
l10n.repeater_features,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1102,7 +1175,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.refresh, size: 20),
|
: const Icon(Icons.refresh, size: 20),
|
||||||
onPressed: isRefreshing ? null : onRefresh,
|
onPressed: isRefreshing ? null : onRefresh,
|
||||||
tooltip: refreshTooltip,
|
tooltip: refreshTooltip,
|
||||||
@@ -1130,40 +1203,72 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.repeater_localAdvertInterval),
|
title: Text(l10n.repeater_localAdvertInterval),
|
||||||
subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
|
subtitle: Text(
|
||||||
trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
|
l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: _advertEnable,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_advertInterval = value ? 60 : 0;
|
||||||
|
_advertEnable = value;
|
||||||
|
});
|
||||||
|
_markChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _advertInterval.toDouble(),
|
value: _advertInterval == 0
|
||||||
|
? 60.toDouble()
|
||||||
|
: _advertInterval.toDouble(),
|
||||||
min: 60,
|
min: 60,
|
||||||
max: 240,
|
max: 240,
|
||||||
divisions: 18,
|
divisions: 18,
|
||||||
label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
|
label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
|
||||||
onChanged: (value) {
|
onChanged: _advertEnable
|
||||||
setState(() {
|
? (value) {
|
||||||
_advertInterval = value.toInt();
|
setState(() {
|
||||||
});
|
_advertInterval = value.toInt();
|
||||||
_markChanged();
|
});
|
||||||
},
|
_markChanged();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.repeater_floodAdvertInterval),
|
title: Text(l10n.repeater_floodAdvertInterval),
|
||||||
subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
|
subtitle: Text(
|
||||||
trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
|
l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: _floodAdvertEnable,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_floodAdvertInterval = value ? 3 : 0;
|
||||||
|
_floodAdvertEnable = value;
|
||||||
|
});
|
||||||
|
_markChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _floodAdvertInterval.toDouble(),
|
value: _floodAdvertInterval == 0
|
||||||
|
? 3.toDouble()
|
||||||
|
: _floodAdvertInterval.toDouble(),
|
||||||
min: 3,
|
min: 3,
|
||||||
max: 48,
|
max: 168,
|
||||||
divisions: 45,
|
divisions: 165,
|
||||||
label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
|
label: l10n.repeater_floodAdvertIntervalHours(
|
||||||
onChanged: (value) {
|
_floodAdvertInterval,
|
||||||
setState(() {
|
),
|
||||||
_floodAdvertInterval = value.toInt();
|
onChanged: _floodAdvertEnable
|
||||||
});
|
? (value) {
|
||||||
_markChanged();
|
setState(() {
|
||||||
},
|
_floodAdvertInterval = value.toInt();
|
||||||
|
});
|
||||||
|
_markChanged();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
// Encrypted advertisement interval - hidden until privacy mode is implemented
|
// Encrypted advertisement interval - hidden until privacy mode is implemented
|
||||||
// if (_privacyMode) ...[
|
// if (_privacyMode) ...[
|
||||||
@@ -1220,10 +1325,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer),
|
leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer),
|
||||||
title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)),
|
title: Text(
|
||||||
|
l10n.repeater_rebootRepeater,
|
||||||
|
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
l10n.repeater_rebootRepeaterSubtitle,
|
l10n.repeater_rebootRepeaterSubtitle,
|
||||||
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
|
style: TextStyle(
|
||||||
|
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () => _confirmAction(
|
onTap: () => _confirmAction(
|
||||||
l10n.repeater_rebootRepeater,
|
l10n.repeater_rebootRepeater,
|
||||||
@@ -1246,11 +1356,19 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer),
|
leading: Icon(
|
||||||
title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)),
|
Icons.delete_forever,
|
||||||
|
color: colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
l10n.repeater_eraseFileSystem,
|
||||||
|
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
l10n.repeater_eraseFileSystemSubtitle,
|
l10n.repeater_eraseFileSystemSubtitle,
|
||||||
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
|
style: TextStyle(
|
||||||
|
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () => _confirmAction(
|
onTap: () => _confirmAction(
|
||||||
l10n.repeater_eraseFileSystem,
|
l10n.repeater_eraseFileSystem,
|
||||||
@@ -1272,9 +1390,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
|
|
||||||
if (command == 'erase') {
|
if (command == 'erase') {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(l10n.repeater_eraseSerialOnly)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ class RepeaterStatusScreen extends StatefulWidget {
|
|||||||
class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||||
static const int _statusPayloadOffset = 8;
|
static const int _statusPayloadOffset = 8;
|
||||||
static const int _statusStatsSize = 52;
|
static const int _statusStatsSize = 52;
|
||||||
static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize;
|
static const int _statusResponseBytes =
|
||||||
|
_statusPayloadOffset + _statusStatsSize;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
@@ -293,7 +294,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
|
content: Text(
|
||||||
|
context.l10n.repeater_errorLoadingStatus(e.toString()),
|
||||||
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -327,7 +330,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
Text(l10n.repeater_statusTitle),
|
Text(l10n.repeater_statusTitle),
|
||||||
Text(
|
Text(
|
||||||
repeater.name,
|
repeater.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -348,12 +354,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
value: 'auto',
|
value: 'auto',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.auto_mode,
|
||||||
|
size: 20,
|
||||||
|
color: !isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_autoUseSavedPath,
|
l10n.repeater_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: !isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -363,12 +377,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
value: 'flood',
|
value: 'flood',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
Icon(
|
||||||
|
Icons.waves,
|
||||||
|
size: 20,
|
||||||
|
color: isFloodMode
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_forceFloodMode,
|
l10n.repeater_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isFloodMode
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -379,7 +401,8 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.timeline),
|
icon: const Icon(Icons.timeline),
|
||||||
tooltip: l10n.repeater_pathManagement,
|
tooltip: l10n.repeater_pathManagement,
|
||||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
onPressed: () =>
|
||||||
|
PathManagementDialog.show(context, contact: repeater),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _isLoading
|
icon: _isLoading
|
||||||
@@ -423,11 +446,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.info_outline, color: Theme.of(context).textTheme.headlineSmall?.color),
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_systemInformation,
|
l10n.repeater_systemInformation,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -453,18 +482,30 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.radio, color: Theme.of(context).textTheme.headlineSmall?.color),
|
Icon(
|
||||||
|
Icons.radio,
|
||||||
|
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_radioStatistics,
|
l10n.repeater_radioStatistics,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildInfoRow(l10n.repeater_lastRssi, _formatValue(_lastRssi, suffix: ' dB')),
|
_buildInfoRow(
|
||||||
|
l10n.repeater_lastRssi,
|
||||||
|
_formatValue(_lastRssi, suffix: ' dB'),
|
||||||
|
),
|
||||||
_buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)),
|
_buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)),
|
||||||
_buildInfoRow(l10n.repeater_noiseFloor, _formatValue(_noiseFloor, suffix: ' dB')),
|
_buildInfoRow(
|
||||||
|
l10n.repeater_noiseFloor,
|
||||||
|
_formatValue(_noiseFloor, suffix: ' dB'),
|
||||||
|
),
|
||||||
_buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)),
|
_buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)),
|
||||||
_buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)),
|
_buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)),
|
||||||
],
|
],
|
||||||
@@ -483,11 +524,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.analytics, color: Theme.of(context).textTheme.headlineSmall?.color),
|
Icon(
|
||||||
|
Icons.analytics,
|
||||||
|
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_packetStatistics,
|
l10n.repeater_packetStatistics,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -561,7 +608,8 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
if (_statusRequestedAt == null) return '—';
|
if (_statusRequestedAt == null) return '—';
|
||||||
final dt = _statusRequestedAt!;
|
final dt = _statusRequestedAt!;
|
||||||
final date = '${dt.day}/${dt.month}/${dt.year}';
|
final date = '${dt.day}/${dt.month}/${dt.year}';
|
||||||
final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
final time =
|
||||||
|
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||||
return '$date $time';
|
return '$date $time';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +646,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
final direct = _formatValue(_dupDirect);
|
final direct = _formatValue(_dupDirect);
|
||||||
return l10n.repeater_duplicatesFloodDirect(flood, direct);
|
return l10n.repeater_duplicatesFloodDirect(flood, direct);
|
||||||
}
|
}
|
||||||
if (_packetsRecv == null || _floodRx == null || _directRx == null) return '—';
|
if (_packetsRecv == null || _floodRx == null || _directRx == null) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
final dupTotal = _packetsRecv! - _floodRx! - _directRx!;
|
final dupTotal = _packetsRecv! - _floodRx! - _directRx!;
|
||||||
if (dupTotal < 0) return '—';
|
if (dupTotal < 0) return '—';
|
||||||
return l10n.repeater_duplicatesTotal(dupTotal);
|
return l10n.repeater_duplicatesTotal(dupTotal);
|
||||||
|
|||||||
+126
-34
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -8,9 +11,61 @@ import '../widgets/device_tile.dart';
|
|||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
|
|
||||||
/// Screen for scanning and connecting to MeshCore devices
|
/// Screen for scanning and connecting to MeshCore devices
|
||||||
class ScannerScreen extends StatelessWidget {
|
class ScannerScreen extends StatefulWidget {
|
||||||
const ScannerScreen({super.key});
|
const ScannerScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScannerScreen> createState() => _ScannerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScannerScreenState extends State<ScannerScreen> {
|
||||||
|
bool _changedNavigation = false;
|
||||||
|
late final VoidCallback _connectionListener;
|
||||||
|
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||||
|
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_bluetoothState = state;
|
||||||
|
});
|
||||||
|
// Cancel scan if Bluetooth turns off while scanning
|
||||||
|
if (state != BluetoothAdapterState.on) {
|
||||||
|
unawaited(connector.stopScan());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
connector.removeListener(_connectionListener);
|
||||||
|
unawaited(_bluetoothStateSubscription.cancel());
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -25,13 +80,15 @@ class ScannerScreen extends StatelessWidget {
|
|||||||
builder: (context, connector, child) {
|
builder: (context, connector, child) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
// Bluetooth off warning
|
||||||
|
if (_bluetoothState == BluetoothAdapterState.off)
|
||||||
|
_bluetoothOffWarning(context),
|
||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
_buildStatusBar(context, connector),
|
_buildStatusBar(context, connector),
|
||||||
|
|
||||||
// Device list
|
// Device list
|
||||||
Expanded(
|
Expanded(child: _buildDeviceList(context, connector)),
|
||||||
child: _buildDeviceList(context, connector),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -39,17 +96,21 @@ class ScannerScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, child) {
|
builder: (context, connector, child) {
|
||||||
final isScanning = connector.state == MeshCoreConnectionState.scanning;
|
final isScanning =
|
||||||
|
connector.state == MeshCoreConnectionState.scanning;
|
||||||
|
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||||
|
|
||||||
return FloatingActionButton.extended(
|
return FloatingActionButton.extended(
|
||||||
onPressed: () {
|
onPressed: isBluetoothOff
|
||||||
if (isScanning) {
|
? null
|
||||||
connector.stopScan();
|
: () {
|
||||||
} else {
|
if (isScanning) {
|
||||||
connector.startScan();
|
connector.stopScan();
|
||||||
}
|
} else {
|
||||||
},
|
connector.startScan();
|
||||||
icon: isScanning
|
}
|
||||||
|
},
|
||||||
|
icon: isScanning
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -59,7 +120,11 @@ class ScannerScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.bluetooth_searching),
|
: const Icon(Icons.bluetooth_searching),
|
||||||
label: Text(isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan),
|
label: Text(
|
||||||
|
isScanning
|
||||||
|
? context.l10n.scanner_stop
|
||||||
|
: context.l10n.scanner_scan,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -70,7 +135,7 @@ class ScannerScreen extends StatelessWidget {
|
|||||||
String statusText;
|
String statusText;
|
||||||
Color statusColor;
|
Color statusColor;
|
||||||
|
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
switch (connector.state) {
|
switch (connector.state) {
|
||||||
case MeshCoreConnectionState.scanning:
|
case MeshCoreConnectionState.scanning:
|
||||||
statusText = l10n.scanner_scanning;
|
statusText = l10n.scanner_scanning;
|
||||||
@@ -117,20 +182,13 @@ final l10n = context.l10n;
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]),
|
||||||
Icons.bluetooth,
|
|
||||||
size: 64,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
connector.state == MeshCoreConnectionState.scanning
|
connector.state == MeshCoreConnectionState.scanning
|
||||||
? context.l10n.scanner_searchingDevices
|
? context.l10n.scanner_searchingDevices
|
||||||
: context.l10n.scanner_tapToScan,
|
: context.l10n.scanner_tapToScan,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -161,15 +219,6 @@ final l10n = context.l10n;
|
|||||||
? result.device.platformName
|
? result.device.platformName
|
||||||
: result.advertisementData.advName;
|
: result.advertisementData.advName;
|
||||||
await connector.connect(result.device, displayName: name);
|
await connector.connect(result.device, displayName: name);
|
||||||
|
|
||||||
if (context.mounted && connector.isConnected) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const ContactsScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -181,4 +230,47 @@ final l10n = context.l10n;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _bluetoothOffWarning(BuildContext context) {
|
||||||
|
final errorColor = Theme.of(context).colorScheme.error;
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
color: errorColor.withValues(alpha: 0.15),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.scanner_bluetoothOff,
|
||||||
|
style: TextStyle(
|
||||||
|
color: errorColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
context.l10n.scanner_bluetoothOffMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
color: errorColor.withValues(alpha: 0.85),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => FlutterBluePlus.turnOn(),
|
||||||
|
child: Text(context.l10n.scanner_enableBluetooth),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/utils/gpx_export.dart';
|
||||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
@@ -20,6 +21,7 @@ class SettingsScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SettingsScreenState extends State<SettingsScreen> {
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
bool _showBatteryVoltage = false;
|
bool _showBatteryVoltage = false;
|
||||||
|
bool _deviceInfoExpanded = false;
|
||||||
String _appVersion = '';
|
String _appVersion = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -57,6 +59,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildDebugCard(context),
|
_buildDebugCard(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
_buildExportCard(connector),
|
||||||
|
const SizedBox(height: 16),
|
||||||
_buildAboutCard(context),
|
_buildAboutCard(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -71,43 +75,84 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
) {
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
InkWell(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
Text(
|
onTap: () {
|
||||||
l10n.settings_deviceInfo,
|
setState(() {
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
_deviceInfoExpanded = !_deviceInfoExpanded;
|
||||||
),
|
});
|
||||||
const SizedBox(height: 16),
|
},
|
||||||
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
|
child: Padding(
|
||||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||||
_buildInfoRow(
|
child: Row(
|
||||||
l10n.settings_infoStatus,
|
children: [
|
||||||
connector.isConnected
|
Expanded(
|
||||||
? l10n.common_connected
|
child: Text(
|
||||||
: l10n.common_disconnected,
|
l10n.settings_deviceInfo,
|
||||||
),
|
style: const TextStyle(
|
||||||
_buildBatteryInfoRow(context, connector),
|
fontSize: 18,
|
||||||
if (connector.selfName != null)
|
fontWeight: FontWeight.bold,
|
||||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
),
|
||||||
if (connector.selfPublicKey != null)
|
),
|
||||||
_buildInfoRow(
|
),
|
||||||
l10n.settings_infoPublicKey,
|
AnimatedRotation(
|
||||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
turns: _deviceInfoExpanded ? 0.5 : 0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: const Icon(Icons.expand_more),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
|
||||||
l10n.settings_infoContactsCount,
|
|
||||||
'${connector.contacts.length}',
|
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
),
|
||||||
l10n.settings_infoChannelCount,
|
|
||||||
'${connector.channels.length}',
|
AnimatedCrossFade(
|
||||||
|
firstChild: const SizedBox.shrink(),
|
||||||
|
secondChild: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildInfoRow(
|
||||||
|
l10n.settings_infoName,
|
||||||
|
connector.deviceDisplayName,
|
||||||
|
),
|
||||||
|
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||||
|
_buildInfoRow(
|
||||||
|
l10n.settings_infoStatus,
|
||||||
|
connector.isConnected
|
||||||
|
? l10n.common_connected
|
||||||
|
: l10n.common_disconnected,
|
||||||
|
),
|
||||||
|
_buildBatteryInfoRow(context, connector),
|
||||||
|
if (connector.selfName != null)
|
||||||
|
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||||
|
if (connector.selfPublicKey != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
l10n.settings_infoPublicKey,
|
||||||
|
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
l10n.settings_infoContactsCount,
|
||||||
|
'${connector.contacts.length}',
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
l10n.settings_infoChannelCount,
|
||||||
|
'${connector.channels.length}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
crossFadeState: _deviceInfoExpanded
|
||||||
),
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -352,22 +397,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Color? valueColor,
|
Color? valueColor,
|
||||||
VoidCallback? onTap,
|
VoidCallback? onTap,
|
||||||
}) {
|
}) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final row = Padding(
|
final row = Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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])),
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Flexible(
|
const SizedBox(height: 4),
|
||||||
child: Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
overflow: TextOverflow.ellipsis,
|
color: valueColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -376,11 +432,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
if (onTap != null) {
|
if (onTap != null) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: row,
|
child: row,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,7 +499,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool isGPSEnabled = customVars["gps"] == "1";
|
bool isGPSEnabled = customVars["gps"] == "1";
|
||||||
|
|
||||||
// Read current interval or default to 900 (15 minutes)
|
// Read current interval or default to 900 (15 minutes)
|
||||||
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
final currentInterval =
|
||||||
|
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||||
intervalController.text = currentInterval.toString();
|
intervalController.text = currentInterval.toString();
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -683,6 +741,110 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _gpxExport(
|
||||||
|
GpxExport exporter,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
String filename,
|
||||||
|
String shareText,
|
||||||
|
String subject,
|
||||||
|
) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final result = await exporter.exportGPX(
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
filename,
|
||||||
|
shareText,
|
||||||
|
subject,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
switch (result) {
|
||||||
|
case gpxExportSuccess:
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess)));
|
||||||
|
case gpxExportNoContacts:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case gpxExportNotAvailable:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case gpxExportFailed:
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExportCard(MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.download_outlined),
|
||||||
|
title: Text(l10n.settings_gpxExportRepeaters),
|
||||||
|
subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final exporter = GpxExport(connector);
|
||||||
|
exporter.addRepeaters();
|
||||||
|
_gpxExport(
|
||||||
|
exporter,
|
||||||
|
l10n.map_repeater,
|
||||||
|
l10n.settings_gpxExportRepeatersRoom,
|
||||||
|
"meshcore_repeaters_",
|
||||||
|
l10n.settings_gpxExportShareText,
|
||||||
|
l10n.settings_gpxExportShareSubject,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.download_outlined),
|
||||||
|
title: Text(l10n.settings_gpxExportContacts),
|
||||||
|
subtitle: Text(l10n.settings_gpxExportContactsSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final exporter = GpxExport(connector);
|
||||||
|
exporter.addContacts();
|
||||||
|
_gpxExport(
|
||||||
|
exporter,
|
||||||
|
l10n.map_repeater,
|
||||||
|
l10n.settings_gpxExportChat,
|
||||||
|
"meshcore_contacts_",
|
||||||
|
l10n.settings_gpxExportShareText,
|
||||||
|
l10n.settings_gpxExportShareSubject,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.download_outlined),
|
||||||
|
title: Text(l10n.settings_gpxExportAll),
|
||||||
|
subtitle: Text(l10n.settings_gpxExportAllSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final exporter = GpxExport(connector);
|
||||||
|
exporter.addAll();
|
||||||
|
_gpxExport(
|
||||||
|
exporter,
|
||||||
|
l10n.map_repeater,
|
||||||
|
l10n.settings_gpxExportAllContacts,
|
||||||
|
"meshcore_all_",
|
||||||
|
l10n.settings_gpxExportShareText,
|
||||||
|
l10n.settings_gpxExportShareSubject,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RadioSettingsDialog extends StatefulWidget {
|
class _RadioSettingsDialog extends StatefulWidget {
|
||||||
@@ -700,6 +862,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
|
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
|
||||||
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
|
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
|
||||||
final _txPowerController = TextEditingController(text: '20');
|
final _txPowerController = TextEditingController(text: '20');
|
||||||
|
bool _clientRepeat = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -749,6 +912,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
if (widget.connector.currentTxPower != null) {
|
if (widget.connector.currentTxPower != null) {
|
||||||
_txPowerController.text = widget.connector.currentTxPower.toString();
|
_txPowerController.text = widget.connector.currentTxPower.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clientRepeat = widget.connector.clientRepeat ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -780,10 +945,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txPower == null || txPower < 0 || txPower > 22) {
|
final maxTxPower = widget.connector.maxTxPower ?? 22;
|
||||||
ScaffoldMessenger.of(
|
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
|
||||||
context,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
|
SnackBar(
|
||||||
|
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,9 +963,29 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
widget.connector.currentCr,
|
widget.connector.currentCr,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if the client repeat isnt null then we know its supported
|
||||||
|
//otherwise we leave it out of the frame to avoid accidentally enabling
|
||||||
|
final knownRepeat = widget.connector.clientRepeat != null;
|
||||||
|
|
||||||
|
if (knownRepeat) {
|
||||||
|
const validRepeatFreqsKHz = {433000, 869000, 918000};
|
||||||
|
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await widget.connector.sendFrame(
|
await widget.connector.sendFrame(
|
||||||
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
|
buildSetRadioParamsFrame(
|
||||||
|
freqHz,
|
||||||
|
bwHz,
|
||||||
|
sf,
|
||||||
|
cr,
|
||||||
|
clientRepeat: knownRepeat ? _clientRepeat : null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
||||||
await widget.connector.refreshDeviceInfo();
|
await widget.connector.refreshDeviceInfo();
|
||||||
@@ -836,37 +1024,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
DropdownButtonFormField<int>(
|
||||||
l10n.settings_presets,
|
decoration: InputDecoration(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
labelText: l10n.settings_presets,
|
||||||
),
|
border: const OutlineInputBorder(),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Wrap(
|
items: [
|
||||||
spacing: 8,
|
for (var i = 0; i < RadioSettings.presets.length; i++)
|
||||||
children: [
|
DropdownMenuItem(
|
||||||
_PresetChip(
|
value: i,
|
||||||
label: l10n.settings_preset915Mhz,
|
child: Text(RadioSettings.presets[i].$1),
|
||||||
onTap: () => _applyPreset(RadioSettings.preset915MHz),
|
),
|
||||||
),
|
|
||||||
_PresetChip(
|
|
||||||
label: l10n.settings_preset868Mhz,
|
|
||||||
onTap: () => _applyPreset(RadioSettings.preset868MHz),
|
|
||||||
),
|
|
||||||
_PresetChip(
|
|
||||||
label: l10n.settings_preset433Mhz,
|
|
||||||
onTap: () => _applyPreset(RadioSettings.preset433MHz),
|
|
||||||
),
|
|
||||||
_PresetChip(
|
|
||||||
label: l10n.settings_longRange,
|
|
||||||
onTap: () => _applyPreset(RadioSettings.presetLongRange),
|
|
||||||
),
|
|
||||||
_PresetChip(
|
|
||||||
label: l10n.settings_fastSpeed,
|
|
||||||
onTap: () => _applyPreset(RadioSettings.presetFastSpeed),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
onChanged: (index) {
|
||||||
|
if (index != null) {
|
||||||
|
_applyPreset(RadioSettings.presets[index].$2);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _frequencyController,
|
controller: _frequencyController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -932,10 +1108,22 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.settings_txPower,
|
labelText: l10n.settings_txPower,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
helperText: l10n.settings_txPowerHelper,
|
helperText: widget.connector.maxTxPower != null
|
||||||
|
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
|
||||||
|
: l10n.settings_txPowerHelper,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
|
if (widget.connector.clientRepeat != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text(l10n.settings_clientRepeat),
|
||||||
|
subtitle: Text(l10n.settings_clientRepeatSubtitle),
|
||||||
|
value: _clientRepeat,
|
||||||
|
onChanged: (value) => setState(() => _clientRepeat = value),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -949,15 +1137,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PresetChip extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
const _PresetChip({required this.label, required this.onTap});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ActionChip(label: Text(label), onPressed: onTap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
enum AppDebugLogLevel {
|
enum AppDebugLogLevel { info, warning, error }
|
||||||
info,
|
|
||||||
warning,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppDebugLogEntry {
|
class AppDebugLogEntry {
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
@@ -51,7 +47,11 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void log(String message, {String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info}) {
|
void log(
|
||||||
|
String message, {
|
||||||
|
String tag = 'App',
|
||||||
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
|
}) {
|
||||||
if (!_enabled) return;
|
if (!_enabled) return;
|
||||||
|
|
||||||
_entries.add(
|
_entries.add(
|
||||||
|
|||||||
@@ -82,10 +82,7 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
final safeMin = minZoom <= maxZoom ? minZoom : maxZoom;
|
final safeMin = minZoom <= maxZoom ? minZoom : maxZoom;
|
||||||
final safeMax = minZoom <= maxZoom ? maxZoom : minZoom;
|
final safeMax = minZoom <= maxZoom ? maxZoom : minZoom;
|
||||||
await updateSettings(
|
await updateSettings(
|
||||||
_settings.copyWith(
|
_settings.copyWith(mapCacheMinZoom: safeMin, mapCacheMaxZoom: safeMax),
|
||||||
mapCacheMinZoom: safeMin,
|
|
||||||
mapCacheMaxZoom: safeMax,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +120,16 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
appLogger.setEnabled(value);
|
appLogger.setEnabled(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setBatteryChemistryForDevice(String deviceId, String chemistry) async {
|
Future<void> setBatteryChemistryForDevice(
|
||||||
final updated = Map<String, String>.from(_settings.batteryChemistryByDeviceId);
|
String deviceId,
|
||||||
|
String chemistry,
|
||||||
|
) async {
|
||||||
|
final updated = Map<String, String>.from(
|
||||||
|
_settings.batteryChemistryByDeviceId,
|
||||||
|
);
|
||||||
updated[deviceId] = chemistry;
|
updated[deviceId] = chemistry;
|
||||||
await updateSettings(_settings.copyWith(batteryChemistryByDeviceId: updated));
|
await updateSettings(
|
||||||
|
_settings.copyWith(batteryChemistryByDeviceId: updated),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:isolate';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
@@ -15,20 +14,14 @@ class BackgroundService {
|
|||||||
channelDescription: 'Keeps MeshCore running in the background.',
|
channelDescription: 'Keeps MeshCore running in the background.',
|
||||||
channelImportance: NotificationChannelImportance.LOW,
|
channelImportance: NotificationChannelImportance.LOW,
|
||||||
priority: NotificationPriority.LOW,
|
priority: NotificationPriority.LOW,
|
||||||
iconData: const NotificationIconData(
|
|
||||||
resType: ResourceType.mipmap,
|
|
||||||
resPrefix: ResourcePrefix.ic,
|
|
||||||
name: 'launcher',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
iosNotificationOptions: const IOSNotificationOptions(
|
iosNotificationOptions: const IOSNotificationOptions(
|
||||||
showNotification: false,
|
showNotification: false,
|
||||||
playSound: false,
|
playSound: false,
|
||||||
),
|
),
|
||||||
foregroundTaskOptions: const ForegroundTaskOptions(
|
foregroundTaskOptions: ForegroundTaskOptions(
|
||||||
interval: 5000,
|
eventAction: ForegroundTaskEventAction.repeat(5000),
|
||||||
autoRunOnBoot: false,
|
autoRunOnBoot: false,
|
||||||
allowWakeLock: true,
|
|
||||||
allowWifiLock: false,
|
allowWifiLock: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -64,13 +57,13 @@ void startCallback() {
|
|||||||
|
|
||||||
class _MeshCoreTaskHandler extends TaskHandler {
|
class _MeshCoreTaskHandler extends TaskHandler {
|
||||||
@override
|
@override
|
||||||
void onStart(DateTime timestamp, SendPort? sendPort) {}
|
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {}
|
void onRepeatEvent(DateTime timestamp) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onDestroy(DateTime timestamp, SendPort? sendPort) {}
|
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onNotificationButtonPressed(String id) {}
|
void onNotificationButtonPressed(String id) {}
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
return 'CMD_GET_RADIO_SETTINGS';
|
return 'CMD_GET_RADIO_SETTINGS';
|
||||||
case cmdSetCustomVar:
|
case cmdSetCustomVar:
|
||||||
return 'CMD_SET_CUSTOM_VAR';
|
return 'CMD_SET_CUSTOM_VAR';
|
||||||
|
case cmdSendTracePath:
|
||||||
|
return 'CMD_SEND_TRACE_PATH';
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
return 'RESP_CODE_CHANNEL_INFO';
|
return 'RESP_CODE_CHANNEL_INFO';
|
||||||
case respCodeRadioSettings:
|
case respCodeRadioSettings:
|
||||||
return 'RESP_CODE_RADIO_SETTINGS';
|
return 'RESP_CODE_RADIO_SETTINGS';
|
||||||
|
case pushCodeTraceData:
|
||||||
|
return 'PUSH_CODE_TRACE_DATA';
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,20 +42,21 @@ class MapTileCacheService {
|
|||||||
late final TileProvider tileProvider;
|
late final TileProvider tileProvider;
|
||||||
|
|
||||||
MapTileCacheService({BaseCacheManager? cacheManager})
|
MapTileCacheService({BaseCacheManager? cacheManager})
|
||||||
: cacheManager = cacheManager ??
|
: cacheManager =
|
||||||
CacheManager(
|
cacheManager ??
|
||||||
Config(
|
CacheManager(
|
||||||
cacheKey,
|
Config(
|
||||||
stalePeriod: const Duration(days: 365),
|
cacheKey,
|
||||||
maxNrOfCacheObjects: 200000,
|
stalePeriod: const Duration(days: 365),
|
||||||
),
|
maxNrOfCacheObjects: 200000,
|
||||||
) {
|
),
|
||||||
|
) {
|
||||||
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
|
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> get defaultHeaders => {
|
Map<String, String> get defaultHeaders => {
|
||||||
'User-Agent': 'flutter_map ($userAgentPackageName)',
|
'User-Agent': 'flutter_map ($userAgentPackageName)',
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
await cacheManager.emptyCache();
|
await cacheManager.emptyCache();
|
||||||
@@ -96,17 +97,21 @@ class MapTileCacheService {
|
|||||||
final future = cacheManager
|
final future = cacheManager
|
||||||
.downloadFile(url, key: url, authHeaders: authHeaders)
|
.downloadFile(url, key: url, authHeaders: authHeaders)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
completed += 1;
|
completed += 1;
|
||||||
}).catchError((_) {
|
})
|
||||||
completed += 1;
|
.catchError((_) {
|
||||||
failed += 1;
|
completed += 1;
|
||||||
}).whenComplete(() {
|
failed += 1;
|
||||||
onProgress?.call(MapTileCacheProgress(
|
})
|
||||||
completed: completed,
|
.whenComplete(() {
|
||||||
total: total,
|
onProgress?.call(
|
||||||
failed: failed,
|
MapTileCacheProgress(
|
||||||
));
|
completed: completed,
|
||||||
});
|
total: total,
|
||||||
|
failed: failed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
pending.add(future);
|
pending.add(future);
|
||||||
if (pending.length >= safeConcurrency) {
|
if (pending.length >= safeConcurrency) {
|
||||||
@@ -189,11 +194,9 @@ class MapTileCacheService {
|
|||||||
int _latToTileY(double lat, int zoom, int maxIndex) {
|
int _latToTileY(double lat, int zoom, int maxIndex) {
|
||||||
final n = 1 << zoom;
|
final n = 1 << zoom;
|
||||||
final rad = lat * math.pi / 180.0;
|
final rad = lat * math.pi / 180.0;
|
||||||
final value = ((1 -
|
final value =
|
||||||
math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) /
|
((1 - math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) / 2 * n)
|
||||||
2 *
|
.floor();
|
||||||
n)
|
|
||||||
.floor();
|
|
||||||
return value.clamp(0, maxIndex);
|
return value.clamp(0, maxIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ class _AckHashMapping {
|
|||||||
final String messageId;
|
final String messageId;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
_AckHashMapping({
|
_AckHashMapping({required this.messageId, required this.timestamp});
|
||||||
required this.messageId,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageRetryService extends ChangeNotifier {
|
class MessageRetryService extends ChangeNotifier {
|
||||||
@@ -39,11 +36,16 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final Map<String, Message> _pendingMessages = {};
|
final Map<String, Message> _pendingMessages = {};
|
||||||
final Map<String, Contact> _pendingContacts = {};
|
final Map<String, Contact> _pendingContacts = {};
|
||||||
final Map<String, PathSelection> _pendingPathSelections = {};
|
final Map<String, PathSelection> _pendingPathSelections = {};
|
||||||
final Map<String, _AckHashMapping> _ackHashToMessageId = {}; // ackHashHex → messageId + timestamp for O(1) lookup
|
final Map<String, _AckHashMapping> _ackHashToMessageId =
|
||||||
final Map<String, List<Uint8List>> _expectedAckHashes = {}; // Track all expected ACKs for retries (for history)
|
{}; // ackHashHex → messageId + timestamp for O(1) lookup
|
||||||
final List<_AckHistoryEntry> _ackHistory = []; // Rolling buffer of recent ACK hashes
|
final Map<String, List<Uint8List>> _expectedAckHashes =
|
||||||
final Map<String, List<String>> _pendingMessageQueuePerContact = {}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed)
|
{}; // Track all expected ACKs for retries (for history)
|
||||||
final Map<String, String> _expectedHashToMessageId = {}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash)
|
final List<_AckHistoryEntry> _ackHistory =
|
||||||
|
[]; // Rolling buffer of recent ACK hashes
|
||||||
|
final Map<String, List<String>> _pendingMessageQueuePerContact =
|
||||||
|
{}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed)
|
||||||
|
final Map<String, String> _expectedHashToMessageId =
|
||||||
|
{}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash)
|
||||||
|
|
||||||
Function(Contact, String, int, int)? _sendMessageCallback;
|
Function(Contact, String, int, int)? _sendMessageCallback;
|
||||||
Function(String, Message)? _addMessageCallback;
|
Function(String, Message)? _addMessageCallback;
|
||||||
@@ -130,7 +132,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final messagePathBytes =
|
final messagePathBytes =
|
||||||
pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection);
|
pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection);
|
||||||
final messagePathLength =
|
final messagePathLength =
|
||||||
pathLength ?? _resolveMessagePathLength(contact, useFlood, pathSelection);
|
pathLength ??
|
||||||
|
_resolveMessagePathLength(contact, useFlood, pathSelection);
|
||||||
final message = Message(
|
final message = Message(
|
||||||
senderKey: contact.publicKey,
|
senderKey: contact.publicKey,
|
||||||
text: text,
|
text: text,
|
||||||
@@ -167,15 +170,25 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
|
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
|
||||||
if (message.pathLength != null && message.pathLength! < 0) {
|
if (message.pathLength != null && message.pathLength! < 0) {
|
||||||
// Flood mode - clear the path
|
// Flood mode - clear the path
|
||||||
debugPrint('Setting flood mode for retry attempt ${message.retryCount}');
|
debugPrint(
|
||||||
|
'Setting flood mode for retry attempt ${message.retryCount}',
|
||||||
|
);
|
||||||
_clearContactPathCallback!(contact);
|
_clearContactPathCallback!(contact);
|
||||||
} else if (message.pathLength != null && message.pathLength! >= 0) {
|
} else if (message.pathLength != null && message.pathLength! >= 0) {
|
||||||
// Specific path (including direct neighbor with pathLength=0)
|
// Specific path (including direct neighbor with pathLength=0)
|
||||||
final pathStr = message.pathBytes.isEmpty
|
final pathStr = message.pathBytes.isEmpty
|
||||||
? 'direct'
|
? 'direct'
|
||||||
: message.pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',');
|
: message.pathBytes
|
||||||
debugPrint('Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}');
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
await _setContactPathCallback!(contact, message.pathBytes, message.pathLength!);
|
.join(',');
|
||||||
|
debugPrint(
|
||||||
|
'Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}',
|
||||||
|
);
|
||||||
|
await _setContactPathCallback!(
|
||||||
|
contact,
|
||||||
|
message.pathBytes,
|
||||||
|
message.pathLength!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,22 +199,30 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
// IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash
|
// IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash
|
||||||
final selfPubKey = _getSelfPublicKeyCallback?.call();
|
final selfPubKey = _getSelfPublicKeyCallback?.call();
|
||||||
if (selfPubKey != null) {
|
if (selfPubKey != null) {
|
||||||
final outboundText = _prepareContactOutboundTextCallback?.call(contact, message.text) ?? message.text;
|
final outboundText =
|
||||||
|
_prepareContactOutboundTextCallback?.call(contact, message.text) ??
|
||||||
|
message.text;
|
||||||
final expectedHash = MessageRetryService.computeExpectedAckHash(
|
final expectedHash = MessageRetryService.computeExpectedAckHash(
|
||||||
timestampSeconds,
|
timestampSeconds,
|
||||||
attempt,
|
attempt,
|
||||||
outboundText,
|
outboundText,
|
||||||
selfPubKey,
|
selfPubKey,
|
||||||
);
|
);
|
||||||
final expectedHashHex = expectedHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final expectedHashHex = expectedHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
_expectedHashToMessageId[expectedHashHex] = messageId;
|
_expectedHashToMessageId[expectedHashHex] = messageId;
|
||||||
|
|
||||||
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
|
final shortText = message.text.length > 20
|
||||||
|
? '${message.text.substring(0, 20)}...'
|
||||||
|
: message.text;
|
||||||
_debugLogService?.info(
|
_debugLogService?.info(
|
||||||
'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)',
|
'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Computed expected ACK hash $expectedHashHex for message $messageId');
|
debugPrint(
|
||||||
|
'Computed expected ACK hash $expectedHashHex for message $messageId',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED: Old queue-based matching (kept for fallback)
|
// DEPRECATED: Old queue-based matching (kept for fallback)
|
||||||
@@ -209,17 +230,14 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId);
|
_pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId);
|
||||||
|
|
||||||
if (_sendMessageCallback != null) {
|
if (_sendMessageCallback != null) {
|
||||||
_sendMessageCallback!(
|
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
|
||||||
contact,
|
|
||||||
message.text,
|
|
||||||
attempt,
|
|
||||||
timestampSeconds,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
|
||||||
// NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches)
|
// NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches)
|
||||||
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
||||||
@@ -230,16 +248,21 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final message = _pendingMessages[messageId];
|
final message = _pendingMessages[messageId];
|
||||||
|
|
||||||
if (contact != null && message != null) {
|
if (contact != null && message != null) {
|
||||||
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
|
final shortText = message.text.length > 20
|
||||||
|
? '${message.text.substring(0, 20)}...'
|
||||||
|
: message.text;
|
||||||
_debugLogService?.info(
|
_debugLogService?.info(
|
||||||
'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}',
|
'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Hash-based match: ACK hash $ackHashHex → message $messageId ✓');
|
debugPrint(
|
||||||
|
'Hash-based match: ACK hash $ackHashHex → message $messageId ✓',
|
||||||
|
);
|
||||||
|
|
||||||
// Remove from old queue since we matched
|
// Remove from old queue since we matched
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
|
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
||||||
|
false) {
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -259,7 +282,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Hash-based match failed for $ackHashHex, falling back to queue-based matching');
|
debugPrint(
|
||||||
|
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
|
||||||
|
);
|
||||||
|
|
||||||
for (var entry in _pendingMessageQueuePerContact.entries) {
|
for (var entry in _pendingMessageQueuePerContact.entries) {
|
||||||
final contactKey = entry.key;
|
final contactKey = entry.key;
|
||||||
@@ -271,7 +296,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
if (_pendingMessages.containsKey(candidateMessageId)) {
|
if (_pendingMessages.containsKey(candidateMessageId)) {
|
||||||
messageId = candidateMessageId;
|
messageId = candidateMessageId;
|
||||||
contact = _pendingContacts[candidateMessageId];
|
contact = _pendingContacts[candidateMessageId];
|
||||||
debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey');
|
debugPrint(
|
||||||
|
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
||||||
@@ -280,7 +307,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
if (_pendingMessages.containsKey(nextMessageId)) {
|
if (_pendingMessages.containsKey(nextMessageId)) {
|
||||||
messageId = nextMessageId;
|
messageId = nextMessageId;
|
||||||
contact = _pendingContacts[nextMessageId];
|
contact = _pendingContacts[nextMessageId];
|
||||||
debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId');
|
debugPrint(
|
||||||
|
'Queue-based match (fallback): $ackHashHex → message $messageId',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,16 +335,22 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final selection = _pendingPathSelections[messageId];
|
final selection = _pendingPathSelections[messageId];
|
||||||
|
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
debugPrint('Message $messageId no longer pending for ACK hash: $ackHashHex');
|
debugPrint(
|
||||||
|
'Message $messageId no longer pending for ACK hash: $ackHashHex',
|
||||||
|
);
|
||||||
_ackHashToMessageId.remove(ackHashHex);
|
_ackHashToMessageId.remove(ackHashHex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||||
_expectedAckHashes[messageId] ??= [];
|
_expectedAckHashes[messageId] ??= [];
|
||||||
if (!_expectedAckHashes[messageId]!.any((hash) => listEquals(hash, ackHash))) {
|
if (!_expectedAckHashes[messageId]!.any(
|
||||||
|
(hash) => listEquals(hash, ackHash),
|
||||||
|
)) {
|
||||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
||||||
debugPrint('Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})');
|
debugPrint(
|
||||||
|
'Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
|
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
|
||||||
@@ -330,8 +365,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
pathLengthValue = contact.pathLength;
|
pathLengthValue = contact.pathLength;
|
||||||
}
|
}
|
||||||
actualTimeout = _calculateTimeoutCallback!(pathLengthValue, message.text.length);
|
actualTimeout = _calculateTimeoutCallback!(
|
||||||
debugPrint('Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue');
|
pathLengthValue,
|
||||||
|
message.text.length,
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = message.copyWith(
|
||||||
@@ -364,16 +404,22 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final selection = _pendingPathSelections[messageId];
|
final selection = _pendingPathSelections[messageId];
|
||||||
|
|
||||||
if (message == null || contact == null) {
|
if (message == null || contact == null) {
|
||||||
debugPrint('Timeout fired but message $messageId no longer pending (likely already delivered)');
|
debugPrint(
|
||||||
|
'Timeout fired but message $messageId no longer pending (likely already delivered)',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
|
final shortText = message.text.length > 20
|
||||||
|
? '${message.text.substring(0, 20)}...'
|
||||||
|
: message.text;
|
||||||
_debugLogService?.warn(
|
_debugLogService?.warn(
|
||||||
'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying',
|
'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})');
|
debugPrint(
|
||||||
|
'Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})',
|
||||||
|
);
|
||||||
|
|
||||||
if (message.retryCount < maxRetries - 1) {
|
if (message.retryCount < maxRetries - 1) {
|
||||||
final backoffMs = 1000 * (1 << message.retryCount);
|
final backoffMs = 1000 * (1 << message.retryCount);
|
||||||
@@ -402,7 +448,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
if (_pendingMessages.containsKey(messageId)) {
|
if (_pendingMessages.containsKey(messageId)) {
|
||||||
_attemptSend(messageId);
|
_attemptSend(messageId);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Retry cancelled: message $messageId was delivered while waiting');
|
debugPrint(
|
||||||
|
'Retry cancelled: message $messageId was delivered while waiting',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -420,7 +468,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
// Clean up the queue entry for this contact
|
// Clean up the queue entry for this contact
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
|
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
||||||
|
false) {
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +479,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
_clearContactPathCallback!(contact);
|
_clearContactPathCallback!(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
_recordPathResultFromMessage(contact.publicKeyHex, message, selection, false, null);
|
_recordPathResultFromMessage(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
message,
|
||||||
|
selection,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
if (_updateMessageCallback != null) {
|
if (_updateMessageCallback != null) {
|
||||||
_updateMessageCallback!(failedMessage);
|
_updateMessageCallback!(failedMessage);
|
||||||
@@ -443,18 +498,22 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
void _moveAckHashesToHistory(String messageId) {
|
void _moveAckHashesToHistory(String messageId) {
|
||||||
final ackHashes = _expectedAckHashes.remove(messageId);
|
final ackHashes = _expectedAckHashes.remove(messageId);
|
||||||
if (ackHashes != null && ackHashes.isNotEmpty) {
|
if (ackHashes != null && ackHashes.isNotEmpty) {
|
||||||
_ackHistory.add(_AckHistoryEntry(
|
_ackHistory.add(
|
||||||
messageId: messageId,
|
_AckHistoryEntry(
|
||||||
ackHashes: ackHashes,
|
messageId: messageId,
|
||||||
timestamp: DateTime.now(),
|
ackHashes: ackHashes,
|
||||||
));
|
timestamp: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Trim history to max size (rolling buffer)
|
// Trim history to max size (rolling buffer)
|
||||||
while (_ackHistory.length > maxAckHistorySize) {
|
while (_ackHistory.length > maxAckHistorySize) {
|
||||||
_ackHistory.removeAt(0);
|
_ackHistory.removeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})');
|
debugPrint(
|
||||||
|
'Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +521,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
for (final entry in _ackHistory) {
|
for (final entry in _ackHistory) {
|
||||||
for (final expectedHash in entry.ackHashes) {
|
for (final expectedHash in entry.ackHashes) {
|
||||||
if (listEquals(expectedHash, ackHash)) {
|
if (listEquals(expectedHash, ackHash)) {
|
||||||
debugPrint('Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s');
|
debugPrint(
|
||||||
|
'Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s',
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +533,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
||||||
String? matchedMessageId;
|
String? matchedMessageId;
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
|
||||||
debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms');
|
debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms');
|
||||||
|
|
||||||
@@ -502,7 +565,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
|
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
|
||||||
debugPrint('ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)');
|
debugPrint(
|
||||||
|
'ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)',
|
||||||
|
);
|
||||||
for (var entry in _expectedAckHashes.entries) {
|
for (var entry in _expectedAckHashes.entries) {
|
||||||
final messageId = entry.key;
|
final messageId = entry.key;
|
||||||
final expectedHashes = entry.value;
|
final expectedHashes = entry.value;
|
||||||
@@ -510,7 +575,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
for (final expectedHash in expectedHashes) {
|
for (final expectedHash in expectedHashes) {
|
||||||
if (listEquals(expectedHash, ackHash)) {
|
if (listEquals(expectedHash, ackHash)) {
|
||||||
matchedMessageId = messageId;
|
matchedMessageId = messageId;
|
||||||
debugPrint('Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})');
|
debugPrint(
|
||||||
|
'Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +591,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final contact = _pendingContacts[matchedMessageId];
|
final contact = _pendingContacts[matchedMessageId];
|
||||||
final selection = _pendingPathSelections[matchedMessageId];
|
final selection = _pendingPathSelections[matchedMessageId];
|
||||||
|
|
||||||
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
|
final shortText = message.text.length > 20
|
||||||
|
? '${message.text.substring(0, 20)}...'
|
||||||
|
: message.text;
|
||||||
_debugLogService?.info(
|
_debugLogService?.info(
|
||||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms',
|
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
@@ -549,8 +618,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(matchedMessageId);
|
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
|
matchedMessageId,
|
||||||
|
);
|
||||||
|
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
||||||
|
false) {
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +632,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_recordPathResultFromMessage(contact.publicKeyHex, message, selection, true, tripTimeMs);
|
_recordPathResultFromMessage(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
message,
|
||||||
|
selection,
|
||||||
|
true,
|
||||||
|
tripTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -663,7 +741,12 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
if (_recordPathResultCallback == null) return;
|
if (_recordPathResultCallback == null) return;
|
||||||
final recordSelection = selection ?? _selectionFromMessage(message);
|
final recordSelection = selection ?? _selectionFromMessage(message);
|
||||||
if (recordSelection == null) return;
|
if (recordSelection == null) return;
|
||||||
_recordPathResultCallback!(contactKey, recordSelection, success, tripTimeMs);
|
_recordPathResultCallback!(
|
||||||
|
contactKey,
|
||||||
|
recordSelection,
|
||||||
|
success,
|
||||||
|
tripTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PathSelection? _selectionFromMessage(Message message) {
|
PathSelection? _selectionFromMessage(Message message) {
|
||||||
|
|||||||
@@ -1,18 +1,53 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
factory NotificationService() => _instance;
|
factory NotificationService() => _instance;
|
||||||
NotificationService._internal();
|
NotificationService._internal();
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
|
final FlutterLocalNotificationsPlugin _notifications =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
// Locale for localized notification strings
|
||||||
|
Locale _locale = const Locale('en');
|
||||||
|
|
||||||
|
/// Set the locale for notification strings (call when app locale changes)
|
||||||
|
void setLocale(Locale locale) {
|
||||||
|
_locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLocalizations get _l10n => lookupAppLocalizations(_locale);
|
||||||
|
|
||||||
|
// Rate limiting to prevent notification storms
|
||||||
|
// (Added after getting notification-flooded while evaluating RF flood management. The irony.)
|
||||||
|
static const _minNotificationInterval = Duration(seconds: 3);
|
||||||
|
static const _batchWindow = Duration(seconds: 5);
|
||||||
|
|
||||||
|
DateTime? _lastNotificationTime;
|
||||||
|
final List<_PendingNotification> _pendingNotifications = [];
|
||||||
|
bool _isBatchingActive = false;
|
||||||
|
bool _suppressNotifications = false;
|
||||||
|
|
||||||
|
/// Temporarily suppress all notifications (e.g., during sync)
|
||||||
|
void suppressNotifications(bool suppress) {
|
||||||
|
_suppressNotifications = suppress;
|
||||||
|
if (suppress) {
|
||||||
|
_pendingNotifications.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
const androidSettings = AndroidInitializationSettings(
|
||||||
|
'@mipmap/ic_launcher',
|
||||||
|
);
|
||||||
const iosSettings = DarwinInitializationSettings(
|
const iosSettings = DarwinInitializationSettings(
|
||||||
requestAlertPermission: true,
|
requestAlertPermission: true,
|
||||||
requestBadgePermission: true,
|
requestBadgePermission: true,
|
||||||
@@ -32,7 +67,7 @@ class NotificationService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _notifications.initialize(
|
await _notifications.initialize(
|
||||||
initSettings,
|
settings: initSettings,
|
||||||
onDidReceiveNotificationResponse: _onNotificationTapped,
|
onDidReceiveNotificationResponse: _onNotificationTapped,
|
||||||
);
|
);
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
@@ -47,16 +82,20 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request Android 13+ notification permission
|
// Request Android 13+ notification permission
|
||||||
final androidPlugin = _notifications.resolvePlatformSpecificImplementation<
|
final androidPlugin = _notifications
|
||||||
AndroidFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
if (androidPlugin != null) {
|
if (androidPlugin != null) {
|
||||||
final granted = await androidPlugin.requestNotificationsPermission();
|
final granted = await androidPlugin.requestNotificationsPermission();
|
||||||
return granted ?? false;
|
return granted ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS permissions are requested during initialization
|
// iOS permissions are requested during initialization
|
||||||
final iosPlugin = _notifications.resolvePlatformSpecificImplementation<
|
final iosPlugin = _notifications
|
||||||
IOSFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
if (iosPlugin != null) {
|
if (iosPlugin != null) {
|
||||||
final granted = await iosPlugin.requestPermissions(
|
final granted = await iosPlugin.requestPermissions(
|
||||||
alert: true,
|
alert: true,
|
||||||
@@ -69,7 +108,7 @@ class NotificationService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showMessageNotification({
|
Future<void> _showMessageNotificationImpl({
|
||||||
required String contactName,
|
required String contactName,
|
||||||
required String message,
|
required String message,
|
||||||
String? contactId,
|
String? contactId,
|
||||||
@@ -110,15 +149,15 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
contactId?.hashCode ?? 0,
|
id: contactId?.hashCode ?? 0,
|
||||||
'New message from $contactName',
|
title: contactName,
|
||||||
message.length > 100 ? '${message.substring(0, 100)}...' : message,
|
body: message,
|
||||||
notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
payload: 'message:$contactId',
|
payload: 'message:$contactId',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showAdvertNotification({
|
Future<void> _showAdvertNotificationImpl({
|
||||||
required String contactName,
|
required String contactName,
|
||||||
required String contactType,
|
required String contactType,
|
||||||
String? contactId,
|
String? contactId,
|
||||||
@@ -155,15 +194,15 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
'New $contactType discovered',
|
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||||
contactName,
|
body: contactName,
|
||||||
notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
payload: 'advert:$contactId',
|
payload: 'advert:$contactId',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showChannelMessageNotification({
|
Future<void> _showChannelMessageNotificationImpl({
|
||||||
required String channelName,
|
required String channelName,
|
||||||
required String message,
|
required String message,
|
||||||
int? channelIndex,
|
int? channelIndex,
|
||||||
@@ -203,24 +242,33 @@ class NotificationService {
|
|||||||
macOS: macDetails,
|
macOS: macDetails,
|
||||||
);
|
);
|
||||||
|
|
||||||
final preview = _truncateMessage(message, 30);
|
final preview = message.trim();
|
||||||
final body = preview.isEmpty
|
final body = preview.isEmpty
|
||||||
? 'Received new message'
|
? _l10n.notification_receivedNewMessage
|
||||||
: preview;
|
: preview;
|
||||||
|
|
||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
channelName,
|
title: channelName,
|
||||||
body,
|
body: body,
|
||||||
notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
payload: 'channel:$channelIndex',
|
payload: 'channel:$channelIndex',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _truncateMessage(String message, int maxLength) {
|
/// Returns a privacy-safe identifier for debug logging.
|
||||||
final trimmed = message.trim();
|
/// - advert: shows device name (body contains contactName)
|
||||||
if (trimmed.length <= maxLength) return trimmed;
|
/// - message: shows "from: sender" (avoids logging message content)
|
||||||
return '${trimmed.substring(0, maxLength)}...';
|
/// - channelMessage: shows "in: channel" (avoids logging message content)
|
||||||
|
String _getNotificationIdentifier(_PendingNotification n) {
|
||||||
|
switch (n.type) {
|
||||||
|
case _NotificationType.advert:
|
||||||
|
return n.body;
|
||||||
|
case _NotificationType.message:
|
||||||
|
return 'from: ${n.title}';
|
||||||
|
case _NotificationType.channelMessage:
|
||||||
|
return 'in: ${n.title}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNotificationTapped(NotificationResponse response) {
|
void _onNotificationTapped(NotificationResponse response) {
|
||||||
@@ -237,6 +285,214 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancel(int id) async {
|
Future<void> cancel(int id) async {
|
||||||
await _notifications.cancel(id);
|
await _notifications.cancel(id: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Public notification methods (rate limiting is enforced automatically)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> showMessageNotification({
|
||||||
|
required String contactName,
|
||||||
|
required String message,
|
||||||
|
String? contactId,
|
||||||
|
int? badgeCount,
|
||||||
|
}) async {
|
||||||
|
if (_suppressNotifications) return;
|
||||||
|
|
||||||
|
_queueNotification(
|
||||||
|
_PendingNotification(
|
||||||
|
type: _NotificationType.message,
|
||||||
|
title: contactName,
|
||||||
|
body: message,
|
||||||
|
id: contactId,
|
||||||
|
badgeCount: badgeCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showAdvertNotification({
|
||||||
|
required String contactName,
|
||||||
|
required String contactType,
|
||||||
|
String? contactId,
|
||||||
|
}) async {
|
||||||
|
if (_suppressNotifications) return;
|
||||||
|
|
||||||
|
_queueNotification(
|
||||||
|
_PendingNotification(
|
||||||
|
type: _NotificationType.advert,
|
||||||
|
title: contactType,
|
||||||
|
body: contactName,
|
||||||
|
id: contactId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showChannelMessageNotification({
|
||||||
|
required String channelName,
|
||||||
|
required String message,
|
||||||
|
int? channelIndex,
|
||||||
|
int? badgeCount,
|
||||||
|
}) async {
|
||||||
|
if (_suppressNotifications) return;
|
||||||
|
|
||||||
|
_queueNotification(
|
||||||
|
_PendingNotification(
|
||||||
|
type: _NotificationType.channelMessage,
|
||||||
|
title: channelName,
|
||||||
|
body: message,
|
||||||
|
id: channelIndex?.toString(),
|
||||||
|
badgeCount: badgeCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _queueNotification(_PendingNotification notification) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// If we recently showed a notification, start batching
|
||||||
|
if (_lastNotificationTime != null &&
|
||||||
|
now.difference(_lastNotificationTime!) < _minNotificationInterval) {
|
||||||
|
_pendingNotifications.add(notification);
|
||||||
|
debugPrint(
|
||||||
|
'[Notification] queued: ${notification.type.name} (${_getNotificationIdentifier(notification)})',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start batch timer if not already running
|
||||||
|
if (!_isBatchingActive) {
|
||||||
|
_isBatchingActive = true;
|
||||||
|
Future.delayed(_batchWindow, _processBatch);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show immediately if enough time has passed
|
||||||
|
debugPrint(
|
||||||
|
'[Notification] sent immediately: ${notification.type.name} (${_getNotificationIdentifier(notification)})',
|
||||||
|
);
|
||||||
|
_showNotificationImmediately(notification);
|
||||||
|
_lastNotificationTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processBatch() async {
|
||||||
|
_isBatchingActive = false;
|
||||||
|
|
||||||
|
if (_pendingNotifications.isEmpty) return;
|
||||||
|
|
||||||
|
final batch = List<_PendingNotification>.from(_pendingNotifications);
|
||||||
|
_pendingNotifications.clear();
|
||||||
|
|
||||||
|
if (batch.length == 1) {
|
||||||
|
// Single notification, show normally
|
||||||
|
_showNotificationImmediately(batch.first);
|
||||||
|
} else {
|
||||||
|
// Multiple notifications, show summary
|
||||||
|
await _showBatchSummary(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastNotificationTime = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showNotificationImmediately(
|
||||||
|
_PendingNotification notification,
|
||||||
|
) async {
|
||||||
|
switch (notification.type) {
|
||||||
|
case _NotificationType.message:
|
||||||
|
await _showMessageNotificationImpl(
|
||||||
|
contactName: notification.title,
|
||||||
|
message: notification.body,
|
||||||
|
contactId: notification.id,
|
||||||
|
badgeCount: notification.badgeCount,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case _NotificationType.advert:
|
||||||
|
await _showAdvertNotificationImpl(
|
||||||
|
contactName: notification.body,
|
||||||
|
contactType: notification.title,
|
||||||
|
contactId: notification.id,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case _NotificationType.channelMessage:
|
||||||
|
await _showChannelMessageNotificationImpl(
|
||||||
|
channelName: notification.title,
|
||||||
|
message: notification.body,
|
||||||
|
channelIndex: int.tryParse(notification.id ?? ''),
|
||||||
|
badgeCount: notification.badgeCount,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
|
||||||
|
if (!_isInitialized) await initialize();
|
||||||
|
|
||||||
|
// Group by type
|
||||||
|
final messages = batch
|
||||||
|
.where((n) => n.type == _NotificationType.message)
|
||||||
|
.toList();
|
||||||
|
final adverts = batch
|
||||||
|
.where((n) => n.type == _NotificationType.advert)
|
||||||
|
.toList();
|
||||||
|
final channelMsgs = batch
|
||||||
|
.where((n) => n.type == _NotificationType.channelMessage)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Build summary text using localized plurals
|
||||||
|
final parts = <String>[];
|
||||||
|
if (messages.isNotEmpty) {
|
||||||
|
parts.add(_l10n.notification_messagesCount(messages.length));
|
||||||
|
}
|
||||||
|
if (channelMsgs.isNotEmpty) {
|
||||||
|
parts.add(_l10n.notification_channelMessagesCount(channelMsgs.length));
|
||||||
|
}
|
||||||
|
if (adverts.isNotEmpty) {
|
||||||
|
parts.add(_l10n.notification_newNodesCount(adverts.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.isEmpty) return;
|
||||||
|
|
||||||
|
// Show first few device names in batch summary for debugging (only if adverts exist)
|
||||||
|
final deviceInfo = adverts.isNotEmpty
|
||||||
|
? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})'
|
||||||
|
: '';
|
||||||
|
debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo');
|
||||||
|
|
||||||
|
const androidDetails = AndroidNotificationDetails(
|
||||||
|
'batch_summary',
|
||||||
|
'Activity Summary',
|
||||||
|
channelDescription: 'Batched notification summaries',
|
||||||
|
importance: Importance.defaultImportance,
|
||||||
|
priority: Priority.defaultPriority,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
);
|
||||||
|
|
||||||
|
const notificationDetails = NotificationDetails(android: androidDetails);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
id: 'batch_summary'.hashCode,
|
||||||
|
title: _l10n.notification_activityTitle,
|
||||||
|
body: parts.join(', '),
|
||||||
|
notificationDetails: notificationDetails,
|
||||||
|
payload: 'batch',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper class for pending notifications
|
||||||
|
enum _NotificationType { message, advert, channelMessage }
|
||||||
|
|
||||||
|
class _PendingNotification {
|
||||||
|
final _NotificationType type;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String? id;
|
||||||
|
final int? badgeCount;
|
||||||
|
|
||||||
|
_PendingNotification({
|
||||||
|
required this.type,
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
this.id,
|
||||||
|
this.badgeCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
int? tripTimeMs,
|
int? tripTimeMs,
|
||||||
}) {
|
}) {
|
||||||
if (selection.useFlood) {
|
if (selection.useFlood) {
|
||||||
final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats());
|
final stats = _floodStats.putIfAbsent(
|
||||||
|
contactPubKeyHex,
|
||||||
|
() => _FloodStats(),
|
||||||
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
stats.successCount += 1;
|
stats.successCount += 1;
|
||||||
if (tripTimeMs != null) stats.lastTripTimeMs = tripTimeMs;
|
if (tripTimeMs != null) stats.lastTripTimeMs = tripTimeMs;
|
||||||
@@ -88,23 +91,28 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PathSelection getNextAutoPathSelection(String contactPubKeyHex) {
|
PathSelection getNextAutoPathSelection(String contactPubKeyHex) {
|
||||||
final ranked = _getRankedPaths(contactPubKeyHex)
|
final ranked = _getRankedPaths(
|
||||||
.take(_autoRotationTopCount)
|
contactPubKeyHex,
|
||||||
.toList();
|
).take(_autoRotationTopCount).toList();
|
||||||
if (ranked.isEmpty) {
|
if (ranked.isEmpty) {
|
||||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_trackAccess(contactPubKeyHex);
|
_trackAccess(contactPubKeyHex);
|
||||||
|
|
||||||
final selections = ranked
|
final selections =
|
||||||
.map((path) => PathSelection(
|
ranked
|
||||||
pathBytes: path.pathBytes,
|
.map(
|
||||||
hopCount: path.hopCount,
|
(path) => PathSelection(
|
||||||
useFlood: false,
|
pathBytes: path.pathBytes,
|
||||||
))
|
hopCount: path.hopCount,
|
||||||
.toList()
|
useFlood: false,
|
||||||
..add(const PathSelection(pathBytes: [], hopCount: -1, useFlood: true));
|
),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..add(
|
||||||
|
const PathSelection(pathBytes: [], hopCount: -1, useFlood: true),
|
||||||
|
);
|
||||||
|
|
||||||
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
|
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
|
||||||
final selection = selections[currentIndex % selections.length];
|
final selection = selections[currentIndex % selections.length];
|
||||||
@@ -241,7 +249,8 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ContactPathHistory?> _loadHistoryFromStorage(
|
Future<ContactPathHistory?> _loadHistoryFromStorage(
|
||||||
String contactPubKeyHex) async {
|
String contactPubKeyHex,
|
||||||
|
) async {
|
||||||
return await _storage.loadPathHistory(contactPubKeyHex);
|
return await _storage.loadPathHistory(contactPubKeyHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,8 +317,10 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
..removeWhere((p) => p.pathBytes.isEmpty);
|
..removeWhere((p) => p.pathBytes.isEmpty);
|
||||||
|
|
||||||
ranked.sort((a, b) {
|
ranked.sort((a, b) {
|
||||||
final aRate = (a.successCount + 1) / (a.successCount + a.failureCount + 2);
|
final aRate =
|
||||||
final bRate = (b.successCount + 1) / (b.successCount + b.failureCount + 2);
|
(a.successCount + 1) / (a.successCount + a.failureCount + 2);
|
||||||
|
final bRate =
|
||||||
|
(b.successCount + 1) / (b.successCount + b.failureCount + 2);
|
||||||
if (aRate != bRate) return bRate.compareTo(aRate);
|
if (aRate != bRate) return bRate.compareTo(aRate);
|
||||||
if (a.successCount != b.successCount) {
|
if (a.successCount != b.successCount) {
|
||||||
return b.successCount.compareTo(a.successCount);
|
return b.successCount.compareTo(a.successCount);
|
||||||
@@ -329,7 +340,10 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateFloodStats(String contactPubKeyHex) {
|
void _updateFloodStats(String contactPubKeyHex) {
|
||||||
final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats());
|
final stats = _floodStats.putIfAbsent(
|
||||||
|
contactPubKeyHex,
|
||||||
|
() => _FloodStats(),
|
||||||
|
);
|
||||||
stats.lastUsed = DateTime.now();
|
stats.lastUsed = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class RepeaterCommandService {
|
|||||||
int retries = maxRetries,
|
int retries = maxRetries,
|
||||||
}) async {
|
}) async {
|
||||||
final repeaterKey = repeater.publicKeyHex;
|
final repeaterKey = repeater.publicKeyHex;
|
||||||
final hasPending = _pendingCommands.keys.any((id) => id.startsWith(repeaterKey));
|
final hasPending = _pendingCommands.keys.any(
|
||||||
|
(id) => id.startsWith(repeaterKey),
|
||||||
|
);
|
||||||
if (hasPending) {
|
if (hasPending) {
|
||||||
throw Exception('Another command is still awaiting a response.');
|
throw Exception('Another command is still awaiting a response.');
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,9 @@ class RepeaterCommandService {
|
|||||||
attempt: attempt,
|
attempt: attempt,
|
||||||
timestampSeconds: timestampSeconds,
|
timestampSeconds: timestampSeconds,
|
||||||
);
|
);
|
||||||
final responseBytes = frame.length > maxFrameSize ? frame.length : maxFrameSize;
|
final responseBytes = frame.length > maxFrameSize
|
||||||
|
? frame.length
|
||||||
|
: maxFrameSize;
|
||||||
final timeoutMs = _connector.calculateTimeout(
|
final timeoutMs = _connector.calculateTimeout(
|
||||||
pathLength: pathLengthValue,
|
pathLength: pathLengthValue,
|
||||||
messageBytes: responseBytes,
|
messageBytes: responseBytes,
|
||||||
@@ -97,7 +101,9 @@ class RepeaterCommandService {
|
|||||||
() {
|
() {
|
||||||
final completer = _pendingCommands[commandId];
|
final completer = _pendingCommands[commandId];
|
||||||
if (completer != null && !completer.isCompleted) {
|
if (completer != null && !completer.isCompleted) {
|
||||||
completer.completeError('Command timeout after $timeoutSeconds seconds');
|
completer.completeError(
|
||||||
|
'Command timeout after $timeoutSeconds seconds',
|
||||||
|
);
|
||||||
_cleanup(commandId);
|
_cleanup(commandId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ class StorageService {
|
|||||||
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
||||||
|
|
||||||
Future<void> savePathHistory(
|
Future<void> savePathHistory(
|
||||||
String contactPubKeyHex, ContactPathHistory history) async {
|
String contactPubKeyHex,
|
||||||
|
ContactPathHistory history,
|
||||||
|
) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_pathHistoryPrefix$contactPubKeyHex';
|
final key = '$_pathHistoryPrefix$contactPubKeyHex';
|
||||||
final jsonStr = jsonEncode(history.toJson());
|
final jsonStr = jsonEncode(history.toJson());
|
||||||
@@ -39,8 +41,9 @@ class StorageService {
|
|||||||
Future<void> clearAllPathHistories() async {
|
Future<void> clearAllPathHistories() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final keys = prefs.getKeys();
|
final keys = prefs.getKeys();
|
||||||
final pathHistoryKeys =
|
final pathHistoryKeys = keys.where(
|
||||||
keys.where((key) => key.startsWith(_pathHistoryPrefix));
|
(key) => key.startsWith(_pathHistoryPrefix),
|
||||||
|
);
|
||||||
|
|
||||||
for (final key in pathHistoryKeys) {
|
for (final key in pathHistoryKeys) {
|
||||||
await prefs.remove(key);
|
await prefs.remove(key);
|
||||||
@@ -74,7 +77,9 @@ class StorageService {
|
|||||||
|
|
||||||
/// Save a repeater password by public key hex
|
/// Save a repeater password by public key hex
|
||||||
Future<void> saveRepeaterPassword(
|
Future<void> saveRepeaterPassword(
|
||||||
String repeaterPubKeyHex, String password) async {
|
String repeaterPubKeyHex,
|
||||||
|
String password,
|
||||||
|
) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final passwords = await loadRepeaterPasswords();
|
final passwords = await loadRepeaterPasswords();
|
||||||
passwords[repeaterPubKeyHex] = password;
|
passwords[repeaterPubKeyHex] = password;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ class ChannelMessageStore {
|
|||||||
static const String _keyPrefix = 'channel_messages_';
|
static const String _keyPrefix = 'channel_messages_';
|
||||||
|
|
||||||
/// Save messages for a specific channel
|
/// Save messages for a specific channel
|
||||||
Future<void> saveChannelMessages(int channelIndex, List<ChannelMessage> messages) async {
|
Future<void> saveChannelMessages(
|
||||||
|
int channelIndex,
|
||||||
|
List<ChannelMessage> messages,
|
||||||
|
) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$channelIndex';
|
final key = '$_keyPrefix$channelIndex';
|
||||||
|
|
||||||
@@ -96,7 +99,8 @@ class ChannelMessageStore {
|
|||||||
pathVariants: (json['pathVariants'] as List<dynamic>?)
|
pathVariants: (json['pathVariants'] as List<dynamic>?)
|
||||||
?.map((entry) => Uint8List.fromList(base64Decode(entry as String)))
|
?.map((entry) => Uint8List.fromList(base64Decode(entry as String)))
|
||||||
.toList(),
|
.toList(),
|
||||||
repeats: (json['repeats'] as List<dynamic>?)
|
repeats:
|
||||||
|
(json['repeats'] as List<dynamic>?)
|
||||||
?.map((entry) => _repeatFromJson(entry as Map<String, dynamic>))
|
?.map((entry) => _repeatFromJson(entry as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
@@ -105,15 +109,19 @@ class ChannelMessageStore {
|
|||||||
replyToMessageId: json['replyToMessageId'] as String?,
|
replyToMessageId: json['replyToMessageId'] as String?,
|
||||||
replyToSenderName: json['replyToSenderName'] as String?,
|
replyToSenderName: json['replyToSenderName'] as String?,
|
||||||
replyToText: json['replyToText'] as String?,
|
replyToText: json['replyToText'] as String?,
|
||||||
reactions: (json['reactions'] as Map<String, dynamic>?)?.map(
|
reactions:
|
||||||
(key, value) => MapEntry(key, value as int),
|
(json['reactions'] as Map<String, dynamic>?)?.map(
|
||||||
) ?? {},
|
(key, value) => MapEntry(key, value as int),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _repeatToJson(Repeat repeat) {
|
Map<String, dynamic> _repeatToJson(Repeat repeat) {
|
||||||
return {
|
return {
|
||||||
'repeaterKey': repeat.repeaterKey != null ? base64Encode(repeat.repeaterKey!) : null,
|
'repeaterKey': repeat.repeaterKey != null
|
||||||
|
? base64Encode(repeat.repeaterKey!)
|
||||||
|
: null,
|
||||||
'repeaterName': repeat.repeaterName,
|
'repeaterName': repeat.repeaterName,
|
||||||
'tripTimeMs': repeat.tripTimeMs,
|
'tripTimeMs': repeat.tripTimeMs,
|
||||||
'path': repeat.path?.map((bytes) => base64Encode(bytes)).toList() ?? [],
|
'path': repeat.path?.map((bytes) => base64Encode(bytes)).toList() ?? [],
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ class ChannelOrderStore {
|
|||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(raw);
|
final decoded = jsonDecode(raw);
|
||||||
if (decoded is List) {
|
if (decoded is List) {
|
||||||
return decoded.map((value) => value is int ? value : int.tryParse('$value')).whereType<int>().toList();
|
return decoded
|
||||||
|
.map((value) => value is int ? value : int.tryParse('$value'))
|
||||||
|
.whereType<int>()
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// fall through to legacy parse
|
// fall through to legacy parse
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../models/channel.dart';
|
||||||
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
|
class ChannelStore {
|
||||||
|
static const String _key = 'channels';
|
||||||
|
|
||||||
|
Future<List<Channel>> loadChannels() async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonStr = prefs.getString(_key);
|
||||||
|
if (jsonStr == null) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||||
|
return jsonList
|
||||||
|
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveChannels(List<Channel> channels) async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonList = channels.map(_toJson).toList();
|
||||||
|
await prefs.setString(_key, jsonEncode(jsonList));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _toJson(Channel channel) {
|
||||||
|
return {
|
||||||
|
'index': channel.index,
|
||||||
|
'name': channel.name,
|
||||||
|
'psk': base64Encode(channel.psk),
|
||||||
|
'unreadCount': channel.unreadCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel _fromJson(Map<String, dynamic> json) {
|
||||||
|
return Channel(
|
||||||
|
index: json['index'] as int,
|
||||||
|
name: json['name'] as String? ?? '',
|
||||||
|
psk: json['psk'] != null
|
||||||
|
? Uint8List.fromList(base64Decode(json['psk'] as String))
|
||||||
|
: Uint8List(16),
|
||||||
|
unreadCount: json['unreadCount'] as int? ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ class CommunityStore {
|
|||||||
/// Add a new community
|
/// Add a new community
|
||||||
Future<void> addCommunity(Community community) async {
|
Future<void> addCommunity(Community community) async {
|
||||||
final communities = await loadCommunities();
|
final communities = await loadCommunities();
|
||||||
|
|
||||||
// Check if community with same ID already exists
|
// Check if community with same ID already exists
|
||||||
final existingIndex = communities.indexWhere((c) => c.id == community.id);
|
final existingIndex = communities.indexWhere((c) => c.id == community.id);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
@@ -49,7 +49,7 @@ class CommunityStore {
|
|||||||
} else {
|
} else {
|
||||||
communities.add(community);
|
communities.add(community);
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveCommunities(communities);
|
await saveCommunities(communities);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +92,7 @@ class CommunityStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a hashtag channel to a community
|
/// Add a hashtag channel to a community
|
||||||
Future<void> addHashtagChannel(
|
Future<void> addHashtagChannel(String communityId, String hashtag) async {
|
||||||
String communityId,
|
|
||||||
String hashtag,
|
|
||||||
) async {
|
|
||||||
final community = await getCommunity(communityId);
|
final community = await getCommunity(communityId);
|
||||||
if (community != null) {
|
if (community != null) {
|
||||||
final updated = community.addHashtagChannel(hashtag);
|
final updated = community.addHashtagChannel(hashtag);
|
||||||
@@ -104,10 +101,7 @@ class CommunityStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a hashtag channel from a community
|
/// Remove a hashtag channel from a community
|
||||||
Future<void> removeHashtagChannel(
|
Future<void> removeHashtagChannel(String communityId, String hashtag) async {
|
||||||
String communityId,
|
|
||||||
String hashtag,
|
|
||||||
) async {
|
|
||||||
final community = await getCommunity(communityId);
|
final community = await getCommunity(communityId);
|
||||||
if (community != null) {
|
if (community != null) {
|
||||||
final updated = community.removeHashtagChannel(hashtag);
|
final updated = community.removeHashtagChannel(hashtag);
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ class ContactStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||||
return jsonList.map((entry) => _fromJson(entry as Map<String, dynamic>)).toList();
|
return jsonList
|
||||||
|
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -57,12 +59,16 @@ class ContactStore {
|
|||||||
: Uint8List(0),
|
: Uint8List(0),
|
||||||
pathOverride: json['pathOverride'] as int?,
|
pathOverride: json['pathOverride'] as int?,
|
||||||
pathOverrideBytes: json['pathOverrideBytes'] != null
|
pathOverrideBytes: json['pathOverrideBytes'] != null
|
||||||
? Uint8List.fromList(base64Decode(json['pathOverrideBytes'] as String))
|
? Uint8List.fromList(
|
||||||
|
base64Decode(json['pathOverrideBytes'] as String),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(lastMessageMs ?? lastSeenMs),
|
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
lastMessageMs ?? lastSeenMs,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import 'prefs_manager.dart';
|
|||||||
class MessageStore {
|
class MessageStore {
|
||||||
static const String _keyPrefix = 'messages_';
|
static const String _keyPrefix = 'messages_';
|
||||||
|
|
||||||
Future<void> saveMessages(String contactKeyHex, List<Message> messages) async {
|
Future<void> saveMessages(
|
||||||
|
String contactKeyHex,
|
||||||
|
List<Message> messages,
|
||||||
|
) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$contactKeyHex';
|
final key = '$_keyPrefix$contactKeyHex';
|
||||||
final jsonList = messages.map(_messageToJson).toList();
|
final jsonList = messages.map(_messageToJson).toList();
|
||||||
@@ -45,12 +48,16 @@ class MessageStore {
|
|||||||
'messageId': msg.messageId,
|
'messageId': msg.messageId,
|
||||||
'retryCount': msg.retryCount,
|
'retryCount': msg.retryCount,
|
||||||
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
||||||
'expectedAckHash': msg.expectedAckHash != null ? base64Encode(msg.expectedAckHash!) : null,
|
'expectedAckHash': msg.expectedAckHash != null
|
||||||
|
? base64Encode(msg.expectedAckHash!)
|
||||||
|
: null,
|
||||||
'sentAt': msg.sentAt?.millisecondsSinceEpoch,
|
'sentAt': msg.sentAt?.millisecondsSinceEpoch,
|
||||||
'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch,
|
'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch,
|
||||||
'tripTimeMs': msg.tripTimeMs,
|
'tripTimeMs': msg.tripTimeMs,
|
||||||
'pathLength': msg.pathLength,
|
'pathLength': msg.pathLength,
|
||||||
'pathBytes': msg.pathBytes.isNotEmpty ? base64Encode(msg.pathBytes) : null,
|
'pathBytes': msg.pathBytes.isNotEmpty
|
||||||
|
? base64Encode(msg.pathBytes)
|
||||||
|
: null,
|
||||||
'reactions': msg.reactions,
|
'reactions': msg.reactions,
|
||||||
'fourByteRoomContactKey': base64Encode(msg.fourByteRoomContactKey),
|
'fourByteRoomContactKey': base64Encode(msg.fourByteRoomContactKey),
|
||||||
};
|
};
|
||||||
@@ -59,7 +66,9 @@ class MessageStore {
|
|||||||
Message _messageFromJson(Map<String, dynamic> json) {
|
Message _messageFromJson(Map<String, dynamic> json) {
|
||||||
final rawText = json['text'] as String;
|
final rawText = json['text'] as String;
|
||||||
final isCli = json['isCli'] as bool? ?? false;
|
final isCli = json['isCli'] as bool? ?? false;
|
||||||
final decodedText = isCli ? rawText : (Smaz.tryDecodePrefixed(rawText) ?? rawText);
|
final decodedText = isCli
|
||||||
|
? rawText
|
||||||
|
: (Smaz.tryDecodePrefixed(rawText) ?? rawText);
|
||||||
return Message(
|
return Message(
|
||||||
senderKey: Uint8List.fromList(base64Decode(json['senderKey'] as String)),
|
senderKey: Uint8List.fromList(base64Decode(json['senderKey'] as String)),
|
||||||
text: decodedText,
|
text: decodedText,
|
||||||
@@ -84,11 +93,15 @@ class MessageStore {
|
|||||||
pathBytes: json['pathBytes'] != null
|
pathBytes: json['pathBytes'] != null
|
||||||
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
|
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
|
||||||
: Uint8List(0),
|
: Uint8List(0),
|
||||||
reactions: (json['reactions'] as Map<String, dynamic>?)?.map(
|
reactions:
|
||||||
(key, value) => MapEntry(key, value as int),
|
(json['reactions'] as Map<String, dynamic>?)?.map(
|
||||||
) ?? {},
|
(key, value) => MapEntry(key, value as int),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
fourByteRoomContactKey: json['fourByteRoomContactKey'] != null
|
fourByteRoomContactKey: json['fourByteRoomContactKey'] != null
|
||||||
? Uint8List.fromList(base64Decode(json['fourByteRoomContactKey'] as String))
|
? Uint8List.fromList(
|
||||||
|
base64Decode(json['fourByteRoomContactKey'] as String),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class PrefsManager {
|
|||||||
static SharedPreferences get instance {
|
static SharedPreferences get instance {
|
||||||
if (_instance == null) {
|
if (_instance == null) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'PrefsManager not initialized. Call PrefsManager.initialize() in main() before use.');
|
'PrefsManager not initialized. Call PrefsManager.initialize() in main() before use.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,23 @@ import 'prefs_manager.dart';
|
|||||||
|
|
||||||
/// Storage for unread message tracking with debounced writes to reduce I/O.
|
/// Storage for unread message tracking with debounced writes to reduce I/O.
|
||||||
class UnreadStore {
|
class UnreadStore {
|
||||||
static const String _contactLastReadKey = 'contact_last_read';
|
static const String _contactUnreadCountKey = 'contact_unread_count';
|
||||||
static const String _channelLastReadKey = 'channel_last_read';
|
|
||||||
|
|
||||||
// Debounce timers to batch rapid writes
|
// Debounce timers to batch rapid writes
|
||||||
Timer? _contactSaveTimer;
|
Timer? _contactUnreadSaveTimer;
|
||||||
Timer? _channelSaveTimer;
|
|
||||||
static const Duration _saveDebounceDuration = Duration(milliseconds: 500);
|
static const Duration _saveDebounceDuration = Duration(milliseconds: 500);
|
||||||
|
|
||||||
// Pending write data
|
// Pending write data
|
||||||
Map<String, int>? _pendingContactLastRead;
|
Map<String, int>? _pendingContactUnreadCount;
|
||||||
Map<int, int>? _pendingChannelLastRead;
|
|
||||||
|
|
||||||
/// Dispose timers when no longer needed
|
/// Dispose timers when no longer needed
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contactSaveTimer?.cancel();
|
_contactUnreadSaveTimer?.cancel();
|
||||||
_channelSaveTimer?.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, int>> loadContactLastRead() async {
|
Future<Map<String, int>> loadContactUnreadCount() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_contactLastReadKey);
|
final jsonStr = prefs.getString(_contactUnreadCountKey);
|
||||||
if (jsonStr == null) return {};
|
if (jsonStr == null) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -36,77 +32,30 @@ class UnreadStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save contact last read timestamps with debouncing.
|
void saveContactUnreadCount(Map<String, int> counts) {
|
||||||
/// Writes are delayed by 500ms and batched to reduce I/O operations.
|
_pendingContactUnreadCount = counts;
|
||||||
void saveContactLastRead(Map<String, int> lastReadMs) {
|
|
||||||
_pendingContactLastRead = lastReadMs;
|
|
||||||
|
|
||||||
// Cancel existing timer
|
_contactUnreadSaveTimer?.cancel();
|
||||||
_contactSaveTimer?.cancel();
|
|
||||||
|
|
||||||
// Schedule new write
|
_contactUnreadSaveTimer = Timer(_saveDebounceDuration, () async {
|
||||||
_contactSaveTimer = Timer(_saveDebounceDuration, () async {
|
if (_pendingContactUnreadCount != null) {
|
||||||
if (_pendingContactLastRead != null) {
|
await _flushContactUnreadCount();
|
||||||
await _flushContactLastRead();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<int, int>> loadChannelLastRead() async {
|
Future<void> _flushContactUnreadCount() async {
|
||||||
final prefs = PrefsManager.instance;
|
if (_pendingContactUnreadCount == null) return;
|
||||||
final jsonStr = prefs.getString(_channelLastReadKey);
|
|
||||||
if (jsonStr == null) return {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
|
||||||
return json.map((key, value) => MapEntry(int.parse(key), value as int));
|
|
||||||
} catch (_) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save channel last read timestamps with debouncing.
|
|
||||||
/// Writes are delayed by 500ms and batched to reduce I/O operations.
|
|
||||||
void saveChannelLastRead(Map<int, int> lastReadMs) {
|
|
||||||
_pendingChannelLastRead = lastReadMs;
|
|
||||||
|
|
||||||
_channelSaveTimer?.cancel();
|
|
||||||
|
|
||||||
_channelSaveTimer = Timer(_saveDebounceDuration, () async {
|
|
||||||
if (_pendingChannelLastRead != null) {
|
|
||||||
await _flushChannelLastRead();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _flushContactLastRead() async {
|
|
||||||
if (_pendingContactLastRead == null) return;
|
|
||||||
|
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = jsonEncode(_pendingContactLastRead);
|
final jsonStr = jsonEncode(_pendingContactUnreadCount);
|
||||||
await prefs.setString(_contactLastReadKey, jsonStr);
|
await prefs.setString(_contactUnreadCountKey, jsonStr);
|
||||||
_pendingContactLastRead = null;
|
_pendingContactUnreadCount = null;
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _flushChannelLastRead() async {
|
|
||||||
if (_pendingChannelLastRead == null) return;
|
|
||||||
|
|
||||||
final prefs = PrefsManager.instance;
|
|
||||||
final asString =
|
|
||||||
_pendingChannelLastRead!.map((key, value) => MapEntry(key.toString(), value));
|
|
||||||
final jsonStr = jsonEncode(asString);
|
|
||||||
await prefs.setString(_channelLastReadKey, jsonStr);
|
|
||||||
_pendingChannelLastRead = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immediately flush pending writes (call before app termination or disposal)
|
/// Immediately flush pending writes (call before app termination or disposal)
|
||||||
Future<void> flush() async {
|
Future<void> flush() async {
|
||||||
_contactSaveTimer?.cancel();
|
_contactUnreadSaveTimer?.cancel();
|
||||||
_channelSaveTimer?.cancel();
|
await _flushContactUnreadCount();
|
||||||
|
|
||||||
await Future.wait([
|
|
||||||
_flushContactLastRead(),
|
|
||||||
_flushChannelLastRead(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ class AppLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Log a message with custom level
|
/// Log a message with custom level
|
||||||
void log(String message, {String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info}) {
|
void log(
|
||||||
|
String message, {
|
||||||
|
String tag = 'App',
|
||||||
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
|
}) {
|
||||||
if (_enabled && _service != null) {
|
if (_enabled && _service != null) {
|
||||||
_service!.log(message, tag: tag, level: level);
|
_service!.log(message, tag: tag, level: level);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:gpx/gpx.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
class ContactExport {
|
||||||
|
final String name;
|
||||||
|
final double lat;
|
||||||
|
final double lon;
|
||||||
|
final String desc;
|
||||||
|
final double? ele;
|
||||||
|
|
||||||
|
ContactExport({
|
||||||
|
required this.name,
|
||||||
|
required this.lat,
|
||||||
|
required this.lon,
|
||||||
|
required this.desc,
|
||||||
|
this.ele,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const int gpxExportFailed = -1;
|
||||||
|
const int gpxExportSuccess = 1;
|
||||||
|
const int gpxExportNoContacts = 2;
|
||||||
|
const int gpxExportCancelled = 3;
|
||||||
|
const int gpxExportNotAvailable = 4;
|
||||||
|
|
||||||
|
class GpxExport {
|
||||||
|
final MeshCoreConnector _connector;
|
||||||
|
final List<ContactExport> _contacts = [];
|
||||||
|
|
||||||
|
GpxExport(this._connector);
|
||||||
|
|
||||||
|
void _addContact(
|
||||||
|
String name,
|
||||||
|
double lat,
|
||||||
|
double lon,
|
||||||
|
String desc, [
|
||||||
|
double? ele,
|
||||||
|
]) {
|
||||||
|
_contacts.add(
|
||||||
|
ContactExport(
|
||||||
|
name: name.trim(),
|
||||||
|
lat: lat,
|
||||||
|
lon: lon,
|
||||||
|
desc: desc.trim(),
|
||||||
|
ele: ele,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addRepeaters() {
|
||||||
|
final contacts = _connector.contacts
|
||||||
|
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
||||||
|
.toList();
|
||||||
|
for (var contact in contacts) {
|
||||||
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addContact(
|
||||||
|
contact.name,
|
||||||
|
contact.latitude!,
|
||||||
|
contact.longitude!,
|
||||||
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addContacts() {
|
||||||
|
final contacts = _connector.contacts
|
||||||
|
.where((c) => c.type == advTypeChat)
|
||||||
|
.toList();
|
||||||
|
for (var contact in contacts) {
|
||||||
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addContact(
|
||||||
|
contact.name,
|
||||||
|
contact.latitude!,
|
||||||
|
contact.longitude!,
|
||||||
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAll() {
|
||||||
|
final contacts = _connector.contacts;
|
||||||
|
for (var contact in contacts.toList()) {
|
||||||
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addContact(
|
||||||
|
contact.name,
|
||||||
|
contact.latitude ?? 0.0,
|
||||||
|
contact.longitude ?? 0.0,
|
||||||
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> exportGPX(
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
String filename,
|
||||||
|
String shareText,
|
||||||
|
String subject,
|
||||||
|
) async {
|
||||||
|
if (_contacts.isEmpty) {
|
||||||
|
debugPrint("No repeaters to export – nothing to share.");
|
||||||
|
return gpxExportNoContacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Build GPX content (your existing logic – unchanged here)
|
||||||
|
final gpx = Gpx()
|
||||||
|
..version = '1.1'
|
||||||
|
..creator = 'meshcore-open exporter'
|
||||||
|
..metadata = Metadata(
|
||||||
|
name: name,
|
||||||
|
desc: description,
|
||||||
|
time: DateTime.now().toUtc(),
|
||||||
|
);
|
||||||
|
|
||||||
|
gpx.wpts = _contacts
|
||||||
|
.map(
|
||||||
|
(c) => Wpt(
|
||||||
|
lat: c.lat,
|
||||||
|
lon: c.lon,
|
||||||
|
ele: c.ele,
|
||||||
|
name: c.name,
|
||||||
|
desc: c.desc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final xml = GpxWriter().asString(gpx, pretty: true);
|
||||||
|
|
||||||
|
// 2. Save to file
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
final timestamp = DateTime.now()
|
||||||
|
.toUtc()
|
||||||
|
.toIso8601String()
|
||||||
|
.replaceAll(':', '-')
|
||||||
|
.replaceAll('.', '-')
|
||||||
|
.split('T')
|
||||||
|
.join('_');
|
||||||
|
|
||||||
|
final path = '${dir.path}/$filename$timestamp.gpx';
|
||||||
|
|
||||||
|
final file = File(path);
|
||||||
|
await file.writeAsString(xml);
|
||||||
|
|
||||||
|
final result = await SharePlus.instance.share(
|
||||||
|
ShareParams(text: shareText, subject: subject, files: [XFile(path)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await file.delete();
|
||||||
|
|
||||||
|
switch (result.status) {
|
||||||
|
case ShareResultStatus.success:
|
||||||
|
debugPrint('Share successful – user completed the action.');
|
||||||
|
return gpxExportSuccess;
|
||||||
|
case ShareResultStatus.dismissed:
|
||||||
|
debugPrint('Share sheet was dismissed / cancelled by user.');
|
||||||
|
return gpxExportCancelled;
|
||||||
|
case ShareResultStatus.unavailable:
|
||||||
|
debugPrint('Sharing is not available on this platform / context.');
|
||||||
|
return gpxExportNotAvailable;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
debugPrint('Export or share failed: $e\n$stack');
|
||||||
|
}
|
||||||
|
return gpxExportFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,10 +29,7 @@ BatteryUi batteryUiForPercent(int? percent) {
|
|||||||
class BatteryIndicator extends StatefulWidget {
|
class BatteryIndicator extends StatefulWidget {
|
||||||
final MeshCoreConnector connector;
|
final MeshCoreConnector connector;
|
||||||
|
|
||||||
const BatteryIndicator({
|
const BatteryIndicator({super.key, required this.connector});
|
||||||
super.key,
|
|
||||||
required this.connector,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BatteryIndicator> createState() => _BatteryIndicatorState();
|
State<BatteryIndicator> createState() => _BatteryIndicatorState();
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import '../connector/meshcore_protocol.dart';
|
|||||||
|
|
||||||
/// Debug widget to show the hex dump of a frame
|
/// Debug widget to show the hex dump of a frame
|
||||||
class DebugFrameViewer {
|
class DebugFrameViewer {
|
||||||
static void showFrameDebug(BuildContext context, Uint8List frame, String title) {
|
static void showFrameDebug(
|
||||||
|
BuildContext context,
|
||||||
|
Uint8List frame,
|
||||||
|
String title,
|
||||||
|
) {
|
||||||
final hexString = frame
|
final hexString = frame
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
@@ -14,16 +18,26 @@ class DebugFrameViewer {
|
|||||||
details.writeln(context.l10n.debugFrame_length(frame.length));
|
details.writeln(context.l10n.debugFrame_length(frame.length));
|
||||||
details.writeln('');
|
details.writeln('');
|
||||||
details.writeln(
|
details.writeln(
|
||||||
context.l10n.debugFrame_command(frame[0].toRadixString(16).padLeft(2, '0')),
|
context.l10n.debugFrame_command(
|
||||||
|
frame[0].toRadixString(16).padLeft(2, '0'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (frame[0] == cmdSendTxtMsg && frame.length > 37) {
|
if (frame[0] == cmdSendTxtMsg && frame.length > 37) {
|
||||||
details.writeln('');
|
details.writeln('');
|
||||||
details.writeln(context.l10n.debugFrame_textMessageHeader);
|
details.writeln(context.l10n.debugFrame_textMessageHeader);
|
||||||
details.writeln(context.l10n.debugFrame_destinationPubKey(pubKeyToHex(frame.sublist(1, 33))));
|
|
||||||
details.writeln(context.l10n.debugFrame_timestamp(readUint32LE(frame, 33)));
|
|
||||||
details.writeln(
|
details.writeln(
|
||||||
context.l10n.debugFrame_flags(frame[37].toRadixString(16).padLeft(2, '0')),
|
context.l10n.debugFrame_destinationPubKey(
|
||||||
|
pubKeyToHex(frame.sublist(1, 33)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
details.writeln(
|
||||||
|
context.l10n.debugFrame_timestamp(readUint32LE(frame, 33)),
|
||||||
|
);
|
||||||
|
details.writeln(
|
||||||
|
context.l10n.debugFrame_flags(
|
||||||
|
frame[37].toRadixString(16).padLeft(2, '0'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final txtType = (frame[37] >> 2) & 0x03;
|
final txtType = (frame[37] >> 2) & 0x03;
|
||||||
final typeLabel = txtType == txtTypeCliData
|
final typeLabel = txtType == txtTypeCliData
|
||||||
@@ -34,7 +48,7 @@ class DebugFrameViewer {
|
|||||||
final textBytes = frame.sublist(38);
|
final textBytes = frame.sublist(38);
|
||||||
final nullIdx = textBytes.indexOf(0);
|
final nullIdx = textBytes.indexOf(0);
|
||||||
final text = String.fromCharCodes(
|
final text = String.fromCharCodes(
|
||||||
nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes
|
nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes,
|
||||||
);
|
);
|
||||||
details.writeln(context.l10n.debugFrame_text(text));
|
details.writeln(context.l10n.debugFrame_text(text));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,14 @@ class DeviceTile extends StatelessWidget {
|
|||||||
final ScanResult scanResult;
|
final ScanResult scanResult;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const DeviceTile({
|
const DeviceTile({super.key, required this.scanResult, required this.onTap});
|
||||||
super.key,
|
|
||||||
required this.scanResult,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final device = scanResult.device;
|
final device = scanResult.device;
|
||||||
final rssi = scanResult.rssi;
|
final rssi = scanResult.rssi;
|
||||||
final name = device.platformName.isNotEmpty
|
final name = device.platformName.isNotEmpty
|
||||||
? device.platformName
|
? device.platformName
|
||||||
: scanResult.advertisementData.advName;
|
: scanResult.advertisementData.advName;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -58,12 +54,8 @@ class DeviceTile extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color),
|
Icon(icon, color: color),
|
||||||
Text(
|
Text('$rssi dBm', style: TextStyle(fontSize: 10, color: color)),
|
||||||
'$rssi dBm',
|
|
||||||
style: TextStyle(fontSize: 10, color: color),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+204
-34
@@ -5,39 +5,203 @@ import '../l10n/l10n.dart';
|
|||||||
class EmojiPicker extends StatelessWidget {
|
class EmojiPicker extends StatelessWidget {
|
||||||
final Function(String) onEmojiSelected;
|
final Function(String) onEmojiSelected;
|
||||||
|
|
||||||
const EmojiPicker({
|
const EmojiPicker({super.key, required this.onEmojiSelected});
|
||||||
super.key,
|
|
||||||
required this.onEmojiSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const List<String> quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥'];
|
static const List<String> quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥'];
|
||||||
|
|
||||||
static const List<String> _smileys = [
|
static const List<String> smileys = [
|
||||||
'😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘',
|
'😀',
|
||||||
'😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏',
|
'😃',
|
||||||
'😒', '😞', '😔', '😟', '😕', '🙁', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡',
|
'😄',
|
||||||
'🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶',
|
'😁',
|
||||||
];
|
'😅',
|
||||||
static const List<String> _gestures = [
|
'😂',
|
||||||
'👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '🤌', '🤏', '👈', '👉', '👆',
|
'🤣',
|
||||||
'👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳',
|
'😊',
|
||||||
];
|
'😇',
|
||||||
static const List<String> _hearts = [
|
'🙂',
|
||||||
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❤️🔥', '❤️🩹', '💕', '💞', '💓', '💗',
|
'🙃',
|
||||||
'💖', '💘', '💝', '💟', '💌', '💢', '💥', '💫', '💦', '💨', '🕳️', '💬', '👁️🗨️', '🗨️', '🗯️', '💭',
|
'😉',
|
||||||
];
|
'😌',
|
||||||
static const List<String> _objects = [
|
'😍',
|
||||||
'🎉', '🎊', '🎈', '🎁', '🎀', '🪅', '🪆', '🏆', '🥇', '🥈', '🥉', '⚽', '⚾', '🥎', '🏀', '🏐',
|
'🥰',
|
||||||
'🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', '🏓', '🏸', '🥊', '🥋', '🥅', '⛳', '🔥',
|
'😘',
|
||||||
'⭐', '🌟', '✨', '⚡', '💡', '🔦', '🏮', '🪔', '📱', '💻', '⌚', '📷', '📺', '📻', '🎵', '🎶',
|
'😗',
|
||||||
];
|
'😙',
|
||||||
|
'😚',
|
||||||
|
'😋',
|
||||||
|
'😛',
|
||||||
|
'😝',
|
||||||
|
'😜',
|
||||||
|
'🤪',
|
||||||
|
'🤨',
|
||||||
|
'🧐',
|
||||||
|
'🤓',
|
||||||
|
'😎',
|
||||||
|
'🥸',
|
||||||
|
'🤩',
|
||||||
|
'🥳',
|
||||||
|
'😏',
|
||||||
|
'😒',
|
||||||
|
'😞',
|
||||||
|
'😔',
|
||||||
|
'😟',
|
||||||
|
'😕',
|
||||||
|
'🙁',
|
||||||
|
'😣',
|
||||||
|
'😖',
|
||||||
|
'😫',
|
||||||
|
'😩',
|
||||||
|
'🥺',
|
||||||
|
'😢',
|
||||||
|
'😭',
|
||||||
|
'😤',
|
||||||
|
'😠',
|
||||||
|
'😡',
|
||||||
|
'🤬',
|
||||||
|
'🤯',
|
||||||
|
'😳',
|
||||||
|
'🥵',
|
||||||
|
'🥶',
|
||||||
|
'😱',
|
||||||
|
'😨',
|
||||||
|
'😰',
|
||||||
|
'😥',
|
||||||
|
'😓',
|
||||||
|
'🤗',
|
||||||
|
'🤔',
|
||||||
|
'🤭',
|
||||||
|
'🤫',
|
||||||
|
'🤥',
|
||||||
|
'😶',
|
||||||
|
];
|
||||||
|
static const List<String> gestures = [
|
||||||
|
'👍',
|
||||||
|
'👎',
|
||||||
|
'👊',
|
||||||
|
'✊',
|
||||||
|
'🤛',
|
||||||
|
'🤜',
|
||||||
|
'🤞',
|
||||||
|
'✌️',
|
||||||
|
'🤟',
|
||||||
|
'🤘',
|
||||||
|
'👌',
|
||||||
|
'🤌',
|
||||||
|
'🤏',
|
||||||
|
'👈',
|
||||||
|
'👉',
|
||||||
|
'👆',
|
||||||
|
'👇',
|
||||||
|
'☝️',
|
||||||
|
'👋',
|
||||||
|
'🤚',
|
||||||
|
'🖐️',
|
||||||
|
'✋',
|
||||||
|
'🖖',
|
||||||
|
'👏',
|
||||||
|
'🙌',
|
||||||
|
'👐',
|
||||||
|
'🤲',
|
||||||
|
'🤝',
|
||||||
|
'🙏',
|
||||||
|
'✍️',
|
||||||
|
'💅',
|
||||||
|
'🤳',
|
||||||
|
'💪',
|
||||||
|
];
|
||||||
|
static const List<String> hearts = [
|
||||||
|
'❤️',
|
||||||
|
'🧡',
|
||||||
|
'💛',
|
||||||
|
'💚',
|
||||||
|
'💙',
|
||||||
|
'💜',
|
||||||
|
'🖤',
|
||||||
|
'🤍',
|
||||||
|
'🤎',
|
||||||
|
'💔',
|
||||||
|
'❤️🔥',
|
||||||
|
'❤️🩹',
|
||||||
|
'💕',
|
||||||
|
'💞',
|
||||||
|
'💓',
|
||||||
|
'💗',
|
||||||
|
'💖',
|
||||||
|
'💘',
|
||||||
|
'💝',
|
||||||
|
'💟',
|
||||||
|
'💌',
|
||||||
|
'💢',
|
||||||
|
'💥',
|
||||||
|
'💫',
|
||||||
|
'💦',
|
||||||
|
'💨',
|
||||||
|
'🕳️',
|
||||||
|
'💬',
|
||||||
|
'👁️🗨️',
|
||||||
|
'🗨️',
|
||||||
|
'🗯️',
|
||||||
|
'💭',
|
||||||
|
];
|
||||||
|
static const List<String> objects = [
|
||||||
|
'🎉',
|
||||||
|
'🎊',
|
||||||
|
'🎈',
|
||||||
|
'🎁',
|
||||||
|
'🎀',
|
||||||
|
'🪅',
|
||||||
|
'🪆',
|
||||||
|
'🏆',
|
||||||
|
'🥇',
|
||||||
|
'🥈',
|
||||||
|
'🥉',
|
||||||
|
'⚽',
|
||||||
|
'⚾',
|
||||||
|
'🥎',
|
||||||
|
'🏀',
|
||||||
|
'🏐',
|
||||||
|
'🏈',
|
||||||
|
'🏉',
|
||||||
|
'🎾',
|
||||||
|
'🥏',
|
||||||
|
'🎳',
|
||||||
|
'🏏',
|
||||||
|
'🏑',
|
||||||
|
'🏒',
|
||||||
|
'🥍',
|
||||||
|
'🏓',
|
||||||
|
'🏸',
|
||||||
|
'🥊',
|
||||||
|
'🥋',
|
||||||
|
'🥅',
|
||||||
|
'⛳',
|
||||||
|
'🔥',
|
||||||
|
'⭐',
|
||||||
|
'🌟',
|
||||||
|
'✨',
|
||||||
|
'⚡',
|
||||||
|
'💡',
|
||||||
|
'🔦',
|
||||||
|
'🏮',
|
||||||
|
'🪔',
|
||||||
|
'📱',
|
||||||
|
'💻',
|
||||||
|
'⌚',
|
||||||
|
'📷',
|
||||||
|
'📺',
|
||||||
|
'📻',
|
||||||
|
'🎵',
|
||||||
|
'🎶',
|
||||||
|
'🚀',
|
||||||
|
];
|
||||||
|
|
||||||
Map<String, List<String>> _emojiCategories(AppLocalizations l10n) {
|
Map<String, List<String>> _emojiCategories(AppLocalizations l10n) {
|
||||||
return {
|
return {
|
||||||
l10n.emojiCategorySmileys: _smileys,
|
l10n.emojiCategorySmileys: smileys,
|
||||||
l10n.emojiCategoryGestures: _gestures,
|
l10n.emojiCategoryGestures: gestures,
|
||||||
l10n.emojiCategoryHearts: _hearts,
|
l10n.emojiCategoryHearts: hearts,
|
||||||
l10n.emojiCategoryObjects: _objects,
|
l10n.emojiCategoryObjects: objects,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +224,10 @@ class EmojiPicker extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.chat_addReaction,
|
l10n.chat_addReaction,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
@@ -83,7 +250,9 @@ class EmojiPicker extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -114,11 +283,12 @@ class EmojiPicker extends StatelessWidget {
|
|||||||
.map(
|
.map(
|
||||||
(emojis) => GridView.builder(
|
(emojis) => GridView.builder(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
crossAxisCount: 8,
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
mainAxisSpacing: 8,
|
crossAxisCount: 8,
|
||||||
crossAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
),
|
crossAxisSpacing: 8,
|
||||||
|
),
|
||||||
itemCount: emojis.length,
|
itemCount: emojis.length,
|
||||||
itemBuilder: (context, index) => InkWell(
|
itemBuilder: (context, index) => InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user