Compare commits
2 Commits
dev
..
PRE-BETA-9.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 9957e1fc1f | |||
| 880df84828 |
@@ -1,3 +0,0 @@
|
||||
github: [zjs81]
|
||||
custom:
|
||||
- "https://paypal.me/zjs82"
|
||||
@@ -48,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **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
|
||||
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
||||
- **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key)
|
||||
- **Offline Maps**: Download map tiles for offline use in remote areas
|
||||
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
||||
|
||||
### Device Management
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:label="MeshCore Open"
|
||||
android:label="meshcore_open"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 183 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#9F9D95</color>
|
||||
</resources>
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
Before Width: | Height: | Size: 669 KiB |
@@ -14,8 +14,7 @@ MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking
|
||||
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
|
||||
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
|
||||
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
|
||||
11. [Routing Paths](routing-paths.md) - Path encoding, validation, device capability detection, and storage
|
||||
12. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||
|
||||
## App Overview
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ An ML-based service that predicts expected delivery timeouts:
|
||||
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
|
||||
- Features with zero variance are automatically excluded from training
|
||||
- Blends per-contact statistics with ML predictions
|
||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data. Note: `pathLength` here refers to the stored hop count in the app's model/storage (number of hops), not the on-air encoded byte length.
|
||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
|
||||
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
||||
|
||||
---
|
||||
|
||||
@@ -120,8 +120,6 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||
| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) |
|
||||
| 55 | CMD_SEND_CONTROL_DATA | Send control data (e.g. zero-hop discovery, v8+) |
|
||||
| 56 | CMD_GET_STATS | Request companion radio stats |
|
||||
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||
@@ -164,7 +162,6 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
||||
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
||||
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
||||
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
|
||||
|
||||
## Data Models
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# Routing Paths
|
||||
|
||||
This page covers how MeshCore Open represents, selects, validates, and stores routing paths in the UI and data layer.
|
||||
|
||||
## Path Routing
|
||||
|
||||
MeshCore supports variable-length multi-byte routing paths so the app can scale from small meshes to very large node sets.
|
||||
|
||||
### Hash Width and Multi-Byte Paths
|
||||
|
||||
The device capability determines the hash width (number of bytes per hop):
|
||||
|
||||
| Width | Max Unique Nodes | Typical Use |
|
||||
|-------|-----------------|-------------|
|
||||
| 1 byte | 256 | Single-byte node IDs |
|
||||
| 2 bytes | 65,536 | Medium meshes |
|
||||
| 3 bytes | 16.7M | Large networks |
|
||||
| 4 bytes | 4.3G | Very large meshes / future-proofing |
|
||||
|
||||
### Device Capability Detection
|
||||
|
||||
On device connection, the app reads the firmware capability to set `pathHashByteWidth`:
|
||||
|
||||
```dart
|
||||
// Read from device info response (offset 81)
|
||||
final modeRaw = firmwareBytes.length >= 82
|
||||
? (firmwareBytes[81] & 0xFF)
|
||||
: 0;
|
||||
final mode = modeRaw.clamp(0, 3);
|
||||
_pathHashByteWidth = mode + 1; // 1, 2, 3, or 4 bytes per hop
|
||||
```
|
||||
|
||||
The connector reads a single-mode byte and clamps to `0..3`, so the supported hop width is `1..4` bytes. UI code also clamps widths when rendering (typically to `1..4`) so 4-byte hops are handled end-to-end in the current codebase.
|
||||
|
||||
### Path Data Structure
|
||||
|
||||
Paths in messages and storage consist of:
|
||||
|
||||
- **`pathLength` (model/storage)**: Hop count (number of hops). Negative values (e.g. `-1`) are used as a flood sentinel.
|
||||
- **On-air `path_len` byte**: A packed byte that encodes hop count + hash width and is decoded into `pathLength` + `pathBytes` when parsing frames.
|
||||
- **`pathBytes`**: Raw bytes of the path (concatenated hop prefixes), grouped by `pathHashByteWidth`.
|
||||
- **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth`.
|
||||
- **Example**: With `pathHashByteWidth=2`, a 3-hop path has 6 bytes (`pathBytes.length = 6`) and `pathLength = 3`:
|
||||
- `pathBytes = [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]`
|
||||
- Hops: `[0xA1A2]`, `[0xB1B2]`, `[0xC1C2]`
|
||||
|
||||
### Hop Count Calculation
|
||||
|
||||
Convert path byte length to hop count:
|
||||
|
||||
```dart
|
||||
int hopCount = (byteCount + hashByteWidth - 1) ~/ hashByteWidth;
|
||||
```
|
||||
|
||||
Use this consistently when displaying hop counts in UI. Do not treat `pathLength` as a hop count when the path uses multi-byte hop hashes.
|
||||
|
||||
### Path Usage in Different Message Types
|
||||
|
||||
- **Direct messages**: Extract path from decrypted payload to trace sender route.
|
||||
- **Channel messages**: Decrypt hop-by-hop routing chain from payload; the header carries the encoded byte length for the path blob, not the derived hop count.
|
||||
- **Contact storage**: Path length in byte 32, raw path bytes in bytes 33-96, grouped by detected `pathHashByteWidth`.
|
||||
|
||||
### UI Hop Count Display
|
||||
|
||||
⚠️ **Important**: In screens like "Channel Message Path", prefer the actual decoded `pathBytes` hop count over `pathLength` metadata:
|
||||
|
||||
```dart
|
||||
// Preferred: use actual observed path length
|
||||
final effectiveHopCount = (pathBytes.length + width - 1) ~/ width;
|
||||
|
||||
// Avoid: using encoded byte length as if it were a hop count
|
||||
// pathLength is bytes; converting it twice causes inflated counts
|
||||
```
|
||||
|
||||
Example scenario:
|
||||
- Radio header reports `pathLength: 32` bytes
|
||||
- Decoded path bytes: `[0xAB, 0xCD]` (2 bytes = 1 hop with width=2)
|
||||
- **Display**: "1 hop" (from `pathBytes`), not "32 hops" (which would double-count the encoded length)
|
||||
|
||||
@@ -52,7 +52,7 @@ These settings are sent directly to the connected device firmware.
|
||||
|
||||
### Radio Settings
|
||||
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
||||
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
||||
- **Spreading Factor**: SF5–SF12
|
||||
@@ -129,10 +129,6 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data:
|
||||
- OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.)
|
||||
- Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.)
|
||||
There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles.
|
||||
- **Offline Map Cache**: Navigate to tile download screen
|
||||
|
||||
### Translation
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>X74VJ4UAST</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,21 +1,73 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_blue_plus_darwin (0.0.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- flutter_foreground_task (0.0.1):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_blue_plus_darwin:
|
||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||
flutter_foreground_task:
|
||||
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
|
||||
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@@ -53,7 +52,6 @@
|
||||
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -69,7 +67,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -88,7 +85,6 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -184,15 +180,13 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@@ -226,9 +220,6 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@@ -292,6 +283,39 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||
);
|
||||
name = "Fix Native Asset Minimum OS";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||
};
|
||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -314,22 +338,6 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||
);
|
||||
name = "Fix Native Asset Minimum OS";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -438,9 +446,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -619,9 +625,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -643,9 +647,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -694,20 +696,6 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -5,24 +5,6 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:meshcore_open/storage/region_store.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
@@ -36,7 +35,6 @@ import 'meshcore_connector_tcp.dart';
|
||||
import '../storage/channel_message_store.dart';
|
||||
import '../storage/channel_order_store.dart';
|
||||
import '../storage/channel_settings_store.dart';
|
||||
import '../storage/channel_region_store.dart';
|
||||
import '../storage/channel_store.dart';
|
||||
import '../storage/contact_discovery_store.dart';
|
||||
import '../storage/contact_settings_store.dart';
|
||||
@@ -51,53 +49,18 @@ import 'meshcore_protocol.dart';
|
||||
|
||||
class DirectRepeater {
|
||||
static const int maxAgeMinutes = 30; // Max age for direct repeater info
|
||||
List<int> pubkeyPrefix;
|
||||
int pathHashWidth;
|
||||
String? contactKeyHex;
|
||||
final int pubkeyFirstByte;
|
||||
double snr;
|
||||
DateTime lastUpdated;
|
||||
|
||||
DirectRepeater({
|
||||
required List<int> pubkeyPrefix,
|
||||
required this.pathHashWidth,
|
||||
this.contactKeyHex,
|
||||
required this.pubkeyFirstByte,
|
||||
required this.snr,
|
||||
DateTime? lastUpdated,
|
||||
}) : pubkeyPrefix = List.unmodifiable(pubkeyPrefix),
|
||||
lastUpdated = lastUpdated ?? DateTime.now();
|
||||
}) : lastUpdated = lastUpdated ?? DateTime.now();
|
||||
|
||||
String get pubkeyPrefixHex => pubkeyPrefix
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join();
|
||||
|
||||
bool matchesPrefix(List<int> prefix) {
|
||||
return pubkeyPrefix.length == prefix.length &&
|
||||
listEquals(pubkeyPrefix, prefix);
|
||||
}
|
||||
|
||||
bool matchesPathStart(List<int> pathBytes, int pathHashByteWidth) {
|
||||
final width = pathHashByteWidth.clamp(1, 4).toInt();
|
||||
if (pathBytes.length < width || pubkeyPrefix.length != width) {
|
||||
return false;
|
||||
}
|
||||
return listEquals(pubkeyPrefix, pathBytes.sublist(0, width));
|
||||
}
|
||||
|
||||
void update(
|
||||
double newSNR, {
|
||||
List<int>? pubkeyPrefix,
|
||||
int? pathHashWidth,
|
||||
String? contactKeyHex,
|
||||
}) {
|
||||
void update(double newSNR) {
|
||||
snr = newSNR;
|
||||
if (pubkeyPrefix != null &&
|
||||
pubkeyPrefix.length >= this.pubkeyPrefix.length) {
|
||||
this.pubkeyPrefix = List.unmodifiable(pubkeyPrefix);
|
||||
}
|
||||
if (pathHashWidth != null && pathHashWidth >= this.pathHashWidth) {
|
||||
this.pathHashWidth = pathHashWidth;
|
||||
}
|
||||
this.contactKeyHex ??= contactKeyHex;
|
||||
lastUpdated = DateTime.now();
|
||||
}
|
||||
|
||||
@@ -162,12 +125,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Message windowing to limit memory usage
|
||||
static const int _messageWindowSize = 200;
|
||||
|
||||
// Cap on discovered (non-contact) nodes retained in memory. Adverts arrive
|
||||
// continuously from the whole mesh, so without a bound this list grows for
|
||||
// as long as the app stays connected. When full, the stalest node (oldest
|
||||
// lastSeen) is evicted to make room for a newly heard one.
|
||||
static const int _maxDiscoveredContacts = 500;
|
||||
|
||||
MeshCoreConnectionState _state = MeshCoreConnectionState.disconnected;
|
||||
BluetoothDevice? _device;
|
||||
BluetoothCharacteristic? _rxCharacteristic;
|
||||
@@ -257,9 +214,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastChannelMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastZeroHopAdvertAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
double? _lastZeroHopAdvertLatitude;
|
||||
double? _lastZeroHopAdvertLongitude;
|
||||
static const int _radioQuietMs = 3000;
|
||||
static const int _radioQuietMaxWaitMs = 3000;
|
||||
|
||||
@@ -312,10 +266,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Serializes path operations (setContactPath/clearContactPath) to prevent
|
||||
// interleaved async calls from leaving in-memory state inconsistent with device.
|
||||
Future<void> _pathOpLock = Future.value();
|
||||
// Flood scope is a global firmware setting, so scoped channel sends must not
|
||||
// overlap or a message may inherit another channel's region.
|
||||
Future<void> _channelScopedSendLock = Future.value();
|
||||
static const Duration _commandAckTimeout = Duration(seconds: 5);
|
||||
Map<String, String>? _currentCustomVars;
|
||||
|
||||
/// Maps repeater pubkey-prefix hex (12 hex chars = first 6 bytes) → the
|
||||
@@ -347,7 +297,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final MessageStore _messageStore = MessageStore();
|
||||
final ChannelOrderStore _channelOrderStore = ChannelOrderStore();
|
||||
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
||||
final ChannelRegionStore _channelRegionStore = ChannelRegionStore();
|
||||
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
||||
final ContactStore _contactStore = ContactStore();
|
||||
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
|
||||
@@ -357,7 +306,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final Map<int, bool> _channelSmazEnabled = {};
|
||||
final Map<int, bool> _channelCyr2LatEnabled = {};
|
||||
final Map<int, String?> _channelCyr2LatProfileId = {};
|
||||
final Map<int, Region> _channelRegions = {};
|
||||
bool _lastSentWasCliCommand =
|
||||
false; // Track if last sent message was a CLI command
|
||||
final Map<String, bool> _contactSmazEnabled = {};
|
||||
@@ -756,14 +704,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return _contactSmazEnabled[contactKeyHex] ?? false;
|
||||
}
|
||||
|
||||
bool hasChannelRegion(int channelIndex) {
|
||||
return (_channelRegions[channelIndex] ?? '').isNotEmpty;
|
||||
}
|
||||
|
||||
Region getChannelRegion(int channelIndex) {
|
||||
return _channelRegions[channelIndex] ?? '';
|
||||
}
|
||||
|
||||
void ensureContactSmazSettingLoaded(String contactKeyHex) {
|
||||
_ensureContactSmazSettingLoaded(contactKeyHex);
|
||||
}
|
||||
@@ -917,14 +857,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setChannelRegion(int channelIndex, String region) async {
|
||||
// Update in-memory state and notify synchronously so the UI reflects the
|
||||
// change immediately; persistence happens in the background.
|
||||
_channelRegions[channelIndex] = region;
|
||||
notifyListeners();
|
||||
await _channelRegionStore.saveRegion(channelIndex, region);
|
||||
}
|
||||
|
||||
Future<void> _loadChannelOrder() async {
|
||||
_channelOrder = await _channelOrderStore.loadChannelOrder();
|
||||
_applyChannelOrder();
|
||||
@@ -1075,13 +1007,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> _loadDiscoveredContactCache() async {
|
||||
final cached = await _discoveryContactStore.loadContacts();
|
||||
// Trim a previously-saved oversized list down to the freshest entries so a
|
||||
// device that grew unbounded before the cap existed recovers on load.
|
||||
if (cached.length > _maxDiscoveredContacts) {
|
||||
cached.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
cached.removeRange(_maxDiscoveredContacts, cached.length);
|
||||
unawaited(_discoveryContactStore.saveContacts(cached));
|
||||
}
|
||||
_discoveredContacts
|
||||
..clear()
|
||||
..addAll(cached);
|
||||
@@ -1090,13 +1015,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> loadChannelSettings({int? maxChannels}) async {
|
||||
_channelSmazEnabled.clear();
|
||||
_channelCyr2LatEnabled.clear();
|
||||
_channelRegions.clear();
|
||||
final channelCount = maxChannels ?? _maxChannels;
|
||||
for (int i = 0; i < channelCount; i++) {
|
||||
_channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i);
|
||||
_channelCyr2LatEnabled[i] = await _channelSettingsStore
|
||||
.loadCyr2LatEnabled(i);
|
||||
_channelRegions[i] = await _channelRegionStore.loadRegion(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2574,8 +2497,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfName = null;
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_lastZeroHopAdvertLatitude = null;
|
||||
_lastZeroHopAdvertLongitude = null;
|
||||
_awaitingSelfInfo = false;
|
||||
_webInitialHandshakeRequestSent = false;
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
@@ -2742,8 +2663,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfName = null;
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_lastZeroHopAdvertLatitude = null;
|
||||
_lastZeroHopAdvertLongitude = null;
|
||||
_clientRepeat = null;
|
||||
_rememberedNonRepeatRadioState = null;
|
||||
_firmwareVerCode = null;
|
||||
@@ -2924,32 +2843,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> setPathHashMode(int mode) async {
|
||||
if (!isConnected) return;
|
||||
final clampedMode = mode.clamp(0, 3).toInt();
|
||||
await sendFrame(buildSetPathHashModeFrame(clampedMode));
|
||||
final nextWidth = clampedMode + 1;
|
||||
if (_pathHashByteWidth != nextWidth) {
|
||||
_pathHashByteWidth = nextWidth;
|
||||
_directRepeaters.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isPathLenValidForCurrentMode(int pathLen, List<int> pathBytes) {
|
||||
return _isPathLenValidForMode(pathLen, pathBytes, _pathHashByteWidth);
|
||||
}
|
||||
|
||||
int? _encodePathLenForCurrentMode(int pathLen, List<int> pathBytes) {
|
||||
if (pathLen < 0 || pathLen == 0xFF) return pathLen;
|
||||
if (!_isPathLenValidForCurrentMode(pathLen, pathBytes)) {
|
||||
appLogger.warn(
|
||||
'Invalid path_len for mode: pathLen=$pathLen, '
|
||||
'bytesLen=${pathBytes.length}, width=$_pathHashByteWidth',
|
||||
tag: 'Connector',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final mode = (_pathHashByteWidth - 1) & 0x03;
|
||||
return (pathLen & 0x3F) | (mode << 6);
|
||||
await sendFrame(buildSetPathHashModeFrame(mode.clamp(0, 2)));
|
||||
}
|
||||
|
||||
Future<void> refreshDeviceInfo() async {
|
||||
@@ -3166,15 +3060,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
try {
|
||||
if (!isConnected) return;
|
||||
|
||||
final encodedPathLen = _encodePathLenForCurrentMode(pathLen, customPath);
|
||||
if (encodedPathLen == null) {
|
||||
return;
|
||||
}
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
contact.publicKey,
|
||||
customPath,
|
||||
encodedPathLen,
|
||||
pathLen,
|
||||
type: contact.type,
|
||||
flags: contact.flags,
|
||||
name: contact.name,
|
||||
@@ -3190,7 +3080,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
if (idx != -1) {
|
||||
_contacts[idx] = _contacts[idx].copyWith(
|
||||
pathLength: pathLen,
|
||||
pathLength: customPath.length,
|
||||
path: customPath,
|
||||
);
|
||||
notifyListeners();
|
||||
@@ -3231,16 +3121,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
: (updatedFlags & ~contactFlagTeleEnv))
|
||||
: updatedFlags;
|
||||
|
||||
final encodedPathLen = _encodePathLenForCurrentMode(
|
||||
latestContact.pathLength,
|
||||
latestContact.path,
|
||||
);
|
||||
if (encodedPathLen == null) return;
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
latestContact.publicKey,
|
||||
latestContact.path,
|
||||
encodedPathLen,
|
||||
latestContact.pathLength,
|
||||
type: latestContact.type,
|
||||
flags: updatedFlags,
|
||||
name: latestContact.name,
|
||||
@@ -3323,18 +3208,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
if (pathLen != null &&
|
||||
pathLen >= 0 &&
|
||||
!_isPathLenValidForCurrentMode(pathLen, pathBytes ?? Uint8List(0))) {
|
||||
appLogger.warn(
|
||||
'setPathOverride: invalid path for ${contact.name}: '
|
||||
'pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}, '
|
||||
'width=$_pathHashByteWidth',
|
||||
tag: 'Connector',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update contact with new path override
|
||||
_contacts[index] = _contacts[index].copyWith(
|
||||
pathOverride: pathLen,
|
||||
@@ -3510,15 +3383,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Send the reaction to the device (don't add as a visible message)
|
||||
final reactionQueueId = _nextReactionSendQueueId();
|
||||
_pendingChannelSentQueue.add(reactionQueueId);
|
||||
await _runScopedChannelSend(() async {
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await _sendFrameAndWaitForCommandAck(
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, text),
|
||||
channelSendQueueId: reactionQueueId,
|
||||
expectsGenericAck: true,
|
||||
successCode: respCodeSent,
|
||||
);
|
||||
}, region: getChannelRegion(channel.index));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3535,97 +3405,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
final outboundText = prepareChannelOutboundText(channel.index, text);
|
||||
await _runScopedChannelSend(() async {
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await _sendFrameAndWaitForCommandAck(
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
channelSendQueueId: message.messageId,
|
||||
expectsGenericAck: true,
|
||||
successCode: respCodeSent,
|
||||
);
|
||||
}, region: getChannelRegion(channel.index));
|
||||
}
|
||||
|
||||
Future<void> _runScopedChannelSend(
|
||||
Future<void> Function() action, {
|
||||
required String region,
|
||||
}) async {
|
||||
final prev = _channelScopedSendLock;
|
||||
final completer = Completer<void>();
|
||||
_channelScopedSendLock = completer.future;
|
||||
await prev;
|
||||
|
||||
try {
|
||||
// Only touch the global flood scope for region-scoped channels. Plain
|
||||
// channels send exactly as before, which also stays compatible with
|
||||
// firmware that predates CMD_SET_FLOOD_SCOPE. The lock is still held so an
|
||||
// unscoped send can't interleave with (and inherit the scope of) a
|
||||
// concurrent scoped send.
|
||||
if (region.isEmpty) {
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(region));
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
if (isConnected) {
|
||||
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(''));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
// Sends [data] and resolves once the device replies. [successCode] is the
|
||||
// response code that signals success for this frame: SET_FLOOD_SCOPE replies
|
||||
// with RESP_CODE_OK, whereas a channel text send replies with RESP_CODE_SENT.
|
||||
// Waiting for the text send's RESP_CODE_SENT before the scope is reset
|
||||
// guarantees the firmware has already built the packet with the active scope.
|
||||
Future<void> _sendFrameAndWaitForCommandAck(
|
||||
Uint8List data, {
|
||||
String? channelSendQueueId,
|
||||
bool expectsGenericAck = false,
|
||||
int successCode = respCodeOk,
|
||||
}) async {
|
||||
final completer = Completer<void>();
|
||||
late final StreamSubscription<Uint8List> subscription;
|
||||
late final Timer timeout;
|
||||
|
||||
void complete() {
|
||||
if (!completer.isCompleted) completer.complete();
|
||||
}
|
||||
|
||||
void completeError(Object error) {
|
||||
if (!completer.isCompleted) completer.completeError(error);
|
||||
}
|
||||
|
||||
subscription = receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
if (frame[0] == successCode) {
|
||||
complete();
|
||||
} else if (frame[0] == respCodeErr) {
|
||||
final errCode = frame.length > 1 ? frame[1] : -1;
|
||||
completeError(Exception('Command failed with error code $errCode'));
|
||||
}
|
||||
});
|
||||
|
||||
timeout = Timer(_commandAckTimeout, () {
|
||||
completeError(TimeoutException('Command ACK timed out'));
|
||||
});
|
||||
|
||||
try {
|
||||
await sendFrame(
|
||||
data,
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
expectsGenericAck: expectsGenericAck,
|
||||
);
|
||||
await completer.future;
|
||||
} finally {
|
||||
timeout.cancel();
|
||||
await subscription.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeContact(Contact contact) async {
|
||||
@@ -3682,16 +3467,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Manual saves must bypass the firmware's auto-add discovery policy.
|
||||
// CMD_IMPORT_CONTACT replays an advert and may remain discovery-only.
|
||||
final encodedPathLen = _encodePathLenForCurrentMode(
|
||||
contact.pathLength,
|
||||
contact.path,
|
||||
);
|
||||
if (encodedPathLen == null) return false;
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
contact.publicKey,
|
||||
contact.path,
|
||||
encodedPathLen,
|
||||
contact.pathLength,
|
||||
type: contact.type,
|
||||
flags: contact.flags,
|
||||
name: contact.name,
|
||||
@@ -3903,11 +3683,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> sendSelfAdvert({bool flood = true}) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
|
||||
if (!flood) {
|
||||
_lastZeroHopAdvertAt = DateTime.now();
|
||||
_lastZeroHopAdvertLatitude = _selfLatitude;
|
||||
_lastZeroHopAdvertLongitude = _selfLongitude;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> rebootDevice() async {
|
||||
@@ -4242,9 +4017,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case pushCodePathUpdated:
|
||||
_handlePathUpdated(frame);
|
||||
break;
|
||||
case pushCodeControlData:
|
||||
// Optional feature-specific services listen to receivedFrames directly.
|
||||
break;
|
||||
case pushCodeLoginSuccess:
|
||||
_handleLoginSuccess(frame);
|
||||
break;
|
||||
@@ -4371,42 +4143,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
|
||||
const locationChangeEpsilon = 2.25e-4; // ~25 meters in degrees.
|
||||
final lastAdvertLatitude = _lastZeroHopAdvertLatitude;
|
||||
final lastAdvertLongitude = _lastZeroHopAdvertLongitude;
|
||||
final currentLatitude = _selfLatitude;
|
||||
final currentLongitude = _selfLongitude;
|
||||
final latChanged =
|
||||
lastAdvertLatitude != null &&
|
||||
currentLatitude != null &&
|
||||
(currentLatitude - lastAdvertLatitude).abs() >= locationChangeEpsilon;
|
||||
final lonChanged =
|
||||
lastAdvertLongitude != null &&
|
||||
currentLongitude != null &&
|
||||
(currentLongitude - lastAdvertLongitude).abs() >= locationChangeEpsilon;
|
||||
final gpsSampleChanged =
|
||||
hasValidLocation(currentLatitude, currentLongitude) &&
|
||||
(!hasValidLocation(lastAdvertLatitude, lastAdvertLongitude) ||
|
||||
latChanged ||
|
||||
lonChanged);
|
||||
final effectiveGpsIntervalSeconds =
|
||||
_appSettingsService?.resolvedGpsIntervalSeconds(_currentCustomVars) ??
|
||||
0;
|
||||
final timeSinceLastZeroHopAdvert = DateTime.now().difference(
|
||||
_lastZeroHopAdvertAt,
|
||||
);
|
||||
final shouldAutoSendZeroHopAdvert =
|
||||
(gpsSampleChanged || (_clientRepeat ?? false)) &&
|
||||
_advertLocPolicy == 1 &&
|
||||
(_appSettingsService?.settings.autoSendZeroHopAdvertOnGpsUpdate ??
|
||||
false) &&
|
||||
effectiveGpsIntervalSeconds > 0 &&
|
||||
timeSinceLastZeroHopAdvert.inSeconds >= effectiveGpsIntervalSeconds;
|
||||
if (shouldAutoSendZeroHopAdvert) {
|
||||
unawaited(sendSelfAdvert(flood: false));
|
||||
}
|
||||
|
||||
final selfName = _selfName?.trim();
|
||||
if (_activeTransport == MeshCoreTransportType.usb &&
|
||||
selfName != null &&
|
||||
@@ -4427,7 +4163,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_messageStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_channelRegionStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_contactStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
_channelStore.setPublicKeyHex = selfPublicKeyHex;
|
||||
@@ -4465,16 +4200,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_clientRepeat = frame[80] != 0;
|
||||
}
|
||||
// Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
|
||||
final previousPathHashByteWidth = _pathHashByteWidth;
|
||||
if (frame.length >= 82) {
|
||||
final mode = (frame[81] & 0xFF).clamp(0, 3);
|
||||
final mode = (frame[81] & 0xFF).clamp(0, 2);
|
||||
_pathHashByteWidth = mode + 1;
|
||||
} else {
|
||||
_pathHashByteWidth = 1;
|
||||
}
|
||||
if (_pathHashByteWidth != previousPathHashByteWidth) {
|
||||
_directRepeaters.clear();
|
||||
}
|
||||
|
||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||
final reportedContacts = frame[2];
|
||||
@@ -4992,7 +4723,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String senderName,
|
||||
DateTime timestamp, {
|
||||
Uint8List? pathBytes,
|
||||
int? pathHashWidth,
|
||||
bool notify = false,
|
||||
}) {
|
||||
final normalized = senderName.trim().toLowerCase();
|
||||
@@ -5016,11 +4746,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
for (var i = 0; i < _contacts.length; i++) {
|
||||
final contact = _contacts[i];
|
||||
if (contact.type != advTypeChat) continue;
|
||||
if (_pathMatchesContact(
|
||||
pathBytes,
|
||||
contact.publicKey,
|
||||
pathHashWidth: pathHashWidth,
|
||||
)) {
|
||||
if (_pathMatchesContact(pathBytes, contact.publicKey)) {
|
||||
matches.add(i);
|
||||
}
|
||||
}
|
||||
@@ -5037,12 +4763,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _pathMatchesContact(
|
||||
Uint8List pathBytes,
|
||||
Uint8List publicKey, {
|
||||
int? pathHashWidth,
|
||||
}) {
|
||||
final w = pathHashWidth ?? _pathHashByteWidth;
|
||||
bool _pathMatchesContact(Uint8List pathBytes, Uint8List publicKey) {
|
||||
final w = _pathHashByteWidth;
|
||||
if (pathBytes.isEmpty || publicKey.length < w) return false;
|
||||
for (int i = 0; i + w <= pathBytes.length; i += w) {
|
||||
final prefix = pathBytes.sublist(i, i + w);
|
||||
@@ -5264,7 +4986,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
isOutgoing: false,
|
||||
isCli: isCli,
|
||||
status: MessageStatus.delivered,
|
||||
pathLength: pathLength == 0xFF ? -1 : (pathLength & 0x3F),
|
||||
pathLength: pathLength == 0xFF ? 0 : pathLength,
|
||||
pathBytes: Uint8List(0),
|
||||
fourByteRoomContactKey: roomAuthorPrefix,
|
||||
);
|
||||
@@ -5497,7 +5219,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
message.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
pathHashWidth: message.pathHashWidth,
|
||||
);
|
||||
final isNew = _addChannelMessage(message.channelIndex!, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
@@ -5575,7 +5296,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: packet.isFlood ? packet.hopCount : 0,
|
||||
pathHashWidth: packet.pathHashWidth,
|
||||
pathBytes: packet.pathBytes,
|
||||
channelIndex: channel.index,
|
||||
packetHash: pktHash,
|
||||
@@ -5585,7 +5305,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
parsed.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
pathHashWidth: message.pathHashWidth,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
@@ -6329,7 +6048,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
repeats: message.repeats,
|
||||
repeatCount: message.repeatCount,
|
||||
pathLength: message.pathLength,
|
||||
pathHashWidth: message.pathHashWidth,
|
||||
pathBytes: message.pathBytes,
|
||||
pathVariants: message.pathVariants,
|
||||
channelIndex: message.channelIndex,
|
||||
@@ -6366,7 +6084,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
messages[existingIndex] = existing.copyWith(
|
||||
repeatCount: newRepeatCount,
|
||||
pathLength: mergedPathLength,
|
||||
pathHashWidth: existing.pathHashWidth ?? processedMessage.pathHashWidth,
|
||||
pathBytes: mergedPathBytes,
|
||||
pathVariants: mergedPathVariants,
|
||||
packetHash: existing.packetHash ?? processedMessage.packetHash,
|
||||
@@ -6738,7 +6455,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
//final payloadVer = (header >> 6) & 0x03;
|
||||
final pathLenRaw = packet.readByte();
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
final pathHashWidth = _decodePathHashWidth(pathLenRaw);
|
||||
final pathBytes = packet.readBytes(pathByteLen);
|
||||
final payload = packet.readBytes(packet.remaining);
|
||||
|
||||
@@ -6749,7 +6465,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
rawPacket,
|
||||
payload,
|
||||
pathBytes,
|
||||
pathHashWidth,
|
||||
routeType,
|
||||
snr,
|
||||
);
|
||||
@@ -6762,86 +6477,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void importContact(Uint8List frame) {
|
||||
final packet = BufferReader(frame);
|
||||
int payloadType = 0;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int pathHashWidth = 1;
|
||||
try {
|
||||
packet.skipBytes(1); // Skip frame type byte
|
||||
packet.skipBytes(1); // Skip SNR byte
|
||||
packet.skipBytes(1); // Skip RSSI byte
|
||||
final header = packet.readByte();
|
||||
final routeType = header & 0x03;
|
||||
payloadType = (header >> 2) & 0x0F;
|
||||
if (routeType == _routeTransportFlood ||
|
||||
routeType == _routeTransportDirect) {
|
||||
packet.skipBytes(4); // Skip transport-specific bytes
|
||||
}
|
||||
//final payloadVer = (header >> 6) & 0x03;
|
||||
final pathLenRaw = packet.readByte();
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
pathHashWidth = _decodePathHashWidth(pathLenRaw);
|
||||
pathBytes = packet.readBytes(pathByteLen);
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
double? latitude;
|
||||
double? longitude;
|
||||
String name = '';
|
||||
Uint8List publicKey = Uint8List(0);
|
||||
int type = 0;
|
||||
int timestamp = 0;
|
||||
bool hasLocation = false;
|
||||
bool hasName = false;
|
||||
if (payloadType != payloadTypeADVERT) {
|
||||
appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
publicKey = packet.readBytes(32);
|
||||
timestamp = packet.readInt32LE();
|
||||
//TODO add signature verification
|
||||
packet.skipBytes(64); // Skip signature for now
|
||||
final flags = packet.readByte();
|
||||
type = flags & 0x0F;
|
||||
hasLocation = (flags & 0x10) != 0;
|
||||
// For future use:
|
||||
//final hasFeature1 = (flags & 0x20) != 0;
|
||||
//final hasFeature2 = (flags & 0x40) != 0;
|
||||
hasName = (flags & 0x80) != 0;
|
||||
if (hasLocation && packet.remaining >= 8) {
|
||||
latitude = packet.readInt32LE() / 1e6;
|
||||
longitude = packet.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && packet.remaining > 0) {
|
||||
name = packet.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
importDiscoveredContact(
|
||||
Contact(
|
||||
rawPacket: frame,
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
type: type,
|
||||
pathLength: pathBytes.isEmpty
|
||||
? -1
|
||||
: (pathBytes.length ~/ pathHashWidth),
|
||||
// Store hop order reversed for easier outgoing messages; keep bytes
|
||||
// inside each multi-byte hop in their original order.
|
||||
path: _reversePathByHop(pathBytes, pathHashWidth),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasValidLocation(double? latitude, double? longitude) {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
@@ -6857,7 +6492,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Uint8List rawPacket,
|
||||
Uint8List payload,
|
||||
Uint8List path,
|
||||
int pathHashWidth,
|
||||
int routeType,
|
||||
double snr,
|
||||
) {
|
||||
@@ -6916,10 +6550,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
type: type,
|
||||
pathLength: path.isEmpty ? -1 : (path.length ~/ pathHashWidth),
|
||||
// Store hop order reversed for easier outgoing messages; keep bytes
|
||||
// inside each multi-byte hop in their original order.
|
||||
path: _reversePathByHop(path, pathHashWidth),
|
||||
pathLength: path.length,
|
||||
path: Uint8List.fromList(
|
||||
path.reversed.toList(),
|
||||
), // Store path in reverse for easier use in outgoing messages
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
@@ -6938,12 +6572,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
} else {
|
||||
_handleDiscovery(newContact, rawPacket);
|
||||
}
|
||||
_updateDirectRepeater(
|
||||
newContact,
|
||||
snr,
|
||||
path,
|
||||
pathHashWidth: pathHashWidth,
|
||||
);
|
||||
_updateDirectRepeater(newContact, snr, path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6967,8 +6596,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
latitude: hasLocation ? latitude : existing.latitude,
|
||||
longitude: hasLocation ? longitude : existing.longitude,
|
||||
name: hasName ? name : existing.name,
|
||||
path: _reversePathByHop(path, pathHashWidth),
|
||||
pathLength: path.isEmpty ? -1 : (path.length ~/ pathHashWidth),
|
||||
path: Uint8List.fromList(path.reversed.toList()),
|
||||
pathLength: path.length,
|
||||
lastMessageAt: mergedLastMessageAt,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||
@@ -6981,12 +6610,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_pathHistoryService!.handlePathUpdated(_contacts[existingIndex]);
|
||||
}
|
||||
|
||||
_updateDirectRepeater(
|
||||
_contacts[existingIndex],
|
||||
snr,
|
||||
path,
|
||||
pathHashWidth: pathHashWidth,
|
||||
);
|
||||
_updateDirectRepeater(_contacts[existingIndex], snr, path);
|
||||
|
||||
appLogger.info(
|
||||
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||
@@ -6995,42 +6619,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDirectRepeater(
|
||||
Contact contact,
|
||||
double snr,
|
||||
Uint8List path, {
|
||||
required int pathHashWidth,
|
||||
}) {
|
||||
final hashWidth = pathHashWidth.clamp(1, 4).toInt();
|
||||
if (path.isNotEmpty && path.length < hashWidth) {
|
||||
return;
|
||||
}
|
||||
final pathStartIndex = path.isNotEmpty ? path.length - hashWidth : 0;
|
||||
final publicKeyPrefixEnd = math
|
||||
.min(hashWidth, contact.publicKey.length)
|
||||
.toInt();
|
||||
final pubkeyPrefix = path.isNotEmpty
|
||||
? path.sublist(pathStartIndex)
|
||||
: contact.publicKey.sublist(0, publicKeyPrefixEnd);
|
||||
final contactKeyHex = _resolveDirectRepeaterContactKeyHex(
|
||||
contact,
|
||||
pubkeyPrefix,
|
||||
path.isEmpty,
|
||||
);
|
||||
final knownRepeaters = contactKeyHex == null
|
||||
? _directRepeaters
|
||||
.where(
|
||||
(r) =>
|
||||
r.contactKeyHex != null &&
|
||||
_contactKeyMatchesPrefix(r.contactKeyHex!, pubkeyPrefix),
|
||||
)
|
||||
.toList()
|
||||
: const <DirectRepeater>[];
|
||||
final knownRepeater = knownRepeaters.length == 1
|
||||
? knownRepeaters.first
|
||||
: null;
|
||||
final effectiveContactKeyHex =
|
||||
contactKeyHex ?? knownRepeater?.contactKeyHex;
|
||||
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
|
||||
final pubkeyFirstByte = path.isNotEmpty
|
||||
? path.last
|
||||
: contact.publicKey.first;
|
||||
|
||||
_directRepeaters.removeWhere((r) => r.isStale());
|
||||
|
||||
@@ -7041,25 +6633,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final isTracked = _directRepeaters.where((r) {
|
||||
if (knownRepeater != null) {
|
||||
return identical(r, knownRepeater);
|
||||
}
|
||||
if (effectiveContactKeyHex != null) {
|
||||
return r.contactKeyHex == effectiveContactKeyHex ||
|
||||
(r.contactKeyHex == null &&
|
||||
_contactKeyMatchesPrefix(
|
||||
effectiveContactKeyHex,
|
||||
r.pubkeyPrefix,
|
||||
));
|
||||
}
|
||||
if (r.contactKeyHex == null &&
|
||||
r.pathHashWidth == hashWidth &&
|
||||
r.matchesPrefix(pubkeyPrefix)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).toList();
|
||||
final isTracked = _directRepeaters.where(
|
||||
(r) => r.pubkeyFirstByte == pubkeyFirstByte,
|
||||
);
|
||||
|
||||
final sortedRepeaters = List<DirectRepeater>.from(_directRepeaters)
|
||||
..sort((a, b) => b.snr.compareTo(a.snr));
|
||||
@@ -7075,70 +6651,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
if (isTracked.isNotEmpty) {
|
||||
final repeater = isTracked.first;
|
||||
repeater.update(
|
||||
snr,
|
||||
pubkeyPrefix: pubkeyPrefix,
|
||||
pathHashWidth: hashWidth,
|
||||
contactKeyHex: effectiveContactKeyHex,
|
||||
);
|
||||
if (effectiveContactKeyHex != null) {
|
||||
_directRepeaters.removeWhere(
|
||||
(r) =>
|
||||
!identical(r, repeater) &&
|
||||
(r.contactKeyHex == effectiveContactKeyHex ||
|
||||
(r.contactKeyHex == null &&
|
||||
_contactKeyMatchesPrefix(
|
||||
effectiveContactKeyHex,
|
||||
r.pubkeyPrefix,
|
||||
))),
|
||||
);
|
||||
}
|
||||
repeater.update(snr);
|
||||
} else if (_directRepeaters.length < 5) {
|
||||
_directRepeaters.add(
|
||||
DirectRepeater(
|
||||
pubkeyPrefix: pubkeyPrefix,
|
||||
pathHashWidth: hashWidth,
|
||||
contactKeyHex: effectiveContactKeyHex,
|
||||
snr: snr,
|
||||
),
|
||||
DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? _resolveDirectRepeaterContactKeyHex(
|
||||
Contact contact,
|
||||
List<int> pubkeyPrefix,
|
||||
bool pathIsEmpty,
|
||||
) {
|
||||
if (pathIsEmpty &&
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom)) {
|
||||
return contact.publicKeyHex;
|
||||
}
|
||||
|
||||
final prefixMatches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
_contactKeyMatchesPrefix(c.publicKeyHex, pubkeyPrefix),
|
||||
)
|
||||
.toList();
|
||||
if (prefixMatches.length == 1) {
|
||||
return prefixMatches.first.publicKeyHex;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _contactKeyMatchesPrefix(String contactKeyHex, List<int> pubkeyPrefix) {
|
||||
// Normalize both sides to upper-case hex to avoid case-sensitive mismatches.
|
||||
final normalizedPrefixHex = pubkeyPrefix
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join();
|
||||
final normalizedContactKeyHex = contactKeyHex.toUpperCase();
|
||||
return normalizedContactKeyHex.startsWith(normalizedPrefixHex);
|
||||
}
|
||||
|
||||
void _handleAutoAddConfig(Uint8List frame) {
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
@@ -7200,10 +6721,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
isActive: addActive,
|
||||
flags: 0,
|
||||
);
|
||||
|
||||
if (_discoveredContacts.length >= _maxDiscoveredContacts) {
|
||||
_evictStalestDiscoveredContact();
|
||||
}
|
||||
_discoveredContacts.add(disContact);
|
||||
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
@@ -7221,19 +6738,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _evictStalestDiscoveredContact() {
|
||||
if (_discoveredContacts.isEmpty) return;
|
||||
var stalestIndex = 0;
|
||||
for (int i = 1; i < _discoveredContacts.length; i++) {
|
||||
if (_discoveredContacts[i].lastSeen.isBefore(
|
||||
_discoveredContacts[stalestIndex].lastSeen,
|
||||
)) {
|
||||
stalestIndex = i;
|
||||
}
|
||||
}
|
||||
_discoveredContacts.removeAt(stalestIndex);
|
||||
}
|
||||
|
||||
void removeAllDiscoveredContacts() {
|
||||
_discoveredContacts.clear();
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
@@ -7278,42 +6782,13 @@ const int _payloadTypeGroupText = 0x05;
|
||||
const int _cipherMacSize = 2;
|
||||
|
||||
/// Decodes the firmware's encoded path_len byte into actual byte length.
|
||||
/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte ... 3=4bytes).
|
||||
/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte, 1=2bytes, 2=3bytes).
|
||||
int _decodePathByteLen(int pathLenRaw) {
|
||||
if (pathLenRaw == 0xFF || pathLenRaw == 0) return 0;
|
||||
final hashCount = pathLenRaw & 63;
|
||||
final hashSize = _decodePathHashWidth(pathLenRaw);
|
||||
final hashSize = ((pathLenRaw >> 6) & 0x03) + 1;
|
||||
return hashCount * hashSize;
|
||||
}
|
||||
|
||||
int _decodePathHashWidth(int pathLenRaw) {
|
||||
if (pathLenRaw == 0xFF) return 1;
|
||||
return ((pathLenRaw >> 6) & 0x03) + 1;
|
||||
}
|
||||
|
||||
bool _isPathLenValidForMode(
|
||||
int pathLen,
|
||||
List<int> pathBytes,
|
||||
int pathHashWidth,
|
||||
) {
|
||||
if (pathLen < 0 || pathLen > 0x3F) return false;
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
final maxHopCountByBytes = maxPathSize ~/ width;
|
||||
if (pathLen > maxHopCountByBytes) return false;
|
||||
return pathBytes.length <= maxPathSize && pathBytes.length == pathLen * width;
|
||||
}
|
||||
|
||||
Uint8List _reversePathByHop(Uint8List pathBytes, int pathHashWidth) {
|
||||
if (pathBytes.isEmpty) return Uint8List(0);
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
final reversed = <int>[];
|
||||
for (var i = pathBytes.length; i > 0; i -= width) {
|
||||
final start = (i - width).clamp(0, pathBytes.length).toInt();
|
||||
reversed.addAll(pathBytes.sublist(start, i));
|
||||
}
|
||||
return Uint8List.fromList(reversed);
|
||||
}
|
||||
|
||||
class _RawPacket {
|
||||
final int header;
|
||||
final int routeType;
|
||||
@@ -7337,7 +6812,6 @@ class _RawPacket {
|
||||
routeType == _routeFlood || routeType == _routeTransportFlood;
|
||||
|
||||
int get hopCount => pathLenRaw & 63;
|
||||
int get pathHashWidth => ((pathLenRaw >> 6) & 0x03) + 1;
|
||||
}
|
||||
|
||||
class _ParsedText {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||
@@ -207,8 +206,6 @@ const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdSetFloodScope = 54;
|
||||
const int cmdSendControlData = 55;
|
||||
const int cmdGetStats = 56;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
@@ -233,12 +230,6 @@ Uint8List buildTelemetryBinaryPayload() {
|
||||
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
const int anonReqTypeRegions = 0x01;
|
||||
|
||||
// Control data sub-types used by MeshCore discovery packets.
|
||||
const int controlSubtypeDiscoverReq = 0x08;
|
||||
const int controlSubtypeDiscoverResp = 0x09;
|
||||
|
||||
// Repeater response codes
|
||||
const int respServerLoginOk = 0;
|
||||
|
||||
@@ -281,7 +272,6 @@ const int pushCodeTraceData = 0x89;
|
||||
const int pushCodeNewAdvert = 0x8A;
|
||||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
const int pushCodeControlData = 0x8E;
|
||||
|
||||
// Contact/advertisement types
|
||||
const int advTypeChat = 1;
|
||||
@@ -587,9 +577,9 @@ Uint8List buildGetStatsFrame(int statsType) {
|
||||
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||
}
|
||||
|
||||
/// Path hash width on air: [61][0][mode], mode 0..3 → (mode+1) bytes per hop hash.
|
||||
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
|
||||
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||
final m = mode.clamp(0, 3).toInt();
|
||||
final m = mode.clamp(0, 2);
|
||||
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||
}
|
||||
|
||||
@@ -876,67 +866,6 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List buildSendControlDataFrame(Uint8List payload) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendControlData);
|
||||
writer.writeBytes(payload);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List buildDiscoveryRequestPayload(
|
||||
int tag, {
|
||||
bool prefixOnly = false,
|
||||
int typeMask = 1 << advTypeRepeater,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
|
||||
// subtype 0x8, with the low bit selecting short/full public keys in replies.
|
||||
writer.writeByte(
|
||||
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
|
||||
);
|
||||
writer.writeByte(typeMask);
|
||||
writer.writeUInt32LE(tag);
|
||||
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
|
||||
if (path.isEmpty) return Uint8List(0);
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
if (path.length % width != 0) {
|
||||
return Uint8List.fromList(path.reversed.toList());
|
||||
}
|
||||
|
||||
final reversed = Uint8List(path.length);
|
||||
final hops = path.length ~/ width;
|
||||
for (var i = 0; i < hops; i++) {
|
||||
final from = (hops - 1 - i) * width;
|
||||
reversed.setRange(i * width, (i + 1) * width, path, from);
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
// Build CMD_SEND_ANON_REQ frame.
|
||||
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
|
||||
Uint8List buildSendAnonReqFrame(
|
||||
Uint8List repeaterPubKey, {
|
||||
required int requestType,
|
||||
Uint8List? replyPath,
|
||||
int replyHopCount = 0,
|
||||
int pathHashWidth = pathHashSize,
|
||||
}) {
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
final path = replyPath ?? Uint8List(0);
|
||||
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendAnonReq);
|
||||
writer.writeBytes(repeaterPubKey);
|
||||
writer.writeByte(requestType);
|
||||
writer.writeByte(encodedPathLen);
|
||||
writer.writeBytes(_reversePathByHop(path, width));
|
||||
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}) {
|
||||
@@ -1031,18 +960,3 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
//Build CMD_SET_FLOOD_SCOPE
|
||||
// Format: [cmd][scope]
|
||||
Uint8List buildSetFloodScopeFrame(String region) {
|
||||
if (region == '') {
|
||||
// reset scope
|
||||
return Uint8List.fromList([cmdSetFloodScope, 0]);
|
||||
}
|
||||
|
||||
final name = region.startsWith('#') ? region : '#$region';
|
||||
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
||||
final scope = Uint8List.fromList(hash.sublist(0, 16));
|
||||
|
||||
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -21,7 +20,6 @@ class LinkHandler {
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
TextStyle? linkStyle,
|
||||
VoidCallback? onSecondaryTap,
|
||||
}) {
|
||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||
@@ -29,7 +27,7 @@ class LinkHandler {
|
||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||
|
||||
if (PlatformInfo.isDesktop) {
|
||||
final linkify = SelectableLinkify(
|
||||
return SelectableLinkify(
|
||||
text: text,
|
||||
style: style,
|
||||
linkStyle: effectiveLinkStyle,
|
||||
@@ -37,14 +35,6 @@ class LinkHandler {
|
||||
linkifiers: linkifiers,
|
||||
onOpen: onOpen,
|
||||
);
|
||||
if (onSecondaryTap == null) return linkify;
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: linkify,
|
||||
);
|
||||
}
|
||||
return Linkify(
|
||||
text: text,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class PathHelper {
|
||||
static String formatPathHex(List<int> pathBytes) {
|
||||
@@ -14,17 +12,10 @@ class PathHelper {
|
||||
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
static String formatHopHex(List<int> hopBytes) {
|
||||
return hopBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join();
|
||||
}
|
||||
|
||||
static String? hopName(int byte, List<Contact> allContacts) {
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.isNotEmpty &&
|
||||
c.publicKey.first == byte &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
@@ -34,58 +25,12 @@ class PathHelper {
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
}
|
||||
|
||||
static List<Uint8List> splitPathBytes(
|
||||
List<int> pathBytes,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final hops = <Uint8List>[];
|
||||
for (int i = 0; i < pathBytes.length; i += width) {
|
||||
final endIdx = (i + width).clamp(0, pathBytes.length).toInt();
|
||||
final hopBytes = pathBytes.sublist(i, endIdx);
|
||||
if (hopBytes.isNotEmpty) {
|
||||
hops.add(Uint8List.fromList(hopBytes));
|
||||
}
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
|
||||
///
|
||||
/// Groups path bytes according to [hashByteWidth]:
|
||||
/// - 1: Single byte per hop (256 unique nodes)
|
||||
/// - 2: Two bytes per hop (65K unique nodes)
|
||||
/// - 3: Three bytes per hop (16M unique nodes)
|
||||
/// - 4: Four bytes per hop (4.3G unique nodes)
|
||||
static String resolvePathNames(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return '';
|
||||
|
||||
final parts = <String>[];
|
||||
|
||||
for (final hopBytes in splitPathBytes(pathBytes, hashByteWidth)) {
|
||||
final hex = formatHopHex(hopBytes);
|
||||
|
||||
final matches = allContacts.where((c) {
|
||||
if (c.publicKey.length < hopBytes.length) return false;
|
||||
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
||||
return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
|
||||
}).toList();
|
||||
|
||||
if (matches.isEmpty) {
|
||||
parts.add(hex);
|
||||
} else if (matches.length == 1) {
|
||||
parts.add(matches.first.name);
|
||||
} else {
|
||||
parts.add(matches.map((c) => c.name).join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' \u2192 ');
|
||||
return pathBytes
|
||||
.map((b) => hopName(b, allContacts) ?? hopHex(b))
|
||||
.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import 'path_helper.dart';
|
||||
import '../models/contact.dart';
|
||||
|
||||
class PathHopResolver {
|
||||
@@ -12,35 +11,30 @@ class PathHopResolver {
|
||||
required List<Contact> contacts,
|
||||
LatLng? endpoint,
|
||||
bool resolveFromEnd = false,
|
||||
int pathHashByteWidth = 1,
|
||||
}) {
|
||||
final width = pathHashByteWidth.clamp(1, 4).toInt();
|
||||
final candidatesByPrefix = <String, List<Contact>>{};
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
for (final contact in contacts) {
|
||||
if (contact.publicKey.length < width) continue;
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
}
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, width),
|
||||
);
|
||||
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||
candidatesByPrefix
|
||||
.putIfAbsent(contact.publicKey.first, () => <Contact>[])
|
||||
.add(contact);
|
||||
}
|
||||
for (final candidates in candidatesByPrefix.values) {
|
||||
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
}
|
||||
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, width);
|
||||
final resolved = List<Contact?>.filled(hops.length, null);
|
||||
final resolved = List<Contact?>.filled(pathBytes.length, null);
|
||||
final indexes = resolveFromEnd
|
||||
? List<int>.generate(hops.length, (i) => hops.length - 1 - i)
|
||||
: List<int>.generate(hops.length, (i) => i);
|
||||
? List<int>.generate(pathBytes.length, (i) => pathBytes.length - 1 - i)
|
||||
: List<int>.generate(pathBytes.length, (i) => i);
|
||||
final distance = Distance();
|
||||
var previousPosition = endpoint;
|
||||
|
||||
for (final index in indexes) {
|
||||
final candidates =
|
||||
candidatesByPrefix[PathHelper.formatHopHex(hops[index])];
|
||||
final candidates = candidatesByPrefix[pathBytes[index]];
|
||||
if (candidates == null || candidates.isEmpty) continue;
|
||||
|
||||
var bestIndex = 0;
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Невалидна ширина или дължина.",
|
||||
"settings_latitude": "Широчина",
|
||||
"settings_longitude": "Дължина",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Автоматична zero-hop обява при GPS обновяване",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).",
|
||||
"settings_privacyMode": "Режим на поверителност",
|
||||
"settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите",
|
||||
"settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Последни 6 часа",
|
||||
"appSettings_last24Hours": "Последно 24 часа",
|
||||
"appSettings_lastWeek": "Миналата седмица",
|
||||
"appSettings_rasterTileSource": "Източник на растерни плочки",
|
||||
"appSettings_stadiaEndpoint": "Крайна точка на Stadia",
|
||||
"appSettings_stadiaApiKey": "API ключ за Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Задължително за използване на Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Конфигурирано: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.",
|
||||
"appSettings_offlineMapCache": "Кеш на офлайн карти",
|
||||
"appSettings_noAreaSelected": "Няма избрана област",
|
||||
"appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.",
|
||||
"settings_latitude": "Breitengrad",
|
||||
"settings_longitude": "Längengrad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische Zero-Hop-Ankündigung bei GPS-Update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).",
|
||||
"settings_privacyMode": "Privatsphärenmodus",
|
||||
"settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen",
|
||||
"settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Letzte 6 Stunden",
|
||||
"appSettings_last24Hours": "Letzte 24 Stunden",
|
||||
"appSettings_lastWeek": "Letzte Woche",
|
||||
"appSettings_rasterTileSource": "Quelle für Rasterkacheln",
|
||||
"appSettings_stadiaEndpoint": "Stadia-Endpunkt",
|
||||
"appSettings_stadiaApiKey": "Stadia-API-Schlüssel",
|
||||
"appSettings_stadiaApiKeyRequired": "Für die Nutzung von Stadia Maps erforderlich",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfiguriert: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.",
|
||||
"appSettings_offlineMapCache": "Offline-Karten-Cache",
|
||||
"appSettings_noAreaSelected": "Kein Bereich ausgewählt",
|
||||
"appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})",
|
||||
@@ -806,56 +791,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2553,13 +2488,6 @@
|
||||
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
|
||||
"repeater_chanUtil": "Nutzung des Kanals",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
@@ -2567,27 +2495,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regionen",
|
||||
"settings_regionSettingsSubtitle": "Gespeicherte Regionen verwalten",
|
||||
"settings_regionManagement_screenTitle": "Regions-Verwaltung",
|
||||
"settings_regionNameHint": "Regions-Namen eingeben",
|
||||
"settings_regionAddRegion": "Region hinzufügen",
|
||||
"settings_regionDeleted": "Region entfernt",
|
||||
"settings_regionName": "Regions-Name",
|
||||
"settings_deleteRegion": "Region entfernen",
|
||||
"settings_deleteRegionConfirm": "Region \"{region}\" aus der Liste entfernen?",
|
||||
"channels_regionNotSet": "Region: keine",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"channels_regionSelect_Title": "Region auswählen",
|
||||
"channels_clearRegion": "Region zurücksetzen",
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
|
||||
@@ -158,25 +158,6 @@
|
||||
"settings_radioSettings": "Radio Settings",
|
||||
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
|
||||
"settings_radioSettingsUpdated": "Radio settings updated",
|
||||
"settings_regionSettings": "Regions",
|
||||
"settings_regionSettingsSubtitle": "Manage stored regions",
|
||||
"settings_regionManagement_screenTitle": "Region Management",
|
||||
"settings_regionNameHint": "Enter region name",
|
||||
"settings_regionAddRegion": "Add region",
|
||||
"settings_regionFetchRegions": "Fetch regions from repeaters",
|
||||
"settings_regionFetchRegionsFail": "No regions were found",
|
||||
"settings_regionFetchRegionsAlreadyExists": "This region has already been added",
|
||||
"settings_regionName": "Region Name",
|
||||
"settings_regionDeleted": "Region deleted",
|
||||
"settings_deleteRegion": "Delete Region",
|
||||
"settings_deleteRegionConfirm": "Remove \"{region}\" from region list?",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_location": "Location",
|
||||
"settings_locationSubtitle": "GPS coordinates",
|
||||
"settings_locationUpdated": "Location and GPS settings updated",
|
||||
@@ -206,8 +187,6 @@
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Auto Zero-Hop Advert On GPS Update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "When GPS location changes, send a zero-hop advert (requires Advert Location).",
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
@@ -292,10 +271,6 @@
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageRu": "Русский",
|
||||
"appSettings_languageUk": "Українська",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bytes",
|
||||
"appSettings_enableMessageTracing": "Enable Message Tracing",
|
||||
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
|
||||
"appSettings_notifications": "Notifications",
|
||||
@@ -367,19 +342,6 @@
|
||||
"appSettings_last6Hours": "Last 6 hours",
|
||||
"appSettings_last24Hours": "Last 24 hours",
|
||||
"appSettings_lastWeek": "Last week",
|
||||
"appSettings_rasterTileSource": "Raster Tile Source",
|
||||
"appSettings_stadiaEndpoint": "Stadia Endpoint",
|
||||
"appSettings_stadiaApiKey": "Stadia API Key",
|
||||
"appSettings_stadiaApiKeyRequired": "Required for Stadia Maps usage",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configured: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Enter your Stadia Maps API key. This app uses it for raster tile requests.",
|
||||
"appSettings_offlineMapCache": "Offline Map Cache",
|
||||
"appSettings_unitsTitle": "Units",
|
||||
"appSettings_unitsMetric": "Metric (m / km)",
|
||||
@@ -673,18 +635,6 @@
|
||||
"channels_scanQrCodeComingSoon": "Coming soon",
|
||||
"channels_enterHashtag": "Enter hashtag",
|
||||
"channels_hashtagHint": "e.g. #team",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_regionNotSet": "Region: none",
|
||||
"channels_regionSelect_Title": "Select a region",
|
||||
"channels_clearRegion": "Clear region",
|
||||
"chat_noMessages": "No messages yet",
|
||||
"chat_sendMessage": "Send message",
|
||||
"chat_sendMessageTo": "Send a message to {contactName}",
|
||||
@@ -738,7 +688,6 @@
|
||||
}
|
||||
},
|
||||
"chat_sendGif": "Send GIF",
|
||||
"chat_receivedGif": "Received a GIF",
|
||||
"chat_reply": "Reply",
|
||||
"chat_addReaction": "Add Reaction",
|
||||
"chat_me": "Me",
|
||||
@@ -1111,56 +1060,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1478,24 +1377,7 @@
|
||||
"repeater_advancedSettings": "Advanced",
|
||||
"repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators",
|
||||
"repeater_pathHashMode": "Path hash mode",
|
||||
"repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.",
|
||||
"repeater_keySettings": "Change Identity Keys",
|
||||
"repeater_keySettingsSubtitle": "Change the public/private keypair",
|
||||
"repeater_prvKey": "Private key",
|
||||
"repeater_prvKeyHelper": "A new private key for the repeater, a 128-character hex string.",
|
||||
"repeater_generatePrvKey": "Generate a random keypair",
|
||||
"repeater_stopGeneratingPrvKey": "Interrupt search for keypair",
|
||||
"repeater_pubKey": "Public key",
|
||||
"repeater_pubKeyHelper": "This is the public key that goes with the generated private key. You can't set this directly.",
|
||||
"repeater_pubKeyPrefix": "Desired prefix",
|
||||
"repeater_pubKeyPrefixHelper": "Find a public key that starts with these hex digits. Expected tries needed: {tries}.",
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.",
|
||||
"repeater_txDelay": "Flood TX delay",
|
||||
"repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.",
|
||||
"repeater_directTxDelay": "Direct TX delay",
|
||||
@@ -1676,7 +1558,7 @@
|
||||
"repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).",
|
||||
"repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.",
|
||||
"repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.",
|
||||
"repeater_cliHelpSetPrvKey": "Replaces the device identity private key. Reboot required to apply. Generates a new public key.",
|
||||
"repeater_cliHelpSetPrvKey": "(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.",
|
||||
"repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Latitud o longitud inválidos.",
|
||||
"settings_latitude": "Latitud",
|
||||
"settings_longitude": "Longitud",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Anuncio de cero saltos automático al actualizar GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).",
|
||||
"settings_privacyMode": "Modo de privacidad",
|
||||
"settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios",
|
||||
"settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Últimas 6 horas",
|
||||
"appSettings_last24Hours": "Últimas 24 horas",
|
||||
"appSettings_lastWeek": "La semana pasada",
|
||||
"appSettings_rasterTileSource": "Fuente de teselas ráster",
|
||||
"appSettings_stadiaEndpoint": "Punto de acceso de Stadia",
|
||||
"appSettings_stadiaApiKey": "Clave API de Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obligatorio para usar Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.",
|
||||
"appSettings_offlineMapCache": "Caché de mapa sin conexión",
|
||||
"appSettings_noAreaSelected": "No se ha seleccionado ningún área",
|
||||
"appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})",
|
||||
@@ -806,56 +791,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Latitude ou longitude invalide.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Annonce zéro saut automatique lors de la mise à jour GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l'annonce).",
|
||||
"settings_privacyMode": "Mode de confidentialité",
|
||||
"settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces",
|
||||
"settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Dernières 6 heures",
|
||||
"appSettings_last24Hours": "Dernières 24 heures",
|
||||
"appSettings_lastWeek": "La semaine dernière",
|
||||
"appSettings_rasterTileSource": "Source de tuiles raster",
|
||||
"appSettings_stadiaEndpoint": "Point de terminaison Stadia",
|
||||
"appSettings_stadiaApiKey": "Clé API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Requis pour l’utilisation de Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.",
|
||||
"appSettings_offlineMapCache": "Cache de carte hors ligne",
|
||||
"appSettings_noAreaSelected": "Aucune zone sélectionnée",
|
||||
"appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})",
|
||||
@@ -806,56 +791,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -161,8 +161,6 @@
|
||||
"settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.",
|
||||
"settings_latitude": "Szélesség",
|
||||
"settings_longitude": "Hosszúság",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatikus zero-hop hirdetés GPS-frissítéskor",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).",
|
||||
"settings_contactSettings": "Kapcsolati beállítások",
|
||||
"settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.",
|
||||
"settings_privacyMode": "Adatvédelmi mód",
|
||||
@@ -334,19 +332,6 @@
|
||||
"appSettings_last6Hours": "Utolsó 6 óra",
|
||||
"appSettings_last24Hours": "Az elmúlt 24 óra",
|
||||
"appSettings_lastWeek": "Múlt héten",
|
||||
"appSettings_rasterTileSource": "Raszteres csempeforrás",
|
||||
"appSettings_stadiaEndpoint": "Stadia végpont",
|
||||
"appSettings_stadiaApiKey": "Stadia API-kulcs",
|
||||
"appSettings_stadiaApiKeyRequired": "A Stadia Maps használatához kötelező",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfigurálva: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.",
|
||||
"appSettings_offlineMapCache": "Offline térképgyorsítótár",
|
||||
"appSettings_unitsTitle": "Egységek",
|
||||
"appSettings_unitsMetric": "Metrikus (m/km)",
|
||||
@@ -967,56 +952,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -240,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Ultimi 6 ore",
|
||||
"appSettings_last24Hours": "Ultime 24 ore",
|
||||
"appSettings_lastWeek": "La settimana scorsa",
|
||||
"appSettings_rasterTileSource": "Sorgente tile raster",
|
||||
"appSettings_stadiaEndpoint": "Endpoint Stadia",
|
||||
"appSettings_stadiaApiKey": "Chiave API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obbligatoria per usare Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurata: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.",
|
||||
"appSettings_offlineMapCache": "Cache Mappa Offline",
|
||||
"appSettings_noAreaSelected": "Nessun'area selezionata",
|
||||
"appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})",
|
||||
@@ -850,56 +837,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1238,7 +1175,7 @@
|
||||
"repeater_advancedSettings": "Avanzato",
|
||||
"repeater_advancedSettingsSubtitle": "Manopole di regolazione per operatori esperti",
|
||||
"repeater_pathHashMode": "Modalità di hashing del percorso",
|
||||
"repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.",
|
||||
"repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).",
|
||||
"repeater_txDelay": "Ritardo a Flood, TX",
|
||||
"repeater_txDelayHelper": "Riassegnare lo spazio tra i pacchetti per gestire il traffico intenso, come un moltiplicatore del tempo di trasmissione (da 0 a 2, valore predefinito 0,5). Un valore più alto significa meno collisioni, ma una trasmissione più lenta.",
|
||||
"repeater_directTxDelay": "Ritardo diretto TX",
|
||||
@@ -2270,8 +2207,6 @@
|
||||
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
|
||||
"settings_advertLocation": "Posizione dell'annuncio",
|
||||
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Annuncio zero-hop automatico all'aggiornamento GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell'annuncio).",
|
||||
"settings_privacy": "Impostazioni sulla privacy",
|
||||
"settings_denyAll": "Negare tutto",
|
||||
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
|
||||
|
||||
@@ -167,8 +167,6 @@
|
||||
"settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。",
|
||||
"settings_latitude": "緯度",
|
||||
"settings_longitude": "経度",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。",
|
||||
"settings_contactSettings": "連絡先設定",
|
||||
"settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定",
|
||||
"settings_privacyMode": "プライバシーモード",
|
||||
@@ -340,19 +338,6 @@
|
||||
"appSettings_last6Hours": "過去6時間",
|
||||
"appSettings_last24Hours": "過去24時間",
|
||||
"appSettings_lastWeek": "過去1週間",
|
||||
"appSettings_rasterTileSource": "ラスタタイルのソース",
|
||||
"appSettings_stadiaEndpoint": "Stadia エンドポイント",
|
||||
"appSettings_stadiaApiKey": "Stadia API キー",
|
||||
"appSettings_stadiaApiKeyRequired": "Stadia Maps の利用に必要です",
|
||||
"appSettings_stadiaApiKeyConfigured": "設定済み: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。",
|
||||
"appSettings_offlineMapCache": "オフライン地図キャッシュ",
|
||||
"appSettings_unitsTitle": "単位",
|
||||
"appSettings_unitsMetric": "メートル法 (m / km)",
|
||||
@@ -985,56 +970,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -161,8 +161,6 @@
|
||||
"settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.",
|
||||
"settings_latitude": "위도",
|
||||
"settings_longitude": "경도",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).",
|
||||
"settings_contactSettings": "연락처 설정",
|
||||
"settings_contactSettingsSubtitle": "연락처 추가 방식 설정",
|
||||
"settings_privacyMode": "개인 정보 보호 모드",
|
||||
@@ -334,19 +332,6 @@
|
||||
"appSettings_last6Hours": "지난 6시간",
|
||||
"appSettings_last24Hours": "지난 24시간",
|
||||
"appSettings_lastWeek": "지난 주",
|
||||
"appSettings_rasterTileSource": "래스터 타일 소스",
|
||||
"appSettings_stadiaEndpoint": "Stadia 엔드포인트",
|
||||
"appSettings_stadiaApiKey": "Stadia API 키",
|
||||
"appSettings_stadiaApiKeyRequired": "Stadia Maps를 사용하려면 필요합니다",
|
||||
"appSettings_stadiaApiKeyConfigured": "설정됨: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.",
|
||||
"appSettings_offlineMapCache": "오프라인 지도 캐시",
|
||||
"appSettings_unitsTitle": "단위",
|
||||
"appSettings_unitsMetric": "미터법 (m / km)",
|
||||
@@ -967,56 +952,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1358,7 +1293,7 @@
|
||||
"repeater_advancedSettings": "고급",
|
||||
"repeater_advancedSettingsSubtitle": "숙련된 운영자를 위한 조절 노브",
|
||||
"repeater_pathHashMode": "패스 해시 모드",
|
||||
"repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.",
|
||||
"repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개의 ID, 최대 64개의 홉), 1=2 바이트 (65,000개의 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개의 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.",
|
||||
"repeater_txDelay": "플러드 TX 지연",
|
||||
"repeater_txDelayHelper": "홍수 시 교통량에 맞춰 재전송 간격을 설정합니다. 이는 패킷의 전송 시간을 곱한 값 (0-2, 기본값 0.5)으로 설정합니다. 값이 클수록 충돌이 줄어들지만 전송 속도가 느려집니다.",
|
||||
"repeater_directTxDelay": "직접적인 TX 지연",
|
||||
|
||||
@@ -784,78 +784,6 @@ abstract class AppLocalizations {
|
||||
/// **'Radio settings updated'**
|
||||
String get settings_radioSettingsUpdated;
|
||||
|
||||
/// No description provided for @settings_regionSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Regions'**
|
||||
String get settings_regionSettings;
|
||||
|
||||
/// No description provided for @settings_regionSettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manage stored regions'**
|
||||
String get settings_regionSettingsSubtitle;
|
||||
|
||||
/// No description provided for @settings_regionManagement_screenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region Management'**
|
||||
String get settings_regionManagement_screenTitle;
|
||||
|
||||
/// No description provided for @settings_regionNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter region name'**
|
||||
String get settings_regionNameHint;
|
||||
|
||||
/// No description provided for @settings_regionAddRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add region'**
|
||||
String get settings_regionAddRegion;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fetch regions from repeaters'**
|
||||
String get settings_regionFetchRegions;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegionsFail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No regions were found'**
|
||||
String get settings_regionFetchRegionsFail;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegionsAlreadyExists.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This region has already been added'**
|
||||
String get settings_regionFetchRegionsAlreadyExists;
|
||||
|
||||
/// No description provided for @settings_regionName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region Name'**
|
||||
String get settings_regionName;
|
||||
|
||||
/// No description provided for @settings_regionDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region deleted'**
|
||||
String get settings_regionDeleted;
|
||||
|
||||
/// No description provided for @settings_deleteRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Region'**
|
||||
String get settings_deleteRegion;
|
||||
|
||||
/// No description provided for @settings_deleteRegionConfirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove \"{region}\" from region list?'**
|
||||
String settings_deleteRegionConfirm(String region);
|
||||
|
||||
/// No description provided for @settings_location.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1030,18 +958,6 @@ abstract class AppLocalizations {
|
||||
/// **'Include location in advert.'**
|
||||
String get settings_advertLocationSubtitle;
|
||||
|
||||
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto Zero-Hop Advert On GPS Update'**
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate;
|
||||
|
||||
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdateSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When GPS location changes, send a zero-hop advert (requires Advert Location).'**
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle;
|
||||
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1462,30 +1378,6 @@ abstract class AppLocalizations {
|
||||
/// **'Українська'**
|
||||
String get appSettings_languageUk;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption0.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'0 - 1 byte'**
|
||||
String get repeater_pathHashModeOption0;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'1 - 2 bytes'**
|
||||
String get repeater_pathHashModeOption1;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'2 - 3 bytes'**
|
||||
String get repeater_pathHashModeOption2;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'3 - 4 bytes'**
|
||||
String get repeater_pathHashModeOption3;
|
||||
|
||||
/// No description provided for @appSettings_enableMessageTracing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1828,42 +1720,6 @@ abstract class AppLocalizations {
|
||||
/// **'Last week'**
|
||||
String get appSettings_lastWeek;
|
||||
|
||||
/// No description provided for @appSettings_rasterTileSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Raster Tile Source'**
|
||||
String get appSettings_rasterTileSource;
|
||||
|
||||
/// No description provided for @appSettings_stadiaEndpoint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stadia Endpoint'**
|
||||
String get appSettings_stadiaEndpoint;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stadia API Key'**
|
||||
String get appSettings_stadiaApiKey;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required for Stadia Maps usage'**
|
||||
String get appSettings_stadiaApiKeyRequired;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyConfigured.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Configured: {maskedKey}'**
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey);
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyDialogDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your Stadia Maps API key. This app uses it for raster tile requests.'**
|
||||
String get appSettings_stadiaApiKeyDialogDescription;
|
||||
|
||||
/// No description provided for @appSettings_offlineMapCache.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2602,30 +2458,6 @@ abstract class AppLocalizations {
|
||||
/// **'e.g. #team'**
|
||||
String get channels_hashtagHint;
|
||||
|
||||
/// No description provided for @channels_regionSetTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region: {region}'**
|
||||
String channels_regionSetTo(String region);
|
||||
|
||||
/// No description provided for @channels_regionNotSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region: none'**
|
||||
String get channels_regionNotSet;
|
||||
|
||||
/// No description provided for @channels_regionSelect_Title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a region'**
|
||||
String get channels_regionSelect_Title;
|
||||
|
||||
/// No description provided for @channels_clearRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear region'**
|
||||
String get channels_clearRegion;
|
||||
|
||||
/// No description provided for @chat_noMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2716,12 +2548,6 @@ abstract class AppLocalizations {
|
||||
/// **'Send GIF'**
|
||||
String get chat_sendGif;
|
||||
|
||||
/// No description provided for @chat_receivedGif.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Received a GIF'**
|
||||
String get chat_receivedGif;
|
||||
|
||||
/// No description provided for @chat_reply.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3874,54 +3700,6 @@ abstract class AppLocalizations {
|
||||
/// **'Failed downloads: {count}'**
|
||||
String mapCache_failedDownloads(int count);
|
||||
|
||||
/// No description provided for @mapCache_cachedTilesLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tiles'**
|
||||
String get mapCache_cachedTilesLabel;
|
||||
|
||||
/// No description provided for @mapCache_cachedTileSummaryLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tile summary'**
|
||||
String get mapCache_cachedTileSummaryLabel;
|
||||
|
||||
/// No description provided for @mapCache_bulkDownloadDisabledForSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Offline bulk downloads are disabled for {source}.'**
|
||||
String mapCache_bulkDownloadDisabledForSource(String source);
|
||||
|
||||
/// No description provided for @mapCache_bulkDownloadDisabledInConfig.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Offline bulk downloads are disabled for {source} in this app configuration.'**
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source);
|
||||
|
||||
/// No description provided for @mapCache_summarySource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Source: {source}'**
|
||||
String mapCache_summarySource(String source);
|
||||
|
||||
/// No description provided for @mapCache_summaryCachedTilesForSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tiles for source: {count}'**
|
||||
String mapCache_summaryCachedTilesForSource(int count);
|
||||
|
||||
/// No description provided for @mapCache_summaryCachedInSelection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached in selected area/zoom: {count}'**
|
||||
String mapCache_summaryCachedInSelection(int count);
|
||||
|
||||
/// No description provided for @mapCache_summaryApproxCacheSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Approx cache size: {size}'**
|
||||
String mapCache_summaryApproxCacheSize(String size);
|
||||
|
||||
/// No description provided for @mapCache_boundsLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -4871,69 +4649,9 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @repeater_pathHashModeHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.'**
|
||||
/// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.'**
|
||||
String get repeater_pathHashModeHelper;
|
||||
|
||||
/// No description provided for @repeater_keySettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change Identity Keys'**
|
||||
String get repeater_keySettings;
|
||||
|
||||
/// No description provided for @repeater_keySettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change the public/private keypair'**
|
||||
String get repeater_keySettingsSubtitle;
|
||||
|
||||
/// No description provided for @repeater_prvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Private key'**
|
||||
String get repeater_prvKey;
|
||||
|
||||
/// No description provided for @repeater_prvKeyHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A new private key for the repeater, a 128-character hex string.'**
|
||||
String get repeater_prvKeyHelper;
|
||||
|
||||
/// No description provided for @repeater_generatePrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Generate a random keypair'**
|
||||
String get repeater_generatePrvKey;
|
||||
|
||||
/// No description provided for @repeater_stopGeneratingPrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Interrupt search for keypair'**
|
||||
String get repeater_stopGeneratingPrvKey;
|
||||
|
||||
/// No description provided for @repeater_pubKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Public key'**
|
||||
String get repeater_pubKey;
|
||||
|
||||
/// No description provided for @repeater_pubKeyHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is the public key that goes with the generated private key. You can\'t set this directly.'**
|
||||
String get repeater_pubKeyHelper;
|
||||
|
||||
/// No description provided for @repeater_pubKeyPrefix.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Desired prefix'**
|
||||
String get repeater_pubKeyPrefix;
|
||||
|
||||
/// No description provided for @repeater_pubKeyPrefixHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Find a public key that starts with these hex digits. Expected tries needed: {tries}.'**
|
||||
String repeater_pubKeyPrefixHelper(int tries);
|
||||
|
||||
/// No description provided for @repeater_txDelay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5705,7 +5423,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @repeater_cliHelpSetPrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'**
|
||||
/// **'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'**
|
||||
String get repeater_cliHelpSetPrvKey;
|
||||
|
||||
/// No description provided for @repeater_cliHelpSetRadioRxGain.
|
||||
|
||||
@@ -367,45 +367,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Радио настройките са актуализирани';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Местоположение';
|
||||
|
||||
@@ -506,14 +467,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Автоматична zero-hop обява при GPS обновяване';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Множество ACK';
|
||||
|
||||
@@ -741,18 +694,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Украински';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Разрешаване на проследяване на съобщения';
|
||||
@@ -959,28 +900,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Миналата седмица';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Източник на растерни плочки';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Крайна точка на Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'API ключ за Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Задължително за използване на Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Конфигурирано: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||
|
||||
@@ -1411,20 +1330,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'напр. #отбор';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Няма съобщения.';
|
||||
|
||||
@@ -1480,9 +1385,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Изпрати GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Отговори';
|
||||
|
||||
@@ -2135,42 +2037,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Неуспешни изтегляния: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2720,41 +2586,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Забавяне на Flood TX';
|
||||
|
||||
|
||||
@@ -369,46 +369,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regionen';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Gespeicherte Regionen verwalten';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regions-Verwaltung';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Regions-Namen eingeben';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Region hinzufügen';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Regions-Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region entfernt';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Region entfernen';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Region \"$region\" aus der Liste entfernen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Ort';
|
||||
|
||||
@@ -506,14 +466,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Standort in die Ankündigung einschließen.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatische Zero-Hop-Ankündigung bei GPS-Update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Mehrfach-ACKs';
|
||||
|
||||
@@ -738,18 +690,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainisch';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Nachrichtenverfolgung aktivieren';
|
||||
@@ -956,28 +896,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Letzte Woche';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Quelle für Rasterkacheln';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-Endpunkt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia-API-Schlüssel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Für die Nutzung von Stadia Maps erforderlich';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfiguriert: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
|
||||
|
||||
@@ -1411,20 +1329,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'z.B. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: keine';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Region auswählen';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Region zurücksetzen';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||
|
||||
@@ -1481,9 +1385,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF senden';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Antworten';
|
||||
|
||||
@@ -2135,42 +2036,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Fehlgeschlagene Downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2716,41 +2581,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Flood-TX-Verzögerung';
|
||||
|
||||
|
||||
@@ -363,45 +363,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio settings updated';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Location';
|
||||
|
||||
@@ -496,14 +457,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Auto Zero-Hop Advert On GPS Update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'When GPS location changes, send a zero-hop advert (requires Advert Location).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@@ -726,18 +679,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||
|
||||
@@ -939,28 +880,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Last week';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Raster Tile Source';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia Endpoint';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API Key';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Required for Stadia Maps usage';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configured: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Enter your Stadia Maps API key. This app uses it for raster tile requests.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
||||
|
||||
@@ -1381,20 +1300,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'e.g. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'No messages yet';
|
||||
|
||||
@@ -1450,9 +1355,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Send GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reply';
|
||||
|
||||
@@ -2096,42 +1998,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Failed downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2668,42 +2534,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Flood TX delay';
|
||||
@@ -3175,7 +3006,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetPrvKey =>
|
||||
'Replaces the device identity private key. Reboot required to apply. Generates a new public key.';
|
||||
'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetRadioRxGain =>
|
||||
|
||||
@@ -367,45 +367,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Ubicación';
|
||||
|
||||
@@ -503,14 +464,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Anuncio de cero saltos automático al actualizar GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Múltiples respuestas de confirmación';
|
||||
|
||||
@@ -737,18 +690,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Habilitar seguimiento de mensajes';
|
||||
@@ -953,28 +894,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La semana pasada';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Fuente de teselas ráster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Punto de acceso de Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Clave API de Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obligatorio para usar Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurado: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Caché de mapa sin conexión';
|
||||
|
||||
@@ -1408,20 +1327,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ej. #equipo';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aún no hay mensajes';
|
||||
|
||||
@@ -1477,9 +1382,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
@@ -2129,42 +2031,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Descargas fallidas: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2715,41 +2581,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Retraso TX por inundación';
|
||||
|
||||
|
||||
@@ -369,45 +369,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Emplacement';
|
||||
|
||||
@@ -507,14 +468,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Inclure la localisation dans l\'annonce';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Annonce zéro saut automatique lors de la mise à jour GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l\'annonce).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Plusieurs accusés de réception';
|
||||
|
||||
@@ -741,18 +694,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainien';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Activer le traçage des messages';
|
||||
@@ -957,28 +898,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La semaine dernière';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Source de tuiles raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Point de terminaison Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Clé API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Requis pour l’utilisation de Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configuré : $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de carte hors ligne';
|
||||
|
||||
@@ -1410,20 +1329,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #equipe';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aucun message pour le moment';
|
||||
|
||||
@@ -1479,9 +1384,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Envoyer un GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Répondre';
|
||||
|
||||
@@ -2136,42 +2038,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Téléchargements échoués : $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2726,41 +2592,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Délai de transmission en flood';
|
||||
|
||||
|
||||
@@ -365,45 +365,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'hely';
|
||||
|
||||
@@ -502,14 +463,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'A hirdetésben szerepeljen a helyszín.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatikus zero-hop hirdetés GPS-frissítéskor';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACK';
|
||||
|
||||
@@ -736,18 +689,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Üzenetkövetés engedélyezése';
|
||||
|
||||
@@ -949,28 +890,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Múlt héten';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Raszteres csempeforrás';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia végpont';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-kulcs';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'A Stadia Maps használatához kötelező';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfigurálva: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline térképgyorsítótár';
|
||||
|
||||
@@ -1403,20 +1322,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'például #csapat';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Még nincsenek üzenetek';
|
||||
|
||||
@@ -1472,9 +1377,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF küldése';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Válasz';
|
||||
|
||||
@@ -2127,42 +2029,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
return 'Sikertelen letöltések: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2709,41 +2575,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Áradási TX késleltetés';
|
||||
|
||||
|
||||
@@ -369,45 +369,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Posizione';
|
||||
|
||||
@@ -507,14 +468,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Annuncio zero-hop automatico all\'aggiornamento GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell\'annuncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'ACK multipli';
|
||||
|
||||
@@ -739,18 +692,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraino';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Abilita tracciamento messaggi';
|
||||
@@ -956,28 +897,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La settimana scorsa';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Sorgente tile raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Endpoint Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Chiave API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obbligatoria per usare Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurata: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||
|
||||
@@ -1409,20 +1328,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'es. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||
|
||||
@@ -1478,9 +1383,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Invia GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Rispondi';
|
||||
|
||||
@@ -2131,42 +2033,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Download falliti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2715,42 +2581,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Ritardo a Flood, TX';
|
||||
|
||||
@@ -353,45 +353,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '場所';
|
||||
|
||||
@@ -482,13 +443,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS更新時にゼロホップ広告を自動送信';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'マルチ ACK';
|
||||
|
||||
@@ -709,18 +663,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'ウクライナ語';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'メッセージ追跡を有効にする';
|
||||
|
||||
@@ -903,27 +845,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '過去1週間';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'ラスタタイルのソース';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia エンドポイント';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API キー';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => 'Stadia Maps の利用に必要です';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '設定済み: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'オフライン地図キャッシュ';
|
||||
|
||||
@@ -1338,20 +1259,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '例:#チーム';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'まだメッセージは届いていません';
|
||||
|
||||
@@ -1407,9 +1314,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF を送信';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => '返信';
|
||||
|
||||
@@ -2042,42 +1946,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '失敗したダウンロード: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2598,41 +2466,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'フロイド・TXでの遅延';
|
||||
|
||||
|
||||
@@ -353,45 +353,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '위치';
|
||||
|
||||
@@ -483,14 +444,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'GPS 업데이트 시 제로 홉 광고 자동 전송';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => '다중 ACK';
|
||||
|
||||
@@ -710,18 +663,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => '우크라이나어';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화';
|
||||
|
||||
@@ -906,27 +847,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '지난 주';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => '래스터 타일 소스';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia 엔드포인트';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API 키';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => 'Stadia Maps를 사용하려면 필요합니다';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '설정됨: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => '오프라인 지도 캐시';
|
||||
|
||||
@@ -1341,20 +1261,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '예: #팀';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => '아직 메시지가 없습니다.';
|
||||
|
||||
@@ -1410,9 +1316,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF 보내기';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => '답변';
|
||||
|
||||
@@ -2046,42 +1949,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '실패한 다운로드: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2602,42 +2469,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개의 ID, 최대 64개의 홉), 1=2 바이트 (65,000개의 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개의 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => '플러드 TX 지연';
|
||||
|
||||
@@ -365,45 +365,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Locatie';
|
||||
|
||||
@@ -501,14 +462,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatische zero-hop-advertentie bij GPS-update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Meerdere bevestigingen';
|
||||
|
||||
@@ -733,18 +686,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||
|
||||
@@ -948,28 +889,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Afgelopen week';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Rastertegelbron';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-eindpunt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-sleutel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Vereist voor gebruik van Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Geconfigureerd: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaartcache';
|
||||
|
||||
@@ -1397,20 +1316,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'bijv. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nog geen berichten.';
|
||||
|
||||
@@ -1466,9 +1371,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF verzenden';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reageren';
|
||||
|
||||
@@ -2119,42 +2021,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Mislukte downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2696,42 +2562,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden – alleen na het activeren van het netwerk.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Vertraging bij Flood TX';
|
||||
|
||||
@@ -371,45 +371,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Ustawienia radia zostały zaktualizowane';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokalizacja';
|
||||
|
||||
@@ -508,14 +469,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatyczne ogłoszenie zero-hop przy aktualizacji GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
|
||||
|
||||
@@ -742,18 +695,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukraińska';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
|
||||
|
||||
@@ -957,28 +898,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Ostatni tydzień';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Źródło kafelków rastrowych';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Punkt końcowy Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Klucz API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Wymagane do korzystania ze Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Skonfigurowano: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Pamięć podręczna map offline';
|
||||
|
||||
@@ -1419,20 +1338,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'np. #zespół';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
||||
|
||||
@@ -1489,9 +1394,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Wyślij GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpowiedz';
|
||||
|
||||
@@ -2149,42 +2051,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Nieudane pobrania: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2729,42 +2595,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bajty używane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych – wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Opóźnienie w Flood, TX';
|
||||
|
||||
@@ -369,45 +369,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Configurações de rádio atualizadas';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Localização';
|
||||
|
||||
@@ -505,14 +466,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Anúncio zero-hop automático na atualização do GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@@ -739,18 +692,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Ativar rastreamento de mensagens';
|
||||
@@ -954,28 +895,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Da última semana';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Fonte de blocos raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Endpoint da Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Chave da API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obrigatório para usar o Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurado: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
||||
|
||||
@@ -1407,20 +1326,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #equipe';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||
|
||||
@@ -1476,9 +1381,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
@@ -2132,42 +2034,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Downloads falhas: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2714,42 +2580,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Atraso na entrega em Flood, TX';
|
||||
|
||||
@@ -368,45 +368,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Настройки радио обновлены';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Позиция';
|
||||
|
||||
@@ -505,14 +466,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Авто-объявление без хопов при обновлении GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Несколько подтверждений';
|
||||
|
||||
@@ -739,18 +692,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Включить трассировку сообщений';
|
||||
@@ -957,28 +898,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Последнюю неделю';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Источник растровых тайлов';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Конечная точка Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ключ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Требуется для использования Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Настроено: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
||||
|
||||
@@ -1408,20 +1327,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'например, #команда';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Сообщений пока нет';
|
||||
|
||||
@@ -1477,9 +1382,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Отправить GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Ответить';
|
||||
|
||||
@@ -2136,42 +2038,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Неудачных загрузок: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2719,42 +2585,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Задержка в работе системы Flood TX';
|
||||
|
||||
@@ -367,45 +367,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokalita';
|
||||
|
||||
@@ -499,14 +460,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatický zero-hop inzerát pri aktualizácii GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Viaceré ACK';
|
||||
|
||||
@@ -733,18 +686,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinská';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
|
||||
|
||||
@@ -944,28 +885,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Minul týždeň';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Zdroj rastrových dlaždíc';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Koncový bod Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Kľúč API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Vyžaduje sa na používanie Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Nakonfigurované: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
||||
|
||||
@@ -1397,20 +1316,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'napr. #tím';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||
|
||||
@@ -1466,9 +1371,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Odoslať GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpovedať';
|
||||
|
||||
@@ -2122,42 +2024,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Neúspešné stiahnutia: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2700,42 +2566,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Zpoždanie v Flood, TX';
|
||||
|
||||
@@ -366,45 +366,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokacija';
|
||||
|
||||
@@ -500,14 +461,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Samodejni zero-hop oglas ob posodobitvi GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Več potrdil';
|
||||
|
||||
@@ -731,18 +684,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinsko';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
|
||||
|
||||
@@ -945,28 +886,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Prejšnji teden';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Vir rastrskih ploščic';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Končna točka Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ključ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obvezno za uporabo Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Nastavljeno: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
||||
|
||||
@@ -1395,20 +1314,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'npr. #ekipa';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Še ni sporočil.';
|
||||
|
||||
@@ -1465,9 +1370,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Pošlji GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odgovori';
|
||||
|
||||
@@ -2119,42 +2021,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Poslovniški izniki: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2696,42 +2562,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu';
|
||||
|
||||
@@ -365,45 +365,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Radioinställningarna har uppdaterats';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Plats';
|
||||
|
||||
@@ -497,14 +458,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatisk zero-hop-annons vid GPS-uppdatering';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Flera bekräftelser';
|
||||
|
||||
@@ -728,18 +681,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainska';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
|
||||
|
||||
@@ -938,28 +879,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Förra veckan';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Källa för rasterplattor';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-slutpunkt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-nyckel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Krävs för att använda Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfigurerad: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
||||
|
||||
@@ -1387,20 +1306,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 't.ex. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||
|
||||
@@ -1458,9 +1363,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Skicka GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Svara';
|
||||
|
||||
@@ -2107,42 +2009,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Misslyckade nedladdningar: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2684,42 +2550,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Försening i Flood TX';
|
||||
|
||||
@@ -367,45 +367,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Геопозиція';
|
||||
|
||||
@@ -501,14 +462,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включити геопозицію в оголошення';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Автооголошення без хопів при оновленні GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Багато підтверджень';
|
||||
|
||||
@@ -736,18 +689,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Увімкнути відстеження повідомлень';
|
||||
@@ -951,28 +892,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Минулий тиждень';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Джерело растрових тайлів';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Кінцева точка Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ключ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Потрібно для використання Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Налаштовано: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
||||
|
||||
@@ -1401,20 +1320,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'напр. #команда';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Поки немає повідомлень.';
|
||||
|
||||
@@ -1471,9 +1376,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Надіслати GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Відповісти';
|
||||
|
||||
@@ -2129,42 +2031,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'Невдалі завантаження: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2716,42 +2582,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Затримка у Flood, штат Техас';
|
||||
|
||||
@@ -350,45 +350,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => '无线电设置已更新';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '位置';
|
||||
|
||||
@@ -476,13 +437,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS 更新时自动发送零跳广告';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => '多重ACK';
|
||||
|
||||
@@ -701,18 +655,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => '乌克兰语';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => '启用消息追踪';
|
||||
|
||||
@@ -890,27 +832,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '上周';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => '栅格瓦片源';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia 端点';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API 密钥';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => '使用 Stadia Maps 时必需';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '已配置:$maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => '离线地图缓存';
|
||||
|
||||
@@ -1324,20 +1245,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '例如:#团队';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => '暂无消息';
|
||||
|
||||
@@ -1393,9 +1300,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => '发送 GIF';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => '回复';
|
||||
|
||||
@@ -2021,42 +1925,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '下载失败:$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2571,42 +2439,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 跳),1=2 字节(65K 个 ID,最多 32 跳),2=3 字节(16M 个 ID,最多 21 跳)。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
'用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => '洪水(德克萨斯州)延误';
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
|
||||
"settings_latitude": "Breedtegraad",
|
||||
"settings_longitude": "Lengtegraad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische zero-hop-advertentie bij GPS-update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).",
|
||||
"settings_privacyMode": "Privacy-modus",
|
||||
"settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||
"settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||
"appSettings_lastWeek": "Afgelopen week",
|
||||
"appSettings_rasterTileSource": "Rastertegelbron",
|
||||
"appSettings_stadiaEndpoint": "Stadia-eindpunt",
|
||||
"appSettings_stadiaApiKey": "Stadia API-sleutel",
|
||||
"appSettings_stadiaApiKeyRequired": "Vereist voor gebruik van Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Geconfigureerd: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.",
|
||||
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1172,7 +1107,7 @@
|
||||
"repeater_advancedSettings": "Geavanceerd",
|
||||
"repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers",
|
||||
"repeater_pathHashMode": "Hash-modus voor paden",
|
||||
"repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.",
|
||||
"repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden – alleen na het activeren van het netwerk.",
|
||||
"repeater_txDelay": "Vertraging bij Flood TX",
|
||||
"repeater_txDelayHelper": "Herzendinterval voor verkeer tijdens overstromingen, als een veelvoud van de tijd die het pakket nodig heeft (0-2, standaard 0.5). Een hoger getal betekent minder botsingen, maar ook een langere leveringstijd.",
|
||||
"repeater_directTxDelay": "Directe vertraging",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
|
||||
"settings_latitude": "Szerokość",
|
||||
"settings_longitude": "Długość",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatyczne ogłoszenie zero-hop przy aktualizacji GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).",
|
||||
"settings_privacyMode": "Tryb prywatności",
|
||||
"settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
|
||||
"settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Ostatnie 6 godzin",
|
||||
"appSettings_last24Hours": "Ostatnie 24 godziny",
|
||||
"appSettings_lastWeek": "Ostatni tydzień",
|
||||
"appSettings_rasterTileSource": "Źródło kafelków rastrowych",
|
||||
"appSettings_stadiaEndpoint": "Punkt końcowy Stadia",
|
||||
"appSettings_stadiaApiKey": "Klucz API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Wymagane do korzystania ze Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Skonfigurowano: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.",
|
||||
"appSettings_offlineMapCache": "Pamięć podręczna map offline",
|
||||
"appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.",
|
||||
"appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})",
|
||||
@@ -794,56 +779,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1182,7 +1117,7 @@
|
||||
"repeater_advancedSettings": "Zaawansowany",
|
||||
"repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów",
|
||||
"repeater_pathHashMode": "Tryb haszujący ścieżkę",
|
||||
"repeater_pathHashModeHelper": "Bajty używane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.",
|
||||
"repeater_pathHashModeHelper": "Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych – wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.",
|
||||
"repeater_txDelay": "Opóźnienie w Flood, TX",
|
||||
"repeater_txDelayHelper": "Ustawienie odstępu dla ruchu związanego z powodzią, jako mnożnik czasu przesyłania pakietu (0-2, domyślnie 0,5). Wyższe wartości oznaczają mniejszą liczbę kolizji, ale wolniejszą prędkość przesyłania.",
|
||||
"repeater_directTxDelay": "Bezpośrednie opóźnienie sygnału TX",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Latitude ou longitude inválidos.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Anúncio zero-hop automático na atualização do GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).",
|
||||
"settings_privacyMode": "Modo de Privacidade",
|
||||
"settings_privacyModeSubtitle": "Esconder nome/localização em anúncios",
|
||||
"settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Últimos 6 horas",
|
||||
"appSettings_last24Hours": "Últimas 24 horas",
|
||||
"appSettings_lastWeek": "Da última semana",
|
||||
"appSettings_rasterTileSource": "Fonte de blocos raster",
|
||||
"appSettings_stadiaEndpoint": "Endpoint da Stadia",
|
||||
"appSettings_stadiaApiKey": "Chave da API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obrigatório para usar o Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.",
|
||||
"appSettings_offlineMapCache": "Cache de Mapa Offline",
|
||||
"appSettings_noAreaSelected": "Nenhuma área selecionada",
|
||||
"appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1172,7 +1107,7 @@
|
||||
"repeater_advancedSettings": "Avançado",
|
||||
"repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes",
|
||||
"repeater_pathHashMode": "Modo de hash de caminho",
|
||||
"repeater_pathHashModeHelper": "Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.",
|
||||
"repeater_pathHashModeHelper": "Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).",
|
||||
"repeater_txDelay": "Atraso na entrega em Flood, TX",
|
||||
"repeater_txDelayHelper": "Ajuste de espaçamento para tráfego de inundações, como um multiplicador do tempo de transmissão (0-2, padrão 0,5). Quanto maior, menos colisões, mas uma entrega mais lenta.",
|
||||
"repeater_directTxDelay": "Atraso direto no sinal TX",
|
||||
|
||||
@@ -77,8 +77,6 @@
|
||||
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Долгота",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).",
|
||||
"settings_privacyMode": "Режим конфиденциальности",
|
||||
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
|
||||
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
|
||||
@@ -192,19 +190,6 @@
|
||||
"appSettings_last6Hours": "Последние 6 часов",
|
||||
"appSettings_last24Hours": "Последние 24 часа",
|
||||
"appSettings_lastWeek": "Последнюю неделю",
|
||||
"appSettings_rasterTileSource": "Источник растровых тайлов",
|
||||
"appSettings_stadiaEndpoint": "Конечная точка Stadia",
|
||||
"appSettings_stadiaApiKey": "Ключ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Требуется для использования Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Настроено: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.",
|
||||
"appSettings_offlineMapCache": "Кэш офлайн-карты",
|
||||
"appSettings_noAreaSelected": "Область не выбрана",
|
||||
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})",
|
||||
@@ -453,56 +438,6 @@
|
||||
"mapCache_downloadTilesButton": "Загрузить плитки",
|
||||
"mapCache_clearCacheButton": "Очистить кэш",
|
||||
"mapCache_failedDownloads": "Неудачных загрузок: {count}",
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
|
||||
"time_justNow": "Только что",
|
||||
"time_minutesAgo": "{minutes} мин назад",
|
||||
@@ -1543,7 +1478,7 @@
|
||||
"repeater_advancedSettings": "Продвинутый",
|
||||
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
|
||||
"repeater_pathHashMode": "Режим хеширования пути",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.",
|
||||
"repeater_txDelay": "Задержка в работе системы Flood TX",
|
||||
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
|
||||
"repeater_directTxDelay": "Прямая задержка сигнала TX",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Neplatná šírka alebo dĺžka.",
|
||||
"settings_latitude": "Súradnica",
|
||||
"settings_longitude": "Dĺžka",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatický zero-hop inzerát pri aktualizácii GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).",
|
||||
"settings_privacyMode": "Režim ochrany súkromia",
|
||||
"settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách",
|
||||
"settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Posledné 6 hodín",
|
||||
"appSettings_last24Hours": "Posledných 24 hodín",
|
||||
"appSettings_lastWeek": "Minul týždeň",
|
||||
"appSettings_rasterTileSource": "Zdroj rastrových dlaždíc",
|
||||
"appSettings_stadiaEndpoint": "Koncový bod Stadia",
|
||||
"appSettings_stadiaApiKey": "Kľúč API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Vyžaduje sa na používanie Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Nakonfigurované: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.",
|
||||
"appSettings_offlineMapCache": "Offline Mapa Pamäť",
|
||||
"appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť",
|
||||
"appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1172,7 +1107,7 @@
|
||||
"repeater_advancedSettings": "Pokročilé",
|
||||
"repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov",
|
||||
"repeater_pathHashMode": "Režim hashovania cesty",
|
||||
"repeater_pathHashModeHelper": "Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.",
|
||||
"repeater_pathHashModeHelper": "Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.",
|
||||
"repeater_txDelay": "Zpoždanie v Flood, TX",
|
||||
"repeater_txDelayHelper": "Nastavenie pre opakované vysielanie pre dopravu počas povodní, ako násobok času, ktorý paket využije (0-2, výchoce hodnota 0,5). Vyššia hodnota znamená menej kolízii, ale pomalšie doručovanie.",
|
||||
"repeater_directTxDelay": "Priame oneskorenie TX",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
|
||||
"settings_latitude": "Širina",
|
||||
"settings_longitude": "Dolžina",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Samodejni zero-hop oglas ob posodobitvi GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).",
|
||||
"settings_privacyMode": "Zasebnost",
|
||||
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
|
||||
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "Zadnjih 6 ur",
|
||||
"appSettings_last24Hours": "Zadnjih 24 ur",
|
||||
"appSettings_lastWeek": "Prejšnji teden",
|
||||
"appSettings_rasterTileSource": "Vir rastrskih ploščic",
|
||||
"appSettings_stadiaEndpoint": "Končna točka Stadia",
|
||||
"appSettings_stadiaApiKey": "Ključ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obvezno za uporabo Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Nastavljeno: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.",
|
||||
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
|
||||
"appSettings_noAreaSelected": "Območje ni izbrano",
|
||||
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1172,7 +1107,7 @@
|
||||
"repeater_advancedSettings": "Napredno",
|
||||
"repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike",
|
||||
"repeater_pathHashMode": "Način ustvarjanja hash-a poti",
|
||||
"repeater_pathHashModeHelper": "Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.",
|
||||
"repeater_pathHashModeHelper": "Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.",
|
||||
"repeater_txDelay": "Zatemnitevanje zaradi poplav v Texasu",
|
||||
"repeater_txDelayHelper": "Uporaba intervalov za ponovno pošiljanje v primeru prometa zaradi poplav, kot pomnožnik časovne trajanje paketa (0-2, privzeto 0,5). Veje vrednost = manjše kolizije, vendar počasnejše dostavo.",
|
||||
"repeater_directTxDelay": "Neposredni časovno odlašanje",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"settings_locationInvalid": "Ogiltig latitud eller longitud.",
|
||||
"settings_latitude": "Latitud",
|
||||
"settings_longitude": "Längdgrad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatisk zero-hop-annons vid GPS-uppdatering",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).",
|
||||
"settings_privacyMode": "Privatläge",
|
||||
"settings_privacyModeSubtitle": "Dölj namn/plats i annonser",
|
||||
"settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.",
|
||||
@@ -242,19 +240,6 @@
|
||||
"appSettings_last6Hours": "De senaste 6 timmarna",
|
||||
"appSettings_last24Hours": "De senaste 24 timmarna",
|
||||
"appSettings_lastWeek": "Förra veckan",
|
||||
"appSettings_rasterTileSource": "Källa för rasterplattor",
|
||||
"appSettings_stadiaEndpoint": "Stadia-slutpunkt",
|
||||
"appSettings_stadiaApiKey": "Stadia API-nyckel",
|
||||
"appSettings_stadiaApiKeyRequired": "Krävs för att använda Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfigurerad: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.",
|
||||
"appSettings_offlineMapCache": "Offline Kartcache",
|
||||
"appSettings_noAreaSelected": "Ingen area markerad",
|
||||
"appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})",
|
||||
@@ -784,56 +769,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1172,7 +1107,7 @@
|
||||
"repeater_advancedSettings": "Avancerad",
|
||||
"repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare",
|
||||
"repeater_pathHashMode": "Hash-läge för sökväg",
|
||||
"repeater_pathHashModeHelper": "Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.",
|
||||
"repeater_pathHashModeHelper": "Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).",
|
||||
"repeater_txDelay": "Försening i Flood TX",
|
||||
"repeater_txDelayHelper": "Återöverföringsintervall för trafik under perioder med hög belastning, som en multiplikator av paketets överföringstid (0-2, standard 0,5). Högre värde = färre kollisioner, men långsammare leverans.",
|
||||
"repeater_directTxDelay": "Direkt TX-fördröjning",
|
||||
|
||||
@@ -101,8 +101,6 @@
|
||||
"settings_locationInvalid": "Некоректна широта або довгота.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Довгота",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).",
|
||||
"settings_privacyMode": "Режим приватності",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
|
||||
@@ -244,19 +242,6 @@
|
||||
"appSettings_last6Hours": "Останні 6 годин",
|
||||
"appSettings_last24Hours": "Останні 24 години",
|
||||
"appSettings_lastWeek": "Минулий тиждень",
|
||||
"appSettings_rasterTileSource": "Джерело растрових тайлів",
|
||||
"appSettings_stadiaEndpoint": "Кінцева точка Stadia",
|
||||
"appSettings_stadiaApiKey": "Ключ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Потрібно для використання Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Налаштовано: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.",
|
||||
"appSettings_offlineMapCache": "Офлайн-кеш карти",
|
||||
"appSettings_noAreaSelected": "Область не вибрано",
|
||||
"appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})",
|
||||
@@ -794,56 +779,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1182,7 +1117,7 @@
|
||||
"repeater_advancedSettings": "Просунутий",
|
||||
"repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів",
|
||||
"repeater_pathHashMode": "Режим хешування шляху",
|
||||
"repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.",
|
||||
"repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.",
|
||||
"repeater_txDelay": "Затримка у Flood, штат Техас",
|
||||
"repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.",
|
||||
"repeater_directTxDelay": "Пряме затримка TX",
|
||||
|
||||
@@ -105,8 +105,6 @@
|
||||
"settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。",
|
||||
"settings_latitude": "纬度",
|
||||
"settings_longitude": "经度",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。",
|
||||
"settings_privacyMode": "隐私模式",
|
||||
"settings_privacyModeSubtitle": "在广告中隐藏姓名/位置",
|
||||
"settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。",
|
||||
@@ -256,19 +254,6 @@
|
||||
"appSettings_last6Hours": "过去6小时",
|
||||
"appSettings_last24Hours": "过去24小时",
|
||||
"appSettings_lastWeek": "上周",
|
||||
"appSettings_rasterTileSource": "栅格瓦片源",
|
||||
"appSettings_stadiaEndpoint": "Stadia 端点",
|
||||
"appSettings_stadiaApiKey": "Stadia API 密钥",
|
||||
"appSettings_stadiaApiKeyRequired": "使用 Stadia Maps 时必需",
|
||||
"appSettings_stadiaApiKeyConfigured": "已配置:{maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。",
|
||||
"appSettings_offlineMapCache": "离线地图缓存",
|
||||
"appSettings_noAreaSelected": "未选择任何区域",
|
||||
"appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})",
|
||||
@@ -811,56 +796,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1202,7 +1137,7 @@
|
||||
"repeater_advancedSettings": "高级",
|
||||
"repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮",
|
||||
"repeater_pathHashMode": "路径哈希模式",
|
||||
"repeater_pathHashModeHelper": "用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 跳),1=2 字节(65K 个 ID,最多 32 跳),2=3 字节(16M 个 ID,最多 21 跳)。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。",
|
||||
"repeater_pathHashModeHelper": "用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。",
|
||||
"repeater_txDelay": "洪水(德克萨斯州)延误",
|
||||
"repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。",
|
||||
"repeater_directTxDelay": "直接的 TX 延迟",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'app_localizations.dart';
|
||||
|
||||
@@ -24,30 +23,14 @@ extension ContactLocalization on Contact {
|
||||
}
|
||||
}
|
||||
|
||||
String pathLabel(
|
||||
AppLocalizations l10n, {
|
||||
int pathHashByteWidth = pathHashSize,
|
||||
}) {
|
||||
String pathLabel(AppLocalizations l10n) {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return l10n.chat_floodForced;
|
||||
if (pathOverride == 0) return l10n.chat_directForced;
|
||||
return l10n.chat_hopsForced(
|
||||
_displayHopCount(pathOverrideBytes, pathOverride!, pathHashByteWidth),
|
||||
);
|
||||
return l10n.chat_hopsForced(pathOverride!);
|
||||
}
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.chat_direct;
|
||||
return l10n.chat_hopsCount(
|
||||
_displayHopCount(path, pathLength, pathHashByteWidth),
|
||||
);
|
||||
}
|
||||
|
||||
int _displayHopCount(
|
||||
List<int>? pathBytes,
|
||||
int storedHopCount,
|
||||
int hashWidth,
|
||||
) {
|
||||
if (pathBytes == null || pathBytes.isEmpty) return storedHopCount;
|
||||
return PathHelper.splitPathBytes(pathBytes, hashWidth).length;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,7 @@ void main() async {
|
||||
final bleDebugLogService = BleDebugLogService();
|
||||
final appDebugLogService = AppDebugLogService();
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService(
|
||||
appSettingsService: appSettingsService,
|
||||
);
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final translationService = TranslationService(appSettingsService);
|
||||
final uiViewStateService = UiViewStateService();
|
||||
@@ -185,7 +183,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: translationService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
ChangeNotifierProvider.value(value: mapTileCacheService),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||
],
|
||||
child: Consumer<AppSettingsService>(
|
||||
|
||||
@@ -76,8 +76,6 @@ class Cyr2LatProfile {
|
||||
|
||||
class AppSettings {
|
||||
static const Object _unset = Object();
|
||||
static const String stadiaDemo =
|
||||
'51bd0381-4685-4666-bae8-48940f6d77c0';
|
||||
|
||||
final bool clearPathOnMaxRetry;
|
||||
final bool mapShowRepeaters;
|
||||
@@ -93,15 +91,10 @@ class AppSettings {
|
||||
final Map<String, double>? mapCacheBounds;
|
||||
final int mapCacheMinZoom;
|
||||
final int mapCacheMaxZoom;
|
||||
final String mapRasterSourceId;
|
||||
final String mapTileEndpointId;
|
||||
final String? mapTileApiKey;
|
||||
final bool notificationsEnabled;
|
||||
final bool notifyOnNewMessage;
|
||||
final bool notifyOnNewChannelMessage;
|
||||
final bool notifyOnNewAdvert;
|
||||
final bool autoSendZeroHopAdvertOnGpsUpdate;
|
||||
final int gpsIntervalSeconds;
|
||||
final bool autoRouteRotationEnabled;
|
||||
final double maxRouteWeight;
|
||||
final double initialRouteWeight;
|
||||
@@ -129,17 +122,6 @@ class AppSettings {
|
||||
final List<Cyr2LatProfile> cyr2latProfiles;
|
||||
final String selectedCyr2latProfileId;
|
||||
|
||||
String get effectiveMapTileApiKey {
|
||||
final apiKey = mapTileApiKey?.trim();
|
||||
if (apiKey == null || apiKey.isEmpty) {
|
||||
return stadiaDemo;
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
bool get usesstadiaDemo =>
|
||||
effectiveMapTileApiKey == stadiaDemo;
|
||||
|
||||
Map<String, String> get cyr2latCharMap {
|
||||
final profile = cyr2latProfiles.firstWhere(
|
||||
(p) => p.id == selectedCyr2latProfileId,
|
||||
@@ -163,15 +145,10 @@ class AppSettings {
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
this.mapCacheMaxZoom = 15,
|
||||
this.mapRasterSourceId = 'osm_auto',
|
||||
this.mapTileEndpointId = 'standard_2x',
|
||||
this.mapTileApiKey,
|
||||
this.notificationsEnabled = true,
|
||||
this.notifyOnNewMessage = true,
|
||||
this.notifyOnNewChannelMessage = true,
|
||||
this.notifyOnNewAdvert = true,
|
||||
this.autoSendZeroHopAdvertOnGpsUpdate = false,
|
||||
this.gpsIntervalSeconds = 900,
|
||||
this.autoRouteRotationEnabled = true,
|
||||
this.maxRouteWeight = 5.0,
|
||||
this.initialRouteWeight = 3.0,
|
||||
@@ -229,16 +206,10 @@ class AppSettings {
|
||||
'map_cache_bounds': mapCacheBounds,
|
||||
'map_cache_min_zoom': mapCacheMinZoom,
|
||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||
'map_raster_source_id': mapRasterSourceId,
|
||||
'map_tile_endpoint_id': mapTileEndpointId,
|
||||
'map_tile_api_key': mapTileApiKey,
|
||||
'notifications_enabled': notificationsEnabled,
|
||||
'notify_on_new_message': notifyOnNewMessage,
|
||||
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
||||
'notify_on_new_advert': notifyOnNewAdvert,
|
||||
'auto_send_zero_hop_advert_on_gps_update':
|
||||
autoSendZeroHopAdvertOnGpsUpdate,
|
||||
'gps_interval_seconds': gpsIntervalSeconds,
|
||||
'auto_route_rotation_enabled': autoRouteRotationEnabled,
|
||||
'max_route_weight': maxRouteWeight,
|
||||
'initial_route_weight': initialRouteWeight,
|
||||
@@ -299,18 +270,11 @@ class AppSettings {
|
||||
),
|
||||
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
||||
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
||||
mapRasterSourceId: json['map_raster_source_id'] as String? ?? 'osm_auto',
|
||||
mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard',
|
||||
mapTileApiKey: json['map_tile_api_key'] as String?,
|
||||
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
||||
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
|
||||
notifyOnNewChannelMessage:
|
||||
json['notify_on_new_channel_message'] as bool? ?? true,
|
||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||
autoSendZeroHopAdvertOnGpsUpdate:
|
||||
json['auto_send_zero_hop_advert_on_gps_update'] as bool? ?? false,
|
||||
gpsIntervalSeconds:
|
||||
(json['gps_interval_seconds'] as num?)?.toInt() ?? 900,
|
||||
autoRouteRotationEnabled:
|
||||
json['auto_route_rotation_enabled'] as bool? ?? true,
|
||||
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
|
||||
@@ -415,15 +379,10 @@ class AppSettings {
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
int? mapCacheMaxZoom,
|
||||
String? mapRasterSourceId,
|
||||
String? mapTileEndpointId,
|
||||
Object? mapTileApiKey = _unset,
|
||||
bool? notificationsEnabled,
|
||||
bool? notifyOnNewMessage,
|
||||
bool? notifyOnNewChannelMessage,
|
||||
bool? notifyOnNewAdvert,
|
||||
bool? autoSendZeroHopAdvertOnGpsUpdate,
|
||||
int? gpsIntervalSeconds,
|
||||
bool? autoRouteRotationEnabled,
|
||||
double? maxRouteWeight,
|
||||
double? initialRouteWeight,
|
||||
@@ -469,20 +428,11 @@ class AppSettings {
|
||||
: mapCacheBounds as Map<String, double>?,
|
||||
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
||||
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
||||
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
|
||||
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
|
||||
mapTileApiKey: mapTileApiKey == _unset
|
||||
? this.mapTileApiKey
|
||||
: mapTileApiKey as String?,
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
|
||||
notifyOnNewChannelMessage:
|
||||
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
|
||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||
autoSendZeroHopAdvertOnGpsUpdate:
|
||||
autoSendZeroHopAdvertOnGpsUpdate ??
|
||||
this.autoSendZeroHopAdvertOnGpsUpdate,
|
||||
gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds,
|
||||
autoRouteRotationEnabled:
|
||||
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
|
||||
|
||||
@@ -27,10 +27,6 @@ class Channel {
|
||||
|
||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||
|
||||
bool get isHashtagChannel => name.startsWith('#');
|
||||
|
||||
bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel;
|
||||
|
||||
static Channel? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_INFO format:
|
||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||
|
||||
@@ -41,7 +41,6 @@ class ChannelMessage {
|
||||
final List<Repeat> repeats;
|
||||
final int repeatCount;
|
||||
final int? pathLength;
|
||||
final int? pathHashWidth;
|
||||
final Uint8List pathBytes;
|
||||
final List<Uint8List> pathVariants;
|
||||
final int? channelIndex;
|
||||
@@ -67,7 +66,6 @@ class ChannelMessage {
|
||||
this.repeats = const [],
|
||||
this.repeatCount = 0,
|
||||
this.pathLength,
|
||||
this.pathHashWidth,
|
||||
Uint8List? pathBytes,
|
||||
List<Uint8List>? pathVariants,
|
||||
this.channelIndex,
|
||||
@@ -95,7 +93,6 @@ class ChannelMessage {
|
||||
List<Repeat>? repeats,
|
||||
int? repeatCount,
|
||||
int? pathLength,
|
||||
int? pathHashWidth,
|
||||
Uint8List? pathBytes,
|
||||
List<Uint8List>? pathVariants,
|
||||
String? packetHash,
|
||||
@@ -132,7 +129,6 @@ class ChannelMessage {
|
||||
repeats: repeats ?? this.repeats,
|
||||
repeatCount: repeatCount ?? this.repeatCount,
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
pathHashWidth: pathHashWidth ?? this.pathHashWidth,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
pathVariants: pathVariants ?? this.pathVariants,
|
||||
channelIndex: channelIndex,
|
||||
@@ -159,7 +155,6 @@ class ChannelMessage {
|
||||
|
||||
int pathLen;
|
||||
int txtType;
|
||||
int? packetPathHashWidth;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
@@ -168,18 +163,12 @@ class ChannelMessage {
|
||||
final hasPath = (flags & 0x01) != 0;
|
||||
reader.skipBytes(1); // Skip reserved byte
|
||||
channelIdx = reader.readByte();
|
||||
final pathByte = reader.readUInt8();
|
||||
// pathByte packs: top 2 bits = hash width mode, low 6 bits = hop count
|
||||
packetPathHashWidth = ((pathByte & 0xC0) >> 6) + 1;
|
||||
final hopCount = pathByte & 0x3F;
|
||||
pathLen = hopCount;
|
||||
// If a path is present, read hopCount * width bytes
|
||||
if (hasPath && hopCount > 0) {
|
||||
final totalPathBytes = hopCount * packetPathHashWidth;
|
||||
pathBytes = reader.readBytes(totalPathBytes);
|
||||
}
|
||||
// After consuming optional path bytes, read the text type byte.
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
if (hasPath && pathLen > 0) {
|
||||
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||
pathBytes = reader.readBytes(pathLen);
|
||||
}
|
||||
} else {
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
@@ -220,7 +209,6 @@ class ChannelMessage {
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathHashWidth: packetPathHashWidth,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:meshcore_open/utils/keys.dart';
|
||||
|
||||
import 'channel.dart';
|
||||
|
||||
@@ -37,7 +37,11 @@ class Community {
|
||||
|
||||
/// Generate a new community with a random 32-byte secret
|
||||
factory Community.create({required String id, required String name}) {
|
||||
final secret = randomBytes(32);
|
||||
final random = Random.secure();
|
||||
final secret = Uint8List(32);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
secret[i] = random.nextInt(256);
|
||||
}
|
||||
return Community(
|
||||
id: id,
|
||||
name: name,
|
||||
@@ -206,7 +210,12 @@ class Community {
|
||||
|
||||
/// Create a copy of this community with a regenerated random secret
|
||||
Community withRegeneratedSecret() {
|
||||
return withNewSecret(randomBytes(32));
|
||||
final random = Random.secure();
|
||||
final newSecret = Uint8List(32);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
newSecret[i] = random.nextInt(256);
|
||||
}
|
||||
return withNewSecret(newSecret);
|
||||
}
|
||||
|
||||
/// Extract secret from QR data (for updating existing community)
|
||||
|
||||
@@ -119,7 +119,7 @@ class Contact {
|
||||
String pathFormattedIdList(int hashByteWidth) {
|
||||
final pathBytes = pathBytesForDisplay;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final w = hashByteWidth.clamp(1, 4);
|
||||
final w = hashByteWidth.clamp(1, 8);
|
||||
final parts = <String>[];
|
||||
for (int i = 0; i < pathBytes.length; i += w) {
|
||||
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
|
||||
@@ -166,18 +166,8 @@ class Contact {
|
||||
final type = reader.readByte();
|
||||
final flags = reader.readByte();
|
||||
final pathLen = reader.readByte();
|
||||
int hopCount = 0;
|
||||
int byteLen = 0;
|
||||
if (pathLen == 0xFF) {
|
||||
hopCount = -1;
|
||||
} else {
|
||||
final mode = (pathLen & 0xC0) >> 6;
|
||||
hopCount = pathLen & 0x3F;
|
||||
final width = mode + 1;
|
||||
byteLen = hopCount * width;
|
||||
}
|
||||
final safePathLen = byteLen > 0
|
||||
? (byteLen > maxPathSize ? maxPathSize : byteLen)
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||
final name = reader.readCStringGreedy(maxNameSize);
|
||||
@@ -223,7 +213,7 @@ class Contact {
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
flags: flags,
|
||||
pathLength: hopCount,
|
||||
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -14,8 +13,8 @@ class DisplayPath {
|
||||
final Color color;
|
||||
final bool isPrimary;
|
||||
|
||||
/// Outbound hop prefixes, including hops that could not be placed on the map.
|
||||
final List<Uint8List> hopBytes;
|
||||
/// Outbound hop bytes, including hops that could not be placed on the map.
|
||||
final List<int> hopBytes;
|
||||
|
||||
/// Resolved map points: self, each locatable hop, then the target when its
|
||||
/// position is known. Hops with no position are skipped here but still
|
||||
|
||||
@@ -81,16 +81,6 @@ class RadioSettings {
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia (Mid)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 915.075,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia SA, WA, QLD',
|
||||
RadioSettings(
|
||||
|
||||
@@ -8,7 +8,6 @@ import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
@@ -700,9 +699,13 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final children = <Widget>[
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
secondary: const Icon(Icons.router_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showRepeaters),
|
||||
subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle),
|
||||
@@ -711,7 +714,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
secondary: const Icon(Icons.chat_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showChatNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle),
|
||||
@@ -720,7 +726,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
secondary: const Icon(Icons.people_outline, size: 20),
|
||||
title: Text(context.l10n.appSettings_showOtherNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle),
|
||||
@@ -781,7 +790,11 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.straighten, size: 20, color: scheme.onSurfaceVariant),
|
||||
Icon(
|
||||
Icons.straighten,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -795,7 +808,8 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.unitSystem == UnitSystem.imperial
|
||||
settingsService.settings.unitSystem ==
|
||||
UnitSystem.imperial
|
||||
? context.l10n.appSettings_unitsImperial
|
||||
: context.l10n.appSettings_unitsMetric,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
@@ -866,342 +880,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showMapRasterSourceDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_rasterTileSource,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_mapRasterSourceSummary(settingsService.settings),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
if (_isStadiaSource(settingsService.settings)) {
|
||||
children.addAll([
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showMapRasterEndpointDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.public_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_stadiaEndpoint,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_mapRasterEndpointSummary(settingsService.settings),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showMapApiKeyDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.key_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_stadiaApiKey,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_mapApiKeySummary(context, settingsService.settings),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return Column(children: children);
|
||||
}
|
||||
|
||||
String _mapRasterSourceSummary(AppSettings settings) {
|
||||
final source = MapRasterSourceCatalog.fromSettings(settings);
|
||||
return '${source.label} - ${source.description}';
|
||||
}
|
||||
|
||||
bool _isStadiaSource(AppSettings settings) {
|
||||
return MapRasterSourceCatalog.fromSettings(settings).isStadia;
|
||||
}
|
||||
|
||||
String _mapRasterEndpointSummary(AppSettings settings) {
|
||||
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
|
||||
return '${endpoint.label} - ${endpoint.description}';
|
||||
}
|
||||
|
||||
String _mapApiKeySummary(BuildContext context, AppSettings settings) {
|
||||
return context.l10n.appSettings_stadiaApiKeyConfigured(
|
||||
_maskApiKey(settings.effectiveMapTileApiKey),
|
||||
);
|
||||
}
|
||||
|
||||
String _maskApiKey(String value) {
|
||||
if (value.length <= 8) return '********';
|
||||
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
|
||||
}
|
||||
|
||||
void _showMapRasterSourceDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
String selectedId = settingsService.settings.mapRasterSourceId;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_rasterTileSource),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(dialogContext).size.height * 0.6,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: RadioGroup<String>(
|
||||
groupValue: selectedId,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
selectedId = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final preset in MapRasterSourcePreset.values)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final option = MapRasterSourceCatalog.fromPreset(
|
||||
preset,
|
||||
);
|
||||
return RadioListTile<String>(
|
||||
value: preset.id,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.description),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await settingsService.setMapRasterSourceId(selectedId);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMapRasterEndpointDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
String selectedId = settingsService.settings.mapTileEndpointId;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_stadiaEndpoint),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: RadioGroup<String>(
|
||||
groupValue: selectedId,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
selectedId = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final option in MapRasterEndpointCatalog.presets)
|
||||
RadioListTile<String>(
|
||||
value: option.id,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.description),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await settingsService.setMapTileEndpointId(selectedId);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMapApiKeyDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
final currentApiKey = settingsService.settings.mapTileApiKey?.trim() ?? '';
|
||||
final maskedApiKey = _maskApiKey(
|
||||
currentApiKey.isEmpty
|
||||
? AppSettings.stadiaDemo
|
||||
: currentApiKey,
|
||||
);
|
||||
final controller = TextEditingController(text: maskedApiKey);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_stadiaApiKey),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.appSettings_stadiaApiKeyDialogDescription),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '4e1bf343-3d91-4d9c-a8e1-1234567890ab',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final apiKey = controller.text.trim();
|
||||
await settingsService.setMapTileApiKey(
|
||||
apiKey == maskedApiKey ? currentApiKey : apiKey,
|
||||
);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,6 @@ import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
int _decodeRawPathByteLen(int pathLenRaw) {
|
||||
if (pathLenRaw == 0xFF || pathLenRaw == 0) return 0;
|
||||
final hashCount = pathLenRaw & 0x3F;
|
||||
final hashWidth = ((pathLenRaw >> 6) & 0x03) + 1;
|
||||
return hashCount * hashWidth;
|
||||
}
|
||||
|
||||
class BleDebugLogScreen extends StatefulWidget {
|
||||
const BleDebugLogScreen({super.key});
|
||||
|
||||
@@ -117,7 +110,8 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final entry = entries[index];
|
||||
final time =
|
||||
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
||||
Future<void> copyHex() async {
|
||||
return GestureDetector(
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: entry.payload
|
||||
@@ -129,20 +123,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
.join(''),
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.debugLog_bleCopied,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: copyHex,
|
||||
onLongPress: copyHex,
|
||||
onSecondaryTap: copyHex,
|
||||
},
|
||||
child: Container(
|
||||
color: MeshPalette.bg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -321,17 +302,16 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
rawHex: _bytesToHex(raw),
|
||||
);
|
||||
}
|
||||
final pathLenRaw = raw[index++];
|
||||
final pathByteLen = _decodeRawPathByteLen(pathLenRaw);
|
||||
if (raw.length < index + pathByteLen) {
|
||||
final pathLen = raw[index++];
|
||||
if (raw.length < index + pathLen) {
|
||||
return _RawPacketInfo(
|
||||
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
|
||||
summary: 'Truncated path',
|
||||
rawHex: _bytesToHex(raw),
|
||||
);
|
||||
}
|
||||
final pathBytes = raw.sublist(index, index + pathByteLen);
|
||||
index += pathByteLen;
|
||||
final pathBytes = raw.sublist(index, index + pathLen);
|
||||
index += pathLen;
|
||||
if (raw.length <= index) {
|
||||
return _RawPacketInfo(
|
||||
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
|
||||
@@ -344,7 +324,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final title =
|
||||
'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
||||
final summary = _decodePayloadSummary(payloadType, payload);
|
||||
final pathSummary = pathByteLen > 0
|
||||
final pathSummary = pathLen > 0
|
||||
? 'Path=${_bytesToHex(pathBytes)}'
|
||||
: 'Path=none';
|
||||
final detail = '$summary • $pathSummary • len=${raw.length}';
|
||||
|
||||
@@ -16,7 +16,6 @@ import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/cyr2lat.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
@@ -43,8 +42,6 @@ import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import 'region_management_screen.dart';
|
||||
import '../storage/region_store.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@@ -276,10 +273,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => openRegionSelectDialog(widget.channel),
|
||||
child: Row(
|
||||
title: Row(
|
||||
children: [
|
||||
_channelIcon(widget.channel),
|
||||
const SizedBox(width: 8),
|
||||
@@ -300,20 +294,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final unreadCount = connector
|
||||
.getUnreadCountForChannelIndex(
|
||||
widget.channel.index,
|
||||
);
|
||||
.getUnreadCountForChannelIndex(widget.channel.index);
|
||||
final privacy = widget.channel.isPublicChannel
|
||||
? context.l10n.channels_public
|
||||
: context.l10n.channels_private;
|
||||
final region = connector.getChannelRegion(
|
||||
widget.channel.index,
|
||||
);
|
||||
final regionText = region.isNotEmpty
|
||||
? ' • ${context.l10n.channels_regionSetTo(region)}'
|
||||
: '';
|
||||
return Text(
|
||||
'$privacy • ${context.l10n.chat_unread(unreadCount)}$regionText',
|
||||
'$privacy • ${context.l10n.chat_unread(unreadCount)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
);
|
||||
@@ -324,15 +310,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.channels_regionSelect_Title,
|
||||
icon: const Icon(Icons.landscape),
|
||||
onPressed: () => openRegionSelectDialog(widget.channel),
|
||||
),
|
||||
const RadioStatsIconButton(),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
@@ -531,14 +511,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: (message.pathVariants.isNotEmpty
|
||||
? message.pathVariants.first
|
||||
: Uint8List(0));
|
||||
final displayPathHashWidth =
|
||||
message.pathHashWidth ??
|
||||
context.read<MeshCoreConnector>().pathHashByteWidth;
|
||||
final displayHopCount = _displayHopCount(
|
||||
displayPath,
|
||||
displayPathHashWidth,
|
||||
message.pathLength,
|
||||
);
|
||||
|
||||
// Bubble colors — outgoing uses MeshPalette.me / meBorder / meInk.
|
||||
final bubbleColor = isOutgoing
|
||||
@@ -578,10 +550,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
mainAxisAlignment: isOutgoing
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
_buildAvatar(message.senderName, textScale),
|
||||
_buildAvatar(message.senderName),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Flexible(
|
||||
@@ -619,10 +591,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
message.senderName,
|
||||
style: TextStyle(
|
||||
fontSize: 13 * textScale,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: textColor,
|
||||
color: _colorForName(message.senderName),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -674,9 +646,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
fontStyle: FontStyle.italic,
|
||||
color: textColor.withValues(alpha: 0.72),
|
||||
),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? () => _showMessageActions(message)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -689,27 +658,23 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RouteChip(
|
||||
isDirect: (message.pathLength ?? -1) >= 0,
|
||||
hops: displayHopCount,
|
||||
hops: (message.pathLength ?? -1) >= 0
|
||||
? message.pathLength
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(
|
||||
displayPath,
|
||||
displayPathHashWidth,
|
||||
),
|
||||
_formatPathPrefixes(displayPath),
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 9.5 * textScale,
|
||||
color: metaColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1047,11 +1012,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(String senderName, double textScale) {
|
||||
return AvatarCircle(
|
||||
name: senderName,
|
||||
size: (32 * textScale).clamp(28.0, 56.0),
|
||||
);
|
||||
Widget _buildAvatar(String senderName) {
|
||||
return AvatarCircle(name: senderName, size: 32);
|
||||
}
|
||||
|
||||
Widget _buildReplyBanner(double textScale) {
|
||||
@@ -1215,7 +1177,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.md,
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.outlineVariant,
|
||||
@@ -1223,7 +1185,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.md,
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.outlineVariant,
|
||||
@@ -1231,7 +1193,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.md,
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.primary,
|
||||
@@ -1588,133 +1550,27 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes, int pathHashByteWidth) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
int? _displayHopCount(
|
||||
Uint8List pathBytes,
|
||||
int pathHashByteWidth,
|
||||
int? fallbackPathLength,
|
||||
) {
|
||||
if ((fallbackPathLength ?? -1) < 0) return null;
|
||||
if (pathBytes.isEmpty) return fallbackPathLength;
|
||||
return PathHelper.splitPathBytes(pathBytes, pathHashByteWidth).length;
|
||||
/// Deterministic name-to-hue mapping consistent with [AvatarCircle].
|
||||
Color _colorForName(String name) {
|
||||
const hues = [
|
||||
MeshPalette.blue,
|
||||
MeshPalette.magenta,
|
||||
MeshPalette.signal,
|
||||
MeshPalette.warn,
|
||||
Color(0xFF8FA8F0),
|
||||
Color(0xFF6FD9CE),
|
||||
];
|
||||
var h = 0;
|
||||
for (final c in name.codeUnits) {
|
||||
h = (h * 31 + c) & 0x7fffffff;
|
||||
}
|
||||
|
||||
Future<void> openRegionSelectDialog(Channel channel) async {
|
||||
// The AppBar subtitle reads the region from the connector inside a
|
||||
// Consumer, so setChannelRegion's notifyListeners refreshes it directly —
|
||||
// no post-dialog setState needed.
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _RegionSelectDialog(channel: channel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegionSelectDialog extends StatefulWidget {
|
||||
final Channel channel;
|
||||
|
||||
const _RegionSelectDialog({required this.channel});
|
||||
|
||||
@override
|
||||
State<_RegionSelectDialog> createState() => _RegionSelectDialogState();
|
||||
}
|
||||
|
||||
class _RegionSelectDialogState extends State<_RegionSelectDialog> {
|
||||
final RegionStore regionStore = RegionStore();
|
||||
|
||||
List<Region> regions = [];
|
||||
int selectedIndex = -1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadRegions();
|
||||
}
|
||||
|
||||
void loadRegions() {
|
||||
setState(() {
|
||||
regions = regionStore.loadRegions();
|
||||
final channelRegion = context.read<MeshCoreConnector>().getChannelRegion(
|
||||
widget.channel.index,
|
||||
);
|
||||
selectedIndex = regions.indexOf(channelRegion);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
title: Text(context.l10n.channels_regionSelect_Title),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.channels_clearRegion,
|
||||
icon: const Icon(Icons.backspace_outlined),
|
||||
onPressed: () {
|
||||
context.read<MeshCoreConnector>().setChannelRegion(
|
||||
widget.channel.index,
|
||||
'',
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.settings_regionSettingsSubtitle,
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () async {
|
||||
await pushRegionManagementScreen(context);
|
||||
if (!mounted) return;
|
||||
loadRegions();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: regions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final selected = selectedIndex == index;
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
Icons.landscape,
|
||||
color: selected ? MeshPalette.blue : null,
|
||||
),
|
||||
title: Text(regions[index]),
|
||||
trailing: selected
|
||||
? const Icon(Icons.check, color: MeshPalette.blue)
|
||||
: null,
|
||||
tileColor: selected ? MeshPalette.blueBg : null,
|
||||
onTap: () {
|
||||
// Tapping the already-selected region clears it.
|
||||
context.read<MeshCoreConnector>().setChannelRegion(
|
||||
widget.channel.index,
|
||||
selected ? '' : regions[index],
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return hues[h % hues.length];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -23,6 +22,7 @@ import '../theme/mesh_theme.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/path_map_ui.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final ChannelMessage message;
|
||||
@@ -43,40 +43,19 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
message.pathVariants,
|
||||
);
|
||||
|
||||
final hashByteWidth =
|
||||
(message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final primaryPath = _orientPathBytes(
|
||||
primaryPathTmp,
|
||||
hashByteWidth,
|
||||
reverse: !channelMessage && !message.isOutgoing,
|
||||
);
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
final hops = _buildPathHops(
|
||||
primaryPath,
|
||||
connector,
|
||||
l10n,
|
||||
hashByteWidth,
|
||||
resolveFromEnd: !message.isOutgoing,
|
||||
);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
|
||||
// Convert observed path byte length to hop count using the packet width.
|
||||
// Legacy messages fall back to the current connector width.
|
||||
// Reported path length (V3+) is already stored as a hop count; preserve
|
||||
// the negative flood sentinel when present.
|
||||
final observedHopCount = _hopCountFromBytes(
|
||||
primaryPath.length,
|
||||
hashByteWidth,
|
||||
);
|
||||
final reportedHopCount = message.pathLength;
|
||||
final effectiveHopCount = observedHopCount > 0
|
||||
? observedHopCount
|
||||
: reportedHopCount;
|
||||
|
||||
final observedLabel = _formatObservedHops(
|
||||
observedHopCount,
|
||||
effectiveHopCount,
|
||||
primaryPath.length,
|
||||
message.pathLength,
|
||||
l10n,
|
||||
);
|
||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||
@@ -94,8 +73,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: primaryPath,
|
||||
flipPathAround: true,
|
||||
reversePathAround: false,
|
||||
pathHashByteWidth: hashByteWidth,
|
||||
reversePathAround:
|
||||
!(!channelMessage && !message.isOutgoing),
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -116,20 +98,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
observedLabel: observedLabel,
|
||||
effectiveHopCount: effectiveHopCount,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSummaryCard(context, observedLabel: observedLabel),
|
||||
if (extraPaths.isNotEmpty) ...[
|
||||
SectionHeader(
|
||||
l10n.channelPath_otherObservedPaths,
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPathVariants(context, extraPaths, hashByteWidth),
|
||||
const SizedBox(height: 16),
|
||||
_buildPathVariants(context, extraPaths),
|
||||
],
|
||||
SectionHeader(
|
||||
l10n.channelPath_repeaterHops,
|
||||
@@ -148,18 +123,14 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(
|
||||
BuildContext context, {
|
||||
String? observedLabel,
|
||||
required int? effectiveHopCount,
|
||||
}) {
|
||||
Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final routeChip = effectiveHopCount == null
|
||||
final routeChip = message.pathLength == null
|
||||
? null
|
||||
: effectiveHopCount < 0
|
||||
: message.pathLength! < 0
|
||||
? const RouteChip(isDirect: false)
|
||||
: RouteChip(isDirect: true, hops: effectiveHopCount);
|
||||
: RouteChip(isDirect: true, hops: message.pathLength);
|
||||
|
||||
return MeshCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
@@ -201,7 +172,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.channelPath_pathLabelTitle,
|
||||
_formatPathLabel(effectiveHopCount, l10n),
|
||||
_formatPathLabel(message.pathLength, l10n),
|
||||
scheme: scheme,
|
||||
),
|
||||
if (observedLabel != null)
|
||||
@@ -216,11 +187,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPathVariants(
|
||||
BuildContext context,
|
||||
List<Uint8List> variants,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
Widget _buildPathVariants(BuildContext context, List<Uint8List> variants) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -243,11 +210,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
Text(
|
||||
l10n.channelPath_observedPathTitle(
|
||||
i + 1,
|
||||
_formatHopCount(
|
||||
variants[i].length,
|
||||
hashByteWidth,
|
||||
l10n,
|
||||
),
|
||||
_formatHopCount(variants[i].length, l10n),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -255,7 +218,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatPathPrefixes(variants[i], hashByteWidth),
|
||||
_formatPathPrefixes(variants[i]),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@@ -439,31 +402,31 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathLabel(int? hopCount, AppLocalizations l10n) {
|
||||
if (hopCount == null) return l10n.channelPath_unknownPath;
|
||||
if (hopCount < 0) return l10n.channelPath_floodPath;
|
||||
if (hopCount == 0) return l10n.channelPath_directPath;
|
||||
return l10n.chat_hopsCount(hopCount);
|
||||
String _formatPathLabel(int? pathLength, AppLocalizations l10n) {
|
||||
if (pathLength == null) return l10n.channelPath_unknownPath;
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.channelPath_directPath;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
}
|
||||
|
||||
String? _formatObservedHops(
|
||||
int observedCount,
|
||||
int? targetHopCount,
|
||||
int? pathLength,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
if (observedCount <= 0 && (targetHopCount == null || targetHopCount <= 0)) {
|
||||
if (observedCount <= 0 && (pathLength == null || pathLength <= 0)) {
|
||||
return null;
|
||||
}
|
||||
if (targetHopCount == null || targetHopCount < 0) {
|
||||
if (pathLength == null || pathLength < 0) {
|
||||
return observedCount > 0 ? l10n.chat_hopsCount(observedCount) : null;
|
||||
}
|
||||
if (observedCount == 0) {
|
||||
return l10n.channelPath_observedZeroOf(targetHopCount);
|
||||
return l10n.channelPath_observedZeroOf(pathLength);
|
||||
}
|
||||
if (observedCount == targetHopCount) {
|
||||
if (observedCount == pathLength) {
|
||||
return l10n.chat_hopsCount(observedCount);
|
||||
}
|
||||
return l10n.channelPath_observedSomeOf(observedCount, targetHopCount);
|
||||
return l10n.channelPath_observedSomeOf(observedCount, pathLength);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(
|
||||
@@ -660,10 +623,7 @@ class _ChannelMessagePathMapScreenState
|
||||
? kPrimaryPathColor
|
||||
: kAlternatePathColors[(index - 1) % kAlternatePathColors.length],
|
||||
isPrimary: isPrimary,
|
||||
hopBytes: [
|
||||
for (final hop in hops)
|
||||
if (hop.hopBytes != null) Uint8List.fromList(hop.hopBytes!),
|
||||
],
|
||||
hopBytes: List<int>.from(orientedBytes),
|
||||
points: points,
|
||||
pointLabels: labels,
|
||||
pointConfirmed: confirmed,
|
||||
@@ -848,11 +808,7 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
);
|
||||
|
||||
final width =
|
||||
(widget.message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final selectedPath = _orientPath(selectedPathTmp, width);
|
||||
final selectedPath = _orientPath(selectedPathTmp);
|
||||
|
||||
// Match on the unoriented bytes — observedPaths stores them as
|
||||
// recorded, while selectedPath may be reversed for display.
|
||||
@@ -861,21 +817,19 @@ class _ChannelMessagePathMapScreenState
|
||||
selectedPath,
|
||||
connector,
|
||||
context.l10n,
|
||||
width,
|
||||
resolveFromEnd: !widget.message.isOutgoing,
|
||||
);
|
||||
|
||||
// Renderable paths for the animation and combined view.
|
||||
final entries = <_ObservedPathEntry>[];
|
||||
for (var i = 0; i < observedPaths.length; i++) {
|
||||
final oriented = _orientPath(observedPaths[i].pathBytes, width);
|
||||
final oriented = _orientPath(observedPaths[i].pathBytes);
|
||||
final pathHops = i == selectedIndex
|
||||
? hops
|
||||
: _buildPathHops(
|
||||
oriented,
|
||||
connector,
|
||||
context.l10n,
|
||||
width,
|
||||
resolveFromEnd: !widget.message.isOutgoing,
|
||||
);
|
||||
final display = _buildDisplayPath(
|
||||
@@ -954,7 +908,7 @@ class _ChannelMessagePathMapScreenState
|
||||
? LatLngBounds.fromPoints(points)
|
||||
: null;
|
||||
final mapKey = ValueKey(
|
||||
'${_formatPathPrefixes(selectedPath, width)},${context.l10n.pathTrace_you}',
|
||||
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}',
|
||||
);
|
||||
_pathDistance = _getPathDistance(points);
|
||||
|
||||
@@ -1013,7 +967,7 @@ class _ChannelMessagePathMapScreenState
|
||||
},
|
||||
),
|
||||
children: [
|
||||
tileCache.buildTileLayer(context),
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
AnimatedBuilder(
|
||||
animation: _playback,
|
||||
builder: (context, _) {
|
||||
@@ -1091,7 +1045,6 @@ class _ChannelMessagePathMapScreenState
|
||||
context,
|
||||
observedPaths,
|
||||
selectedIndex,
|
||||
width,
|
||||
(index) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
@@ -1133,12 +1086,10 @@ class _ChannelMessagePathMapScreenState
|
||||
BuildContext context,
|
||||
List<_ObservedPath> paths,
|
||||
int selectedIndex,
|
||||
int hashByteWidth,
|
||||
ValueChanged<int> onSelected, {
|
||||
double topOffset = 16,
|
||||
}) {
|
||||
final l10n = context.l10n;
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final selectedPath = paths[selectedIndex];
|
||||
final label = selectedPath.isPrimary
|
||||
? l10n.channelPath_primaryPath(selectedIndex + 1)
|
||||
@@ -1169,7 +1120,7 @@ class _ChannelMessagePathMapScreenState
|
||||
value: i,
|
||||
child: Text(
|
||||
'${paths[i].isPrimary ? l10n.channelPath_primaryPath(i + 1) : l10n.channelPath_pathLabel(i + 1)}'
|
||||
' • ${_formatHopCount(paths[i].pathBytes.length, width, l10n)}',
|
||||
' • ${_formatHopCount(paths[i].pathBytes.length, l10n)}',
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1183,7 +1134,7 @@ class _ChannelMessagePathMapScreenState
|
||||
Text(
|
||||
l10n.channelPath_selectedPathLabel(
|
||||
label,
|
||||
_formatPathPrefixes(selectedPath.pathBytes, width),
|
||||
_formatPathPrefixes(selectedPath.pathBytes),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@@ -1427,11 +1378,11 @@ class _ChannelMessagePathMapScreenState
|
||||
}
|
||||
|
||||
/// Orients recorded path bytes in the direction the packet traveled.
|
||||
Uint8List _orientPath(Uint8List bytes, int hashByteWidth) {
|
||||
Uint8List _orientPath(Uint8List bytes) {
|
||||
final reverse =
|
||||
(!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(widget.message.isOutgoing && widget.channelMessage);
|
||||
return _orientPathBytes(bytes, hashByteWidth, reverse: reverse);
|
||||
return reverse ? Uint8List.fromList(bytes.reversed.toList()) : bytes;
|
||||
}
|
||||
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
@@ -1787,7 +1738,6 @@ class _PathHop {
|
||||
final Contact? contact;
|
||||
final LatLng? position;
|
||||
final AppLocalizations l10n;
|
||||
final Uint8List? hopBytes;
|
||||
|
||||
const _PathHop({
|
||||
required this.index,
|
||||
@@ -1795,18 +1745,12 @@ class _PathHop {
|
||||
required this.contact,
|
||||
required this.position,
|
||||
required this.l10n,
|
||||
this.hopBytes,
|
||||
});
|
||||
|
||||
bool get hasLocation => position != null;
|
||||
|
||||
String get displayLabel {
|
||||
final prefixLabel = hopBytes != null && hopBytes!.isNotEmpty
|
||||
? hopBytes!
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
: _formatPrefix(prefix);
|
||||
final prefixLabel = _formatPrefix(prefix);
|
||||
return '($prefixLabel) ${_resolveName(contact, l10n)}';
|
||||
}
|
||||
}
|
||||
@@ -1821,12 +1765,10 @@ class _ObservedPath {
|
||||
List<_PathHop> _buildPathHops(
|
||||
Uint8List pathBytes,
|
||||
MeshCoreConnector connector,
|
||||
AppLocalizations l10n,
|
||||
int hashByteWidth, {
|
||||
AppLocalizations l10n, {
|
||||
bool resolveFromEnd = false,
|
||||
}) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final endpoint =
|
||||
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||
@@ -1836,23 +1778,19 @@ List<_PathHop> _buildPathHops(
|
||||
contacts: connector.allContacts,
|
||||
endpoint: endpoint,
|
||||
resolveFromEnd: resolveFromEnd,
|
||||
pathHashByteWidth: width,
|
||||
);
|
||||
|
||||
final hopChunks = PathHelper.splitPathBytes(pathBytes, width);
|
||||
final hops = <_PathHop>[];
|
||||
for (var i = 0; i < hopChunks.length; i++) {
|
||||
final hopBytes = hopChunks[i];
|
||||
final contact = i < resolvedContacts.length ? resolvedContacts[i] : null;
|
||||
for (var i = 0; i < pathBytes.length; i++) {
|
||||
final contact = resolvedContacts[i];
|
||||
final resolvedPosition = _resolvePosition(contact);
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
prefix: hopBytes.isNotEmpty ? hopBytes[0] : 0,
|
||||
prefix: pathBytes[i],
|
||||
contact: contact,
|
||||
position: resolvedPosition,
|
||||
l10n: l10n,
|
||||
hopBytes: hopBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1872,35 +1810,14 @@ String _formatPrefix(int prefix) {
|
||||
return prefix.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes, int hashByteWidth) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
Uint8List _orientPathBytes(
|
||||
Uint8List pathBytes,
|
||||
int hashByteWidth, {
|
||||
required bool reverse,
|
||||
}) {
|
||||
if (!reverse || pathBytes.isEmpty) return pathBytes;
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, hashByteWidth);
|
||||
return Uint8List.fromList([for (final hop in hops.reversed) ...hop]);
|
||||
}
|
||||
|
||||
int _hopCountFromBytes(int byteCount, int hashByteWidth) {
|
||||
if (byteCount <= 0) return 0;
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
return (byteCount + width - 1) ~/ width;
|
||||
}
|
||||
|
||||
String _formatHopCount(
|
||||
int byteCount,
|
||||
int hashByteWidth,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return l10n.chat_hopsCount(_hopCountFromBytes(byteCount, hashByteWidth));
|
||||
String _formatHopCount(int count, AppLocalizations l10n) {
|
||||
return l10n.chat_hopsCount(count);
|
||||
}
|
||||
|
||||
String _resolveName(Contact? contact, AppLocalizations l10n) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||
import 'package:meshcore_open/utils/keys.dart';
|
||||
import 'package:meshcore_open/utils/platform_info.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -27,7 +27,6 @@ import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
import 'community_qr_scanner_screen.dart';
|
||||
@@ -283,6 +282,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: filteredChannels.length,
|
||||
onReorderItem: (oldIndex, newIndex) {
|
||||
// onReorderItem already adjusts newIndex after the
|
||||
// removed item, unlike the deprecated onReorder.
|
||||
final reordered = List<Channel>.from(
|
||||
filteredChannels,
|
||||
);
|
||||
@@ -373,30 +374,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
final community = isCommunityChannel
|
||||
? _communityIndex.getCommunityForChannel(channel)
|
||||
: null;
|
||||
// Only flood-routed channels carry a region; show it when one is set.
|
||||
String subtitle = connector.hasChannelRegion(channel.index)
|
||||
? context.l10n.channels_regionSetTo(
|
||||
connector.getChannelRegion(channel.index),
|
||||
)
|
||||
: '';
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
iconColor = MeshPalette.magenta;
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_publicChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.groups;
|
||||
iconColor = MeshPalette.magenta;
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_hashtagChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
iconColor = MeshPalette.signal;
|
||||
@@ -411,11 +395,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
// Last message preview
|
||||
final messages = connector.getChannelMessages(channel);
|
||||
final lastMessage = messages.isNotEmpty ? messages.last : null;
|
||||
final lastMessageText = lastMessage?.text ?? '';
|
||||
final lastPreview = lastMessageText.isNotEmpty &&
|
||||
GifHelper.parseGif(lastMessageText) != null
|
||||
? context.l10n.chat_receivedGif
|
||||
: lastMessageText;
|
||||
final lastPreview = lastMessage?.text ?? '';
|
||||
final lastTime = lastMessage?.timestamp;
|
||||
|
||||
final channelLabel = channel.name.isEmpty
|
||||
@@ -449,8 +429,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
channelMessageStore,
|
||||
channel,
|
||||
),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? () => _showChannelActions(
|
||||
child: GestureDetector(
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||
? (_) => _showChannelActions(
|
||||
this.context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
@@ -458,7 +439,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Leading avatar with optional community badge
|
||||
Stack(
|
||||
@@ -516,26 +497,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'CH ${channel.index}',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: scheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
StatusChip(
|
||||
label: 'CH ${channel.index}',
|
||||
color: MeshPalette.blue,
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (lastPreview.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
@@ -586,13 +554,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
const SizedBox(width: 4),
|
||||
ReorderableDragStartListener(
|
||||
index: dragIndex,
|
||||
// Top-aligned with the "CH n" / time line. Bottom padding keeps
|
||||
// a comfortable drag target without pushing the icon down.
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
size: 18,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
@@ -601,6 +566,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -946,7 +912,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
return;
|
||||
}
|
||||
final psk = randomBytes(16);
|
||||
final random = Random.secure();
|
||||
final psk = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
psk[i] = random.nextInt(256);
|
||||
}
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setChannel(
|
||||
nextIndex,
|
||||
@@ -1575,7 +1545,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
icon: const Icon(Icons.casino),
|
||||
tooltip: sheetContext.l10n.channels_generateRandomPsk,
|
||||
onPressed: () {
|
||||
final bytes = randomBytes(16);
|
||||
final random = Random.secure();
|
||||
final bytes = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
bytes[i] = random.nextInt(256);
|
||||
}
|
||||
pskController.text = Channel.formatPskHex(bytes);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -12,7 +12,6 @@ import '../utils/platform_info.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/cyr2lat.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
@@ -547,15 +546,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.primary,
|
||||
width: 1.5,
|
||||
@@ -777,35 +776,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
String _currentPathLabel(Contact contact) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
|
||||
// Check if user has set a path override
|
||||
if (contact.pathOverride != null) {
|
||||
if (contact.pathOverride! < 0) return context.l10n.chat_floodForced;
|
||||
if (contact.pathOverride == 0) return context.l10n.chat_directForced;
|
||||
final bytes = contact.pathOverrideBytes ?? Uint8List(0);
|
||||
final hopCount = _displayHopCount(
|
||||
bytes,
|
||||
contact.pathOverride!,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
return context.l10n.chat_hopsForced(hopCount);
|
||||
return context.l10n.chat_hopsForced(contact.pathOverride!);
|
||||
}
|
||||
|
||||
// Use device's path
|
||||
if (contact.pathLength < 0) return context.l10n.chat_floodAuto;
|
||||
if (contact.pathLength == 0) return context.l10n.chat_direct;
|
||||
final hopCount = _displayHopCount(
|
||||
contact.path,
|
||||
contact.pathLength,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
return context.l10n.chat_hopsCount(hopCount);
|
||||
}
|
||||
|
||||
int _displayHopCount(List<int> pathBytes, int storedHopCount, int hashWidth) {
|
||||
if (pathBytes.isEmpty) return storedHopCount;
|
||||
return PathHelper.splitPathBytes(pathBytes, hashWidth).length;
|
||||
return context.l10n.chat_hopsCount(contact.pathLength);
|
||||
}
|
||||
|
||||
void _showContactInfo(BuildContext context) {
|
||||
@@ -826,10 +807,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_path,
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
@@ -1457,9 +1435,6 @@ class _MessageBubble extends StatelessWidget {
|
||||
color: textColor.withValues(alpha: 0.72),
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? onLongPress
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -915,7 +915,6 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return _ContactTileEntrance(
|
||||
index: index,
|
||||
contact: contact,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: contact.isFavorite,
|
||||
@@ -1372,10 +1371,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPing,
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: _contactPathPrefix(contact, hw),
|
||||
flipPathAround: true,
|
||||
path: Uint8List.fromList([contact.publicKey.first]),
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1409,8 +1405,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
: context.l10n.contacts_roomPing,
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: _contactPathPrefix(contact, hw),
|
||||
flipPathAround: true,
|
||||
: Uint8List.fromList([contact.publicKey.first]),
|
||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1520,16 +1516,6 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List _contactPathPrefix(Contact contact, int hashByteWidth) {
|
||||
if (contact.publicKey.isEmpty) return Uint8List(0);
|
||||
final width = hashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt()
|
||||
.clamp(1, contact.publicKey.length)
|
||||
.toInt();
|
||||
return Uint8List.fromList(contact.publicKey.sublist(0, width));
|
||||
}
|
||||
|
||||
void _confirmDelete(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
@@ -1563,7 +1549,6 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
|
||||
class _ContactTile extends StatelessWidget {
|
||||
final Contact contact;
|
||||
final int pathHashByteWidth;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
@@ -1572,7 +1557,6 @@ class _ContactTile extends StatelessWidget {
|
||||
|
||||
const _ContactTile({
|
||||
required this.contact,
|
||||
required this.pathHashByteWidth,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
@@ -1692,10 +1676,7 @@ class _ContactTile extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: pathHashByteWidth,
|
||||
),
|
||||
contact.pathLabel(context.l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@@ -1782,7 +1763,6 @@ class _ContactTile extends StatelessWidget {
|
||||
class _ContactTileEntrance extends StatelessWidget {
|
||||
final int index;
|
||||
final Contact contact;
|
||||
final int pathHashByteWidth;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
@@ -1792,7 +1772,6 @@ class _ContactTileEntrance extends StatelessWidget {
|
||||
const _ContactTileEntrance({
|
||||
required this.index,
|
||||
required this.contact,
|
||||
required this.pathHashByteWidth,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
@@ -1806,7 +1785,6 @@ class _ContactTileEntrance extends StatelessWidget {
|
||||
index: index,
|
||||
child: _ContactTile(
|
||||
contact: contact,
|
||||
pathHashByteWidth: pathHashByteWidth,
|
||||
lastSeen: lastSeen,
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: isFavorite,
|
||||
|
||||
@@ -147,6 +147,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
connector,
|
||||
index,
|
||||
);
|
||||
if (PlatformInfo.isDesktop) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapUp: (_) =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
return tile;
|
||||
},
|
||||
),
|
||||
@@ -197,9 +204,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showContactContextMenu(contact, connector),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? () => _showContactContextMenu(contact, connector)
|
||||
: null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../widgets/app_bar.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class LineOfSightEndpoint {
|
||||
final String label;
|
||||
@@ -356,7 +357,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.watch<MapTileCacheService>();
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final endpoints = _visibleEndpoints();
|
||||
final mapPoints = [
|
||||
if (_start != null) _start!.point,
|
||||
@@ -429,7 +430,6 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onLongPress: (_, point) => _addCustomPoint(point),
|
||||
onSecondaryTap: (_, point) => _addCustomPoint(point),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (!_didReceivePositionUpdate ||
|
||||
@@ -442,8 +442,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
tileCache.buildTileLayer(
|
||||
context,
|
||||
ThemedMapTileLayer(
|
||||
tileCache: tileCache,
|
||||
opacity: _showTerrainLayer ? 1 : 0.72,
|
||||
),
|
||||
if (_result != null && _result!.segments.isNotEmpty)
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -33,11 +34,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
bool _isDownloading = false;
|
||||
int _completedTiles = 0;
|
||||
int _failedTiles = 0;
|
||||
List<CachedTileInfo> _cachedTiles = const [];
|
||||
int _cachedTileBytes = 0;
|
||||
bool _isLoadingCachedTiles = false;
|
||||
double _overlayZoom = 2.0;
|
||||
LatLngBounds? _visibleBounds;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -127,10 +123,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
_minZoom = safeMin;
|
||||
_maxZoom = safeMax;
|
||||
_selectedBounds = bounds;
|
||||
_visibleBounds = bounds;
|
||||
});
|
||||
_updateEstimate();
|
||||
_refreshCachedTiles();
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
@@ -187,7 +181,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
final bounds = _selectedBounds;
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
if (bounds == null) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
@@ -204,18 +197,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cacheService.source.allowsBulkDownload) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.mapCache_bulkDownloadDisabledInConfig(
|
||||
cacheService.source.label,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
@@ -238,6 +219,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
_completedTiles = 0;
|
||||
@@ -271,8 +254,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
result.failed,
|
||||
)
|
||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||
await _refreshCachedTiles();
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(context, content: Text(message));
|
||||
}
|
||||
|
||||
@@ -299,61 +280,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
await cacheService.clearCache();
|
||||
if (!mounted) return;
|
||||
await _refreshCachedTiles();
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_offlineCacheCleared),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshCachedTiles() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoadingCachedTiles = true;
|
||||
});
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
final inventory = await cacheService.getCachedTileInventory();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_cachedTiles = inventory.tiles;
|
||||
_cachedTileBytes = inventory.totalBytes;
|
||||
_isLoadingCachedTiles = false;
|
||||
});
|
||||
}
|
||||
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tileCache = context.watch<MapTileCacheService>();
|
||||
final source = tileCache.source;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final selectedBounds = _selectedBounds;
|
||||
final activeCachedTiles = tileCache.filterTilesForActiveSource(
|
||||
_cachedTiles,
|
||||
);
|
||||
final cachedInSelection = tileCache.countTilesForBounds(
|
||||
activeCachedTiles,
|
||||
bounds: selectedBounds,
|
||||
minZoom: _minZoom,
|
||||
maxZoom: _maxZoom,
|
||||
);
|
||||
final overlayPolygons = tileCache.buildCachedTilePolygons(
|
||||
activeCachedTiles,
|
||||
zoom: _overlayZoom.round().clamp(0, 19).toInt(),
|
||||
visibleBounds: _visibleBounds,
|
||||
);
|
||||
final cacheSummary =
|
||||
'${context.l10n.mapCache_summarySource(source.label)}\n'
|
||||
'${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n'
|
||||
'${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n'
|
||||
'${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}';
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
@@ -391,23 +327,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final nextZoom = camera.zoom;
|
||||
final nextBounds = camera.visibleBounds;
|
||||
if (_overlayZoom != nextZoom ||
|
||||
_visibleBounds?.north != nextBounds.north ||
|
||||
_visibleBounds?.south != nextBounds.south ||
|
||||
_visibleBounds?.east != nextBounds.east ||
|
||||
_visibleBounds?.west != nextBounds.west) {
|
||||
setState(() {
|
||||
_overlayZoom = nextZoom;
|
||||
_visibleBounds = nextBounds;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
tileCache.buildTileLayer(context),
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
if (selectedBounds != null)
|
||||
PolygonLayer(
|
||||
polygons: [
|
||||
@@ -419,24 +341,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (overlayPolygons.isNotEmpty)
|
||||
PolygonLayer(polygons: overlayPolygons),
|
||||
],
|
||||
),
|
||||
if (isDesktop) _buildDesktopMapControls(),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: isDesktop ? 84 : 12,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'Z: ${_overlayZoom.round()}:',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
@@ -538,34 +445,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
key: ValueKey(
|
||||
'$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes',
|
||||
),
|
||||
initialValue: cacheSummary,
|
||||
readOnly: true,
|
||||
minLines: 4,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: _isLoadingCachedTiles
|
||||
? l10n.mapCache_cachedTilesLabel
|
||||
: l10n.mapCache_cachedTileSummaryLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
if (!source.allowsBulkDownload) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.mapCache_bulkDownloadDisabledForSource(
|
||||
source.label,
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: MeshPalette.alert,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_isDownloading) ...[
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
@@ -592,10 +471,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(l10n.mapCache_downloadTilesButton),
|
||||
onPressed:
|
||||
_isDownloading ||
|
||||
selectedBounds == null ||
|
||||
!source.allowsBulkDownload
|
||||
onPressed: _isDownloading || selectedBounds == null
|
||||
? null
|
||||
: _startDownload,
|
||||
),
|
||||
|
||||
@@ -5,14 +5,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -26,6 +25,7 @@ import '../utils/battery_utils.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
@@ -79,7 +79,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
bool _hasInitializedMap = false;
|
||||
bool _removedMarkersLoaded = false;
|
||||
final List<int> _pathTrace = [];
|
||||
final List<int> _pathTraceHopWidths = [];
|
||||
final List<Contact> _pathTraceContacts = [];
|
||||
final List<LatLng> _points = [];
|
||||
final List<Polyline> _polylines = [];
|
||||
@@ -108,31 +107,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ColorScheme get _overlayScheme => Theme.of(context).colorScheme;
|
||||
|
||||
bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
Color get _overlayPanelColor => _useDarkOverlay
|
||||
? MapPalette.panelDark
|
||||
: _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96);
|
||||
|
||||
Color get _overlayPrimaryTextColor =>
|
||||
_useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface;
|
||||
|
||||
Color get _overlaySecondaryTextColor => _useDarkOverlay
|
||||
? MapPalette.textSecondary
|
||||
: _overlayScheme.onSurfaceVariant;
|
||||
|
||||
Color get _overlayMutedTextColor =>
|
||||
_useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant;
|
||||
|
||||
Color get _overlayBorderColor =>
|
||||
_useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant;
|
||||
|
||||
Color get _overlayShadowColor => _useDarkOverlay
|
||||
? MapPalette.markerShadow
|
||||
: Colors.black.withValues(alpha: 0.18);
|
||||
|
||||
_NodeAge _ageOf(Contact contact) {
|
||||
final d = DateTime.now().difference(contact.lastSeen);
|
||||
if (d.inMinutes <= 60) return _NodeAge.online;
|
||||
@@ -259,12 +233,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
bottom: 96,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
@@ -276,20 +250,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.add),
|
||||
visualDensity: VisualDensity.standard,
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.crop_free),
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
@@ -311,31 +285,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMapContextPress(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
LatLng latLng,
|
||||
) {
|
||||
if (_isSelectingPoi) {
|
||||
setState(() {
|
||||
_isSelectingPoi = false;
|
||||
});
|
||||
_shareMarker(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_showShareMarkerAtPositionSheet(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
@@ -354,7 +303,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final pathHistory = context.read<PathHistoryService>();
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
@@ -428,20 +376,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Compute guessed locations with caching
|
||||
final maxRangeKm = _estimateLoRaRangeKm(connector);
|
||||
final pathHashByteWidth = connector.pathHashByteWidth
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final filteredKeys = guessCandidates
|
||||
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
|
||||
.join(',');
|
||||
final anchorKeys = allContactsWithLocation
|
||||
.map(
|
||||
(c) =>
|
||||
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${PathHelper.formatHopHex(c.path.isNotEmpty ? c.path.sublist(max(0, c.path.length - pathHashByteWidth)) : const [])}',
|
||||
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
|
||||
)
|
||||
.join(',');
|
||||
final cacheKey =
|
||||
'$filteredKeys|$anchorKeys|$pathHistoryVersion:$pathHashByteWidth:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
'$filteredKeys|$anchorKeys|$pathHistoryVersion:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
if (cacheKey != _guessedLocationsCacheKey) {
|
||||
_guessedLocationsCacheKey = cacheKey;
|
||||
_cachedGuessedLocations = settings.mapShowGuessedLocations
|
||||
@@ -450,7 +395,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
allContactsWithLocation,
|
||||
pathHistory,
|
||||
maxRangeKm,
|
||||
pathHashByteWidth,
|
||||
)
|
||||
: [];
|
||||
}
|
||||
@@ -610,8 +554,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
backgroundColor: MapPalette.panelDark,
|
||||
foregroundColor: MapPalette.textPrimary,
|
||||
title: AppBarTitle(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -764,10 +708,24 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
},
|
||||
onLongPress: (_, latLng) {
|
||||
_handleMapContextPress(context, connector, latLng);
|
||||
},
|
||||
onSecondaryTap: (_, latLng) {
|
||||
_handleMapContextPress(context, connector, latLng);
|
||||
if (_isSelectingPoi) {
|
||||
setState(() {
|
||||
_isSelectingPoi = false;
|
||||
});
|
||||
_shareMarker(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_showShareMarkerAtPositionSheet(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
);
|
||||
},
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
// Track zoom in half-step buckets so cluster/marker
|
||||
@@ -784,7 +742,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
tileCache.buildTileLayer(context),
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
if (sharedMarkerPolylines.isNotEmpty)
|
||||
@@ -937,7 +895,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
highContrast: _useDarkOverlay,
|
||||
highContrast: true,
|
||||
),
|
||||
),
|
||||
floatingActionButton:
|
||||
@@ -1012,19 +970,23 @@ class _MapScreenState extends State<MapScreen> {
|
||||
List<Contact> withLocation,
|
||||
PathHistoryService pathHistory,
|
||||
double? maxRangeKm,
|
||||
int pathHashByteWidth,
|
||||
) {
|
||||
final result = <_GuessedLocation>[];
|
||||
final hopWidth = pathHashByteWidth.clamp(1, 4).toInt();
|
||||
final anchorsByPrefix = <String, List<Contact>>{};
|
||||
for (final repeater in withLocation) {
|
||||
if (repeater.type != advTypeRepeater) continue;
|
||||
if (repeater.publicKey.length < hopWidth) continue;
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
repeater.publicKey.sublist(0, hopWidth),
|
||||
);
|
||||
anchorsByPrefix.putIfAbsent(prefix, () => []).add(repeater);
|
||||
// Index known-location repeaters by their 1-byte hash.
|
||||
// null value = two repeaters share the same hash byte (ambiguous collision).
|
||||
final repeaterByHash = <int, Contact?>{};
|
||||
|
||||
for (final c in withLocation) {
|
||||
if (c.type == advTypeRepeater) {
|
||||
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
||||
repeaterByHash[c.publicKey[0]] =
|
||||
null; // collision: can't disambiguate
|
||||
} else {
|
||||
repeaterByHash[c.publicKey[0]] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final result = <_GuessedLocation>[];
|
||||
|
||||
for (final contact in allContacts) {
|
||||
if (contact.hasLocation) continue;
|
||||
@@ -1038,23 +1000,21 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Collect the contact-side (last-hop) repeater from every known path.
|
||||
// path = [device-side hop, ..., contact-side hop]
|
||||
// Only the last hop chunk is actually within radio range of the contact.
|
||||
// Only path.last is actually within radio range of the contact — using
|
||||
// earlier bytes would anchor against our own side of the network.
|
||||
final pathSets = <List<int>>[
|
||||
contact.path.toList(),
|
||||
...pathHistory
|
||||
.getRecentPaths(contact.publicKeyHex)
|
||||
.map((r) => r.pathBytes),
|
||||
];
|
||||
final lastHopBytes = <int>{};
|
||||
for (final pathBytes in pathSets) {
|
||||
if (pathBytes.isEmpty) continue;
|
||||
final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth));
|
||||
if (lastHop.isEmpty) continue;
|
||||
|
||||
final repeaters = anchorsByPrefix[PathHelper.formatHopHex(lastHop)];
|
||||
if (repeaters != null && repeaters.isNotEmpty) {
|
||||
final repeater = repeaters.first;
|
||||
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
|
||||
}
|
||||
final lastHop = pathBytes.last;
|
||||
lastHopBytes.add(lastHop);
|
||||
final r = repeaterByHash[lastHop];
|
||||
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
|
||||
}
|
||||
|
||||
// Filter anchors that are geometrically inconsistent with radio range.
|
||||
@@ -1221,12 +1181,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||
},
|
||||
onLongPress: () => _isBuildingPathTrace
|
||||
? _showNodeInfo(context, guess.contact)
|
||||
: null,
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, guess.contact, position: guess.position)
|
||||
: _selectNode(guess.contact, guessedPosition: guess.position),
|
||||
@@ -1236,14 +1193,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
border: Border.all(
|
||||
color: guess.highConfidence ? color : _overlayMutedTextColor,
|
||||
color: guess.highConfidence ? color : MapPalette.textMuted,
|
||||
width: guess.highConfidence ? 2.5 : 2,
|
||||
),
|
||||
boxShadow: [
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 7,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
@@ -1252,7 +1209,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.not_listed_location,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
@@ -1329,20 +1286,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms
|
||||
// whose first key byte collides with another repeater/room on the map.
|
||||
final overlapPrefixes = <String>{};
|
||||
final overlapPrefixes = <int>{};
|
||||
if (overlapsMode) {
|
||||
final hopWidth = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt();
|
||||
final counts = <String, int>{};
|
||||
final counts = <int, int>{};
|
||||
for (final contact in contacts) {
|
||||
if ((contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
contact.publicKey.length >= hopWidth) {
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, hopWidth),
|
||||
);
|
||||
if (contact.type == advTypeRepeater || contact.type == advTypeRoom) {
|
||||
final prefix = contact.publicKey.first;
|
||||
counts[prefix] = (counts[prefix] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
@@ -1350,20 +1299,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (count > 1) overlapPrefixes.add(prefix);
|
||||
});
|
||||
}
|
||||
final overlapHopWidth = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt();
|
||||
bool isOverlap(Contact contact) =>
|
||||
overlapsMode &&
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
contact.publicKey.length >= overlapHopWidth &&
|
||||
overlapPrefixes.contains(
|
||||
PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, overlapHopWidth),
|
||||
),
|
||||
);
|
||||
overlapPrefixes.contains(contact.publicKey.first);
|
||||
|
||||
void addNode(Contact contact, {bool dot = false}) {
|
||||
final overlap = isOverlap(contact);
|
||||
@@ -1444,12 +1383,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
height: size,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||
},
|
||||
onLongPress: () =>
|
||||
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, contact)
|
||||
: _selectNode(contact),
|
||||
@@ -1588,12 +1523,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.xs),
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
@@ -1607,7 +1542,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1728,7 +1663,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: statusColor,
|
||||
border: Border.all(color: _overlayPanelColor, width: 2),
|
||||
border: Border.all(color: MapPalette.panelDark, width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: batteryLow
|
||||
@@ -1751,10 +1686,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _overlaySecondaryTextColor,
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -1812,9 +1747,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(color: _overlayBorderColor),
|
||||
side: const BorderSide(color: MapPalette.border),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: TextField(
|
||||
@@ -1822,15 +1757,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
focusNode: _searchFocus,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.map_searchHint,
|
||||
hintStyle: TextStyle(color: _overlaySecondaryTextColor),
|
||||
prefixIcon: Icon(
|
||||
hintStyle: const TextStyle(
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
suffixIcon: hasQuery
|
||||
? IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -1850,9 +1787,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
cursorColor: MapPalette.selected,
|
||||
@@ -1864,9 +1801,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(color: _overlayBorderColor),
|
||||
side: const BorderSide(color: MapPalette.border),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
@@ -1890,17 +1827,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
AnimatedRotation(
|
||||
turns: _statsExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
child: const Icon(
|
||||
Icons.expand_more,
|
||||
size: 16,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1999,12 +1936,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
color: selected
|
||||
? Color.alphaBlend(
|
||||
accent.withValues(alpha: 0.34),
|
||||
_overlayPanelColor,
|
||||
MapPalette.panelDark,
|
||||
)
|
||||
: _overlayPanelColor,
|
||||
: MapPalette.panelDark,
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: selected ? accent : _overlayBorderColor,
|
||||
color: selected ? accent : MapPalette.border,
|
||||
width: selected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
@@ -2020,7 +1957,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (selected) ...[
|
||||
Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor),
|
||||
const Icon(
|
||||
Icons.check,
|
||||
size: 13,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
@@ -2029,8 +1970,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: selected
|
||||
? _overlayPrimaryTextColor
|
||||
: _overlaySecondaryTextColor,
|
||||
? MapPalette.textPrimary
|
||||
: MapPalette.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2060,14 +2001,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2076,8 +2017,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.l10n.map_noResults,
|
||||
style: TextStyle(
|
||||
color: _overlaySecondaryTextColor,
|
||||
style: const TextStyle(
|
||||
color: MapPalette.textSecondary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
@@ -2087,7 +2028,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: results.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
Divider(height: 1, color: _overlayBorderColor),
|
||||
const Divider(height: 1, color: MapPalette.border),
|
||||
itemBuilder: (context, index) {
|
||||
final c = results[index];
|
||||
final color = _getNodeColor(c.type);
|
||||
@@ -2108,10 +2049,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Text(
|
||||
c.name,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2119,7 +2060,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
c.publicKeyHex.substring(0, 12),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10.5,
|
||||
color: _overlaySecondaryTextColor,
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2129,13 +2070,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: _overlaySecondaryTextColor,
|
||||
color: MapPalette.textSecondary,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
context.l10n.map_noGps.toUpperCase(),
|
||||
style: MeshTheme.accentLabel(
|
||||
color: _overlayMutedTextColor,
|
||||
color: MapPalette.textMuted,
|
||||
fontSize: 8.5,
|
||||
),
|
||||
),
|
||||
@@ -2195,14 +2136,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 230,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2219,7 +2160,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
_statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline),
|
||||
_statRow(context.l10n.map_markers, pinCount, MapPalette.shared),
|
||||
Divider(height: 16, color: _overlayBorderColor),
|
||||
const Divider(height: 16, color: MapPalette.border),
|
||||
_buildLegendItem(
|
||||
Icons.person,
|
||||
context.l10n.map_chat,
|
||||
@@ -2249,7 +2190,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_buildLegendItem(
|
||||
Icons.not_listed_location,
|
||||
context.l10n.map_guessedLocation,
|
||||
_overlayMutedTextColor,
|
||||
MapPalette.textMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2270,10 +2211,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -2282,7 +2220,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2315,8 +2253,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: MeshCard(
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
|
||||
color: _overlayPanelColor,
|
||||
borderColor: _overlayBorderColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderColor: MapPalette.border,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -2339,10 +2277,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
contact.name,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2370,9 +2308,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
contact.typeLabel(context.l10n),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: _overlaySecondaryTextColor,
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2384,13 +2322,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
if (pos != null)
|
||||
IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.center_focus_strong, size: 20),
|
||||
tooltip: context.l10n.map_centerOnNode,
|
||||
onPressed: () => _mapController.move(pos, max(_zoom, 15)),
|
||||
),
|
||||
IconButton(
|
||||
color: _overlayPrimaryTextColor,
|
||||
color: MapPalette.textPrimary,
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: _clearSelection,
|
||||
),
|
||||
@@ -2407,10 +2345,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
_miniMeta(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
_miniMeta('ID', contact.publicKeyHex.substring(0, 12)),
|
||||
if (pos != null)
|
||||
@@ -2454,17 +2389,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: MeshTheme.accentLabel(
|
||||
color: _overlayMutedTextColor,
|
||||
color: MapPalette.textMuted,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
value,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -2814,10 +2746,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
@@ -3563,23 +3492,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final hopWidth = min(
|
||||
connector.pathHashByteWidth.clamp(1, pubKeySize),
|
||||
contact.publicKey.length,
|
||||
).toInt();
|
||||
final hopPrefix = contact.publicKey.sublist(0, hopWidth);
|
||||
for (final existingHop in PathHelper.splitPathBytes(
|
||||
_pathTrace,
|
||||
connector.pathHashByteWidth,
|
||||
)) {
|
||||
if (listEquals(existingHop, hopPrefix)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_pathTrace.addAll(hopPrefix); // Add the hop-width pubkey prefix.
|
||||
_pathTraceHopWidths.add(hopWidth);
|
||||
_pathTrace.add(
|
||||
contact.publicKey[0],
|
||||
); // Add first 16 bytes of public key to path trace
|
||||
_pathTraceContacts.add(
|
||||
contact.copyWith(
|
||||
latitude: position?.latitude ?? contact.latitude,
|
||||
@@ -3594,7 +3510,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = true;
|
||||
_pathTrace.clear();
|
||||
_pathTraceHopWidths.clear();
|
||||
_pathTraceContacts.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
@@ -3604,19 +3519,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
void _removePath() {
|
||||
setState(() {
|
||||
final recordedHopWidth = _pathTraceHopWidths.isNotEmpty
|
||||
? _pathTraceHopWidths.removeLast()
|
||||
: context.read<MeshCoreConnector>().pathHashByteWidth.clamp(
|
||||
1,
|
||||
pubKeySize,
|
||||
);
|
||||
final hopByteCount = min(recordedHopWidth, _pathTrace.length).toInt();
|
||||
_pathTraceContacts.removeLast();
|
||||
// A path trace hop can be wider than one byte; remove the full hash prefix.
|
||||
_pathTrace.removeRange(
|
||||
_pathTrace.length - hopByteCount,
|
||||
_pathTrace.length,
|
||||
);
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
_points.removeLast(); // Remove last point from points list
|
||||
_polylines.clear(); // Clear polylines
|
||||
});
|
||||
@@ -3633,14 +3537,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
right: 16,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _overlayPanelColor,
|
||||
color: MapPalette.panelDark,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _overlayShadowColor,
|
||||
color: MapPalette.markerShadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -3653,34 +3557,24 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Text(
|
||||
l10n.contacts_pathTrace,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (_pathTrace.isEmpty) const SizedBox(height: 8),
|
||||
if (_pathTrace.isEmpty)
|
||||
Text(
|
||||
l10n.map_tapToAdd,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
),
|
||||
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
Text(
|
||||
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: _overlaySecondaryTextColor,
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
PathHelper.splitPathBytes(
|
||||
_pathTrace,
|
||||
context.read<MeshCoreConnector>().pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(','),
|
||||
_pathTrace
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(','),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -3705,7 +3599,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
flipPathAround: false,
|
||||
pathHashByteWidth: hashW,
|
||||
pathContacts: _pathTraceContacts,
|
||||
),
|
||||
@@ -3728,10 +3621,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
flipPathAround: true,
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
pathContacts: _pathTraceContacts,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -3754,7 +3643,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
_pathTrace.clear();
|
||||
_pathTraceHopWidths.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
@@ -296,7 +296,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
|
||||
@@ -7,7 +7,6 @@ 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/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/models/app_settings.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
@@ -20,6 +19,7 @@ import 'package:meshcore_open/services/path_history_service.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:meshcore_open/widgets/path_map_ui.dart';
|
||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||
import 'package:meshcore_open/widgets/themed_map_tile_layer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
|
||||
@@ -27,9 +27,9 @@ export 'package:meshcore_open/widgets/path_map_ui.dart'
|
||||
show formatDistance, getPathDistanceMeters;
|
||||
|
||||
class PathTraceData {
|
||||
final List<Uint8List> pathData;
|
||||
final Uint8List pathData;
|
||||
final List<double> snrData;
|
||||
final Map<String, Contact> pathContacts;
|
||||
final Map<int, Contact> pathContacts;
|
||||
|
||||
PathTraceData({
|
||||
required this.pathData,
|
||||
@@ -38,20 +38,6 @@ class PathTraceData {
|
||||
});
|
||||
}
|
||||
|
||||
String _hopKey(Uint8List hopBytes) => PathHelper.formatHopHex(hopBytes);
|
||||
|
||||
Uint8List _lastHopChunk(Uint8List path, int hopWidth) {
|
||||
if (path.isEmpty) return Uint8List(0);
|
||||
final width = hopWidth.clamp(1, path.length).toInt();
|
||||
return Uint8List.fromList(path.sublist(path.length - width));
|
||||
}
|
||||
|
||||
bool _matchesHopPrefix(Uint8List a, Uint8List b) {
|
||||
if (a.isEmpty || b.isEmpty) return false;
|
||||
final width = min(a.length, b.length);
|
||||
return listEquals(a.sublist(0, width), b.sublist(0, width));
|
||||
}
|
||||
|
||||
class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
@@ -94,8 +80,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
bool _failed2Loaded = false;
|
||||
bool _hasData = false;
|
||||
PathTraceData? _traceData;
|
||||
// Inferred positions for hops that have no GPS location, keyed by hop prefix.
|
||||
Map<String, LatLng> _inferredHopPositions = {};
|
||||
// Inferred positions for hops that have no GPS location, keyed by hop byte.
|
||||
Map<int, LatLng> _inferredHopPositions = {};
|
||||
// Endpoint position for the target contact (GPS or guessed).
|
||||
LatLng? _targetContactPosition;
|
||||
bool _targetContactIsGuessed = false;
|
||||
@@ -108,7 +94,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
Contact? _targetContact;
|
||||
Uint8List? _sentTagBytes;
|
||||
// Live path resolved at trace time; used by the response handler for
|
||||
// endpoint inference so it matches the path that was actually traced.
|
||||
Uint8List _tracedPath = Uint8List(0);
|
||||
@@ -118,82 +103,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
PathHistoryService? _pathHistory;
|
||||
PathViewMode _viewMode = PathViewMode.single;
|
||||
List<DisplayPath> _displayPaths = [];
|
||||
List<Uint8List> _primaryOutboundHops = [];
|
||||
List<int> _primaryOutboundHops = [];
|
||||
String _selectedPathId = 'primary';
|
||||
final Set<String> _hiddenPathIds = {};
|
||||
bool _panelCollapsed = false;
|
||||
bool _animationEnabled = true;
|
||||
bool _followPacket = false;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes, [int? hashByteWidth]) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hashByteWidth ?? widget.pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
int _traceHashByteWidth(int pathHashByteWidth) {
|
||||
final width = pathHashByteWidth.clamp(1, pubKeySize).toInt();
|
||||
if (width <= 1) return 1;
|
||||
if (width == 2) return 2;
|
||||
// Trace packets encode hash width as 1 << flags, so 3-byte path hashes
|
||||
// must be traced with a 4-byte public-key prefix.
|
||||
return 4;
|
||||
}
|
||||
|
||||
int _traceFlagsForHashWidth(int traceHashByteWidth) {
|
||||
if (traceHashByteWidth <= 1) return 0;
|
||||
if (traceHashByteWidth == 2) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
Uint8List _reversePathByHop(Uint8List pathBytes, int hopWidth) {
|
||||
final reversedHops = PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hopWidth,
|
||||
).reversed;
|
||||
final bytes = <int>[];
|
||||
for (final hop in reversedHops) {
|
||||
bytes.addAll(hop);
|
||||
}
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
Uint8List? _expandHopForTrace(
|
||||
Uint8List hop,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
if (hop.length == traceHashByteWidth) return hop;
|
||||
|
||||
final candidates = <Contact>[
|
||||
...?widget.pathContacts,
|
||||
if (widget.targetContact != null) widget.targetContact!,
|
||||
...connector.allContactsUnfiltered,
|
||||
];
|
||||
for (final contact in candidates) {
|
||||
if (contact.publicKey.length < traceHashByteWidth) continue;
|
||||
if (!listEquals(contact.publicKey.sublist(0, hop.length), hop)) continue;
|
||||
return Uint8List.fromList(
|
||||
contact.publicKey.sublist(0, traceHashByteWidth),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Uint8List? _tracePathFromBytes(
|
||||
Uint8List pathBytes,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth);
|
||||
final traceBytes = <int>[];
|
||||
for (final hop in hops) {
|
||||
final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
|
||||
if (traceHop == null) return null;
|
||||
traceBytes.addAll(traceHop);
|
||||
}
|
||||
return Uint8List.fromList(traceBytes);
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -313,62 +233,45 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List? buildPath(
|
||||
Uint8List pathBytes,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final pathHops = PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
final hopWidth = traceHashByteWidth.clamp(1, pubKeySize).toInt();
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
Uint8List traceBytes;
|
||||
|
||||
// Compute targetPrefix if targetContact is provided
|
||||
Uint8List? targetPrefix;
|
||||
if (widget.targetContact != null) {
|
||||
final pk = widget.targetContact!.publicKey;
|
||||
if (pk.isNotEmpty) {
|
||||
final len = pk.length >= hopWidth ? hopWidth : pk.length;
|
||||
targetPrefix = Uint8List.fromList(pk.sublist(0, len));
|
||||
if (pathBytes.isEmpty) {
|
||||
final pk = widget.targetContact?.publicKey;
|
||||
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
|
||||
if (pk != null && pk.length >= n) {
|
||||
return Uint8List.fromList(pk.sublist(0, n));
|
||||
}
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = pk?[0] ?? 0;
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
final outboundHops = <Uint8List>[];
|
||||
for (final hop in pathHops) {
|
||||
final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
|
||||
if (traceHop == null) return null;
|
||||
outboundHops.add(traceHop);
|
||||
}
|
||||
if (targetPrefix != null) {
|
||||
// Check if targetPrefix is already the last hop in pathHops to avoid duplication
|
||||
bool alreadyEndedWithTarget = false;
|
||||
if (outboundHops.isNotEmpty) {
|
||||
if (listEquals(outboundHops.last, targetPrefix)) {
|
||||
alreadyEndedWithTarget = true;
|
||||
if (widget.targetContact?.type == advTypeRepeater ||
|
||||
widget.targetContact?.type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
if (!alreadyEndedWithTarget) {
|
||||
outboundHops.add(targetPrefix);
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? Uint8List(0) : 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];
|
||||
}
|
||||
}
|
||||
|
||||
if (outboundHops.isEmpty) {
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
final mirroredHops = <Uint8List>[...outboundHops];
|
||||
if (outboundHops.length > 1) {
|
||||
mirroredHops.addAll(
|
||||
outboundHops.sublist(0, outboundHops.length - 1).reversed,
|
||||
);
|
||||
}
|
||||
|
||||
final traceBytes = <int>[];
|
||||
for (final hop in mirroredHops) {
|
||||
traceBytes.addAll(hop);
|
||||
}
|
||||
return Uint8List.fromList(traceBytes);
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
/// Resolves the path bytes to trace. When tracing a specific contact's
|
||||
@@ -398,49 +301,26 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final traceHashByteWidth = _traceHashByteWidth(widget.pathHashByteWidth);
|
||||
final livePath = _resolveLivePath(connector);
|
||||
_tracedPath = livePath;
|
||||
|
||||
final pathTmp = widget.reversePathAround
|
||||
? _reversePathByHop(livePath, widget.pathHashByteWidth)
|
||||
? Uint8List.fromList(livePath.reversed.toList())
|
||||
: livePath;
|
||||
|
||||
final path = widget.flipPathAround
|
||||
? buildPath(pathTmp, traceHashByteWidth, connector)
|
||||
: _tracePathFromBytes(pathTmp, traceHashByteWidth, connector);
|
||||
|
||||
if (path == null) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||
|
||||
appLogger.info(
|
||||
'Initiating path trace with path: '
|
||||
'${_formatPathPrefixes(path, traceHashByteWidth)}',
|
||||
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||
tag: 'PathTraceMapScreen',
|
||||
noNotify: !mounted,
|
||||
);
|
||||
|
||||
final sentTag = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
_sentTagBytes = Uint8List(4)
|
||||
..[0] = sentTag & 0xFF
|
||||
..[1] = (sentTag >> 8) & 0xFF
|
||||
..[2] = (sentTag >> 16) & 0xFF
|
||||
..[3] = (sentTag >> 24) & 0xFF;
|
||||
|
||||
final flags = _traceFlagsForHashWidth(traceHashByteWidth);
|
||||
final tracePayload = path.isEmpty ? Uint8List.fromList([0x00]) : path;
|
||||
|
||||
final frame = buildTraceReq(
|
||||
sentTag,
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0, //auth
|
||||
flags, // flag
|
||||
payload: tracePayload,
|
||||
0, //flag
|
||||
payload: path,
|
||||
);
|
||||
connector.sendFrame(frame);
|
||||
}
|
||||
@@ -484,14 +364,16 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame.length >= 12 &&
|
||||
if (frame.length > 8 &&
|
||||
code == pushCodeTraceData &&
|
||||
(listEquals(frame.sublist(4, 8), _sentTagBytes) ||
|
||||
listEquals(frame.sublist(4, 8), tagData))) {
|
||||
listEquals(frame.sublist(4, 8), tagData)) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||
_handleTraceResponse(frame);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -511,30 +393,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final buffer = BufferReader(frame);
|
||||
try {
|
||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||
final pathLenByte = buffer.readUInt8();
|
||||
final flags = buffer.readUInt8();
|
||||
int pathLength = buffer.readUInt8();
|
||||
final int flags = buffer
|
||||
.readUInt8(); // path_sz = flags & 0x03 (path-hash mode, fw v1.11+)
|
||||
buffer.skipBytes(4); // Skip tag data
|
||||
buffer.skipBytes(4); // Skip auth code
|
||||
var width = _traceHashByteWidth(1 << (flags & 0x03));
|
||||
var pathLength = pathLenByte == 0xFF ? 0 : pathLenByte;
|
||||
if (pathLength > buffer.remaining && (pathLenByte & 0xC0) != 0) {
|
||||
final packedWidth = ((pathLenByte & 0xC0) >> 6) + 1;
|
||||
final packedLength = (pathLenByte & 0x3F) * packedWidth;
|
||||
if (packedLength <= buffer.remaining) {
|
||||
width = packedWidth;
|
||||
pathLength = packedLength;
|
||||
}
|
||||
}
|
||||
final pathBytes = buffer.readBytes(pathLength);
|
||||
final pathData = PathHelper.splitPathBytes(pathBytes, width);
|
||||
final int pathSz = flags & 0x03;
|
||||
Uint8List pathData = buffer.readBytes(pathLength);
|
||||
// Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node).
|
||||
final snrCount = (pathLength ~/ width) + 1;
|
||||
final int snrCount = (pathLength >> pathSz) + 1;
|
||||
List<double> snrData = buffer
|
||||
.readBytes(snrCount)
|
||||
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||
.toList();
|
||||
|
||||
Map<String, Contact> pathContacts = {};
|
||||
Map<int, Contact> pathContacts = {};
|
||||
Contact lastContact = Contact(
|
||||
path: Uint8List(0),
|
||||
pathLength: 0,
|
||||
@@ -546,12 +419,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
lastSeen: DateTime.now(),
|
||||
);
|
||||
if (widget.pathContacts != null) {
|
||||
final hopWidth = width.clamp(1, pubKeySize).toInt();
|
||||
pathContacts = {
|
||||
for (var c in widget.pathContacts!)
|
||||
if (c.publicKey.length >= hopWidth)
|
||||
_hopKey(Uint8List.fromList(c.publicKey.sublist(0, hopWidth))): c,
|
||||
};
|
||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||
} else {
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
@@ -566,14 +434,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
_maxRepeaterMatchDistanceMeters) {
|
||||
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||
}
|
||||
for (final repeaterData in pathData) {
|
||||
final hopWidth = repeaterData.length;
|
||||
if (repeater.publicKey.length < hopWidth) continue;
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, hopWidth),
|
||||
repeaterData,
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[_hopKey(repeaterData)] = repeater;
|
||||
pathContacts[repeaterData] = repeater;
|
||||
lastContact = repeater;
|
||||
}
|
||||
}
|
||||
@@ -582,20 +448,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
final Map<String, LatLng> inferredPositions = {};
|
||||
final Map<int, LatLng> inferredPositions = {};
|
||||
for (final hop in pathData) {
|
||||
final hopKey = _hopKey(hop);
|
||||
final contact = pathContacts[hopKey];
|
||||
final contact = pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) continue;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
hop,
|
||||
),
|
||||
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
@@ -605,7 +464,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
inferredPositions[hopKey] = LatLng(lat, lon);
|
||||
inferredPositions[hop] = LatLng(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,31 +486,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final tc = _targetContact!;
|
||||
if (tc.hasLocation) {
|
||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||
} else if (pathData.length > 1 || _tracedPath.isNotEmpty) {
|
||||
} else if (_tracedPath.length > 1) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||
final tracedHops = PathHelper.splitPathBytes(
|
||||
_tracedPath,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
final hopsForEndpoint = tracedHops.isNotEmpty
|
||||
? tracedHops
|
||||
: pathData;
|
||||
final lastHop = widget.reversePathAround
|
||||
? hopsForEndpoint.first
|
||||
: hopsForEndpoint.last;
|
||||
final lastHopKey = _hopKey(lastHop);
|
||||
? _tracedPath.first
|
||||
: _tracedPath.last;
|
||||
|
||||
final peers = connector.allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
lastHop,
|
||||
),
|
||||
c.path.last == lastHop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
@@ -668,9 +516,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else if (inferredPositions.containsKey(lastHopKey)) {
|
||||
final lat = inferredPositions[lastHopKey]!.latitude;
|
||||
final lon = inferredPositions[lastHopKey]!.longitude;
|
||||
} else if (inferredPositions.containsKey(lastHop)) {
|
||||
final lat = inferredPositions[lastHop]!.latitude;
|
||||
final lon = inferredPositions[lastHop]!.longitude;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
@@ -680,7 +528,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
targetGuessed = true;
|
||||
} else {
|
||||
// As a last resort, just place it at the same position as the last hop.
|
||||
final contact = pathContacts[lastHopKey];
|
||||
final contact = pathContacts[lastHop];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
@@ -698,22 +546,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
String hopLast = '';
|
||||
String hopLastLast = '';
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in _traceData!.pathData) {
|
||||
final hopKey = _hopKey(hop);
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
break; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
final contact = _traceData!.pathContacts[hopKey];
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
} else {
|
||||
final inferred = inferredPositions[hopKey];
|
||||
final inferred = inferredPositions[hop];
|
||||
if (inferred != null) _points.add(inferred);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hopKey;
|
||||
hopLast = hop;
|
||||
}
|
||||
if (targetPos != null) {
|
||||
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
||||
@@ -736,7 +583,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||
_mapKey = ValueKey(
|
||||
'${context.l10n.pathTrace_you},${_traceData!.pathData.map(PathHelper.formatHopHex).join(',')}',
|
||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||
);
|
||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||
_primaryOutboundHops = _outboundHops(pathData);
|
||||
@@ -758,47 +605,37 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
/// Outbound hop bytes of the traced path, mirroring the round-trip
|
||||
/// dedup logic used when building [_points].
|
||||
List<Uint8List> _outboundHops(List<Uint8List> pathData) {
|
||||
final hops = <Uint8List>[];
|
||||
var hopLast = '';
|
||||
var hopLastLast = '';
|
||||
List<int> _outboundHops(Uint8List pathData) {
|
||||
final hops = <int>[];
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in pathData) {
|
||||
final hopKey = _hopKey(hop);
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) break;
|
||||
if (hop == hopLastLast && widget.flipPathAround) break;
|
||||
hops.add(hop);
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hopKey;
|
||||
hopLast = hop;
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
Contact? _contactForHop(Uint8List hop, MeshCoreConnector connector) {
|
||||
final traced = _traceData?.pathContacts[_hopKey(hop)];
|
||||
Contact? _contactForHop(int hop, MeshCoreConnector connector) {
|
||||
final traced = _traceData?.pathContacts[hop];
|
||||
if (traced != null) return traced;
|
||||
for (final c in connector.allContactsUnfiltered) {
|
||||
if (c.type != advTypeChat &&
|
||||
c.publicKey.length >= hop.length &&
|
||||
listEquals(c.publicKey.sublist(0, hop.length), hop)) {
|
||||
c.publicKey.isNotEmpty &&
|
||||
c.publicKey[0] == hop) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng? _inferredPositionForHop(Uint8List hop, MeshCoreConnector connector) {
|
||||
final hopKey = _hopKey(hop);
|
||||
final cached = _inferredHopPositions[hopKey];
|
||||
LatLng? _inferredPositionForHop(int hop, MeshCoreConnector connector) {
|
||||
final cached = _inferredHopPositions[hop];
|
||||
if (cached != null) return cached;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
hop,
|
||||
),
|
||||
)
|
||||
.where((c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop)
|
||||
.toList();
|
||||
if (peers.isEmpty) return null;
|
||||
final lat =
|
||||
@@ -806,14 +643,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length;
|
||||
final pos = LatLng(lat, lon);
|
||||
_inferredHopPositions[hopKey] = pos;
|
||||
_inferredHopPositions[hop] = pos;
|
||||
return pos;
|
||||
}
|
||||
|
||||
String _pathKeyForHops(List<Uint8List> hops) {
|
||||
return hops.map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
/// Rebuilds the renderable paths: the traced path as primary plus up to
|
||||
/// four distinct alternates from the target contact's path history.
|
||||
void _rebuildDisplayPaths(MeshCoreConnector connector) {
|
||||
@@ -831,22 +664,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final target = widget.targetContact;
|
||||
final history = _pathHistory;
|
||||
if (target != null && history != null) {
|
||||
final seen = <String>{_pathKeyForHops(_primaryOutboundHops)};
|
||||
final seen = <String>{_primaryOutboundHops.join(',')};
|
||||
var altIndex = 0;
|
||||
for (final record in history.getRecentPaths(target.publicKeyHex)) {
|
||||
if (record.pathBytes.isEmpty) continue;
|
||||
final recordHops = PathHelper.splitPathBytes(
|
||||
record.pathBytes,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
if (!seen.add(_pathKeyForHops(recordHops))) continue;
|
||||
if (!seen.add(record.pathBytes.join(','))) continue;
|
||||
if (altIndex >= kAlternatePathColors.length) break;
|
||||
final alt = _buildDisplayPath(
|
||||
id: 'alt-${_pathKeyForHops(recordHops)}',
|
||||
id: 'alt-${record.pathBytes.join('-')}',
|
||||
label: context.l10n.pathMap_alternate(altIndex + 1),
|
||||
color: kAlternatePathColors[altIndex],
|
||||
isPrimary: false,
|
||||
hops: recordHops,
|
||||
hops: record.pathBytes,
|
||||
record: record,
|
||||
connector: connector,
|
||||
);
|
||||
@@ -871,7 +700,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
required String label,
|
||||
required Color color,
|
||||
required bool isPrimary,
|
||||
required List<Uint8List> hops,
|
||||
required List<int> hops,
|
||||
required MeshCoreConnector connector,
|
||||
PathRecord? record,
|
||||
}) {
|
||||
@@ -890,7 +719,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
for (var i = 0; i < hops.length; i++) {
|
||||
final hop = hops[i];
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final contact = _contactForHop(hop, connector);
|
||||
LatLng? pos;
|
||||
var isGps = false;
|
||||
@@ -944,7 +773,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
label: label,
|
||||
color: color,
|
||||
isPrimary: isPrimary,
|
||||
hopBytes: List<Uint8List>.from(hops),
|
||||
hopBytes: List<int>.from(hops),
|
||||
points: points,
|
||||
pointLabels: labels,
|
||||
pointConfirmed: confirmed,
|
||||
@@ -1105,34 +934,29 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<Uint8List> pathData, {
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
required Contact? target,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
String hopLast = '';
|
||||
String hopLastLast = '';
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in pathData) {
|
||||
final hopKey = _hopKey(hop);
|
||||
final contact = _traceData!.pathContacts[hopKey];
|
||||
final inferred = _inferredHopPositions[hopKey];
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
final inferred = _inferredHopPositions[hop];
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
continue; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
if (!hasGps && inferred == null) {
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hopKey;
|
||||
hopLast = hop;
|
||||
continue; //skip hops with no GPS and no inferred position
|
||||
}
|
||||
final point = hasGps
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: inferred!;
|
||||
final label = PathHelper.formatHopHex(hop);
|
||||
final shortLabel = label.length > 2 ? label.substring(0, 2) : label;
|
||||
final fullLabel = label.length > 2
|
||||
? (contact?.name != null ? '$label: ${contact!.name}' : label)
|
||||
: (contact?.name ?? label);
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
@@ -1165,9 +989,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
shortLabel,
|
||||
hasGps ? label : '~$label',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasGps ? MeshPalette.signal : MeshPalette.warn,
|
||||
),
|
||||
@@ -1177,10 +1001,15 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(_buildNodeLabelMarker(point: point, label: fullLabel));
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: contact?.name ?? '~$label',
|
||||
),
|
||||
);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hopKey;
|
||||
hopLast = hop;
|
||||
}
|
||||
|
||||
_addEndpointMarkers(markers, showLabels: showLabels, target: target);
|
||||
@@ -1308,22 +1137,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final markers = <Marker>[];
|
||||
|
||||
// Hop prefix -> paths that use it, in display order.
|
||||
final hopPaths = <String, List<DisplayPath>>{};
|
||||
final hopsByKey = <String, Uint8List>{};
|
||||
// Hop byte -> paths that use it, in display order.
|
||||
final hopPaths = <int, List<DisplayPath>>{};
|
||||
for (final path in _visiblePaths) {
|
||||
for (final hop in path.hopBytes) {
|
||||
final hopKey = _hopKey(hop);
|
||||
hopsByKey[hopKey] = hop;
|
||||
final list = hopPaths.putIfAbsent(hopKey, () => []);
|
||||
final list = hopPaths.putIfAbsent(hop, () => []);
|
||||
if (!list.contains(path)) list.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (final entry in hopPaths.entries) {
|
||||
final hopKey = entry.key;
|
||||
final hop = hopsByKey[hopKey];
|
||||
if (hop == null) continue;
|
||||
final hop = entry.key;
|
||||
final paths = entry.value;
|
||||
final contact = _contactForHop(hop, connector);
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
@@ -1331,7 +1155,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: _inferredPositionForHop(hop, connector);
|
||||
if (point == null) continue;
|
||||
final label = PathHelper.formatHopHex(hop);
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final baseColor = hasGps ? MeshPalette.signal : MeshPalette.warn;
|
||||
final shared = paths.length > 1;
|
||||
|
||||
@@ -1416,11 +1240,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
void _showSharedNodeSheet(
|
||||
Uint8List hop,
|
||||
int hop,
|
||||
Contact? contact,
|
||||
List<DisplayPath> paths,
|
||||
) {
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
showSharedNodeSheet(
|
||||
context,
|
||||
title:
|
||||
@@ -1467,28 +1291,29 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||
if (pathTraceData.pathData.isEmpty) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
if (index < 0 || index > pathTraceData.pathData.length) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
|
||||
if (index == 0 || index == pathTraceData.pathData.length) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (index == 0) {
|
||||
return context.l10n.pathTrace_you;
|
||||
} else {
|
||||
final hop = pathTraceData.pathData.last;
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
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 hop = pathTraceData.pathData[index - 1];
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
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}";
|
||||
@@ -1496,18 +1321,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
String formatDirectionSubText(PathTraceData pathTraceData, int index) {
|
||||
if (pathTraceData.pathData.isEmpty) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
if (index < 0 || index > pathTraceData.pathData.length) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
|
||||
if (index == 0 || index == pathTraceData.pathData.length) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (index == 0) {
|
||||
final hop = pathTraceData.pathData.first;
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
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}";
|
||||
@@ -1515,9 +1336,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
} else {
|
||||
final hop = pathTraceData.pathData[index];
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
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}";
|
||||
@@ -1575,7 +1399,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
},
|
||||
),
|
||||
children: [
|
||||
tileCache.buildTileLayer(context),
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
AnimatedBuilder(
|
||||
animation: _playback,
|
||||
builder: (context, _) {
|
||||
@@ -1846,11 +1670,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final l10n = context.l10n;
|
||||
|
||||
final hopUseCount = <String, int>{};
|
||||
final hopUseCount = <int, int>{};
|
||||
if (_viewMode == PathViewMode.combined) {
|
||||
for (final p in _visiblePaths) {
|
||||
for (final hopKey in p.hopBytes.map(PathHelper.formatHopHex).toSet()) {
|
||||
hopUseCount.update(hopKey, (v) => v + 1, ifAbsent: () => 1);
|
||||
for (final hop in p.hopBytes.toSet()) {
|
||||
hopUseCount.update(hop, (v) => v + 1, ifAbsent: () => 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1866,7 +1690,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
Widget? trailing;
|
||||
if (index < path.hopBytes.length) {
|
||||
final hop = path.hopBytes[index];
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final contact = _contactForHop(hop, connector);
|
||||
title = contact != null
|
||||
? '$hex: ${contact.name}'
|
||||
@@ -1879,7 +1703,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
: inferred
|
||||
? l10n.pathTrace_legendInferred
|
||||
: l10n.pathMap_noLocation;
|
||||
final sharedCount = hopUseCount[_hopKey(hop)] ?? 0;
|
||||
final sharedCount = hopUseCount[hop] ?? 0;
|
||||
subtitle = sharedCount > 1
|
||||
? '$status · ${l10n.pathMap_sharedNodeCount(sharedCount)}'
|
||||
: status;
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/storage/region_store.dart';
|
||||
import 'package:meshcore_open/theme/mesh_theme.dart';
|
||||
import 'package:meshcore_open/widgets/mesh_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<void> pushRegionManagementScreen(BuildContext context) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const RegionManagementScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RegionManagementScreen extends StatefulWidget {
|
||||
const RegionManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
|
||||
}
|
||||
|
||||
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
||||
static final RegExp _validFetchedRegion = RegExp(r'^[a-z0-9-]{1,30}$');
|
||||
|
||||
final RegionStore _regionStore = RegionStore();
|
||||
List<Region> _regions = [];
|
||||
bool _isFetchingRegions = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
void _loadRegions() {
|
||||
final regions = _regionStore.loadRegions();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_regions = regions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settings_regionManagement_screenTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: l10n.settings_regionAddRegion,
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddRegionDialog(context),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.settings_regionFetchRegions,
|
||||
icon: _isFetchingRegions
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.travel_explore),
|
||||
onPressed: _isFetchingRegions ? null : _showFetchRegionsDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
|
||||
itemCount: _regions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final region = _regions[index];
|
||||
return _buildRegionTile(context, region);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddRegionDialog(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final controller = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_regionName),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _handleAddRegion(controller.text, context),
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.settings_regionNameHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
|
||||
],
|
||||
maxLength: 30,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _handleAddRegion(controller.text, context),
|
||||
child: Text(l10n.common_add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showFetchRegionsDialog() async {
|
||||
if (_isFetchingRegions) return;
|
||||
|
||||
setState(() {
|
||||
_isFetchingRegions = true;
|
||||
});
|
||||
|
||||
Set<Region> fetchedRegions = {};
|
||||
try {
|
||||
fetchedRegions = await _fetchRegionsFromRepeaters();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isFetchingRegions = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final l10n = context.l10n;
|
||||
final sortedRegions = fetchedRegions.toList()..sort();
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(l10n.settings_regionFetchRegions),
|
||||
content: sortedRegions.isEmpty
|
||||
? Text(l10n.settings_regionFetchRegionsFail)
|
||||
: StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: sortedRegions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final fetchedRegion = sortedRegions[index];
|
||||
final alreadyExists = _regions.contains(fetchedRegion);
|
||||
return MeshCard(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.landscape,
|
||||
color: MeshPalette.blue,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fetchedRegion,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: alreadyExists
|
||||
? TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(
|
||||
context,
|
||||
).disabledColor,
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
if (alreadyExists) {
|
||||
_showDialogSnackBar(
|
||||
context,
|
||||
l10n.settings_regionFetchRegionsAlreadyExists,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_regionStore.addRegion(fetchedRegion);
|
||||
_loadRegions();
|
||||
setDialogState(() {});
|
||||
},
|
||||
child: Text(l10n.common_add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDialogSnackBar(BuildContext context, String message) {
|
||||
final overlay = Overlay.maybeOf(context);
|
||||
if (overlay == null) return;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final entry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 32,
|
||||
child: SafeArea(
|
||||
child: Material(
|
||||
color: theme.colorScheme.inverseSurface,
|
||||
elevation: 6,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Text(
|
||||
message,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(entry);
|
||||
Timer(const Duration(seconds: 3), entry.remove);
|
||||
}
|
||||
|
||||
Future<Set<Region>> _fetchRegionsFromRepeaters() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final repeaters = await _discoverNearbyRepeaters(connector);
|
||||
final regions = <Region>{};
|
||||
|
||||
for (final repeater in repeaters) {
|
||||
if (!mounted || !connector.isConnected) break;
|
||||
regions.addAll(await _requestRegionsFromRepeater(connector, repeater));
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
Future<List<Contact>> _discoverNearbyRepeaters(
|
||||
MeshCoreConnector connector,
|
||||
) async {
|
||||
final repeaters = connector.contacts
|
||||
.where((contact) => contact.type == advTypeRepeater)
|
||||
.toList();
|
||||
if (repeaters.isEmpty || !connector.isConnected) return <Contact>[];
|
||||
|
||||
StreamSubscription<Uint8List>? subscription;
|
||||
Timer? timeout;
|
||||
final completer = Completer<Set<String>>();
|
||||
final respondingPrefixes = <String>{};
|
||||
final tag = DateTime.now().microsecondsSinceEpoch & 0xFFFFFFFF;
|
||||
|
||||
void complete() {
|
||||
if (completer.isCompleted) return;
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
completer.complete(respondingPrefixes);
|
||||
}
|
||||
|
||||
subscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty || completer.isCompleted) return;
|
||||
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
if (reader.readByte() != pushCodeControlData) return;
|
||||
if (reader.remaining < 9) return;
|
||||
reader.skipBytes(3); // SNR, RSSI, path_len from companion firmware.
|
||||
|
||||
final payloadType = reader.readByte();
|
||||
if (((payloadType >> 4) & 0x0F) != controlSubtypeDiscoverResp ||
|
||||
(payloadType & 0x0F) != advTypeRepeater) {
|
||||
return;
|
||||
}
|
||||
|
||||
reader.skipBytes(1); // Inbound SNR reported by the responding repeater.
|
||||
if (reader.readUInt32LE() != tag) return;
|
||||
|
||||
final publicKeyPrefix = reader.readRemainingBytes();
|
||||
if (publicKeyPrefix.isEmpty) return;
|
||||
respondingPrefixes.add(pubKeyToHex(publicKeyPrefix));
|
||||
} catch (_) {
|
||||
// Ignore malformed discovery frames; another response may still arrive.
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final payload = buildDiscoveryRequestPayload(tag, prefixOnly: true);
|
||||
await connector.sendFrame(buildSendControlDataFrame(payload));
|
||||
timeout = Timer(const Duration(seconds: 10), complete);
|
||||
final prefixes = await completer.future;
|
||||
return repeaters.where((contact) {
|
||||
final contactKey = contact.publicKeyHex.toLowerCase();
|
||||
return prefixes.any((prefix) => contactKey.startsWith(prefix));
|
||||
}).toList();
|
||||
} catch (_) {
|
||||
timeout?.cancel();
|
||||
await subscription.cancel();
|
||||
return <Contact>[];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<Region>> _requestRegionsFromRepeater(
|
||||
MeshCoreConnector connector,
|
||||
Contact repeater,
|
||||
) async {
|
||||
StreamSubscription<Uint8List>? subscription;
|
||||
Timer? timeout;
|
||||
final completer = Completer<Set<Region>>();
|
||||
int? expectedTag;
|
||||
final originalPath = Uint8List.fromList(repeater.path);
|
||||
final originalPathLength = repeater.pathLength;
|
||||
var pathChangedForRequest = false;
|
||||
|
||||
void complete(Set<Region> regions) {
|
||||
if (completer.isCompleted) return;
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
completer.complete(regions);
|
||||
}
|
||||
|
||||
void restartTimeout(Duration duration) {
|
||||
timeout?.cancel();
|
||||
timeout = Timer(duration, () => complete(<Region>{}));
|
||||
}
|
||||
|
||||
try {
|
||||
final replyPath = Uint8List(0);
|
||||
const replyHopCount = 0;
|
||||
await connector.setContactPath(repeater, replyPath, replyHopCount);
|
||||
pathChangedForRequest = true;
|
||||
|
||||
subscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty || completer.isCompleted) return;
|
||||
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final cmd = reader.readByte();
|
||||
if (cmd == respCodeSent) {
|
||||
reader.skipBytes(1);
|
||||
expectedTag = reader.readUInt32LE();
|
||||
final estimatedTimeoutMs = reader.readUInt32LE();
|
||||
restartTimeout(
|
||||
Duration(
|
||||
milliseconds: estimatedTimeoutMs > 0
|
||||
? estimatedTimeoutMs + 2000
|
||||
: 10000,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == respCodeErr) {
|
||||
complete(<Region>{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd != pushCodeBinaryResponse || expectedTag == null) return;
|
||||
|
||||
reader.skipBytes(1);
|
||||
final tag = reader.readUInt32LE();
|
||||
if (tag != expectedTag) return;
|
||||
|
||||
complete(_parseRegionsResponse(reader.readRemainingBytes()));
|
||||
} catch (_) {
|
||||
complete(<Region>{});
|
||||
}
|
||||
});
|
||||
|
||||
restartTimeout(const Duration(seconds: 10));
|
||||
final frame = buildSendAnonReqFrame(
|
||||
repeater.publicKey,
|
||||
requestType: anonReqTypeRegions,
|
||||
replyPath: replyPath,
|
||||
replyHopCount: replyHopCount,
|
||||
pathHashWidth: connector.pathHashByteWidth,
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
final regions = await completer.future;
|
||||
if (pathChangedForRequest && connector.isConnected) {
|
||||
await _restoreRepeaterPath(
|
||||
connector,
|
||||
repeater,
|
||||
originalPathLength,
|
||||
originalPath,
|
||||
);
|
||||
}
|
||||
return regions;
|
||||
} catch (_) {
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
if (pathChangedForRequest && connector.isConnected) {
|
||||
await _restoreRepeaterPath(
|
||||
connector,
|
||||
repeater,
|
||||
originalPathLength,
|
||||
originalPath,
|
||||
);
|
||||
}
|
||||
return <Region>{};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restoreRepeaterPath(
|
||||
MeshCoreConnector connector,
|
||||
Contact repeater,
|
||||
int originalPathLength,
|
||||
Uint8List originalPath,
|
||||
) async {
|
||||
if (originalPathLength < 0) {
|
||||
await connector.clearContactPath(repeater);
|
||||
return;
|
||||
}
|
||||
await connector.setContactPath(repeater, originalPath, originalPathLength);
|
||||
}
|
||||
|
||||
Set<Region> _parseRegionsResponse(Uint8List frame) {
|
||||
if (frame.length <= 4) return <Region>{};
|
||||
final names = utf8
|
||||
.decode(frame.sublist(4), allowMalformed: true)
|
||||
.replaceAll('\x00', '')
|
||||
.split(',');
|
||||
return names
|
||||
.map((name) => name.trim())
|
||||
.where((name) => _validFetchedRegion.hasMatch(name))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _handleAddRegion(Region region, BuildContext context) {
|
||||
Navigator.pop(context);
|
||||
_regionStore.addRegion(region);
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
Widget _buildRegionTile(BuildContext context, Region region) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return MeshCard(
|
||||
key: ValueKey(region),
|
||||
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.landscape, color: MeshPalette.blue),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
region,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.settings_deleteRegion,
|
||||
icon: Icon(Icons.delete_outline, color: scheme.error),
|
||||
onPressed: () => _confirmDelete(context, region),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, Region region) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.settings_deleteRegion),
|
||||
content: Text(context.l10n.settings_deleteRegionConfirm(region)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
Navigator.pop(dialogContext);
|
||||
await _regionStore.removeRegion(region);
|
||||
// Deleting a region clears it from any channels that used it;
|
||||
// refresh the connector's in-memory channel regions to match.
|
||||
await connector.loadChannelSettings();
|
||||
_loadRegions();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.settings_regionDeleted)),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.common_delete,
|
||||
style: TextStyle(
|
||||
color: Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -32,7 +31,6 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final chemistry = settingsService.batteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
);
|
||||
@@ -87,10 +85,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel(
|
||||
l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
repeater.pathLabel(l10n),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -12,7 +11,6 @@ import '../services/storage_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../utils/keys.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterSettingsScreen extends StatefulWidget {
|
||||
@@ -46,7 +44,6 @@ enum _SettingField {
|
||||
advertInterval,
|
||||
floodAdvertInterval,
|
||||
pathHashMode,
|
||||
prvKey,
|
||||
txDelay,
|
||||
directTxDelay,
|
||||
intThresh,
|
||||
@@ -112,8 +109,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
bool _refreshingIntThresh = false;
|
||||
bool _refreshingAgcResetInterval = false;
|
||||
bool _runningAction = false;
|
||||
bool _searchingForKeyPair = false;
|
||||
bool _stopSearchingForKeyPair = false;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
RepeaterCommandService? _commandService;
|
||||
|
||||
@@ -159,11 +154,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
// Advanced
|
||||
int _pathHashMode = 0; // 0-2
|
||||
final TextEditingController _prvKeyController = TextEditingController();
|
||||
final TextEditingController _pubKeyController = TextEditingController();
|
||||
final TextEditingController _prvKeyPrefixController = TextEditingController();
|
||||
final KeyPairSearcher _keyPairSearcher = KeyPairSearcher();
|
||||
final int _searchChunkTime = 30; // time in ms to search before yielding
|
||||
final TextEditingController _txDelayController = TextEditingController();
|
||||
final TextEditingController _directTxDelayController =
|
||||
TextEditingController();
|
||||
@@ -216,10 +206,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_txDelayController.dispose();
|
||||
_directTxDelayController.dispose();
|
||||
_intThreshController.dispose();
|
||||
_prvKeyController.dispose();
|
||||
_pubKeyController.dispose();
|
||||
_prvKeyPrefixController.dispose();
|
||||
_stopSearchingForKeyPair = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -874,12 +860,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
command: 'set path.hash.mode $_pathHashMode',
|
||||
));
|
||||
}
|
||||
if (_dirtyFields.contains(_SettingField.prvKey)) {
|
||||
final v = _prvKeyController.text.trim();
|
||||
if (v != "") {
|
||||
pending.add((field: _SettingField.prvKey, command: 'set prv.key $v'));
|
||||
}
|
||||
}
|
||||
if (_dirtyFields.contains(_SettingField.txDelay) &&
|
||||
_txDelayController.text.isNotEmpty) {
|
||||
final v = double.tryParse(_txDelayController.text.trim());
|
||||
@@ -1025,36 +1005,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _startSearchingForKeyPair() async {
|
||||
if (_searchingForKeyPair) return;
|
||||
setState(() {
|
||||
_searchingForKeyPair = true;
|
||||
_stopSearchingForKeyPair = false;
|
||||
});
|
||||
_searchForKeyPair();
|
||||
}
|
||||
|
||||
void _searchForKeyPair() async {
|
||||
if (_stopSearchingForKeyPair) {
|
||||
if (mounted) setState(() => _searchingForKeyPair = false);
|
||||
return;
|
||||
}
|
||||
final String prefix = _prvKeyPrefixController.text.trim();
|
||||
MCKeyPair? keys = await _keyPairSearcher.findMatchingKeyPair(
|
||||
prefix,
|
||||
_searchChunkTime,
|
||||
);
|
||||
if (keys == null) {
|
||||
// Ran out of time; schedule another attempt.
|
||||
Timer.run(_searchForKeyPair);
|
||||
} else {
|
||||
setState(() => _searchingForKeyPair = false);
|
||||
_prvKeyController.text = pubKeyToHex(keys.private);
|
||||
_pubKeyController.text = pubKeyToHex(keys.public);
|
||||
_markChanged(_SettingField.prvKey);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineRefreshButton({
|
||||
required bool isRefreshing,
|
||||
required VoidCallback onRefresh,
|
||||
@@ -1119,7 +1069,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_buildOwnerInfoCard(),
|
||||
_buildActionsCard(),
|
||||
_buildAdvancedCard(),
|
||||
_buildKeysCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDangerZoneCard(),
|
||||
],
|
||||
@@ -2035,127 +1984,6 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeysCard() {
|
||||
final l10n = context.l10n;
|
||||
return MeshCard(
|
||||
child: ExpansionTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(
|
||||
l10n.repeater_keySettings,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(l10n.repeater_keySettingsSubtitle),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4),
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _prvKeyController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_prvKey,
|
||||
helperText: l10n.repeater_prvKeyHelper,
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) {
|
||||
_pubKeyController.text = "";
|
||||
if (_prvKeyController.text.isNotEmpty) {
|
||||
_markChanged(_SettingField.prvKey);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _searchingForKeyPair
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.cancel, size: 24),
|
||||
onPressed: (() => _stopSearchingForKeyPair = true),
|
||||
tooltip: l10n.repeater_stopGeneratingPrvKey,
|
||||
visualDensity: VisualDensity.compact,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.casino_rounded, size: 24),
|
||||
onPressed: () {
|
||||
_startSearchingForKeyPair();
|
||||
},
|
||||
tooltip: l10n.repeater_generatePrvKey,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
controller: _pubKeyController,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pubKey,
|
||||
helperText: l10n.repeater_pubKeyHelper,
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: _searchingForKeyPair
|
||||
? CircularProgressIndicator(strokeWidth: 2)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _prvKeyPrefixController,
|
||||
maxLength: 4,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
|
||||
],
|
||||
onChanged: (_) => setState(() => {}), // updates helper text
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pubKeyPrefix,
|
||||
helperText: l10n.repeater_pubKeyPrefixHelper(
|
||||
pow(16, _prvKeyPrefixController.text.length).toInt(),
|
||||
),
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDangerZoneCard() {
|
||||
final l10n = context.l10n;
|
||||
return MeshCard(
|
||||
|
||||
@@ -9,7 +9,6 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
@@ -20,7 +19,6 @@ import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import 'region_management_screen.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
@@ -451,14 +449,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () => _showRadioSettings(context, connector),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.landscape,
|
||||
title: l10n.settings_regionSettings,
|
||||
subtitle: l10n.settings_regionSettingsSubtitle,
|
||||
onTap: () => pushRegionManagementScreen(context),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.sensors_outlined,
|
||||
@@ -468,30 +458,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
? () => pushCompanionRadioStatsScreen(context)
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.route_outlined,
|
||||
title: l10n.repeater_pathHashMode,
|
||||
subtitle: _pathHashModeSubtitle(context, connector.pathHashByteWidth),
|
||||
onTap: connector.isConnected
|
||||
? () => _editPathHashMode(context, connector)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _pathHashModeSubtitle(BuildContext context, int pathHashByteWidth) {
|
||||
final l10n = context.l10n;
|
||||
return switch (pathHashByteWidth.clamp(1, 4).toInt()) {
|
||||
1 => l10n.repeater_pathHashModeOption0,
|
||||
2 => l10n.repeater_pathHashModeOption1,
|
||||
3 => l10n.repeater_pathHashModeOption2,
|
||||
_ => l10n.repeater_pathHashModeOption3,
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildLocationCardContent(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
@@ -780,90 +750,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _editPathHashMode(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
var selectedMode = (connector.pathHashByteWidth - 1).clamp(0, 3).toInt();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.repeater_pathHashMode),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: selectedMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pathHashMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 0,
|
||||
child: Text(l10n.repeater_pathHashModeOption0),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 1,
|
||||
child: Text(l10n.repeater_pathHashModeOption1),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 2,
|
||||
child: Text(l10n.repeater_pathHashModeOption2),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 3,
|
||||
child: Text(l10n.repeater_pathHashModeOption3),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setDialogState(() => selectedMode = value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.repeater_pathHashModeHelper,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
try {
|
||||
await connector.setPathHashMode(selectedMode);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_settingsSaved),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_error(e.toString())),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editLocation(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final latController = TextEditingController();
|
||||
final lonController = TextEditingController();
|
||||
final intervalController = TextEditingController();
|
||||
@@ -875,9 +763,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final bool hasGPS = customVars.containsKey("gps");
|
||||
bool isGPSEnabled = customVars["gps"] == "1";
|
||||
|
||||
final currentInterval = settingsService.resolvedGpsIntervalSeconds(
|
||||
customVars,
|
||||
);
|
||||
// Read current interval or default to 900 (15 minutes)
|
||||
final currentInterval =
|
||||
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
intervalController.text = currentInterval.toString();
|
||||
|
||||
String? intervalError;
|
||||
@@ -975,11 +863,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Navigator.pop(context);
|
||||
|
||||
if (interval != null) {
|
||||
await settingsService.setGpsIntervalSeconds(
|
||||
interval,
|
||||
writeToDevice: (value) =>
|
||||
connector.setCustomVar("gps_interval:$value"),
|
||||
);
|
||||
await connector.setCustomVar("gps_interval:$interval");
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
@@ -1279,15 +1163,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
|
||||
int telemetryMode = connector.telemetryModeBase;
|
||||
int telemetryLocMode = connector.telemetryModeLoc;
|
||||
int telemetryEnvMode = connector.telemetryModeEnv;
|
||||
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
|
||||
int multiAcks = connector.multiAcks;
|
||||
bool autoZeroHopAdvertOnGpsUpdate =
|
||||
settingsService.settings.autoSendZeroHopAdvertOnGpsUpdate;
|
||||
|
||||
final telemModeBase = [
|
||||
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
|
||||
@@ -1322,20 +1203,6 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FeatureToggleRow(
|
||||
title: l10n.settings_autoZeroHopAdvertOnGpsUpdate,
|
||||
subtitle: l10n.settings_autoZeroHopAdvertOnGpsUpdateSubtitle,
|
||||
value: autoZeroHopAdvertOnGpsUpdate,
|
||||
enabled: advertLocPolicy,
|
||||
onChanged: advertLocPolicy
|
||||
? (value) {
|
||||
setDialogState(
|
||||
() => autoZeroHopAdvertOnGpsUpdate = value,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.settings_multiAck),
|
||||
value: multiAcks == 1,
|
||||
@@ -1404,9 +1271,6 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
advertLocPolicy ? 1 : 0,
|
||||
multiAcks,
|
||||
);
|
||||
await settingsService.setAutoSendZeroHopAdvertOnGpsUpdate(
|
||||
autoZeroHopAdvertOnGpsUpdate,
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
|
||||
@@ -334,7 +334,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
@@ -350,7 +350,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
IconButton(
|
||||
|
||||
@@ -13,14 +13,6 @@ class AppSettingsService extends ChangeNotifier {
|
||||
|
||||
AppSettings get settings => _settings;
|
||||
|
||||
int resolvedGpsIntervalSeconds(Map<String, String>? deviceCustomVars) {
|
||||
final deviceValue = int.tryParse(deviceCustomVars?['gps_interval'] ?? '');
|
||||
if (deviceValue != null && deviceValue >= 0) {
|
||||
return deviceValue;
|
||||
}
|
||||
return _settings.gpsIntervalSeconds;
|
||||
}
|
||||
|
||||
String batteryChemistryForDevice(String deviceId) {
|
||||
final stored = _settings.batteryChemistryByDeviceId[deviceId];
|
||||
if (stored == 'liion') return 'nmc';
|
||||
@@ -120,25 +112,6 @@ class AppSettingsService extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setMapRasterSourceId(String value) async {
|
||||
await updateSettings(_settings.copyWith(mapRasterSourceId: value));
|
||||
}
|
||||
|
||||
Future<void> setMapTileEndpointId(String value) async {
|
||||
await updateSettings(_settings.copyWith(mapTileEndpointId: value));
|
||||
}
|
||||
|
||||
Future<void> setMapTileApiKey(String? value) async {
|
||||
final normalized = value?.trim();
|
||||
await updateSettings(
|
||||
_settings.copyWith(
|
||||
mapTileApiKey: (normalized == null || normalized.isEmpty)
|
||||
? null
|
||||
: normalized,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setNotificationsEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(notificationsEnabled: value));
|
||||
}
|
||||
@@ -155,28 +128,6 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(notifyOnNewAdvert: value));
|
||||
}
|
||||
|
||||
Future<void> setAutoSendZeroHopAdvertOnGpsUpdate(bool value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(autoSendZeroHopAdvertOnGpsUpdate: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setGpsIntervalSeconds(
|
||||
int value, {
|
||||
Future<void> Function(int value)? writeToDevice,
|
||||
}) async {
|
||||
await updateSettings(_settings.copyWith(gpsIntervalSeconds: value));
|
||||
if (writeToDevice == null) return;
|
||||
try {
|
||||
await writeToDevice(value);
|
||||
} catch (e) {
|
||||
appLogger.warn(
|
||||
'Failed to write GPS interval to device: $e',
|
||||
tag: 'AppSettings',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setAutoRouteRotationEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value));
|
||||
}
|
||||
|
||||
@@ -176,8 +176,6 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
return 'CMD_SET_CUSTOM_VAR';
|
||||
case cmdSendTracePath:
|
||||
return 'CMD_SEND_TRACE_PATH';
|
||||
case cmdSetFloodScope:
|
||||
return 'CMD_SET_FLOOD_SCOPE';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,232 +1,12 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
import '../models/app_settings.dart';
|
||||
import 'app_settings_service.dart';
|
||||
|
||||
enum MapRasterSourcePreset {
|
||||
osmAuto('osm_auto'),
|
||||
osmStandard('osm_standard'),
|
||||
osmDark('osm_dark'),
|
||||
stamenTerrain('stamen_terrain'),
|
||||
alidadeSmoothDark('alidade_smooth_dark'),
|
||||
outdoors('outdoors'),
|
||||
osmBright('osm_bright'),
|
||||
outdoorsDark('outdoors_dark'),
|
||||
osmBrightDark('osm_bright_dark');
|
||||
|
||||
const MapRasterSourcePreset(this.id);
|
||||
|
||||
final String id;
|
||||
|
||||
static MapRasterSourcePreset fromId(String id) {
|
||||
for (final value in values) {
|
||||
if (value.id == id) return value;
|
||||
}
|
||||
return MapRasterSourcePreset.osmAuto;
|
||||
}
|
||||
}
|
||||
|
||||
enum MapRasterEndpointPreset {
|
||||
standard('standard'),
|
||||
standard2x('standard_2x'),
|
||||
eu('eu'),
|
||||
eu2x('eu_2x');
|
||||
|
||||
const MapRasterEndpointPreset(this.id);
|
||||
|
||||
final String id;
|
||||
|
||||
static MapRasterEndpointPreset fromId(String id) {
|
||||
for (final value in values) {
|
||||
if (value.id == id) return value;
|
||||
}
|
||||
return MapRasterEndpointPreset.standard;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MapRasterSourceDefinition {
|
||||
const MapRasterSourceDefinition({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.description,
|
||||
this.isStadia = false,
|
||||
this.allowsBulkDownload = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String description;
|
||||
final bool isStadia;
|
||||
final bool allowsBulkDownload;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MapRasterEndpointDefinition {
|
||||
const MapRasterEndpointDefinition({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.host,
|
||||
this.scaleSuffix = '',
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String description;
|
||||
final String host;
|
||||
final String scaleSuffix;
|
||||
}
|
||||
|
||||
class MapRasterSourceCatalog {
|
||||
static const MapRasterSourceDefinition osmAuto = MapRasterSourceDefinition(
|
||||
id: 'osm_auto',
|
||||
label: 'OpenStreetMap Auto',
|
||||
description:
|
||||
'Automatically uses OpenStreetMap Standard or Dark from the app theme',
|
||||
);
|
||||
static const MapRasterSourceDefinition osmStandard =
|
||||
MapRasterSourceDefinition(
|
||||
id: 'osm_standard',
|
||||
label: 'OpenStreetMap Standard',
|
||||
description: 'Direct tiles from tile.openstreetmap.org',
|
||||
);
|
||||
static const MapRasterSourceDefinition osmDark = MapRasterSourceDefinition(
|
||||
id: 'osm_dark',
|
||||
label: 'OpenStreetMap Dark',
|
||||
description: 'Standard OpenStreetMap tiles with an inverted dark filter',
|
||||
);
|
||||
static const MapRasterSourceDefinition stamenTerrain =
|
||||
MapRasterSourceDefinition(
|
||||
id: 'stamen_terrain',
|
||||
label: 'Stamen Terrain',
|
||||
description: 'Terrain-focused style with hill shading',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
static const MapRasterSourceDefinition alidadeSmoothDark =
|
||||
MapRasterSourceDefinition(
|
||||
id: 'alidade_smooth_dark',
|
||||
label: 'Alidade Smooth Dark',
|
||||
description: 'Dark basemap with smooth contrast',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
static const MapRasterSourceDefinition outdoors = MapRasterSourceDefinition(
|
||||
id: 'outdoors',
|
||||
label: 'Outdoors',
|
||||
description: 'Outdoor-focused map with trails and terrain context',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
static const MapRasterSourceDefinition osmBright = MapRasterSourceDefinition(
|
||||
id: 'osm_bright',
|
||||
label: 'OSM Bright',
|
||||
description: 'Bright general-purpose OpenStreetMap style',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
static const MapRasterSourceDefinition outdoorsDark =
|
||||
MapRasterSourceDefinition(
|
||||
id: 'outdoors',
|
||||
label: 'Outdoors Dark',
|
||||
description: 'Dark version of the Outdoors map style',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
static const MapRasterSourceDefinition osmBrightDark =
|
||||
MapRasterSourceDefinition(
|
||||
id: 'osm_bright',
|
||||
label: 'OSM Bright Dark',
|
||||
description: 'Dark version of the OSM Bright map style',
|
||||
isStadia: true,
|
||||
allowsBulkDownload: true,
|
||||
);
|
||||
|
||||
static MapRasterSourceDefinition fromPreset(MapRasterSourcePreset preset) {
|
||||
switch (preset) {
|
||||
case MapRasterSourcePreset.osmAuto:
|
||||
return osmAuto;
|
||||
case MapRasterSourcePreset.osmStandard:
|
||||
return osmStandard;
|
||||
case MapRasterSourcePreset.osmDark:
|
||||
return osmDark;
|
||||
case MapRasterSourcePreset.alidadeSmoothDark:
|
||||
return alidadeSmoothDark;
|
||||
case MapRasterSourcePreset.outdoors:
|
||||
return outdoors;
|
||||
case MapRasterSourcePreset.outdoorsDark:
|
||||
return outdoorsDark;
|
||||
case MapRasterSourcePreset.osmBright:
|
||||
return osmBright;
|
||||
case MapRasterSourcePreset.osmBrightDark:
|
||||
return osmBrightDark;
|
||||
case MapRasterSourcePreset.stamenTerrain:
|
||||
return stamenTerrain;
|
||||
}
|
||||
}
|
||||
|
||||
static MapRasterSourceDefinition fromSettings(AppSettings settings) {
|
||||
return fromPreset(MapRasterSourcePreset.fromId(settings.mapRasterSourceId));
|
||||
}
|
||||
}
|
||||
|
||||
class MapRasterEndpointCatalog {
|
||||
static const MapRasterEndpointDefinition standard =
|
||||
MapRasterEndpointDefinition(
|
||||
id: 'standard',
|
||||
label: 'Standard Endpoint',
|
||||
description: 'Global CDN routing to the fastest Stadia server',
|
||||
host: 'tiles.stadiamaps.com',
|
||||
);
|
||||
static const MapRasterEndpointDefinition standard2x =
|
||||
MapRasterEndpointDefinition(
|
||||
id: 'standard_2x',
|
||||
label: 'Standard Endpoint (@2x)',
|
||||
description: 'Global Stadia endpoint with HiDPI raster tiles',
|
||||
host: 'tiles.stadiamaps.com',
|
||||
scaleSuffix: '@2x',
|
||||
);
|
||||
static const MapRasterEndpointDefinition eu = MapRasterEndpointDefinition(
|
||||
id: 'eu',
|
||||
label: 'EU Endpoint',
|
||||
description: 'Route tile requests to Stadia EU servers',
|
||||
host: 'tiles-eu.stadiamaps.com',
|
||||
);
|
||||
static const MapRasterEndpointDefinition eu2x = MapRasterEndpointDefinition(
|
||||
id: 'eu_2x',
|
||||
label: 'EU Endpoint (@2x)',
|
||||
description: 'EU Stadia endpoint with HiDPI raster tiles',
|
||||
host: 'tiles-eu.stadiamaps.com',
|
||||
scaleSuffix: '@2x',
|
||||
);
|
||||
|
||||
static const List<MapRasterEndpointDefinition> presets = [
|
||||
standard,
|
||||
standard2x,
|
||||
eu,
|
||||
eu2x,
|
||||
];
|
||||
|
||||
static MapRasterEndpointDefinition fromSettings(AppSettings settings) {
|
||||
final preset = MapRasterEndpointPreset.fromId(settings.mapTileEndpointId);
|
||||
switch (preset) {
|
||||
case MapRasterEndpointPreset.standard2x:
|
||||
return standard2x;
|
||||
case MapRasterEndpointPreset.eu:
|
||||
return eu;
|
||||
case MapRasterEndpointPreset.eu2x:
|
||||
return eu2x;
|
||||
case MapRasterEndpointPreset.standard:
|
||||
return standard;
|
||||
}
|
||||
}
|
||||
}
|
||||
const String kMapTileUrlTemplate =
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
|
||||
class MapTileCacheProgress {
|
||||
final int completed;
|
||||
@@ -252,47 +32,17 @@ class MapTileCacheResult {
|
||||
});
|
||||
}
|
||||
|
||||
class CachedTileInfo {
|
||||
final String key;
|
||||
final String host;
|
||||
final String sourceId;
|
||||
final int zoom;
|
||||
final int x;
|
||||
final int y;
|
||||
final int length;
|
||||
|
||||
const CachedTileInfo({
|
||||
required this.key,
|
||||
required this.host,
|
||||
required this.sourceId,
|
||||
required this.zoom,
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.length,
|
||||
});
|
||||
}
|
||||
|
||||
class CachedTileInventory {
|
||||
final List<CachedTileInfo> tiles;
|
||||
final int totalBytes;
|
||||
|
||||
const CachedTileInventory({required this.tiles, required this.totalBytes});
|
||||
}
|
||||
|
||||
class MapTileCacheService extends ChangeNotifier {
|
||||
class MapTileCacheService {
|
||||
static const String cacheKey = 'map_tile_cache';
|
||||
static const String userAgentPackageName = 'com.meshcore.open';
|
||||
static const int defaultMinZoom = 10;
|
||||
static const int defaultMaxZoom = 15;
|
||||
|
||||
final AppSettingsService appSettingsService;
|
||||
final BaseCacheManager cacheManager;
|
||||
late final TileProvider tileProvider;
|
||||
|
||||
MapTileCacheService({
|
||||
required this.appSettingsService,
|
||||
BaseCacheManager? cacheManager,
|
||||
}) : cacheManager =
|
||||
MapTileCacheService({BaseCacheManager? cacheManager})
|
||||
: cacheManager =
|
||||
cacheManager ??
|
||||
CacheManager(
|
||||
Config(
|
||||
@@ -302,203 +52,16 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
),
|
||||
) {
|
||||
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
|
||||
appSettingsService.addListener(_handleSettingsChanged);
|
||||
}
|
||||
|
||||
MapRasterSourceDefinition get source {
|
||||
final selectedSource = MapRasterSourceCatalog.fromSettings(
|
||||
appSettingsService.settings,
|
||||
);
|
||||
if (!selectedSource.allowsBulkDownload ||
|
||||
!appSettingsService.settings.usesstadiaDemo) {
|
||||
return selectedSource;
|
||||
}
|
||||
return MapRasterSourceDefinition(
|
||||
id: selectedSource.id,
|
||||
label: selectedSource.label,
|
||||
description: selectedSource.description,
|
||||
isStadia: selectedSource.isStadia,
|
||||
allowsBulkDownload: false, // Explicitly disable bulk download for demo.
|
||||
);
|
||||
}
|
||||
|
||||
MapRasterEndpointDefinition get endpoint =>
|
||||
MapRasterEndpointCatalog.fromSettings(appSettingsService.settings);
|
||||
|
||||
String get urlTemplate => _buildUrlTemplate(appSettingsService.settings);
|
||||
|
||||
TileBuilder? get tileBuilder => null;
|
||||
|
||||
static bool shouldApplyDarkFilterForSettings(
|
||||
AppSettings settings,
|
||||
Brightness brightness,
|
||||
) {
|
||||
switch (MapRasterSourcePreset.fromId(settings.mapRasterSourceId)) {
|
||||
case MapRasterSourcePreset.osmDark:
|
||||
case MapRasterSourcePreset.outdoorsDark:
|
||||
case MapRasterSourcePreset.osmBrightDark:
|
||||
return true;
|
||||
case MapRasterSourcePreset.osmAuto:
|
||||
return brightness == Brightness.dark;
|
||||
case MapRasterSourcePreset.osmStandard:
|
||||
case MapRasterSourcePreset.stamenTerrain:
|
||||
case MapRasterSourcePreset.alidadeSmoothDark:
|
||||
case MapRasterSourcePreset.outdoors:
|
||||
case MapRasterSourcePreset.osmBright:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static const ColorFilter _darkMapFilter = ColorFilter.matrix([
|
||||
-0.0850,
|
||||
-0.2861,
|
||||
-0.0289,
|
||||
0,
|
||||
120,
|
||||
-0.0957,
|
||||
-0.3218,
|
||||
-0.0325,
|
||||
0,
|
||||
140,
|
||||
-0.1169,
|
||||
-0.3934,
|
||||
-0.0397,
|
||||
0,
|
||||
170,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
CacheManager get _concreteCacheManager => cacheManager as CacheManager;
|
||||
|
||||
Map<String, String> get defaultHeaders => {
|
||||
'User-Agent': 'flutter_map ($userAgentPackageName)',
|
||||
};
|
||||
|
||||
Widget buildTileLayer(BuildContext context, {double opacity = 1}) {
|
||||
Widget layer = TileLayer(
|
||||
urlTemplate: urlTemplate,
|
||||
tileProvider: tileProvider,
|
||||
tileBuilder: tileBuilder,
|
||||
userAgentPackageName: userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
);
|
||||
|
||||
final shouldApplyDarkFilter = shouldApplyDarkFilterForSettings(
|
||||
appSettingsService.settings,
|
||||
Theme.of(context).brightness,
|
||||
);
|
||||
|
||||
if (shouldApplyDarkFilter) {
|
||||
layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer);
|
||||
}
|
||||
|
||||
if (opacity < 1) {
|
||||
layer = Opacity(opacity: opacity, child: layer);
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
Future<void> clearCache() async {
|
||||
await cacheManager.emptyCache();
|
||||
}
|
||||
|
||||
Future<CachedTileInventory> getCachedTileInventory() async {
|
||||
final repo = _concreteCacheManager.config.repo;
|
||||
await repo.open();
|
||||
final objects = await repo.getAllObjects();
|
||||
final tiles = <CachedTileInfo>[];
|
||||
int totalBytes = 0;
|
||||
|
||||
for (final object in objects) {
|
||||
totalBytes += object.length ?? 0;
|
||||
final tile = _parseCachedTile(object);
|
||||
if (tile != null) {
|
||||
tiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
return CachedTileInventory(tiles: tiles, totalBytes: totalBytes);
|
||||
}
|
||||
|
||||
List<CachedTileInfo> filterTilesForActiveSource(
|
||||
Iterable<CachedTileInfo> tiles,
|
||||
) {
|
||||
final activeSource = source;
|
||||
if (!activeSource.isStadia) {
|
||||
return tiles
|
||||
.where(
|
||||
(tile) =>
|
||||
tile.sourceId == MapRasterSourceCatalog.osmStandard.id &&
|
||||
tile.host == 'tile.openstreetmap.org',
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final activeEndpoint = endpoint;
|
||||
return tiles
|
||||
.where(
|
||||
(tile) =>
|
||||
tile.sourceId == activeSource.id &&
|
||||
tile.host == activeEndpoint.host,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
int countTilesForBounds(
|
||||
Iterable<CachedTileInfo> tiles, {
|
||||
LatLngBounds? bounds,
|
||||
required int minZoom,
|
||||
required int maxZoom,
|
||||
}) {
|
||||
if (bounds == null) return 0;
|
||||
final safeMin = math.min(minZoom, maxZoom);
|
||||
final safeMax = math.max(minZoom, maxZoom);
|
||||
return tiles.where((tile) {
|
||||
if (tile.zoom < safeMin || tile.zoom > safeMax) {
|
||||
return false;
|
||||
}
|
||||
final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom);
|
||||
return _boundsIntersect(bounds, tileBounds);
|
||||
}).length;
|
||||
}
|
||||
|
||||
List<Polygon> buildCachedTilePolygons(
|
||||
Iterable<CachedTileInfo> tiles, {
|
||||
required int zoom,
|
||||
LatLngBounds? visibleBounds,
|
||||
int limit = 250,
|
||||
}) {
|
||||
final polygons = <Polygon>[];
|
||||
for (final tile in tiles) {
|
||||
if (tile.zoom != zoom) continue;
|
||||
final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom);
|
||||
if (visibleBounds != null &&
|
||||
!_boundsIntersect(visibleBounds, tileBounds)) {
|
||||
continue;
|
||||
}
|
||||
polygons.add(
|
||||
Polygon(
|
||||
points: [
|
||||
tileBounds.northWest,
|
||||
tileBounds.northEast,
|
||||
tileBounds.southEast,
|
||||
tileBounds.southWest,
|
||||
],
|
||||
borderStrokeWidth: 0.6,
|
||||
color: const Color(0x5532A852),
|
||||
borderColor: const Color(0xCC2F8F46),
|
||||
),
|
||||
);
|
||||
if (polygons.length >= limit) break;
|
||||
}
|
||||
return polygons;
|
||||
}
|
||||
|
||||
int estimateTileCount(LatLngBounds bounds, int minZoom, int maxZoom) {
|
||||
final safeMin = math.min(minZoom, maxZoom);
|
||||
final safeMax = math.max(minZoom, maxZoom);
|
||||
@@ -526,7 +89,6 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
final total = estimateTileCount(bounds, safeMin, safeMax);
|
||||
final authHeaders = headers ?? defaultHeaders;
|
||||
final safeConcurrency = math.max(1, concurrentDownloads);
|
||||
final currentTemplate = urlTemplate;
|
||||
int completed = 0;
|
||||
int failed = 0;
|
||||
|
||||
@@ -562,7 +124,7 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
final tileBounds = _tileBoundsForBounds(bounds, zoom);
|
||||
for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) {
|
||||
for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) {
|
||||
final url = _buildTileUrl(x, y, zoom, urlTemplate: currentTemplate);
|
||||
final url = _buildTileUrl(x, y, zoom);
|
||||
await queueDownload(url);
|
||||
}
|
||||
}
|
||||
@@ -605,16 +167,6 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSettingsChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
appSettingsService.removeListener(_handleSettingsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) {
|
||||
final north = _clampLatitude(bounds.north);
|
||||
final south = _clampLatitude(bounds.south);
|
||||
@@ -633,56 +185,6 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
String _buildUrlTemplate(AppSettings settings) {
|
||||
final source = MapRasterSourceCatalog.fromSettings(settings);
|
||||
if (!source.isStadia) {
|
||||
return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
}
|
||||
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
|
||||
final apiKey = settings.effectiveMapTileApiKey;
|
||||
final base =
|
||||
'https://${endpoint.host}/tiles/${source.id}/{z}/{x}/{y}${endpoint.scaleSuffix}.png';
|
||||
final query = Uri(queryParameters: {'api_key': apiKey}).query;
|
||||
return '$base?$query';
|
||||
}
|
||||
|
||||
CachedTileInfo? _parseCachedTile(CacheObject object) {
|
||||
final uri = Uri.tryParse(object.key);
|
||||
if (uri == null) return null;
|
||||
final segments = uri.pathSegments;
|
||||
|
||||
if (segments.length >= 3 &&
|
||||
segments[segments.length - 3].isNotEmpty &&
|
||||
segments[segments.length - 2].isNotEmpty) {
|
||||
final zoom = int.tryParse(segments[segments.length - 3]);
|
||||
final x = int.tryParse(segments[segments.length - 2]);
|
||||
final ySegment = segments.last;
|
||||
final yString = ySegment.split('.').first.replaceAll('@2x', '');
|
||||
final y = int.tryParse(yString);
|
||||
|
||||
if (zoom == null || x == null || y == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceId = MapRasterSourceCatalog.osmStandard.id;
|
||||
if (segments.length >= 5 && segments[0] == 'tiles') {
|
||||
sourceId = segments[1];
|
||||
}
|
||||
|
||||
return CachedTileInfo(
|
||||
key: object.key,
|
||||
host: uri.host,
|
||||
sourceId: sourceId,
|
||||
zoom: zoom,
|
||||
x: x,
|
||||
y: y,
|
||||
length: object.length ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int _lonToTileX(double lon, int zoom, int maxIndex) {
|
||||
final n = 1 << zoom;
|
||||
final value = ((lon + 180.0) / 360.0 * n).floor();
|
||||
@@ -703,34 +205,8 @@ class MapTileCacheService extends ChangeNotifier {
|
||||
return lat.clamp(-maxLat, maxLat);
|
||||
}
|
||||
|
||||
LatLngBounds _tileBoundsForTile(int x, int y, int zoom) {
|
||||
return LatLngBounds.unsafe(
|
||||
north: _tileYToLat(y, zoom),
|
||||
south: _tileYToLat(y + 1, zoom),
|
||||
east: _tileXToLon(x + 1, zoom),
|
||||
west: _tileXToLon(x, zoom),
|
||||
);
|
||||
}
|
||||
|
||||
double _tileXToLon(int x, int zoom) {
|
||||
final n = 1 << zoom;
|
||||
return x / n * 360.0 - 180.0;
|
||||
}
|
||||
|
||||
double _tileYToLat(int y, int zoom) {
|
||||
final n = math.pi - (2.0 * math.pi * y) / (1 << zoom);
|
||||
return 180.0 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n)));
|
||||
}
|
||||
|
||||
bool _boundsIntersect(LatLngBounds a, LatLngBounds b) {
|
||||
return a.west <= b.east &&
|
||||
a.east >= b.west &&
|
||||
a.south <= b.north &&
|
||||
a.north >= b.south;
|
||||
}
|
||||
|
||||
String _buildTileUrl(int x, int y, int zoom, {required String urlTemplate}) {
|
||||
return urlTemplate
|
||||
String _buildTileUrl(int x, int y, int zoom) {
|
||||
return kMapTileUrlTemplate
|
||||
.replaceAll('{z}', zoom.toString())
|
||||
.replaceAll('{x}', x.toString())
|
||||
.replaceAll('{y}', y.toString());
|
||||
|
||||
@@ -27,11 +27,6 @@ class NotificationService {
|
||||
|
||||
AppLocalizations get _l10n => lookupAppLocalizations(_locale);
|
||||
|
||||
String _logSafe(String value) {
|
||||
final sanitized = value.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), ' ');
|
||||
return Uri.encodeComponent(sanitized);
|
||||
}
|
||||
|
||||
// Rate limiting to prevent notification storms
|
||||
// (Added after getting notification-flooded while evaluating RF flood management. The irony.)
|
||||
static const _minNotificationInterval = Duration(seconds: 3);
|
||||
@@ -361,11 +356,11 @@ class NotificationService {
|
||||
String _getNotificationIdentifier(_PendingNotification n) {
|
||||
switch (n.type) {
|
||||
case _NotificationType.advert:
|
||||
return _logSafe(n.body);
|
||||
return n.body;
|
||||
case _NotificationType.message:
|
||||
return 'from: ${_logSafe(n.title)}';
|
||||
return 'from: ${n.title}';
|
||||
case _NotificationType.channelMessage:
|
||||
return 'in: ${_logSafe(n.title)}';
|
||||
return 'in: ${n.title}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,7 +606,7 @@ class NotificationService {
|
||||
|
||||
// Show first few device names in batch summary for debugging (only if adverts exist)
|
||||
final deviceInfo = adverts.isNotEmpty
|
||||
? ' (${adverts.take(5).map((n) => _logSafe(n.body)).join(', ')}${adverts.length > 5 ? ', ...' : ''})'
|
||||
? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})'
|
||||
: '';
|
||||
debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo');
|
||||
|
||||
|
||||