Compare commits

..

2 Commits

Author SHA1 Message Date
zjs81 9957e1fc1f Merge pull request #474 from zjs81/dev
Merge Dev into main
2026-06-13 02:14:19 -07:00
zjs81 880df84828 Merge pull request #473 from zjs81/dev
Merge Dev into main
2026-06-13 00:42:09 -07:00
130 changed files with 1181 additions and 11007 deletions
-3
View File
@@ -1,3 +0,0 @@
github: [zjs81]
custom:
- "https://paypal.me/zjs82"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

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>
-93
View File
@@ -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.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -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.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 KiB

+1 -2
View File
@@ -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
+1 -1
View File
@@ -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)
---
-3
View File
@@ -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
-79
View File
@@ -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)
+1 -5
View File
@@ -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 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12
@@ -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
-16
View File
@@ -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>
+52
View File
@@ -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
+34 -46
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
+35 -561
View File
@@ -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 {
+2 -88
View File
@@ -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 -11
View File
@@ -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,
+4 -59
View File
@@ -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 ');
}
}
+9 -15
View File
@@ -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;
-65
View File
@@ -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": {
-93
View File
@@ -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": {
+2 -120
View File
@@ -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.",
-65
View File
@@ -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": {
-65
View File
@@ -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 lutilisation de Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application lutilise 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": {
-65
View File
@@ -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": {
+1 -66
View File
@@ -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.",
-65
View File
@@ -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": {
+1 -66
View File
@@ -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 지연",
+2 -284
View File
@@ -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.
-169
View File
@@ -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';
-170
View File
@@ -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';
+2 -171
View File
@@ -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 =>
-169
View File
@@ -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';
-169
View File
@@ -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 lutilisation 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 lutilise 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';
-169
View File
@@ -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';
+1 -170
View File
@@ -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';
-167
View File
@@ -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での遅延';
+1 -169
View File
@@ -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 지연';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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 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';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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';
+1 -170
View File
@@ -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, штат Техас';
+1 -168
View File
@@ -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 => '洪水(德克萨斯州)延误';
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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 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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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",
+1 -66
View File
@@ -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 延迟",
+3 -20
View File
@@ -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);
}
}
+2 -4
View File
@@ -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>(
-50
View File
@@ -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,
-4
View File
@@ -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)
+5 -17
View File
@@ -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,
);
+12 -3
View File
@@ -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)
+4 -14
View File
@@ -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,
+2 -3
View File
@@ -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
-10
View File
@@ -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(
+21 -342
View File
@@ -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),
),
],
),
);
}
+8 -28
View File
@@ -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}';
+36 -180
View File
@@ -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];
}
}
+52 -135
View File
@@ -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) {
+24 -50
View File
@@ -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);
},
),
+6 -31
View File
@@ -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,
),
),
],
+4 -26
View File
@@ -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,
+7 -3
View File
@@ -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: [
+4 -4
View File
@@ -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)
+6 -130
View File
@@ -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,
),
+152 -264
View File
@@ -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();
});
+2 -2
View File
@@ -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),
+161 -337
View File
@@ -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;
-527
View File
@@ -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 -6
View File
@@ -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 -173
View File
@@ -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(
+4 -140
View File
@@ -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(
+2 -2
View File
@@ -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(
-49
View File
@@ -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));
}
-2
View File
@@ -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;
}
+10 -534
View File
@@ -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());
+4 -9
View File
@@ -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');

Some files were not shown because too many files have changed in this diff Show More