From e4e8bfa4ef0d9fdac8e5680f8c3790b6eb2f9e0a Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sat, 28 Mar 2026 12:20:27 -0400 Subject: [PATCH 01/27] Add additional device name prefixes to MeshCoreUuids --- lib/connector/meshcore_uuids.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index da7f6b5e..cb156b9e 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -7,6 +7,8 @@ class MeshCoreUuids { "MeshCore-", "Whisper-", "WisCore-", + "Seeed", + "Lilygo", "HT-", ]; } From 6b4b2d7ce6bda88601aa7b39642df6e35d9af5c0 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Sat, 4 Apr 2026 19:40:39 -0400 Subject: [PATCH 02/27] Add LowMesh prefix and explain how to add more --- AGENTS.md | 2 +- CLAUDE.md | 2 +- README.md | 13 +++++++++++-- docs/BLE_PROTOCOL.md | 7 ++++++- documentation/ble-protocol.md | 7 ++++++- lib/connector/meshcore_uuids.dart | 1 + 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bac981d4..273bb966 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ ## BLE Frames & Protocol Notes - Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`. -- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`. +- Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`. - Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`. - Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57. - Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25. diff --git a/CLAUDE.md b/CLAUDE.md index 08ef342f..55af890e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ lib/ - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) ### Device Discovery -- Scans for devices with name prefix `MeshCore-` +- Scans for devices with known name prefixes - Filters by `platformName` or `advertisementData.advName` ### Connection States diff --git a/README.md b/README.md index 2f87e919..ac188f6b 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,8 @@ lib/ ├── main.dart # App entry point ├── connector/ │ ├── meshcore_connector.dart # BLE communication & state management -│ └── meshcore_protocol.dart # Protocol definitions & frame parsing +│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing +│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!) ├── screens/ │ ├── scanner_screen.dart # Device scanning (home screen) │ ├── contacts_screen.dart # Contact list @@ -184,7 +185,15 @@ lib/ ### Device Discovery -Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` +Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently: + - `MeshCore-` + - `Whisper-` + - `WisCore-` + - `HT-` + - `LowMesh_MC_` + +New device prefixes can be added in `lib/connector/meshcore_uuids.dart`. + ### Message Format diff --git a/docs/BLE_PROTOCOL.md b/docs/BLE_PROTOCOL.md index 993c3eaa..c17c3e7a 100644 --- a/docs/BLE_PROTOCOL.md +++ b/docs/BLE_PROTOCOL.md @@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u ### Connection Flow -1. **Scan** for devices with name prefix `MeshCore-` +1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`): + - `MeshCore-` + - `Whisper-` + - `WisCore-` + - `HT-` + - `LowMesh_MC_` 2. **Connect** with 15-second timeout 3. **Request MTU** of 185 bytes (falls back to default if unsupported) 4. **Discover services** and locate NUS characteristics diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md index 9f4c1d7a..ec240948 100644 --- a/documentation/ble-protocol.md +++ b/documentation/ble-protocol.md @@ -49,7 +49,12 @@ enum MeshCoreConnectionState { ## BLE Connection Lifecycle -1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]` +1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`): + - `MeshCore-` + - `Whisper-` + - `WisCore-` + - `HT-` + - `LowMesh_MC_` 2. **Connect** with 15-second timeout 3. **Request MTU** 185 bytes (non-web only) 4. **Discover services** and locate NUS diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index da7f6b5e..37e726f7 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -8,5 +8,6 @@ class MeshCoreUuids { "Whisper-", "WisCore-", "HT-", + "LowMesh_MC_", ]; } From 7633327f45a1779d7b199415508fc86c9854fe1e Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 5 Apr 2026 14:06:23 +0200 Subject: [PATCH 03/27] Previously, the merge only preserved path override fields and could overwrite existing GPS with null when the incoming frame had 0,0 coordinates. Now it also preserves prior coordinates when the incoming update omits location. --- lib/connector/meshcore_connector.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c804340a..a8934f10 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3913,11 +3913,14 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Connector', ); - // CRITICAL: Preserve user's path override when contact is refreshed from device + // Preserve user-selected path settings and previously known GPS when + // refreshed frames omit coordinates (lat/lon encoded as 0,0). _contacts[existingIndex] = contact.copyWith( lastMessageAt: mergedLastMessageAt, pathOverride: existing.pathOverride, // Preserve user's path choice pathOverrideBytes: existing.pathOverrideBytes, + latitude: contact.latitude ?? existing.latitude, + longitude: contact.longitude ?? existing.longitude, ); appLogger.info( From 45658a7612de311b7bb2dba456c09eebfa6d10d9 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Sun, 5 Apr 2026 22:39:20 -0400 Subject: [PATCH 04/27] Understand more kinds of Giphy reference as GIF This adds Giphy page URLs and `media.giphy.com` URLs (with and without protocols) as *accepted* encodings for GIF messages, alongside the `g:` syntax. When someone posts such a URL by itself as a message, it will be rendered inline just like `g:` messages are now. This does not change the encoding that GIF messages are *sent* in; that is still the `g:` syntax. --- lib/screens/chat_screen.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 372e3e7c..ec1116c2 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -601,7 +601,23 @@ class _ChatScreenState extends State { String? _parseGifId(String text) { final trimmed = text.trim(); final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - return match?.group(1); + if (match != null) { + return match.group(1); + } + final directUrlMatch = RegExp( + r'^(?:https?://)?media\.giphy\.com/media/([A-Za-z0-9_-]+)/giphy\.gif$', + ).firstMatch(trimmed); + if (directUrlMatch != null) { + return directUrlMatch.group(1); + } + // Giphy understands page URLs with just the ID, or any string and a + // dash before the ID, and redirects to a page with a dash-separated + // title, a dash, and the ID. IDs in this form *probably* can't + // contain dashes. + final pageMatch = RegExp( + r'^(?:https?://)?giphy\.com/gifs/(?:[^/?]*-)?([A-Za-z0-9_]+)/?$', + ).firstMatch(trimmed); + return pageMatch?.group(1); } void _showGifPicker(BuildContext context) { From 45c9823c6f715493c77be016775e243b870489a5 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Sun, 5 Apr 2026 22:51:48 -0400 Subject: [PATCH 05/27] Escape forward slashes in regexes --- lib/screens/chat_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ec1116c2..398e1b5f 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -605,7 +605,7 @@ class _ChatScreenState extends State { return match.group(1); } final directUrlMatch = RegExp( - r'^(?:https?://)?media\.giphy\.com/media/([A-Za-z0-9_-]+)/giphy\.gif$', + r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$', ).firstMatch(trimmed); if (directUrlMatch != null) { return directUrlMatch.group(1); @@ -615,7 +615,7 @@ class _ChatScreenState extends State { // title, a dash, and the ID. IDs in this form *probably* can't // contain dashes. final pageMatch = RegExp( - r'^(?:https?://)?giphy\.com/gifs/(?:[^/?]*-)?([A-Za-z0-9_]+)/?$', + r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', ).firstMatch(trimmed); return pageMatch?.group(1); } From 75ec3b6116eeab412e08bed116e813544ab0bfa6 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 6 Apr 2026 01:55:50 -0400 Subject: [PATCH 06/27] Centralize GIF parsing in a helper like for reactions --- lib/helpers/gif_helper.dart | 33 ++++++++++++++++++++++++++++ lib/screens/channel_chat_screen.dart | 13 ++++------- lib/screens/chat_screen.dart | 33 +++------------------------- 3 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 lib/helpers/gif_helper.dart diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart new file mode 100644 index 00000000..a223ffc6 --- /dev/null +++ b/lib/helpers/gif_helper.dart @@ -0,0 +1,33 @@ +class GifHelper { + /// Parse a known GIF format, which can be any of: + /// g:GIFID + /// https://media.giphy.com/media/GIFID/giphy.gif + /// https://giphy.com/gifs/Optional-title-with-dashes-GIFID + /// + /// GIFID is a Giphy GIF ID. The https:// is optional (and + /// can also be http://). The giphy.com/gifs form can also + /// include a trailing slash. + /// + /// Returns null if text is not a valid GIF format + static String? parseGifId(String text) { + final trimmed = text.trim(); + final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); + if (match != null) { + return match.group(1); + } + final directUrlMatch = RegExp( + r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$', + ).firstMatch(trimmed); + if (directUrlMatch != null) { + return directUrlMatch.group(1); + } + // Giphy understands page URLs with just the ID, or any string and a + // dash before the ID, and redirects to a page with a dash-separated + // title, a dash, and the ID. IDs in this form *probably* can't + // contain dashes. + final pageMatch = RegExp( + r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', + ).firstMatch(trimmed); + return pageMatch?.group(1); + } +} diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 628ae1cc..131d74c0 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart'; import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; @@ -355,7 +356,7 @@ class _ChannelChatScreenState extends State { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; - final gifId = _parseGifId(message.text); + final gifId = GifHelper.parseGifId(message.text); final poi = _parsePoiMessage(message.text); final translatedDisplayText = message.translatedText != null && @@ -699,7 +700,7 @@ class _ChannelChatScreenState extends State { final colorScheme = Theme.of(context).colorScheme; final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); - final gifId = _parseGifId(replyText); + final gifId = GifHelper.parseGifId(replyText); final poi = _parsePoiMessage(replyText); Widget contentPreview; @@ -811,12 +812,6 @@ class _ChannelChatScreenState extends State { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - return match?.group(1); - } - _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); final match = RegExp( @@ -1053,7 +1048,7 @@ class _ChannelChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = _parseGifId(value.text); + final gifId = GifHelper.parseGifId(value.text); if (gifId != null) { return Focus( autofocus: true, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 398e1b5f..daba56b0 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/path_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; @@ -523,7 +524,7 @@ class _ChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = _parseGifId(value.text); + final gifId = GifHelper.parseGifId(value.text); if (gifId != null) { return Focus( autofocus: true, @@ -598,28 +599,6 @@ class _ChatScreenState extends State { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - if (match != null) { - return match.group(1); - } - final directUrlMatch = RegExp( - r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$', - ).firstMatch(trimmed); - if (directUrlMatch != null) { - return directUrlMatch.group(1); - } - // Giphy understands page URLs with just the ID, or any string and a - // dash before the ID, and redirects to a page with a dash-separated - // title, a dash, and the ID. IDs in this form *probably* can't - // contain dashes. - final pageMatch = RegExp( - r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', - ).firstMatch(trimmed); - return pageMatch?.group(1); - } - void _showGifPicker(BuildContext context) { showModalBottomSheet( context: context, @@ -1589,7 +1568,7 @@ class _MessageBubble extends StatelessWidget { final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; - final gifId = _parseGifId(message.text); + final gifId = GifHelper.parseGifId(message.text); final poi = _parsePoiMessage(message.text); final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed @@ -1863,12 +1842,6 @@ class _MessageBubble extends StatelessWidget { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - return match?.group(1); - } - _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); final match = RegExp( From c5ec60638cff225af4cfcfc40d5fc378236650ff Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 6 Apr 2026 02:09:40 -0400 Subject: [PATCH 07/27] Put reaction and GIF helpers in charge of encoding --- lib/helpers/gif_helper.dart | 7 ++++++- lib/helpers/reaction_helper.dart | 5 +++++ lib/screens/channel_chat_screen.dart | 10 +++++----- lib/screens/chat_screen.dart | 8 ++++---- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index a223ffc6..8dd187b1 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -9,7 +9,7 @@ class GifHelper { /// include a trailing slash. /// /// Returns null if text is not a valid GIF format - static String? parseGifId(String text) { + static String? parseGif(String text) { final trimmed = text.trim(); final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); if (match != null) { @@ -30,4 +30,9 @@ class GifHelper { ).firstMatch(trimmed); return pageMatch?.group(1); } + + /// Encode a GIF in a format that parseGif() can parse. + static String encodeGif(String gifId) { + return 'g:$gifId'; + } } diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 90733c3a..169b1a14 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -109,4 +109,9 @@ class ReactionHelper { return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); } + + /// Encode a reaction message that parseReaction() can parse. + static String encodeReaction(String hash, String emojiIndex) { + return 'r:$hash:$emojiIndex'; + } } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 131d74c0..7beaaf4c 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -356,7 +356,7 @@ class _ChannelChatScreenState extends State { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; - final gifId = GifHelper.parseGifId(message.text); + final gifId = GifHelper.parseGif(message.text); final poi = _parsePoiMessage(message.text); final translatedDisplayText = message.translatedText != null && @@ -700,7 +700,7 @@ class _ChannelChatScreenState extends State { final colorScheme = Theme.of(context).colorScheme; final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); - final gifId = GifHelper.parseGifId(replyText); + final gifId = GifHelper.parseGif(replyText); final poi = _parsePoiMessage(replyText); Widget contentPreview; @@ -892,7 +892,7 @@ class _ChannelChatScreenState extends State { isScrollControlled: true, builder: (context) => GifPicker( onGifSelected: (gifId) { - _textController.text = 'g:$gifId'; + _textController.text = GifHelper.encodeGif(gifId); }, ), ); @@ -1048,7 +1048,7 @@ class _ChannelChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = GifHelper.parseGifId(value.text); + final gifId = GifHelper.parseGif(value.text); if (gifId != null) { return Focus( autofocus: true, @@ -1316,7 +1316,7 @@ class _ChannelChatScreenState extends State { message.senderName, message.text, ); - final reactionText = 'r:$hash:$emojiIndex'; + final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex); connector.sendChannelMessage(widget.channel, reactionText); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index daba56b0..8057f1f5 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -524,7 +524,7 @@ class _ChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = GifHelper.parseGifId(value.text); + final gifId = GifHelper.parseGif(value.text); if (gifId != null) { return Focus( autofocus: true, @@ -605,7 +605,7 @@ class _ChatScreenState extends State { isScrollControlled: true, builder: (context) => GifPicker( onGifSelected: (gifId) { - _textController.text = 'g:$gifId'; + _textController.text = GifHelper.encodeGif(gifId); }, ), ); @@ -1538,7 +1538,7 @@ class _ChatScreenState extends State { senderName, message.text, ); - final reactionText = 'r:$hash:$emojiIndex'; + final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex); connector.sendMessage(_resolveContact(connector), reactionText); } } @@ -1568,7 +1568,7 @@ class _MessageBubble extends StatelessWidget { final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; - final gifId = GifHelper.parseGifId(message.text); + final gifId = GifHelper.parseGif(message.text); final poi = _parsePoiMessage(message.text); final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed From 08ffb978cf32fe31caef024c345fd97be6b60782 Mon Sep 17 00:00:00 2001 From: Zach Date: Mon, 6 Apr 2026 14:26:42 -0700 Subject: [PATCH 08/27] fix: gif trnslat --- lib/services/translation_service.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/services/translation_service.dart b/lib/services/translation_service.dart index f8147a11..7d76efab 100644 --- a/lib/services/translation_service.dart +++ b/lib/services/translation_service.dart @@ -6,6 +6,7 @@ import 'package:llamadart/llamadart.dart'; import '../models/app_settings.dart'; import '../models/translation_support.dart'; +import '../helpers/gif_helper.dart'; import '../utils/app_logger.dart'; import 'app_settings_service.dart'; import 'translation_file_store.dart'; @@ -509,8 +510,10 @@ class TranslationService extends ChangeNotifier { if (trimmed.isEmpty) { return false; } - return !(trimmed.startsWith('g:') || - trimmed.startsWith('m:') || + if (GifHelper.parseGif(trimmed) != null) { + return false; + } + return !(trimmed.startsWith('m:') || trimmed.startsWith('V1|') || trimmed.startsWith('r:')); } From 4ad01ed43cd3727999306bb4ba9fdc7ff31df139 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Tue, 7 Apr 2026 12:30:06 -0700 Subject: [PATCH 09/27] init contributing.md --- CONTRIBUTING.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ac727bad --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# How to contribute to Meshcore Open + +Before submitting any pull requests (PR), please review the following information. + +Unsolicited PRs without previous discussion or open issues may be +rejected. As may changes that are too broad (i.e. 100 files changed) or that +cover too many separate changes. If the changes are clearly AI generated they +may also be rejected. [See more](#ai-use) + +## First Step Checklist + +### **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new). +Be sure to include a **title and clear description**, as much relevant +information as possible, and a **code sample** or an **executable test case** +demonstrating the expected behavior that is not occurring. You can also include +screenshots or video. + +* DO NOT start work and submit a PR at this time, please discuss the issue and +your implementation plan first. + +### **Did you fix whitespace, format code, or make a purely cosmetic patch?** + +Changes that are cosmetic in nature and do not add anything substantial to the +stability, functionality, or testability of the application will generally not +be accepted. + +### **Do you intend to add a new feature or change an existing one?** + +* Suggest your change in a new issue as a feature request. + +* DO NOT start work and submit a PR at this time, please discuss the change and +your implementation plan first. + +* After it is generally decided that the feature or change fits the goals of the +project you can start work or open a PR if you have already started. + +## Submitting your patch + +* All changes should be based on the `dev` branch. When creating your PR please +be sure to change the target to merge into dev, and when starting work on a new +branch be sure to start on latest `dev`. + +* Ensure the PR description clearly describes the problem and solution. Include +the relevant issue number if applicable. + +* The PR should contain **one commit** only, the commit message should have a +clear title followed by a new line and then brief description if needed. PR with +multiple commits will be squashed into one before merging if required. See +[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more +information on good commit messages. + +* **Before committing changes** on your branch, be sure to run both +`dart format .` and `flutter analyze`. The continuous development checks will +fail if issues here are not addressed before hand. + +## AI-use + +Everyone loves some help, AI agents are a tool in many of our belts. The project +is not anti-AI. + +There are some limits to acceptable use however. Generally: + +* All code generated by AI should be thoroughly reviewed by the contributor. +* The changes should be tightly controlled to not change anything out of scope +for the patch, bug fix, etc. +* The contributor should have a good understanding of what the code does and how +the application works in order to effectively be able to manage the agent. From 4879b136f8ba56245dcf655e28bc8a3f9fdf89b3 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 10/27] Refactor contact handling and other improvments (#317) * Refactor contact filtering and improve localization strings; enhance path trace handling * Add localization for new CLI commands and update existing strings * Enhance contact handling and UI updates across multiple screens add unfiltered contact access and improve last seen resolution * Add polling interval configuration and improve contact handling * Reorder command constants for better organization and clarity * Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens * Moved RadioStatsIconButton in chat screen for improved UI consistency * Added indicators to AppBar for channels * Ignore contacts with self public key in contact handling * Simplify path removal logic and clean up unused imports in path management dialog * Enhance path hop resolution by adding distance checks to improve candidate selection accuracy * Remove unnecessary reset of radio stats poll reference count in polling interval setter --- lib/connector/meshcore_connector.dart | 45 ++++++++++-- lib/connector/meshcore_protocol.dart | 4 +- lib/l10n/app_bg.arb | 8 ++- lib/l10n/app_de.arb | 9 ++- lib/l10n/app_en.arb | 10 ++- lib/l10n/app_es.arb | 7 +- lib/l10n/app_fr.arb | 8 ++- lib/l10n/app_hu.arb | 7 +- lib/l10n/app_it.arb | 9 ++- lib/l10n/app_ja.arb | 10 ++- lib/l10n/app_ko.arb | 7 +- lib/l10n/app_localizations.dart | 12 ++++ lib/l10n/app_localizations_bg.dart | 6 ++ lib/l10n/app_localizations_de.dart | 6 ++ lib/l10n/app_localizations_en.dart | 6 ++ lib/l10n/app_localizations_es.dart | 6 ++ lib/l10n/app_localizations_fr.dart | 6 ++ lib/l10n/app_localizations_hu.dart | 6 ++ lib/l10n/app_localizations_it.dart | 6 ++ lib/l10n/app_localizations_ja.dart | 6 ++ lib/l10n/app_localizations_ko.dart | 6 ++ lib/l10n/app_localizations_nl.dart | 8 ++- lib/l10n/app_localizations_pl.dart | 6 ++ lib/l10n/app_localizations_pt.dart | 6 ++ lib/l10n/app_localizations_ru.dart | 6 ++ lib/l10n/app_localizations_sk.dart | 6 ++ lib/l10n/app_localizations_sl.dart | 6 ++ lib/l10n/app_localizations_sv.dart | 6 ++ lib/l10n/app_localizations_uk.dart | 6 ++ lib/l10n/app_localizations_zh.dart | 6 ++ lib/l10n/app_nl.arb | 9 ++- lib/l10n/app_pl.arb | 10 ++- lib/l10n/app_pt.arb | 9 ++- lib/l10n/app_ru.arb | 10 ++- lib/l10n/app_sk.arb | 7 +- lib/l10n/app_sl.arb | 9 ++- lib/l10n/app_sv.arb | 10 ++- lib/l10n/app_uk.arb | 10 ++- lib/l10n/app_zh.arb | 7 +- lib/screens/channel_message_path_screen.dart | 18 ++++- lib/screens/channels_screen.dart | 2 +- lib/screens/chat_screen.dart | 2 +- lib/screens/companion_radio_stats_screen.dart | 2 + lib/screens/contacts_screen.dart | 19 ++--- lib/screens/discovery_screen.dart | 62 +++++++++++++++-- lib/screens/map_screen.dart | 69 +++++++++++++------ lib/screens/path_trace_map.dart | 50 +++++++++++--- lib/screens/repeater_cli_screen.dart | 8 ++- lib/utils/gpx_export.dart | 34 ++++++--- lib/widgets/repeater_login_dialog.dart | 2 +- lib/widgets/room_login_dialog.dart | 2 +- 51 files changed, 488 insertions(+), 109 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a8934f10..a436b469 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -196,6 +196,7 @@ class MeshCoreConnector extends ChangeNotifier { static const int _contactMsgBackoffFallbackMs = 5000; static const int _contactMsgBackoffMinMs = 500; static const int _contactMsgBackoffMaxMs = 15000; + int _pollingInterval = 30; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -326,8 +327,14 @@ class MeshCoreConnector extends ChangeNotifier { List get allContacts => List.unmodifiable([ ..._contacts, - ..._discoveredContacts.where((c) => !c.isActive), + ..._discoveredContacts.where( + (c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex, + ), ]); + + List get allContactsUnfiltered => + List.unmodifiable([..._contacts, ..._discoveredContacts]); + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -2368,9 +2375,18 @@ class MeshCoreConnector extends ChangeNotifier { _batteryPollTimer = null; } + void setPollingInterval(int i) { + _pollingInterval = i.clamp(1, 60); + if (isConnected) { + _startRadioStatsPolling(); + } + } + void _startRadioStatsPolling() { _radioStatsPollTimer?.cancel(); - _radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), ( + _, + ) { if (!isConnected) { _stopRadioStatsPolling(); return; @@ -2495,6 +2511,18 @@ class MeshCoreConnector extends ChangeNotifier { }); } + Contact getFromDiscovered(Contact contact) { + final tmp = _discoveredContacts.firstWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + orElse: () => contact, + ); + return contact.copyWith( + rawPacket: tmp.rawPacket, + latitude: tmp.latitude, + longitude: tmp.longitude, + ); + } + Future getContacts({int? since, bool preserveExisting = false}) async { if (!isConnected) return; @@ -3885,8 +3913,17 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleContact(Uint8List frame, {bool isContact = true}) { - final contact = Contact.fromFrame(frame); - if (contact != null) { + final contactTmp = Contact.fromFrame(frame); + if (contactTmp != null) { + if (listEquals(contactTmp.publicKey, _selfPublicKey)) { + appLogger.info( + 'Ignoring contact with self public key: ${contactTmp.name}', + tag: 'Connector', + ); + removeContact(contactTmp); + return; + } + final contact = getFromDiscovered(contactTmp); _handleDiscovery(contact, frame, noNotify: true, addActive: true); if (contact.type == advTypeRepeater) { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index b42e3e5c..396d78b3 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -202,15 +202,15 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdSendAnonReq = 57; const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdGetStats = 56; +const int cmdSendAnonReq = 57; const int cmdSetAutoAddConfig = 58; const int cmdGetAutoAddConfig = 59; const int cmdSetPathHashMode = 61; -const int cmdGetStats = 56; // Text message types const int txtTypePlain = 0; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 13e9de7e..cd822e3e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2059,5 +2059,9 @@ "translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.", "translation_translateTo": "Превеждане на {language}", "translation_translationOptions": "Опции за превод", - "translation_systemLanguage": "Език на системата" -} + "translation_systemLanguage": "Език на системата", + "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth", + "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма).", + "repeater_cliQuickClockSync": "Синхронизация на часовника", + "repeater_cliQuickDiscovery": "Открий Съседи" +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 62badcef..10af5dac 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2087,5 +2087,10 @@ "translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.", "translation_translateTo": "Übersetzen Sie auf {language}", "translation_translationOptions": "Übersetzungsmöglichkeiten", - "translation_systemLanguage": "Sprache des Systems" -} + "translation_systemLanguage": "Sprache des Systems", + "scanner_linuxPairingHidePin": "PIN ausblenden", + "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", + "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", + "repeater_cliQuickClockSync": "Uhr Synchronisieren", + "repeater_cliQuickDiscovery": "Entdecke Nachbarn" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 06175532..b703630e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -303,8 +303,12 @@ "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { - "weight": { "type": "String" }, - "max": { "type": "String" } + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } } }, "appSettings_battery": "Battery", @@ -1333,6 +1337,8 @@ "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Advertise", "repeater_cliQuickClock": "Clock", + "repeater_cliQuickClockSync": "Clock Sync", + "repeater_cliQuickDiscovery": "Discover Neighbors", "repeater_cliHelpAdvert": "Sends an advertisement packet", "repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)", "repeater_cliHelpClock": "Displays current time per device's clock.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4d465bb9..0372dffc 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2087,5 +2087,8 @@ "translation_translateBeforeSending": "Traducir antes de enviar", "translation_translateTo": "Traducir a {language}", "translation_translationOptions": "Opciones de traducción", - "translation_systemLanguage": "Idioma del sistema" -} + "translation_systemLanguage": "Idioma del sistema", + "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).", + "repeater_cliQuickDiscovery": "Descubrir Vecinos", + "repeater_cliQuickClockSync": "Sincronización del reloj" +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 16e1d3db..d74c3588 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2059,5 +2059,9 @@ "translation_messageTranslation": "Traduction du message", "translation_translateTo": "Traduire en {language}", "translation_translationOptions": "Options de traduction", - "translation_systemLanguage": "Langue du système" -} + "translation_systemLanguage": "Langue du système", + "scanner_linuxPairingPinTitle": "Code PIN d’appairage Bluetooth", + "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun).", + "repeater_cliQuickClockSync": "Synchronisation de l'horloge", + "repeater_cliQuickDiscovery": "Découvrir les voisins" +} \ No newline at end of file diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index cf42e1b0..68b3b11a 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2097,5 +2097,8 @@ "translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.", "translation_translateTo": "Fordítás {language}-ra", "translation_translationOptions": "Fordítási lehetőségek", - "translation_systemLanguage": "Rendszer nyelvé" -} + "translation_systemLanguage": "Rendszer nyelvé", + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", + "repeater_cliQuickClockSync": "Óra szinkronizálás", + "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b9676bbd..9b539a05 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2059,5 +2059,10 @@ "translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.", "translation_translateTo": "Tradurre in {language}", "translation_translationOptions": "Opzioni di traduzione", - "translation_systemLanguage": "Lingua del sistema" -} + "translation_systemLanguage": "Lingua del sistema", + "scanner_linuxPairingHidePin": "Nascondi PIN", + "scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth", + "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è).", + "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", + "repeater_cliQuickDiscovery": "Scopri i Vicini" +} \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 6a9c975c..aef8fc05 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2097,5 +2097,11 @@ "translation_composerDisabledHint": "元のタイプされた言語でメッセージを送信してください。", "translation_translateTo": "{language} への翻訳", "translation_translationOptions": "翻訳の選択肢", - "translation_systemLanguage": "システム言語" -} + "translation_systemLanguage": "システム言語", + "scanner_linuxPairingShowPin": "PINを表示", + "scanner_linuxPairingHidePin": "PINを非表示", + "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", + "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", + "repeater_cliQuickClockSync": "クロック同期", + "repeater_cliQuickDiscovery": "近隣を発見する" +} \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 2050e3b2..66ad1ed9 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2097,5 +2097,8 @@ "translation_composerDisabledHint": "원래 작성된 언어로 메시지를 보내세요.", "translation_translateTo": "{language} 번역", "translation_translationOptions": "번역 옵션", - "translation_systemLanguage": "시스템 언어" -} + "translation_systemLanguage": "시스템 언어", + "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).", + "repeater_cliQuickClockSync": "시계 동기화", + "repeater_cliQuickDiscovery": "이웃 발견하기" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e2bd2f3f..408a2436 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4322,6 +4322,18 @@ abstract class AppLocalizations { /// **'Clock'** String get repeater_cliQuickClock; + /// No description provided for @repeater_cliQuickClockSync. + /// + /// In en, this message translates to: + /// **'Clock Sync'** + String get repeater_cliQuickClockSync; + + /// No description provided for @repeater_cliQuickDiscovery. + /// + /// In en, this message translates to: + /// **'Discover Neighbors'** + String get repeater_cliQuickDiscovery; + /// No description provided for @repeater_cliHelpAdvert. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 283860e1..8a433224 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliQuickClock => 'Часовник'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация на часовника'; + + @override + String get repeater_cliQuickDiscovery => 'Открий Съседи'; + @override String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e29ae9e9..1177bc12 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliQuickClock => 'Uhr'; + @override + String get repeater_cliQuickClockSync => 'Uhr Synchronisieren'; + + @override + String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn'; + @override String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 877e11d1..9104f8b7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2379,6 +2379,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliQuickClock => 'Clock'; + @override + String get repeater_cliQuickClockSync => 'Clock Sync'; + + @override + String get repeater_cliQuickDiscovery => 'Discover Neighbors'; + @override String get repeater_cliHelpAdvert => 'Sends an advertisement packet'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c9639028..cc3b7140 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliQuickClock => 'Reloj'; + @override + String get repeater_cliQuickClockSync => 'Sincronización del reloj'; + + @override + String get repeater_cliQuickDiscovery => 'Descubrir Vecinos'; + @override String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eea88f51..402e3738 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2442,6 +2442,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliQuickClock => 'Horloge'; + @override + String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge'; + + @override + String get repeater_cliQuickDiscovery => 'Découvrir les voisins'; + @override String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5e36e94a..204e21ba 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2437,6 +2437,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliQuickClock => 'óra'; + @override + String get repeater_cliQuickClockSync => 'Óra szinkronizálás'; + + @override + String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat'; + @override String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index bb9e0d25..936ecc16 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2426,6 +2426,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Orologio'; + @override + String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio'; + + @override + String get repeater_cliQuickDiscovery => 'Scopri i Vicini'; + @override String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5151ab84..7accee3e 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2322,6 +2322,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliQuickClock => '時計'; + @override + String get repeater_cliQuickClockSync => 'クロック同期'; + + @override + String get repeater_cliQuickDiscovery => '近隣を発見する'; + @override String get repeater_cliHelpAdvert => '広告用資料を送る'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index be645456..06d7db63 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2319,6 +2319,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_cliQuickClock => '시계'; + @override + String get repeater_cliQuickClockSync => '시계 동기화'; + + @override + String get repeater_cliQuickDiscovery => '이웃 발견하기'; + @override String get repeater_cliHelpAdvert => '광고 패킷을 발송'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 86809df5..6b7bbe79 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2410,7 +2410,13 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliQuickClock => 'Tijd opvragen'; @override - String get repeater_cliHelpAdvert => 'Advertentie uitzenden'; + String get repeater_cliQuickClockSync => 'Kloksynchronisatie'; + + @override + String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + + @override + String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; @override String get repeater_cliHelpReboot => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 89528158..b6296a4d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2435,6 +2435,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Godzina'; + @override + String get repeater_cliQuickClockSync => 'Synchronizacja zegara'; + + @override + String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów'; + @override String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 43dc27a3..d1f66af2 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Relógio'; + @override + String get repeater_cliQuickClockSync => 'Sincronização do Relógio'; + + @override + String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos'; + @override String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 703d80dd..cb2ae158 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_cliQuickClock => 'Время'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация часов'; + + @override + String get repeater_cliQuickDiscovery => 'Обнаружить Соседей'; + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 980657df..8ddea4b7 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2406,6 +2406,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Hodiny'; + @override + String get repeater_cliQuickClockSync => 'Synchronizácia hodin'; + + @override + String get repeater_cliQuickDiscovery => 'Objaviť susedov'; + @override String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ad2a2788..07c1c01f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2409,6 +2409,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Ura'; + @override + String get repeater_cliQuickClockSync => 'Usklajevanje ure'; + + @override + String get repeater_cliQuickDiscovery => 'Odkrijte sosede'; + @override String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index cc590c29..87457744 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2394,6 +2394,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliQuickClock => 'Klocka'; + @override + String get repeater_cliQuickClockSync => 'Synkronisera klocka'; + + @override + String get repeater_cliQuickDiscovery => 'Upptäck grannar'; + @override String get repeater_cliHelpAdvert => 'Skickar ett annonspaket'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index dd7bf634..fc0abea7 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Годинник'; + @override + String get repeater_cliQuickClockSync => 'Синхронізація годинника'; + + @override + String get repeater_cliQuickDiscovery => 'Відкрити сусідів'; + @override String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8910dcd1..f9ff7099 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2277,6 +2277,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliQuickClock => '时钟'; + @override + String get repeater_cliQuickClockSync => '同步时钟'; + + @override + String get repeater_cliQuickDiscovery => '发现邻居'; + @override String get repeater_cliHelpAdvert => '发送广播包'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cb1a11c0..ac3ddca3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2059,5 +2059,10 @@ "translation_messageTranslation": "Berichtvertaling", "translation_translationOptions": "Opties voor vertaling", "translation_systemLanguage": "Taal van het systeem", - "translation_translateTo": "Vertalen naar {language}" -} + "translation_translateTo": "Vertalen naar {language}", + "scanner_linuxPairingHidePin": "PIN verbergen", + "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", + "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN", + "repeater_cliQuickDiscovery": "Ontdek Buren", + "repeater_cliQuickClockSync": "Kloksynchronisatie" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index aa3049f3..cf530af8 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2097,5 +2097,11 @@ "translation_messageTranslation": "Tłumaczenie wiadomości", "translation_translationOptions": "Opcje tłumaczenia", "translation_systemLanguage": "Język systemu", - "translation_translateTo": "Tłumacz na {language}" -} + "translation_translateTo": "Tłumacz na {language}", + "scanner_linuxPairingShowPin": "Pokaż PIN", + "scanner_linuxPairingHidePin": "Ukryj PIN", + "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", + "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", + "repeater_cliQuickClockSync": "Synchronizacja zegara", + "repeater_cliQuickDiscovery": "Odkryj Sąsiadów" +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c667cb07..f88c5e0d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2059,5 +2059,10 @@ "translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.", "translation_translateTo": "Traduzir para {language}", "translation_translationOptions": "Opções de tradução", - "translation_systemLanguage": "Idioma do sistema" -} + "translation_systemLanguage": "Idioma do sistema", + "scanner_linuxPairingHidePin": "Ocultar PIN", + "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", + "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", + "repeater_cliQuickClockSync": "Sincronização do Relógio", + "repeater_cliQuickDiscovery": "Descobrir Vizinhos" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 730cfc92..13eac229 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1299,5 +1299,11 @@ "translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.", "translation_translateTo": "Перевести на {language}", "translation_translationOptions": "Варианты перевода", - "translation_systemLanguage": "Язык системы" -} + "translation_systemLanguage": "Язык системы", + "scanner_linuxPairingShowPin": "Показать PIN", + "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", + "scanner_linuxPairingHidePin": "Скрыть PIN", + "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", + "repeater_cliQuickDiscovery": "Обнаружить Соседей", + "repeater_cliQuickClockSync": "Синхронизация часов" +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index cf99ca8a..43e408fd 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2059,5 +2059,8 @@ "translation_messageTranslation": "Preklad textu", "translation_translateTo": "Preložte do {language}", "translation_translationOptions": "Možnosti prekladania", - "translation_systemLanguage": "Jazyk systému" -} + "translation_systemLanguage": "Jazyk systému", + "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN", + "repeater_cliQuickClockSync": "Synchronizácia hodin", + "repeater_cliQuickDiscovery": "Objaviť susedov" +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 0c29a862..3ef08b19 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2059,5 +2059,10 @@ "translation_messageTranslation": "Prevod sporočila", "translation_translateTo": "Prevesti v {language}", "translation_translationOptions": "Možnosti prevoda", - "translation_systemLanguage": "Jezik sistema" -} + "translation_systemLanguage": "Jezik sistema", + "scanner_linuxPairingHidePin": "Skrij PIN", + "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", + "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", + "repeater_cliQuickDiscovery": "Odkrijte sosede", + "repeater_cliQuickClockSync": "Usklajevanje ure" +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3232888e..9f317dba 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2059,5 +2059,11 @@ "translation_messageTranslation": "Meddelandets översättning", "translation_translateTo": "Översätt till {language}", "translation_translationOptions": "Översättningsalternativ", - "translation_systemLanguage": "Språk för systemet" -} + "translation_systemLanguage": "Språk för systemet", + "scanner_linuxPairingShowPin": "Visa PIN", + "scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN", + "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", + "scanner_linuxPairingHidePin": "Dölj PIN", + "repeater_cliQuickDiscovery": "Upptäck grannar", + "repeater_cliQuickClockSync": "Synkronisera klocka" +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ddab5769..a0cce7e9 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2059,5 +2059,11 @@ "translation_translateBeforeSending": "Перекладіть перед відправкою", "translation_translateTo": "Перекласти на {language}", "translation_translationOptions": "Варіанти перекладу", - "translation_systemLanguage": "Мова системи" -} + "translation_systemLanguage": "Мова системи", + "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", + "scanner_linuxPairingShowPin": "Показати PIN", + "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", + "scanner_linuxPairingHidePin": "Приховати PIN", + "repeater_cliQuickClockSync": "Синхронізація годинника", + "repeater_cliQuickDiscovery": "Відкрити сусідів" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 766be449..2e19a8ee 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2064,5 +2064,8 @@ "translation_translateBeforeSending": "在发送前进行翻译", "translation_translateTo": "翻译成 {language}", "translation_translationOptions": "翻译选项", - "translation_systemLanguage": "系统语言" -} + "translation_systemLanguage": "系统语言", + "scanner_linuxPairingHidePin": "隐藏 PIN", + "repeater_cliQuickDiscovery": "发现邻居", + "repeater_cliQuickClockSync": "同步时钟" +} \ No newline at end of file diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 94b8eeec..0eb2c220 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops( ) { if (pathBytes.isEmpty) return const []; final candidatesByPrefix = >{}; - for (final contact in connector.allContacts) { + final allContacts = connector.allContacts; + for (final contact in allContacts) { if (contact.publicKey.isEmpty) continue; if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { continue; @@ -839,7 +840,8 @@ List<_PathHop> _buildPathHops( : null; var previousPosition = startPoint; final distance = Distance(); - + var lastDistance = 0.0; + var bestDistance = 0.0; final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { final searchPoint = i == 0 ? startPoint : previousPosition; @@ -848,7 +850,7 @@ List<_PathHop> _buildPathHops( if (candidates != null && candidates.isNotEmpty) { var bestIndex = 0; if (searchPoint != null) { - var bestDistance = double.infinity; + bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; if (!candidate.hasLocation || @@ -876,6 +878,16 @@ List<_PathHop> _buildPathHops( if (resolvedPosition != null) { previousPosition = resolvedPosition; } + // If the best candidate is much farther than the previous hop, it's likely not the correct match. + if (lastDistance + bestDistance > 70000 && + candidates != null && + candidates.isNotEmpty) { + i--; + lastDistance = bestDistance; + continue; + } + lastDistance = bestDistance; + hops.add( _PathHop( index: i + 1, diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d67d03da..51d24533 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -127,7 +127,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title, indicators: false), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8057f1f5..4cda7127 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -294,6 +294,7 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), + const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { return PopupMenuButton( @@ -366,7 +367,6 @@ class _ChatScreenState extends State { ); }, ), - const RadioStatsIconButton(), ], ), body: Consumer( diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 01fb64d8..9c376769 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State { final c = context.read(); _connector = c; c.acquireRadioStatsPolling(); + c.setPollingInterval(1); c.radioStatsNotifier.addListener(_onStatsUpdate); } @@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State { void dispose() { _connector?.radioStatsNotifier.removeListener(_onStatsUpdate); _connector?.releaseRadioStatsPolling(); + _connector?.setPollingInterval(30); super.dispose(); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5b01f27..62a380bf 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1240,9 +1240,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathBytesForDisplay.isNotEmpty - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_ping), onTap: () { final hw = context .read() @@ -1251,11 +1249,8 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathBytesForDisplay.isNotEmpty - ? context.l10n.contacts_repeaterPathTrace - : context.l10n.contacts_repeaterPing, - path: contact.pathBytesForDisplay, - flipPathAround: true, + title: context.l10n.contacts_repeaterPing, + path: Uint8List.fromList([contact.publicKey.first]), targetContact: contact, pathHashByteWidth: hw, ), @@ -1274,9 +1269,7 @@ class _ContactsScreenState extends State ] else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_pathTrace), onTap: () { final hw = context .read() @@ -1288,7 +1281,9 @@ class _ContactsScreenState extends State title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.pathBytesForDisplay, + path: contact.pathBytesForDisplay.isNotEmpty + ? contact.pathBytesForDisplay + : Uint8List.fromList([contact.publicKey.first]), flipPathAround: contact.pathBytesForDisplay.isNotEmpty, targetContact: contact, pathHashByteWidth: hw, diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 4e7c6e8e..3f9d9655 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State { super.dispose(); } + DateTime _resolveLastSeen(Contact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastMessageAt.isAfter(contact.lastSeen) + ? contact.lastMessageAt + : contact.lastSeen; + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text( - _formatLastSeen(context, contact.lastSeen), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf( + context, + ).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + _formatLastSeen( + context, + _resolveLastSeen(contact), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[400], + ), + if (contact.rawPacket != null) + const SizedBox(width: 2), + if (contact.rawPacket != null) + Icon( + Icons.cell_tower, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ], + ), ), ), onTap: () { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 9616d472..f2d09f35 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -64,6 +64,7 @@ class _MapScreenState extends State { bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; final List _pathTrace = []; + final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; bool _legendExpanded = false; @@ -488,7 +489,7 @@ class _MapScreenState extends State { ), ), ), - if (!_isBuildingPathTrace) + if (!settings.mapShowOverlaps) ..._buildGuessedMarker( guessedLocations, showLabels: _showNodeLabels, @@ -788,17 +789,26 @@ class _MapScreenState extends State { final markers = []; for (final guess in guessed) { + if (guess.contact.type == advTypeChat && _isBuildingPathTrace) { + continue; + } + final color = _getNodeColor(guess.contact.type); final marker = Marker( point: guess.position, width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), + onLongPress: () => _isBuildingPathTrace + ? _showNodeInfo(context, guess.contact) + : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, guess.contact, position: guess.position) + : _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -870,23 +880,29 @@ class _MapScreenState extends State { addContact = true; } - final hasOverlap = contacts - .where( - (c) => - c.publicKeyHex != contact.publicKeyHex && - c.publicKey.first == contact.publicKey.first && - (c.type == advTypeRepeater || c.type == advTypeRoom) && - (contact.type == advTypeRepeater || - contact.type == advTypeRoom), - ) - .firstOrNull; - - if (hasOverlap == null && - settings.mapShowOverlaps && - !_isBuildingPathTrace) { + if (contact.type == advTypeChat && _isBuildingPathTrace) { addContact = false; } + if (settings.mapShowOverlaps) { + final hasOverlap = contacts + .where( + (c) => + c.publicKeyHex != contact.publicKeyHex && + c.publicKey.first == contact.publicKey.first && + (c.type == advTypeRepeater || c.type == advTypeRoom) && + (contact.type == advTypeRepeater || + contact.type == advTypeRoom), + ) + .firstOrNull; + + if (hasOverlap == null && + settings.mapShowOverlaps && + !_isBuildingPathTrace) { + addContact = false; + } + } + if (addContact) { filtered.add(contact); } @@ -2121,12 +2137,18 @@ class _MapScreenState extends State { } } - void _addToPath(BuildContext context, Contact contact) { + void _addToPath(BuildContext context, Contact contact, {LatLng? position}) { setState(() { _pathTrace.add( contact.publicKey[0], ); // Add first 16 bytes of public key to path trace - _points.add(LatLng(contact.latitude!, contact.longitude!)); + _pathTraceContacts.add( + contact.copyWith( + latitude: position?.latitude ?? contact.latitude, + longitude: position?.longitude ?? contact.longitude, + ), + ); // Add contact to path trace contacts + _points.add(position ?? LatLng(contact.latitude!, contact.longitude!)); }); } @@ -2134,6 +2156,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = true; _pathTrace.clear(); + _pathTraceContacts.clear(); _points.clear(); _polylines.clear(); _points.add(position); @@ -2142,6 +2165,7 @@ class _MapScreenState extends State { void _removePath() { setState(() { + _pathTraceContacts.removeLast(); _pathTrace.removeLast(); // Remove last node from path trace _points.removeLast(); // Remove last point from points list _polylines.clear(); // Clear polylines @@ -2201,6 +2225,7 @@ class _MapScreenState extends State { title: l10n.contacts_pathTrace, path: Uint8List.fromList(_pathTrace), pathHashByteWidth: hashW, + pathContacts: _pathTraceContacts, ), ), ); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b029313..7f3b4eb5 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget { final bool reversePathAround; final Contact? targetContact; final int pathHashByteWidth; + final List? pathContacts; const PathTraceMapScreen({ super.key, @@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget { this.reversePathAround = false, this.targetContact, this.pathHashByteWidth = pathHashSize, + this.pathContacts, }); @override @@ -74,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget { class _PathTraceMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + //miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path + static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344; StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = connector.allContacts; - contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + Contact lastContact = Contact( + path: Uint8List(0), + pathLength: 0, + publicKey: connector.selfPublicKey ?? Uint8List(0), + name: context.l10n.pathTrace_you, + type: advTypeChat, + latitude: connector.selfLatitude, + longitude: connector.selfLongitude, + lastSeen: DateTime.now(), + ); + if (widget.pathContacts != null) { + pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c}; + } else { + final contacts = connector.allContactsUnfiltered; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + if (lastContact.latitude != null && + lastContact.longitude != null && + repeater.hasLocation && + lastContact.hasLocation && + Distance().distance( + LatLng(lastContact.latitude!, lastContact.longitude!), + LatLng(repeater.latitude!, repeater.longitude!), + ) > + _maxRepeaterMatchDistanceMeters) { + return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches } - } - }); + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + lastContact = repeater; + } + } + }); + } // For hops with no GPS contact, infer position from other contacts // with known GPS that share the same last-hop byte. diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 52d92aac..5f76828e 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -35,13 +35,15 @@ class _RepeaterCliScreenState extends State { // Common commands for quick access late final List> _quickCommands = [ + {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'getName', 'command': 'get name'}, {'labelKey': 'getRadio', 'command': 'get radio'}, {'labelKey': 'getTx', 'command': 'get tx'}, + {'labelKey': 'discovery', 'command': 'discover.neighbors'}, {'labelKey': 'neighbors', 'command': 'neighbors'}, {'labelKey': 'version', 'command': 'ver'}, - {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'clock', 'command': 'clock'}, + {'labelKey': 'clock sync', 'command': 'clock sync'}, ]; @override @@ -407,6 +409,10 @@ class _RepeaterCliScreenState extends State { return l10n.repeater_cliQuickAdvertise; case 'clock': return l10n.repeater_cliQuickClock; + case 'clock sync': + return l10n.repeater_cliQuickClockSync; + case 'discovery': + return l10n.repeater_cliQuickDiscovery; default: return key; } diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index b0165bdd..296cc3ae 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -14,12 +14,13 @@ class ContactExport { final double lon; final String desc; final double? ele; - + final String url; ContactExport({ required this.name, required this.lat, required this.lon, required this.desc, + required this.url, this.ele, }); } @@ -40,6 +41,7 @@ class GpxExport { String name, double lat, double lon, + String url, String desc, [ double? ele, ]) { @@ -50,55 +52,66 @@ class GpxExport { lon: lon, desc: desc.trim(), ele: ele, + url: url, ), ); } void addRepeaters() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) - .toList(); + final contacts = _connector.allContacts.where( + (c) => c.type == advTypeRepeater || c.type == advTypeRoom, + ); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addContacts() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeChat) - .toList(); + final contacts = _connector.allContacts.where((c) => c.type == advTypeChat); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addAll() { - final contacts = _connector.contacts; - for (var contact in contacts.toList()) { + final contacts = _connector.allContacts; + for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude ?? 0.0, contact.longitude ?? 0.0, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } @@ -138,6 +151,9 @@ class GpxExport { ele: c.ele, name: c.name, desc: c.desc, + extensions: { + "meshcore": {"url": c.url}, + }, ), ) .toList(); diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index ce6c2b7f..48bb6ac9 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 91d2c8c8..3a923fe8 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; From 26516baf67becc1047c78707922101ec0e728712 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 11/27] Update ML timeout handling and adjust distance threshold for path hops --- lib/connector/meshcore_connector.dart | 4 +++- lib/screens/channel_message_path_screen.dart | 2 +- lib/screens/neighbors_screen.dart | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a436b469..5f0ccdb6 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3903,7 +3903,9 @@ class MeshCoreConnector extends ChangeNotifier { if (mlTimeout != null) { if (pathLength < 0) { // Flood: trust ML, only enforce firmware formula as floor - return mlTimeout.clamp(physicsMin, mlTimeout); + if (mlTimeout < physicsMin) { + return physicsMin; + } } return mlTimeout.clamp(physicsMin, physicsMax); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0eb2c220..53769d40 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -879,7 +879,7 @@ List<_PathHop> _buildPathHops( previousPosition = resolvedPosition; } // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 70000 && + if (lastDistance + bestDistance > 50000 && candidates != null && candidates.isNotEmpty) { i--; diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index f4c16734..7286eb03 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -142,7 +142,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = connector.allContacts; + final contacts = connector.allContactsUnfiltered; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); From b5aa294fc196d133679b2073195db65080cced21 Mon Sep 17 00:00:00 2001 From: n-kam <96840503+n-kam@users.noreply.github.com> Date: Fri, 27 Mar 2026 01:39:52 +0300 Subject: [PATCH 12/27] make unread badge max out at 9999+ not 99+ --- lib/widgets/unread_badge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 37db11ac..424cb6f5 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -7,7 +7,7 @@ class UnreadBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final display = count > 99 ? '99+' : count.toString(); + final display = count > 9999 ? '9999+' : count.toString(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( From 32dc0fca22fbeeeee0956672e76524e655ae3294 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 13/27] Refactor contact handling and other improvments (#317) * Refactor contact filtering and improve localization strings; enhance path trace handling * Add localization for new CLI commands and update existing strings * Enhance contact handling and UI updates across multiple screens add unfiltered contact access and improve last seen resolution * Add polling interval configuration and improve contact handling * Reorder command constants for better organization and clarity * Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens * Moved RadioStatsIconButton in chat screen for improved UI consistency * Added indicators to AppBar for channels * Ignore contacts with self public key in contact handling * Simplify path removal logic and clean up unused imports in path management dialog * Enhance path hop resolution by adding distance checks to improve candidate selection accuracy * Remove unnecessary reset of radio stats poll reference count in polling interval setter --- lib/l10n/app_es.arb | 3 +-- lib/l10n/app_hu.arb | 2 +- lib/l10n/app_ja.arb | 2 +- lib/l10n/app_ko.arb | 3 ++- lib/l10n/app_localizations_nl.dart | 6 ++++++ lib/l10n/app_sk.arb | 3 +-- lib/l10n/app_zh.arb | 2 +- lib/screens/channel_message_path_screen.dart | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0372dffc..5d98e4ef 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2088,7 +2088,6 @@ "translation_translateTo": "Traducir a {language}", "translation_translationOptions": "Opciones de traducción", "translation_systemLanguage": "Idioma del sistema", - "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).", "repeater_cliQuickDiscovery": "Descubrir Vecinos", "repeater_cliQuickClockSync": "Sincronización del reloj" -} \ No newline at end of file +} diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 68b3b11a..2a1e7171 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2101,4 +2101,4 @@ "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", "repeater_cliQuickClockSync": "Óra szinkronizálás", "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" -} \ No newline at end of file +} diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index aef8fc05..e11adfe8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2104,4 +2104,4 @@ "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", "repeater_cliQuickClockSync": "クロック同期", "repeater_cliQuickDiscovery": "近隣を発見する" -} \ No newline at end of file +} diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 66ad1ed9..06dc20c0 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2082,6 +2082,7 @@ }, "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN", "scanner_linuxPairingHidePin": "PIN 숨기기", +<<<<<<< HEAD "scanner_linuxPairingShowPin": "PIN 보기", "scanner_linuxPairingPinPrompt": "{deviceName}의 PIN을 입력하세요 (해당하는 경우에만 입력).", "@translation_translateTo": { @@ -2101,4 +2102,4 @@ "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).", "repeater_cliQuickClockSync": "시계 동기화", "repeater_cliQuickDiscovery": "이웃 발견하기" -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6b7bbe79..9ec0118b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2415,6 +2415,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + @override + String get repeater_cliQuickClockSync => 'Kloksynchronisatie'; + + @override + String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + @override String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 43e408fd..50d42d29 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2060,7 +2060,6 @@ "translation_translateTo": "Preložte do {language}", "translation_translationOptions": "Možnosti prekladania", "translation_systemLanguage": "Jazyk systému", - "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN", "repeater_cliQuickClockSync": "Synchronizácia hodin", "repeater_cliQuickDiscovery": "Objaviť susedov" -} \ No newline at end of file +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 2e19a8ee..5dd58962 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2068,4 +2068,4 @@ "scanner_linuxPairingHidePin": "隐藏 PIN", "repeater_cliQuickDiscovery": "发现邻居", "repeater_cliQuickClockSync": "同步时钟" -} \ No newline at end of file +} diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 53769d40..0eb2c220 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -879,7 +879,7 @@ List<_PathHop> _buildPathHops( previousPosition = resolvedPosition; } // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 50000 && + if (lastDistance + bestDistance > 70000 && candidates != null && candidates.isNotEmpty) { i--; From 637e08d22c6ccc21d0be1a02832df38434e64768 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 14/27] Update ML timeout handling and adjust distance threshold for path hops --- lib/screens/channel_message_path_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0eb2c220..53769d40 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -879,7 +879,7 @@ List<_PathHop> _buildPathHops( previousPosition = resolvedPosition; } // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 70000 && + if (lastDistance + bestDistance > 50000 && candidates != null && candidates.isNotEmpty) { i--; From c4f54efd77c4782a52fe63bff3fbefbc24c8ac51 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:22:13 -0700 Subject: [PATCH 15/27] add tooltip to send message buttons --- lib/l10n/app_en.arb | 9 ++++ lib/l10n/app_localizations.dart | 18 ++++--- lib/l10n/app_localizations_bg.dart | 13 ++++-- lib/l10n/app_localizations_de.dart | 13 ++++-- lib/l10n/app_localizations_en.dart | 13 ++++-- lib/l10n/app_localizations_es.dart | 13 ++++-- lib/l10n/app_localizations_fr.dart | 13 ++++-- lib/l10n/app_localizations_hu.dart | 13 ++++-- lib/l10n/app_localizations_it.dart | 13 ++++-- lib/l10n/app_localizations_ja.dart | 13 ++++-- lib/l10n/app_localizations_ko.dart | 13 ++++-- lib/l10n/app_localizations_nl.dart | 13 ++++-- lib/l10n/app_localizations_pl.dart | 13 ++++-- lib/l10n/app_localizations_pt.dart | 13 ++++-- lib/l10n/app_localizations_ru.dart | 13 ++++-- lib/l10n/app_localizations_sk.dart | 13 ++++-- lib/l10n/app_localizations_sl.dart | 13 ++++-- lib/l10n/app_localizations_sv.dart | 13 ++++-- lib/l10n/app_localizations_uk.dart | 13 ++++-- lib/l10n/app_localizations_zh.dart | 13 ++++-- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 3 ++ untranslated.json | 70 +++++++++++++++++++++++++++- 23 files changed, 238 insertions(+), 97 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b703630e..ffdf21de 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -607,6 +607,15 @@ "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", "chat_noMessages": "No messages yet", + "chat_sendMessage": "Send message", + "chat_sendMessageTo": "Send message to {name}", + "@chat_sendMessageTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "chat_sendMessageToStart": "Send a message to get started", "chat_originalMessageNotFound": "Original message not found", "chat_replyingTo": "Replying to {name}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 408a2436..bb390d5b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2296,6 +2296,18 @@ abstract class AppLocalizations { /// **'No messages yet'** String get chat_noMessages; + /// No description provided for @chat_sendMessage. + /// + /// In en, this message translates to: + /// **'Send message'** + String get chat_sendMessage; + + /// No description provided for @chat_sendMessageTo. + /// + /// In en, this message translates to: + /// **'Send a message to {contactName}'** + String chat_sendMessageTo(String contactName); + /// No description provided for @chat_sendMessageToStart. /// /// In en, this message translates to: @@ -2326,12 +2338,6 @@ abstract class AppLocalizations { /// **'Location'** String get chat_location; - /// No description provided for @chat_sendMessageTo. - /// - /// In en, this message translates to: - /// **'Send a message to {contactName}'** - String chat_sendMessageTo(String contactName); - /// No description provided for @chat_typeMessage. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 8a433224..bec54dfd 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_noMessages => 'Няма съобщения.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Изпрати съобщение на $contactName'; + } + @override String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.'; @@ -1258,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Изпрати съобщение на $contactName'; - } - @override String get chat_typeMessage => 'Въведете съобщение...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1177bc12..cc6d6ed5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_noMessages => 'Noch keine Nachrichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Sende eine Nachricht an $contactName'; + } + @override String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.'; @@ -1257,11 +1265,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_location => 'Ort'; - @override - String chat_sendMessageTo(String contactName) { - return 'Sende eine Nachricht an $contactName'; - } - @override String get chat_typeMessage => 'Eine Nachricht eingeben...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9104f8b7..d7a79bd2 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1213,6 +1213,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_noMessages => 'No messages yet'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Send a message to $contactName'; + } + @override String get chat_sendMessageToStart => 'Send a message to get started'; @@ -1232,11 +1240,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_location => 'Location'; - @override - String chat_sendMessageTo(String contactName) { - return 'Send a message to $contactName'; - } - @override String get chat_typeMessage => 'Type a message...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cc3b7140..9a56c6df 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_noMessages => 'Aún no hay mensajes'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar un mensaje a $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_location => 'Ubicación'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar un mensaje a $contactName'; - } - @override String get chat_typeMessage => 'Escribe un mensaje...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 402e3738..4ce4a753 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1243,6 +1243,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_noMessages => 'Aucun message pour le moment.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Envoyer un message à $contactName'; + } + @override String get chat_sendMessageToStart => 'Envoyer un message pour commencer'; @@ -1262,11 +1270,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_location => 'Emplacement'; - @override - String chat_sendMessageTo(String contactName) { - return 'Envoyer un message à $contactName'; - } - @override String get chat_typeMessage => 'Saisir un message...'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 204e21ba..bbf989e0 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1246,6 +1246,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_noMessages => 'Még nincs üzenet.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Küldj üzenetet $contactName-nek'; + } + @override String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!'; @@ -1265,11 +1273,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_location => 'Helyszín'; - @override - String chat_sendMessageTo(String contactName) { - return 'Küldj üzenetet $contactName-nek'; - } - @override String get chat_typeMessage => 'Írjon üzenetet...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 936ecc16..98cbfcb8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_noMessages => 'Nessun messaggio ancora'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Invia un messaggio a $contactName'; + } + @override String get chat_sendMessageToStart => 'Invia un messaggio per iniziare'; @@ -1258,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_location => 'Posizione'; - @override - String chat_sendMessageTo(String contactName) { - return 'Invia un messaggio a $contactName'; - } - @override String get chat_typeMessage => 'Digita un messaggio...'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 7accee3e..40845e9b 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1179,6 +1179,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_noMessages => 'まだメッセージは届いていません'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName へのメッセージを送信する'; + } + @override String get chat_sendMessageToStart => '開始するためにメッセージを送信してください'; @@ -1198,11 +1206,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_location => '場所'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName へのメッセージを送信する'; - } - @override String get chat_typeMessage => 'メッセージを入力してください…'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 06d7db63..b0d849b6 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1174,6 +1174,14 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_noMessages => '아직 메시지가 없습니다.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName에게 메시지를 보내'; + } + @override String get chat_sendMessageToStart => '시작하려면 메시지를 보내세요.'; @@ -1193,11 +1201,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_location => '위치'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName에게 메시지를 보내'; - } - @override String get chat_typeMessage => '메시지를 입력하세요...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 9ec0118b..ae066a98 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1227,6 +1227,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_noMessages => 'Nog geen berichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Verstuur een bericht naar $contactName'; + } + @override String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen'; @@ -1246,11 +1254,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_location => 'Locatie'; - @override - String chat_sendMessageTo(String contactName) { - return 'Verstuur een bericht naar $contactName'; - } - @override String get chat_typeMessage => 'Type een bericht...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b6296a4d..ed66e52f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1247,6 +1247,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_noMessages => 'Brak jeszcze wiadomości'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Wyślij wiadomość do $contactName'; + } + @override String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.'; @@ -1267,11 +1275,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_location => 'Lokalizacja'; - @override - String chat_sendMessageTo(String contactName) { - return 'Wyślij wiadomość do $contactName'; - } - @override String get chat_typeMessage => 'Wpisz wiadomość...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d1f66af2..1aebdcf2 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_noMessages => 'Ainda não existem mensagens.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar uma mensagem para $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_location => 'Localização'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar uma mensagem para $contactName'; - } - @override String get chat_typeMessage => 'Digite uma mensagem...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index cb2ae158..d8f38fed 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_noMessages => 'Сообщений пока нет'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Отправить сообщение $contactName'; + } + @override String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; @@ -1257,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Отправить сообщение $contactName'; - } - @override String get chat_typeMessage => 'Напишите сообщение...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8ddea4b7..b59d6d80 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1226,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_noMessages => 'Zatiaľ žiadne správy.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošli správu $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlite správu na začiatok'; @@ -1245,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_location => 'Lokalita'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošli správu $contactName'; - } - @override String get chat_typeMessage => 'Napište správu...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 07c1c01f..c204c508 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1224,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_noMessages => 'Še ni sporočil.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošlji sporočilo $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.'; @@ -1244,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_location => 'Lokacija'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošlji sporočilo $contactName'; - } - @override String get chat_typeMessage => 'Vnesi sporočilo...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 87457744..6b9ffb5a 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1217,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_noMessages => 'Inga meddelanden ännu'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Skicka ett meddelande till $contactName'; + } + @override String get chat_sendMessageToStart => 'Skicka ett meddelande för att komma igång'; @@ -1238,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_location => 'Plats'; - @override - String chat_sendMessageTo(String contactName) { - return 'Skicka ett meddelande till $contactName'; - } - @override String get chat_typeMessage => 'Skriv ett meddelande...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index fc0abea7..f6745c3b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1230,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_noMessages => 'Поки немає повідомлень.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Надіслати повідомлення $contactName'; + } + @override String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; @@ -1250,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_location => 'Розташування'; - @override - String chat_sendMessageTo(String contactName) { - return 'Надіслати повідомлення $contactName'; - } - @override String get chat_typeMessage => 'Введіть повідомлення...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index f9ff7099..acadc58a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1161,6 +1161,14 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_noMessages => '暂无消息'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '发送消息给 $contactName'; + } + @override String get chat_sendMessageToStart => '发送消息开始对话'; @@ -1180,11 +1188,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_location => '位置'; - @override - String chat_sendMessageTo(String contactName) { - return '发送消息给 $contactName'; - } - @override String get chat_typeMessage => '输入消息...'; diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 7beaaf4c..64da058f 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1121,6 +1121,7 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessage, onPressed: _sendMessage, color: Theme.of(context).colorScheme.primary, ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 4cda7127..a4ebc76f 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -591,6 +591,9 @@ class _ChatScreenState extends State { const SizedBox(width: 8), IconButton.filled( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessageTo( + _resolveContact(connector).name, + ), onPressed: () => _sendMessage(connector), ), ], diff --git a/untranslated.json b/untranslated.json index 9e26dfee..1ebd9bc5 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,69 @@ -{} \ No newline at end of file +{ + "bg": [ + "chat_sendMessage" + ], + + "de": [ + "chat_sendMessage" + ], + + "es": [ + "chat_sendMessage" + ], + + "fr": [ + "chat_sendMessage" + ], + + "hu": [ + "chat_sendMessage" + ], + + "it": [ + "chat_sendMessage" + ], + + "ja": [ + "chat_sendMessage" + ], + + "ko": [ + "chat_sendMessage" + ], + + "nl": [ + "chat_sendMessage" + ], + + "pl": [ + "chat_sendMessage" + ], + + "pt": [ + "chat_sendMessage" + ], + + "ru": [ + "chat_sendMessage" + ], + + "sk": [ + "chat_sendMessage" + ], + + "sl": [ + "chat_sendMessage" + ], + + "sv": [ + "chat_sendMessage" + ], + + "uk": [ + "chat_sendMessage" + ], + + "zh": [ + "chat_sendMessage" + ] +} From 754f8a6c621c59ce7af1315983a595767652670d Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:31:37 -0700 Subject: [PATCH 16/27] add fvm directory and rc file to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 779856c5..88295e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ migrate_working_dir/ pubspec.lock /build/ /coverage/ +# fvm project files +.fvm/ +.fvmrc # Symbolication related app.*.symbols From 45cd8a56a3dcc89ac1b25708c60036d8fbbf628a Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:37:50 -0700 Subject: [PATCH 17/27] add jni to generated plugins linux and windows were missing jni which was being added on fresh builds from dev --- linux/flutter/generated_plugins.cmake | 1 + windows/flutter/generated_plugins.cmake | 1 + 2 files changed, 2 insertions(+) diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES) From 8386f262e18b61322ca093f9aea0464b854c0176 Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 11:42:46 +0100 Subject: [PATCH 18/27] reimplement location aware snr-indikator after alpha7 --- lib/utils/contact_search.dart | 55 +++++++++++++++++++ lib/widgets/snr_indicator.dart | 24 ++++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 7a82c53a..8aa75b0d 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,3 +1,6 @@ +import 'package:latlong2/latlong.dart'; + +import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; export 'contact_filter_types.dart'; @@ -43,3 +46,55 @@ String? _extractHexPrefix(String query) { if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; return cleaned; } + +Contact? getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 30956e22..cf3c275f 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + import '../connector/meshcore_connector.dart'; +import '../utils/contact_search.dart'; import '../l10n/l10n.dart'; import 'signal_ui.dart'; @@ -158,10 +161,23 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); final allContacts = widget.connector.allContacts; - final name = allContacts - .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) - .map((c) => c.name) - .firstOrNull; + + final selfLat = widget.connector.selfLatitude; + final selfLon = widget.connector.selfLongitude; + + LatLng? selfPoint; + if (selfLat != null && selfLon != null) { + selfPoint = LatLng(selfLat, selfLon); + } + + final contact = getRepeaterPrefixMatchNearLocation( + allContacts, + repeater.pubkeyFirstByte, + searchPoint: selfPoint, + preferFavorites: true, + ); + + final name = contact?.name; return Column( children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ffc8c590..2428a778 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From e4684b585a6870ee1db1cb5eb150ff2b63341b26 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Sun, 15 Mar 2026 12:10:47 +0100 Subject: [PATCH 19/27] codex suggested fix: explicit check if contact location is not null Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/utils/contact_search.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 8aa75b0d..6a708e89 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -87,7 +87,7 @@ Contact? getRepeaterPrefixMatchNearLocation( var bestDistance = double.infinity; for (final c in candidates) { - if (c.hasLocation) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); if (d < bestDistance) { bestDistance = d; From 7dcec5b4eed250bfe1915324240fd7e27de72a05 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 28 Mar 2026 17:08:59 +0100 Subject: [PATCH 20/27] moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. --- lib/utils/contact_search.dart | 55 -------------------------- lib/widgets/snr_indicator.dart | 70 ++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 6a708e89..7a82c53a 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,6 +1,3 @@ -import 'package:latlong2/latlong.dart'; - -import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; export 'contact_filter_types.dart'; @@ -46,55 +43,3 @@ String? _extractHexPrefix(String query) { if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; return cleaned; } - -Contact? getRepeaterPrefixMatchNearLocation( - List contacts, - int pubkeyFirstByte, { - LatLng? searchPoint, - bool preferFavorites = false, -}) { - final candidates = contacts - .where( - (c) => - c.publicKey.isNotEmpty && - c.publicKey.first == pubkeyFirstByte && - (c.type == advTypeRepeater || c.type == advTypeRoom), - ) - .toList(); - - if (candidates.isEmpty) return null; - - candidates.sort((a, b) { - if (preferFavorites) { - final favA = a.isFavorite ? 1 : 0; - final favB = b.isFavorite ? 1 : 0; - final favCompare = favB.compareTo(favA); - if (favCompare != 0) return favCompare; - } - - final seenCompare = b.lastSeen.compareTo(a.lastSeen); - if (seenCompare != 0) return seenCompare; - - return a.publicKeyHex.compareTo(b.publicKeyHex); - }); - - if (searchPoint == null) { - return candidates.first; - } - - final distance = Distance(); - Contact best = candidates.first; - var bestDistance = double.infinity; - - for (final c in candidates) { - if (c.hasLocation && c.latitude != null && c.longitude != null) { - final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); - if (d < bestDistance) { - bestDistance = d; - best = c; - } - } - } - - return best; -} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index cf3c275f..99f20539 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -2,10 +2,63 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; -import '../utils/contact_search.dart'; +import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; +import '../models/contact.dart'; import 'signal_ui.dart'; +Contact? _getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} + class SNRUi { final IconData icon; final Color color; @@ -67,6 +120,15 @@ class SNRIndicator extends StatefulWidget { } class _SNRIndicatorState extends State { + bool _isValidSelfLocation(double lat, double lon) { + const double epsilon = 1e-6; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + @override Widget build(BuildContext context) { final directRepeaters = widget.connector.directRepeaters; @@ -166,11 +228,13 @@ class _SNRIndicatorState extends State { final selfLon = widget.connector.selfLongitude; LatLng? selfPoint; - if (selfLat != null && selfLon != null) { + if (selfLat != null && + selfLon != null && + _isValidSelfLocation(selfLat, selfLon)) { selfPoint = LatLng(selfLat, selfLon); } - final contact = getRepeaterPrefixMatchNearLocation( + final contact = _getRepeaterPrefixMatchNearLocation( allContacts, repeater.pubkeyFirstByte, searchPoint: selfPoint, From f29960829662a28af0c06438ba173cf72b53d6eb Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:01:45 -0700 Subject: [PATCH 21/27] use l10n strings for discovered menu item --- lib/screens/contacts_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 62a380bf..46e2be6a 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -394,7 +394,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.person_add_rounded), const SizedBox(width: 8), - Text("Discovered Contacts"), + Text(context.l10n.discoveredContacts_Title), ], ), onTap: () => Navigator.push( From 82e04e80908917fed70dd65bf7382c0143f149bd Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 9 Mar 2026 18:29:17 -0400 Subject: [PATCH 22/27] Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183" This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527. --- lib/screens/settings_screen.dart | 55 +++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d9e0d209..a0dedac1 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1088,6 +1088,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; final _txPowerController = TextEditingController(text: '20'); bool _clientRepeat = false; + int? _selectedPresetIndex; @override void initState() { @@ -1139,6 +1140,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } _clientRepeat = widget.connector.clientRepeat ?? false; + _selectedPresetIndex = _findMatchingPresetIndex(); } @override @@ -1158,6 +1160,55 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { }); } + int? _findMatchingPresetIndex() { + final freqMHz = double.tryParse(_frequencyController.text); + final txPower = int.tryParse(_txPowerController.text); + if (freqMHz == null || txPower == null) return null; + + const epsilon = 0.001; + for (var i = 0; i < RadioSettings.presets.length; i++) { + final preset = RadioSettings.presets[i].$2; + if ((preset.frequencyMHz - freqMHz).abs() < epsilon && + preset.bandwidth == _bandwidth && + preset.spreadingFactor == _spreadingFactor && + preset.codingRate == _codingRate && + preset.txPowerDbm == txPower) { + return i; + } + } + return null; + } + + double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) { + if (baseFrequencyMHz < 500) return 433.0; + if (baseFrequencyMHz < 900) return 869.0; + return 918.0; + } + + double _normalFrequencyForBand(double frequencyMHz) { + if (frequencyMHz < 500) return 433.650; + if (frequencyMHz < 900) return 869.432; + return 915.8; + } + + void _handleClientRepeatChanged(bool enabled) { + setState(() { + _clientRepeat = enabled; + + final baseFrequencyMHz = _selectedPresetIndex != null + ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz + : (double.tryParse(_frequencyController.text) ?? 915.0); + + final nextFrequencyMHz = enabled + ? _offGridFrequencyForBaseFrequency(baseFrequencyMHz) + : (_selectedPresetIndex != null + ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz + : _normalFrequencyForBand(baseFrequencyMHz)); + + _frequencyController.text = nextFrequencyMHz.toStringAsFixed(3); + }); + } + Future _saveSettings() async { final l10n = context.l10n; final freqMHz = double.tryParse(_frequencyController.text); @@ -1250,6 +1301,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( + initialValue: _selectedPresetIndex, decoration: InputDecoration( labelText: l10n.settings_presets, border: const OutlineInputBorder(), @@ -1263,6 +1315,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ], onChanged: (index) { if (index != null) { + _selectedPresetIndex = index; _applyPreset(RadioSettings.presets[index].$2); } }, @@ -1345,7 +1398,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { title: Text(l10n.settings_clientRepeat), subtitle: Text(l10n.settings_clientRepeatSubtitle), value: _clientRepeat, - onChanged: (value) => setState(() => _clientRepeat = value), + onChanged: _handleClientRepeatChanged, contentPadding: EdgeInsets.zero, ), ], From c7b7deb0f6f7e1842b178a945ef441f7ec4928e3 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:18:35 -0400 Subject: [PATCH 23/27] fix(settings): preserve preset across off-grid repeat --- lib/connector/meshcore_connector.dart | 24 ++ lib/screens/settings_screen.dart | 383 +++++++++++++++++++++++--- 2 files changed, 374 insertions(+), 33 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 5f0ccdb6..b4322773 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -104,6 +104,22 @@ class RepeaterBatterySnapshot { }); } +class MeshCoreRadioStateSnapshot { + final int freqHz; + final int bwHz; + final int sf; + final int cr; + final int txPowerDbm; + + const MeshCoreRadioStateSnapshot({ + required this.freqHz, + required this.bwHz, + required this.sf, + required this.cr, + required this.txPowerDbm, + }); +} + class MeshCoreConnector extends ChangeNotifier { // Message windowing to limit memory usage static const int _messageWindowSize = 200; @@ -169,6 +185,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentSf; int? _currentCr; bool? _clientRepeat; + MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState; int? _firmwareVerCode; int _pathHashByteWidth = 1; CompanionRadioStats? _latestRadioStats; @@ -369,6 +386,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + MeshCoreRadioStateSnapshot? get rememberedNonRepeatRadioState => + _rememberedNonRepeatRadioState; bool? get autoAddUsers => _autoAddUsers; bool? get autoAddRepeaters => _autoAddRepeaters; bool? get autoAddRoomServers => _autoAddRoomServers; @@ -380,6 +399,10 @@ class MeshCoreConnector extends ChangeNotifier { int get advertLocationPolicy => _advertLocPolicy; int get multiAcks => _multiAcks; bool? get clientRepeat => _clientRepeat; + void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) { + _rememberedNonRepeatRadioState = snapshot; + } + int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; @@ -2278,6 +2301,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = null; _selfLongitude = null; _clientRepeat = null; + _rememberedNonRepeatRadioState = null; _firmwareVerCode = null; _batteryMillivolts = null; _repeaterBatterySnapshots.clear(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a0dedac1..c90827b5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../services/app_debug_log_service.dart'; import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; @@ -1089,6 +1090,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final _txPowerController = TextEditingController(text: '20'); bool _clientRepeat = false; int? _selectedPresetIndex; + _RadioSettingsSnapshot? _lastNonRepeatSnapshot; + + AppDebugLogService get _appLog => + Provider.of(context, listen: false); @override void initState() { @@ -1141,6 +1146,23 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _clientRepeat = widget.connector.clientRepeat ?? false; _selectedPresetIndex = _findMatchingPresetIndex(); + _lastNonRepeatSnapshot = _currentSnapshot(); + if (_clientRepeat) { + _lastNonRepeatSnapshot = + _sessionRememberedNonRepeatSnapshot() ?? + _inferNonRepeatSnapshotForRepeatEnabled(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + } else { + _lastNonRepeatSnapshot = + _sessionRememberedNonRepeatSnapshot() ?? + _nonRepeatSnapshotForCurrentSelection(); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _logRadioSettingsState('Dialog initialized'); + }); } @override @@ -1150,35 +1172,60 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { super.dispose(); } - void _applyPreset(RadioSettings preset) { + void _applyPreset(int index) { setState(() { - _frequencyController.text = preset.frequencyMHz.toString(); - _bandwidth = preset.bandwidth; - _spreadingFactor = preset.spreadingFactor; - _codingRate = preset.codingRate; - _txPowerController.text = preset.txPowerDbm.toString(); + _applyPresetState(index); }); + _logRadioSettingsState( + 'Applied preset ${RadioSettings.presets[index].$1} (#$index)', + ); } int? _findMatchingPresetIndex() { - final freqMHz = double.tryParse(_frequencyController.text); - final txPower = int.tryParse(_txPowerController.text); - if (freqMHz == null || txPower == null) return null; + return _findMatchingPresetIndexForSnapshot(_currentSnapshot()); + } + int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) { const epsilon = 0.001; - for (var i = 0; i < RadioSettings.presets.length; i++) { + for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - if ((preset.frequencyMHz - freqMHz).abs() < epsilon && - preset.bandwidth == _bandwidth && - preset.spreadingFactor == _spreadingFactor && - preset.codingRate == _codingRate && - preset.txPowerDbm == txPower) { + if ((preset.frequencyMHz - snapshot.frequencyMHz).abs() < epsilon && + preset.bandwidth == snapshot.bandwidth && + preset.spreadingFactor == snapshot.spreadingFactor && + preset.codingRate == snapshot.codingRate && + preset.txPowerDbm == snapshot.txPowerDbm) { return i; } } return null; } + Iterable _visiblePresetIndexes() sync* { + for (var i = 0; i < RadioSettings.presets.length; i++) { + if (_isOffGridPresetIndex(i)) { + continue; + } + yield i; + } + } + + _RadioSettingsSnapshot _currentSnapshot() { + final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0; + final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20; + return _RadioSettingsSnapshot( + frequencyMHz: frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: txPowerDbm, + ); + } + + bool _isOffGridPresetIndex(int? index) { + if (index == null) return false; + return RadioSettings.presets[index].$1.startsWith('Off-Grid '); + } + double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) { if (baseFrequencyMHz < 500) return 433.0; if (baseFrequencyMHz < 900) return 869.0; @@ -1191,22 +1238,182 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { return 915.8; } + _RadioSettingsSnapshot _fallbackNonRepeatSnapshot( + double currentFrequencyMHz, + ) { + return _RadioSettingsSnapshot( + frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz), + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + + _RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() { + final current = _currentSnapshot(); + if (!_isOffGridPresetIndex(_selectedPresetIndex)) { + return current; + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + _RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() { + final snapshot = widget.connector.rememberedNonRepeatRadioState; + if (snapshot == null) { + return null; + } + + final bandwidth = LoRaBandwidth.values + .where((bw) => bw.hz == snapshot.bwHz) + .firstOrNull; + final spreadingFactor = LoRaSpreadingFactor.values + .where((sf) => sf.value == snapshot.sf) + .firstOrNull; + final codingRate = LoRaCodingRate.values + .where((cr) => cr.value == _toUiCodingRate(snapshot.cr)) + .firstOrNull; + + if (bandwidth == null || spreadingFactor == null || codingRate == null) { + return null; + } + + return _RadioSettingsSnapshot( + frequencyMHz: snapshot.freqHz / 1000.0, + bandwidth: bandwidth, + spreadingFactor: spreadingFactor, + codingRate: codingRate, + txPowerDbm: snapshot.txPowerDbm, + ); + } + + _RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() { + final current = _currentSnapshot(); + const epsilon = 0.001; + for (final i in _visiblePresetIndexes()) { + final preset = RadioSettings.presets[i].$2; + final offGridFrequencyMHz = _offGridFrequencyForBaseFrequency( + preset.frequencyMHz, + ); + if ((offGridFrequencyMHz - current.frequencyMHz).abs() < epsilon && + preset.bandwidth == current.bandwidth && + preset.spreadingFactor == current.spreadingFactor && + preset.codingRate == current.codingRate && + preset.txPowerDbm == current.txPowerDbm) { + return _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + } + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + void _applySnapshot(_RadioSettingsSnapshot snapshot) { + _frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3); + _bandwidth = snapshot.bandwidth; + _spreadingFactor = snapshot.spreadingFactor; + _codingRate = snapshot.codingRate; + _txPowerController.text = snapshot.txPowerDbm.toString(); + } + + void _applyPresetState(int index) { + final preset = RadioSettings.presets[index].$2; + final baseSnapshot = _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + final frequencyMHz = _clientRepeat + ? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz) + : baseSnapshot.frequencyMHz; + _frequencyController.text = frequencyMHz.toString(); + _bandwidth = preset.bandwidth; + _spreadingFactor = preset.spreadingFactor; + _codingRate = preset.codingRate; + _txPowerController.text = preset.txPowerDbm.toString(); + _selectedPresetIndex = index; + _lastNonRepeatSnapshot = baseSnapshot; + } + + void _syncPresetSelection() { + final previousPresetIndex = _selectedPresetIndex; + final previousLastNonRepeat = _lastNonRepeatSnapshot; + if (_clientRepeat) { + final baseSnapshot = + previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled(); + if (_bandwidth != baseSnapshot.bandwidth || + _spreadingFactor != baseSnapshot.spreadingFactor || + _codingRate != baseSnapshot.codingRate || + (int.tryParse(_txPowerController.text) ?? 20) != + baseSnapshot.txPowerDbm) { + _lastNonRepeatSnapshot = _RadioSettingsSnapshot( + frequencyMHz: baseSnapshot.frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot ?? baseSnapshot, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}', + ); + } + return; + } + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}', + ); + } + } + + void _handleManualSettingsChanged(String source) { + _logRadioSettingsState('Manual settings edit: $source'); + setState(_syncPresetSelection); + } + void _handleClientRepeatChanged(bool enabled) { + _logRadioSettingsState( + 'Off-grid repeat toggle requested: $_clientRepeat -> $enabled', + ); setState(() { - _clientRepeat = enabled; + final currentSnapshot = _currentSnapshot(); + if (enabled) { + if (!_clientRepeat) { + _syncPresetSelection(); + } + final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot; + _clientRepeat = true; + _frequencyController.text = _offGridFrequencyForBaseFrequency( + baseSnapshot.frequencyMHz, + ).toStringAsFixed(3); + return; + } - final baseFrequencyMHz = _selectedPresetIndex != null - ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz - : (double.tryParse(_frequencyController.text) ?? 915.0); - - final nextFrequencyMHz = enabled - ? _offGridFrequencyForBaseFrequency(baseFrequencyMHz) - : (_selectedPresetIndex != null - ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz - : _normalFrequencyForBand(baseFrequencyMHz)); - - _frequencyController.text = nextFrequencyMHz.toStringAsFixed(3); + _clientRepeat = false; + _applySnapshot( + _lastNonRepeatSnapshot ?? + _fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz), + ); + _syncPresetSelection(); }); + _logRadioSettingsState('Off-grid repeat toggle applied'); } Future _saveSettings() async { @@ -1254,6 +1461,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } try { + final rememberedSnapshot = _clientRepeat + ? _lastNonRepeatSnapshot + : _currentSnapshot(); + if (rememberedSnapshot != null) { + widget.connector.rememberNonRepeatRadioState( + MeshCoreRadioStateSnapshot( + freqHz: (rememberedSnapshot.frequencyMHz * 1000).round(), + bwHz: rememberedSnapshot.bandwidth.hz, + sf: rememberedSnapshot.spreadingFactor.value, + cr: _toDeviceCodingRate( + rememberedSnapshot.codingRate.value, + widget.connector.currentCr, + ), + txPowerDbm: rememberedSnapshot.txPowerDbm, + ), + ); + } + _logRadioSettingsState('Saving radio settings'); await widget.connector.sendFrame( buildSetRadioParamsFrame( freqHz, @@ -1268,10 +1493,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (!mounted) return; Navigator.pop(context); + _logRadioSettingsState('Radio settings saved successfully'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), ); } catch (e) { + _appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_error(e.toString()))), @@ -1290,6 +1517,39 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { return uiCr; } + String _presetLabel(int? index) { + if (index == null) { + return 'custom'; + } + return '${RadioSettings.presets[index].$1} (#$index)'; + } + + String _formatSnapshot(_RadioSettingsSnapshot? snapshot) { + if (snapshot == null) { + return 'null'; + } + return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/' + '${snapshot.bandwidth.label}/' + '${snapshot.spreadingFactor.label}/' + '${snapshot.codingRate.label}/' + '${snapshot.txPowerDbm}dBm'; + } + + void _logRadioSettingsState(String message) { + _appLog.info( + '$message | ' + 'freq=${_frequencyController.text}MHz ' + 'bw=${_bandwidth.label} ' + 'sf=${_spreadingFactor.label} ' + 'cr=${_codingRate.label} ' + 'tx=${_txPowerController.text}dBm ' + 'repeat=$_clientRepeat ' + 'preset=${_presetLabel(_selectedPresetIndex)} ' + 'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}', + tag: 'RadioSettings', + ); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -1301,13 +1561,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( + key: ValueKey(_selectedPresetIndex), initialValue: _selectedPresetIndex, decoration: InputDecoration( labelText: l10n.settings_presets, border: const OutlineInputBorder(), ), items: [ - for (var i = 0; i < RadioSettings.presets.length; i++) + for (final i in _visiblePresetIndexes()) DropdownMenuItem( value: i, child: Text(RadioSettings.presets[i].$1), @@ -1315,14 +1576,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ], onChanged: (index) { if (index != null) { - _selectedPresetIndex = index; - _applyPreset(RadioSettings.presets[index].$2); + _applyPreset(index); } }, ), const SizedBox(height: 16), TextField( controller: _frequencyController, + onChanged: (_) => _handleManualSettingsChanged('frequency'), decoration: InputDecoration( labelText: l10n.settings_frequency, border: const OutlineInputBorder(), @@ -1345,7 +1606,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _bandwidth = value); + if (value != null) { + setState(() { + _bandwidth = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: bandwidth'); + } }, ), const SizedBox(height: 16), @@ -1361,7 +1628,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _spreadingFactor = value); + if (value != null) { + setState(() { + _spreadingFactor = value; + _syncPresetSelection(); + }); + _logRadioSettingsState( + 'Manual settings edit: spreading factor', + ); + } }, ), const SizedBox(height: 16), @@ -1377,12 +1652,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _codingRate = value); + if (value != null) { + setState(() { + _codingRate = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: coding rate'); + } }, ), const SizedBox(height: 16), TextField( controller: _txPowerController, + onChanged: (_) => _handleManualSettingsChanged('tx power'), decoration: InputDecoration( labelText: l10n.settings_txPower, border: const OutlineInputBorder(), @@ -1415,3 +1697,38 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); } } + +class _RadioSettingsSnapshot { + final double frequencyMHz; + final LoRaBandwidth bandwidth; + final LoRaSpreadingFactor spreadingFactor; + final LoRaCodingRate codingRate; + final int txPowerDbm; + + const _RadioSettingsSnapshot({ + required this.frequencyMHz, + required this.bandwidth, + required this.spreadingFactor, + required this.codingRate, + required this.txPowerDbm, + }); + + @override + bool operator ==(Object other) { + return other is _RadioSettingsSnapshot && + frequencyMHz == other.frequencyMHz && + bandwidth == other.bandwidth && + spreadingFactor == other.spreadingFactor && + codingRate == other.codingRate && + txPowerDbm == other.txPowerDbm; + } + + @override + int get hashCode => Object.hash( + frequencyMHz, + bandwidth, + spreadingFactor, + codingRate, + txPowerDbm, + ); +} From 20a993931465968d3e88050ba3af33d42368eccb Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:19:53 -0400 Subject: [PATCH 24/27] fix(settings): scope repeat preset memory to saved state --- lib/screens/settings_screen.dart | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c90827b5..44019dd8 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1155,9 +1155,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _lastNonRepeatSnapshot!, ); } else { - _lastNonRepeatSnapshot = - _sessionRememberedNonRepeatSnapshot() ?? - _nonRepeatSnapshotForCurrentSelection(); + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); } WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -1461,6 +1459,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } try { + _logRadioSettingsState('Saving radio settings'); + await widget.connector.sendFrame( + buildSetRadioParamsFrame( + freqHz, + bwHz, + sf, + cr, + clientRepeat: knownRepeat ? _clientRepeat : null, + ), + ); + await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); + await widget.connector.refreshDeviceInfo(); final rememberedSnapshot = _clientRepeat ? _lastNonRepeatSnapshot : _currentSnapshot(); @@ -1478,18 +1488,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), ); } - _logRadioSettingsState('Saving radio settings'); - await widget.connector.sendFrame( - buildSetRadioParamsFrame( - freqHz, - bwHz, - sf, - cr, - clientRepeat: knownRepeat ? _clientRepeat : null, - ), - ); - await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); - await widget.connector.refreshDeviceInfo(); if (!mounted) return; Navigator.pop(context); From ea3b9609fc3c0cdcb114b2d3bbd963ce3d6993cc Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 15:50:35 -0400 Subject: [PATCH 25/27] fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging - Replace floating-point epsilon frequency comparison with integer Hz - Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot conversion methods on _RadioSettingsSnapshot - Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions - Gate _logRadioSettingsState behind kDebugMode - Use integer Hz in == and hashCode for _RadioSettingsSnapshot Addresses code review findings on preset/off-grid repeat toggle PR. --- lib/screens/settings_screen.dart | 119 +++++++++++++++++-------------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 44019dd8..e7d61ee7 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/widgets/elements_ui.dart'; @@ -15,6 +16,21 @@ import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; import '../widgets/radio_stats_entry.dart'; +/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) +/// to the UI enum range (always 5-8). +int _toUiCodingRate(int deviceCr) { + return deviceCr <= 4 ? deviceCr + 4 : deviceCr; +} + +/// Convert UI coding-rate value (5-8) back to firmware encoding. +/// Uses the current device CR to detect which encoding the firmware expects. +int _toDeviceCodingRate(int uiCr, int? deviceCr) { + if (deviceCr != null && deviceCr <= 4) { + return uiCr - 4; + } + return uiCr; +} + class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -1184,10 +1200,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) { - const epsilon = 0.001; for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - if ((preset.frequencyMHz - snapshot.frequencyMHz).abs() < epsilon && + if (preset.frequencyHz == snapshot.frequencyHz && preset.bandwidth == snapshot.bandwidth && preset.spreadingFactor == snapshot.spreadingFactor && preset.codingRate == snapshot.codingRate && @@ -1258,42 +1273,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() { final snapshot = widget.connector.rememberedNonRepeatRadioState; - if (snapshot == null) { - return null; - } - - final bandwidth = LoRaBandwidth.values - .where((bw) => bw.hz == snapshot.bwHz) - .firstOrNull; - final spreadingFactor = LoRaSpreadingFactor.values - .where((sf) => sf.value == snapshot.sf) - .firstOrNull; - final codingRate = LoRaCodingRate.values - .where((cr) => cr.value == _toUiCodingRate(snapshot.cr)) - .firstOrNull; - - if (bandwidth == null || spreadingFactor == null || codingRate == null) { - return null; - } - - return _RadioSettingsSnapshot( - frequencyMHz: snapshot.freqHz / 1000.0, - bandwidth: bandwidth, - spreadingFactor: spreadingFactor, - codingRate: codingRate, - txPowerDbm: snapshot.txPowerDbm, - ); + if (snapshot == null) return null; + return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot); } _RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() { final current = _currentSnapshot(); - const epsilon = 0.001; for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - final offGridFrequencyMHz = _offGridFrequencyForBaseFrequency( - preset.frequencyMHz, - ); - if ((offGridFrequencyMHz - current.frequencyMHz).abs() < epsilon && + final offGridFreqHz = + (_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000) + .round(); + if (offGridFreqHz == current.frequencyHz && preset.bandwidth == current.bandwidth && preset.spreadingFactor == current.spreadingFactor && preset.codingRate == current.codingRate && @@ -1476,16 +1467,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { : _currentSnapshot(); if (rememberedSnapshot != null) { widget.connector.rememberNonRepeatRadioState( - MeshCoreRadioStateSnapshot( - freqHz: (rememberedSnapshot.frequencyMHz * 1000).round(), - bwHz: rememberedSnapshot.bandwidth.hz, - sf: rememberedSnapshot.spreadingFactor.value, - cr: _toDeviceCodingRate( - rememberedSnapshot.codingRate.value, - widget.connector.currentCr, - ), - txPowerDbm: rememberedSnapshot.txPowerDbm, - ), + rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr), ); } @@ -1504,17 +1486,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } } - int _toUiCodingRate(int deviceCr) { - return deviceCr <= 4 ? deviceCr + 4 : deviceCr; - } - - int _toDeviceCodingRate(int uiCr, int? deviceCr) { - if (deviceCr != null && deviceCr <= 4) { - return uiCr - 4; - } - return uiCr; - } - String _presetLabel(int? index) { if (index == null) { return 'custom'; @@ -1534,6 +1505,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } void _logRadioSettingsState(String message) { + if (!kDebugMode) return; _appLog.info( '$message | ' 'freq=${_frequencyController.text}MHz ' @@ -1711,10 +1683,47 @@ class _RadioSettingsSnapshot { required this.txPowerDbm, }); + /// Frequency in integer Hz — avoids floating-point comparison issues. + int get frequencyHz => (frequencyMHz * 1000).round(); + + /// Convert from the connector's raw-int snapshot to UI-enum snapshot. + static _RadioSettingsSnapshot? fromMeshCoreSnapshot( + MeshCoreRadioStateSnapshot snapshot, + ) { + final bw = LoRaBandwidth.values + .where((b) => b.hz == snapshot.bwHz) + .firstOrNull; + final sf = LoRaSpreadingFactor.values + .where((s) => s.value == snapshot.sf) + .firstOrNull; + final cr = LoRaCodingRate.values + .where((c) => c.value == _toUiCodingRate(snapshot.cr)) + .firstOrNull; + if (bw == null || sf == null || cr == null) return null; + return _RadioSettingsSnapshot( + frequencyMHz: snapshot.freqHz / 1000.0, + bandwidth: bw, + spreadingFactor: sf, + codingRate: cr, + txPowerDbm: snapshot.txPowerDbm, + ); + } + + /// Convert back to the connector's raw-int snapshot. + MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) { + return MeshCoreRadioStateSnapshot( + freqHz: frequencyHz, + bwHz: bandwidth.hz, + sf: spreadingFactor.value, + cr: _toDeviceCodingRate(codingRate.value, deviceCr), + txPowerDbm: txPowerDbm, + ); + } + @override bool operator ==(Object other) { return other is _RadioSettingsSnapshot && - frequencyMHz == other.frequencyMHz && + frequencyHz == other.frequencyHz && bandwidth == other.bandwidth && spreadingFactor == other.spreadingFactor && codingRate == other.codingRate && @@ -1723,7 +1732,7 @@ class _RadioSettingsSnapshot { @override int get hashCode => Object.hash( - frequencyMHz, + frequencyHz, bandwidth, spreadingFactor, codingRate, From 69433b6d896a31d39e48dea09f9475353c81134a Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:23:57 -0700 Subject: [PATCH 26/27] small clean up from PR #275 just removes extraneous assignment to _lastNonRepeatSnapshot and moves the Navigator pop to after all uses of the context in _RadioSettingsDialog --- lib/screens/settings_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e7d61ee7..e9b73f83 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1162,7 +1162,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _clientRepeat = widget.connector.clientRepeat ?? false; _selectedPresetIndex = _findMatchingPresetIndex(); - _lastNonRepeatSnapshot = _currentSnapshot(); if (_clientRepeat) { _lastNonRepeatSnapshot = _sessionRememberedNonRepeatSnapshot() ?? @@ -1472,7 +1471,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } if (!mounted) return; - Navigator.pop(context); _logRadioSettingsState('Radio settings saved successfully'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), @@ -1484,6 +1482,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { SnackBar(content: Text(l10n.settings_error(e.toString()))), ); } + Navigator.pop(context); } String _presetLabel(int? index) { From 5354acb1d3af21606c539871f8b750898be1605d Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 9 Apr 2026 09:57:46 -0700 Subject: [PATCH 27/27] clean up after merge conflicts --- lib/helpers/gif_helper.dart | 2 +- lib/helpers/reaction_helper.dart | 2 +- lib/l10n/app_localizations_hu.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index 8dd187b1..5b68e902 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -30,7 +30,7 @@ class GifHelper { ).firstMatch(trimmed); return pageMatch?.group(1); } - + /// Encode a GIF in a format that parseGif() can parse. static String encodeGif(String gifId) { return 'g:$gifId'; diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 169b1a14..36118ca3 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -109,7 +109,7 @@ class ReactionHelper { return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); } - + /// Encode a reaction message that parseReaction() can parse. static String encodeReaction(String hash, String emojiIndex) { return 'r:$hash:$emojiIndex'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index bbf989e0..5d305ee5 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3677,7 +3677,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).'; + return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 37303a0c..6fcad22b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2419,7 +2419,7 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliQuickDiscovery => 'Ontdek Buren'; @override - String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; + String get repeater_cliHelpAdvert => 'Advertentie uitzenden'; @override String get repeater_cliHelpReboot => diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2428a778..ffc8c590 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))