From 0e074fd806ba09e97ed548e6bee5dcac29bd38cc Mon Sep 17 00:00:00 2001 From: Stephan Rodemeier Date: Sun, 5 Apr 2026 21:35:39 +0200 Subject: [PATCH] Add region management This adds region management: the user can manage a list of available regions and for each channel pick a region from that list to apply to messages. Region discovery from nearby repeaters will be done in a separate PR. This is a part of the work needed for #120. --- documentation/ble-protocol.md | 1 + lib/connector/meshcore_connector.dart | 59 ++++-- lib/connector/meshcore_protocol.dart | 17 ++ lib/l10n/app_de.arb | 32 ++- lib/l10n/app_en.arb | 32 ++- lib/l10n/app_localizations.dart | 78 ++++++++ lib/l10n/app_localizations_bg.dart | 43 ++++ lib/l10n/app_localizations_de.dart | 44 +++++ lib/l10n/app_localizations_en.dart | 43 ++++ lib/l10n/app_localizations_es.dart | 43 ++++ lib/l10n/app_localizations_fr.dart | 43 ++++ lib/l10n/app_localizations_hu.dart | 43 ++++ lib/l10n/app_localizations_it.dart | 43 ++++ lib/l10n/app_localizations_ja.dart | 43 ++++ lib/l10n/app_localizations_ko.dart | 43 ++++ lib/l10n/app_localizations_nl.dart | 43 ++++ lib/l10n/app_localizations_pl.dart | 43 ++++ lib/l10n/app_localizations_pt.dart | 43 ++++ lib/l10n/app_localizations_ru.dart | 43 ++++ lib/l10n/app_localizations_sk.dart | 43 ++++ lib/l10n/app_localizations_sl.dart | 43 ++++ lib/l10n/app_localizations_sv.dart | 43 ++++ lib/l10n/app_localizations_uk.dart | 43 ++++ lib/l10n/app_localizations_zh.dart | 43 ++++ lib/models/channel.dart | 4 + lib/screens/channel_chat_screen.dart | 226 ++++++++++++++++++---- lib/screens/channels_screen.dart | 26 ++- lib/screens/region_management_screen.dart | 160 +++++++++++++++ lib/screens/settings_screen.dart | 9 + lib/services/ble_debug_log_service.dart | 2 + lib/storage/channel_region_store.dart | 39 ++++ lib/storage/region_store.dart | 53 +++++ untranslated.json | 208 ++++++++++++++++++++ 33 files changed, 1653 insertions(+), 68 deletions(-) create mode 100644 lib/screens/region_management_screen.dart create mode 100644 lib/storage/channel_region_store.dart create mode 100644 lib/storage/region_store.dart diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md index ec24094..852f7dc 100644 --- a/documentation/ble-protocol.md +++ b/documentation/ble-protocol.md @@ -118,6 +118,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff: | 40 | CMD_GET_CUSTOM_VAR | Get custom variables | | 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | | 50 | CMD_SEND_BINARY_REQ | Send binary request | +| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) | | 57 | CMD_SEND_ANON_REQ | Send anonymous request | | 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | | 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration | diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b432277..dd31012 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:math' as math; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/storage/region_store.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -34,6 +35,7 @@ import 'meshcore_connector_tcp.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; +import '../storage/channel_region_store.dart'; import '../storage/channel_store.dart'; import '../storage/contact_discovery_store.dart'; import '../storage/contact_settings_store.dart'; @@ -276,6 +278,7 @@ class MeshCoreConnector extends ChangeNotifier { final MessageStore _messageStore = MessageStore(); final ChannelOrderStore _channelOrderStore = ChannelOrderStore(); final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore(); + final ChannelRegionStore _channelRegionStore = ChannelRegionStore(); final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore(); @@ -283,6 +286,7 @@ class MeshCoreConnector extends ChangeNotifier { final UnreadStore _unreadStore = UnreadStore(); List _cachedChannels = []; final Map _channelSmazEnabled = {}; + final Map _channelRegions = {}; bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command final Map _contactSmazEnabled = {}; @@ -603,6 +607,14 @@ class MeshCoreConnector extends ChangeNotifier { return _contactSmazEnabled[contactKeyHex] ?? false; } + bool hasChannelRegion(int channelIndex) { + return _channelRegions[channelIndex] != ''; + } + + Region getChannelRegion(int channelIndex) { + return _channelRegions[channelIndex] ?? ''; + } + void ensureContactSmazSettingLoaded(String contactKeyHex) { _ensureContactSmazSettingLoaded(contactKeyHex); } @@ -692,6 +704,14 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + Future setChannelRegion(int channelIndex, String region) async { + _channelRegions[channelIndex] = await _channelRegionStore.saveRegion( + channelIndex, + region, + ); + notifyListeners(); + } + Future _loadChannelOrder() async { _channelOrder = await _channelOrderStore.loadChannelOrder(); _applyChannelOrder(); @@ -840,9 +860,11 @@ class MeshCoreConnector extends ChangeNotifier { Future loadChannelSettings({int? maxChannels}) async { _channelSmazEnabled.clear(); + _channelRegions.clear(); final channelCount = maxChannels ?? _maxChannels; for (int i = 0; i < channelCount; i++) { _channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i); + _channelRegions[i] = await _channelRegionStore.loadRegion(i); } } @@ -2973,12 +2995,19 @@ class MeshCoreConnector extends ChangeNotifier { // Send the reaction to the device (don't add as a visible message) final reactionQueueId = _nextReactionSendQueueId(); _pendingChannelSentQueue.add(reactionQueueId); - await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); - await sendFrame( - buildSendChannelTextMsgFrame(channel.index, text), - channelSendQueueId: reactionQueueId, - expectsGenericAck: true, - ); + try { + await sendFrame( + buildSetFloodScopeFrame(getChannelRegion(channel.index)), + ); + await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, text), + channelSendQueueId: reactionQueueId, + expectsGenericAck: true, + ); + } finally { + await sendFrame(buildSetFloodScopeFrame('')); + } return; } @@ -3001,12 +3030,17 @@ class MeshCoreConnector extends ChangeNotifier { (isChannelSmazEnabled(channel.index) && !isStructuredPayload) ? Smaz.encodeIfSmaller(text) : text; - await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); - await sendFrame( - buildSendChannelTextMsgFrame(channel.index, outboundText), - channelSendQueueId: message.messageId, - expectsGenericAck: true, - ); + try { + await sendFrame(buildSetFloodScopeFrame(getChannelRegion(channel.index))); + await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, outboundText), + channelSendQueueId: message.messageId, + expectsGenericAck: true, + ); + } finally { + await sendFrame(buildSetFloodScopeFrame('')); + } } Future removeContact(Contact contact) async { @@ -3680,6 +3714,7 @@ class MeshCoreConnector extends ChangeNotifier { _messageStore.setPublicKeyHex = selfPublicKeyHex; _channelOrderStore.setPublicKeyHex = selfPublicKeyHex; _channelSettingsStore.setPublicKeyHex = selfPublicKeyHex; + _channelRegionStore.setPublicKeyHex = selfPublicKeyHex; _contactSettingsStore.setPublicKeyHex = selfPublicKeyHex; _contactStore.setPublicKeyHex = selfPublicKeyHex; _channelStore.setPublicKeyHex = selfPublicKeyHex; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 396d78b..8a1b3a5 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:crypto/crypto.dart' as crypto; import 'package:flutter/widgets.dart'; // Buffer Reader - sequential binary data reader with pointer tracking @@ -211,6 +212,7 @@ const int cmdSendAnonReq = 57; const int cmdSetAutoAddConfig = 58; const int cmdGetAutoAddConfig = 59; const int cmdSetPathHashMode = 61; +const int cmdSetFloodScope = 54; // Text message types const int txtTypePlain = 0; @@ -955,3 +957,18 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) { } return writer.toBytes(); } + +//Build CMD_SET_FLOOD_SCOPE +// Format: [cmd][scope] +Uint8List buildSetFloodScopeFrame(String region) { + if (region == '') { + // reset scope + return Uint8List.fromList([cmdSetFloodScope, 0]); + } + + final name = region.startsWith('#') ? region : '#$region'; + final hash = crypto.sha256.convert(utf8.encode(name)).bytes; + final scope = Uint8List.fromList(hash.sublist(0, 16)); + + return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]); +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 54683d2..9e01dbd 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2097,5 +2097,33 @@ "description": "Repeater setting subtitle: describes the clock sync after login behavior" }, "repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden", - "repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden." -} + "repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.", + "@settings_deleteRegionConfirm": { + "placeholders": { + "region": { + "type": "String" + } + } + }, + "@channels_regionSetTo": { + "placeholders": { + "region": { + "type": "String", + "example": "de-mitte" + } + } + }, + "settings_regionSettings": "Regionen", + "settings_regionSettingsSubtitle": "Gespeicherte Regionen verwalten", + "settings_regionManagement_screenTitle": "Regions-Verwaltung", + "settings_regionNameHint": "Regions-Namen eingeben", + "settings_regionAddRegion": "Region hinzufügen", + "settings_regionDeleted": "Region entfernt", + "settings_regionName": "Regions-Name", + "settings_deleteRegion": "Region entfernen", + "settings_deleteRegionConfirm": "Region \"{region}\" aus der Liste entfernen?", + "channels_regionNotSet": "Region: keine", + "channels_regionSetTo": "Region: {region}", + "channels_regionSelect_Title": "Region auswählen", + "channels_clearRegion": "Region zurücksetzen" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1ac2357..e6a8027 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -127,7 +127,6 @@ } } }, - "scanner_stop": "Stop", "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", @@ -149,6 +148,22 @@ "settings_radioSettings": "Radio Settings", "settings_radioSettingsSubtitle": "Frequency, power, spreading factor", "settings_radioSettingsUpdated": "Radio settings updated", + "settings_regionSettings": "Regions", + "settings_regionSettingsSubtitle": "Manage stored regions", + "settings_regionManagement_screenTitle": "Region Management", + "settings_regionNameHint": "Enter region name", + "settings_regionAddRegion": "Add region", + "settings_regionName": "Region Name", + "settings_regionDeleted": "Region deleted", + "settings_deleteRegion": "Delete Region", + "settings_deleteRegionConfirm": "Remove \"{region}\" from region list?", + "@settings_deleteRegionConfirm": { + "placeholders": { + "region": { + "type": "String" + } + } + }, "settings_location": "Location", "settings_locationSubtitle": "GPS coordinates", "settings_locationUpdated": "Location and GPS settings updated", @@ -606,6 +621,18 @@ "channels_scanQrCodeComingSoon": "Coming soon", "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", + "channels_regionSetTo": "Region: {region}", + "@channels_regionSetTo": { + "placeholders": { + "region": { + "type": "String", + "example": "de-mitte" + } + } + }, + "channels_regionNotSet": "Region: none", + "channels_regionSelect_Title": "Select a region", + "channels_clearRegion": "Clear region", "chat_noMessages": "No messages yet", "chat_sendMessage": "Send message", "chat_sendMessageTo": "Send message to {name}", @@ -2065,7 +2092,6 @@ "radioStats_stripWaiting": "Fetching radio stats…", "radioStats_settingsTile": "Radio stats", "radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime", - "translation_title": "Translation", "translation_enableTitle": "Enable translation", "translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.", @@ -2119,4 +2145,4 @@ }, "translation_translationOptions": "Translation options", "translation_systemLanguage": "System language" -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index efcbd0f..0419b15 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -724,6 +724,60 @@ abstract class AppLocalizations { /// **'Radio settings updated'** String get settings_radioSettingsUpdated; + /// No description provided for @settings_regionSettings. + /// + /// In en, this message translates to: + /// **'Regions'** + String get settings_regionSettings; + + /// No description provided for @settings_regionSettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Manage stored regions'** + String get settings_regionSettingsSubtitle; + + /// No description provided for @settings_regionManagement_screenTitle. + /// + /// In en, this message translates to: + /// **'Region Management'** + String get settings_regionManagement_screenTitle; + + /// No description provided for @settings_regionNameHint. + /// + /// In en, this message translates to: + /// **'Enter region name'** + String get settings_regionNameHint; + + /// No description provided for @settings_regionAddRegion. + /// + /// In en, this message translates to: + /// **'Add region'** + String get settings_regionAddRegion; + + /// No description provided for @settings_regionName. + /// + /// In en, this message translates to: + /// **'Region Name'** + String get settings_regionName; + + /// No description provided for @settings_regionDeleted. + /// + /// In en, this message translates to: + /// **'Region deleted'** + String get settings_regionDeleted; + + /// No description provided for @settings_deleteRegion. + /// + /// In en, this message translates to: + /// **'Delete Region'** + String get settings_deleteRegion; + + /// No description provided for @settings_deleteRegionConfirm. + /// + /// In en, this message translates to: + /// **'Remove \"{region}\" from region list?'** + String settings_deleteRegionConfirm(String region); + /// No description provided for @settings_location. /// /// In en, this message translates to: @@ -2290,6 +2344,30 @@ abstract class AppLocalizations { /// **'e.g. #team'** String get channels_hashtagHint; + /// No description provided for @channels_regionSetTo. + /// + /// In en, this message translates to: + /// **'Region: {region}'** + String channels_regionSetTo(String region); + + /// No description provided for @channels_regionNotSet. + /// + /// In en, this message translates to: + /// **'Region: none'** + String get channels_regionNotSet; + + /// No description provided for @channels_regionSelect_Title. + /// + /// In en, this message translates to: + /// **'Select a region'** + String get channels_regionSelect_Title; + + /// No description provided for @channels_clearRegion. + /// + /// In en, this message translates to: + /// **'Clear region'** + String get channels_clearRegion; + /// No description provided for @chat_noMessages. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index bb07229..ba5145c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -336,6 +336,35 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_radioSettingsUpdated => 'Радио настройките са актуализирани'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Местоположение'; @@ -1236,6 +1265,20 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_hashtagHint => 'напр. #отбор'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Няма съобщения.'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 49cf19a..512ce67 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -338,6 +338,36 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert'; + @override + String get settings_regionSettings => 'Regionen'; + + @override + String get settings_regionSettingsSubtitle => + 'Gespeicherte Regionen verwalten'; + + @override + String get settings_regionManagement_screenTitle => 'Regions-Verwaltung'; + + @override + String get settings_regionNameHint => 'Regions-Namen eingeben'; + + @override + String get settings_regionAddRegion => 'Region hinzufügen'; + + @override + String get settings_regionName => 'Regions-Name'; + + @override + String get settings_regionDeleted => 'Region entfernt'; + + @override + String get settings_deleteRegion => 'Region entfernen'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Region \"$region\" aus der Liste entfernen?'; + } + @override String get settings_location => 'Ort'; @@ -1235,6 +1265,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_hashtagHint => 'z.B. #team'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: keine'; + + @override + String get channels_regionSelect_Title => 'Region auswählen'; + + @override + String get channels_clearRegion => 'Region zurücksetzen'; + @override String get chat_noMessages => 'Noch keine Nachrichten.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e13934b..8122a36 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -332,6 +332,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio settings updated'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Location'; @@ -1210,6 +1239,20 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channels_hashtagHint => 'e.g. #team'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'No messages yet'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ddb9b6e..8afc538 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -336,6 +336,35 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Ubicación'; @@ -1235,6 +1264,20 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channels_hashtagHint => 'ej. #equipo'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Aún no hay mensajes'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index fbe106d..7db2cdf 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -338,6 +338,35 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Emplacement'; @@ -1240,6 +1269,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_hashtagHint => 'ex. #equipe'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Aucun message pour le moment.'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 920efd8..1316bc2 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -335,6 +335,35 @@ class AppLocalizationsHu extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'A rádió beállítások frissítve'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Helyszín'; @@ -1243,6 +1272,20 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channels_hashtagHint => 'pl. #csapat'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Még nincs üzenet.'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b492d6a..c90cdc8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -338,6 +338,35 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Posizione'; @@ -1236,6 +1265,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_hashtagHint => 'es. #team'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Nessun messaggio ancora'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index daebcba..2e4e622 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -323,6 +323,35 @@ class AppLocalizationsJa extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => '場所'; @@ -1176,6 +1205,20 @@ class AppLocalizationsJa extends AppLocalizations { @override String get channels_hashtagHint => '例:#チーム'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'まだメッセージは届いていません'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 605cc96..bfee59c 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -322,6 +322,35 @@ class AppLocalizationsKo extends AppLocalizations { @override String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => '위치'; @@ -1171,6 +1200,20 @@ class AppLocalizationsKo extends AppLocalizations { @override String get channels_hashtagHint => '예: #팀'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => '아직 메시지가 없습니다.'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3d1644f..928ba65 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -334,6 +334,35 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Locatie'; @@ -1224,6 +1253,20 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channels_hashtagHint => 'bijv. #team'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Nog geen berichten.'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index f0006b1..66cd913 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -340,6 +340,35 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_radioSettingsUpdated => 'Ustawienia radia zostały zaktualizowane'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Lokalizacja'; @@ -1244,6 +1273,20 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channels_hashtagHint => 'np. #zespół'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Brak jeszcze wiadomości'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f4b7ffc..512d138 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -338,6 +338,35 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_radioSettingsUpdated => 'Configurações de rádio atualizadas'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Localização'; @@ -1235,6 +1264,20 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channels_hashtagHint => 'ex. #equipe'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Ainda não existem mensagens.'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dcd9d9c..dbab156 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -337,6 +337,35 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Позиция'; @@ -1235,6 +1264,20 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channels_hashtagHint => 'например, #команда'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Сообщений пока нет'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 7323ddc..2b9c969 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -336,6 +336,35 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Lokalita'; @@ -1223,6 +1252,20 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channels_hashtagHint => 'napr. #tím'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Zatiaľ žiadne správy.'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index a374d4b..4ac4d57 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -334,6 +334,35 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Lokacija'; @@ -1221,6 +1250,20 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_hashtagHint => 'npr. #ekipa'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Še ni sporočil.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6e2f563..b629795 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -334,6 +334,35 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_radioSettingsUpdated => 'Radioinställningarna har uppdaterats'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Plats'; @@ -1214,6 +1243,20 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channels_hashtagHint => 't.ex. #team'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Inga meddelanden ännu'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index dd189eb..4b96977 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -336,6 +336,35 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => 'Розташування'; @@ -1227,6 +1256,20 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channels_hashtagHint => 'напр. #команда'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => 'Поки немає повідомлень.'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b48b31d..3ec5ef9 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -320,6 +320,35 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_radioSettingsUpdated => '无线电设置已更新'; + @override + String get settings_regionSettings => 'Regions'; + + @override + String get settings_regionSettingsSubtitle => 'Manage stored regions'; + + @override + String get settings_regionManagement_screenTitle => 'Region Management'; + + @override + String get settings_regionNameHint => 'Enter region name'; + + @override + String get settings_regionAddRegion => 'Add region'; + + @override + String get settings_regionName => 'Region Name'; + + @override + String get settings_regionDeleted => 'Region deleted'; + + @override + String get settings_deleteRegion => 'Delete Region'; + + @override + String settings_deleteRegionConfirm(String region) { + return 'Remove \"$region\" from region list?'; + } + @override String get settings_location => '位置'; @@ -1158,6 +1187,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channels_hashtagHint => '例如:#团队'; + @override + String channels_regionSetTo(String region) { + return 'Region: $region'; + } + + @override + String get channels_regionNotSet => 'Region: none'; + + @override + String get channels_regionSelect_Title => 'Select a region'; + + @override + String get channels_clearRegion => 'Clear region'; + @override String get chat_noMessages => '暂无消息'; diff --git a/lib/models/channel.dart b/lib/models/channel.dart index 4fdd627..6bb73e9 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -24,6 +24,10 @@ class Channel { bool get isPublicChannel => pskHex == publicChannelPsk; + bool get isHashtagChannel => name.startsWith('#'); + + bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel; + static Channel? fromFrame(Uint8List frame) { // CHANNEL_INFO format: // [0] = RESP_CODE_CHANNEL_INFO (18) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 64da058..335a973 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -5,6 +5,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/screens/region_management_screen.dart'; +import 'package:meshcore_open/storage/region_store.dart'; +import 'package:meshcore_open/widgets/adaptive_app_bar_title.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -50,6 +53,7 @@ class _ChannelChatScreenState extends State { ChannelMessage? _replyingToMessage; final Map _messageKeys = {}; bool _isLoadingOlder = false; + Region region = ''; MeshCoreConnector? _connector; DateTime? _lastChannelSendAt; @@ -60,12 +64,17 @@ class _ChannelChatScreenState extends State { super.initState(); _textFieldFocusNode.addListener(_onTextFieldFocusChange); _scrollController.onScrollNearTop = _loadOlderMessages; + region = context.read().getChannelRegion( + widget.channel.index, + ); + SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final connector = context.read(); final settings = context.read().settings; final idx = widget.channel.index; final unread = connector.getUnreadCountForChannelIndex(idx); + ChannelMessage? anchor; if (settings.jumpToOldestUnread && unread > 0) { anchor = _findOldestUnreadChannelAnchor( @@ -166,47 +175,81 @@ class _ChannelChatScreenState extends State { @override Widget build(BuildContext context) { + final connector = context.watch(); + + // Determine icon and colors based on channel type + IconData icon = Icons.lock; + Color iconColor = Colors.blue; + Color bgColor = Colors.blue.withValues(alpha: 0.2); + + // TODO(clauwn): add community handling + final isCommunityChannel = false; + final isCommunityPublic = false; + + if (isCommunityChannel) { + iconColor = Colors.purple; + bgColor = Colors.purple.withValues(alpha: 0.2); + icon = isCommunityPublic ? Icons.groups : Icons.tag; + } else if (widget.channel.isPublicChannel) { + icon = Icons.public; + iconColor = Colors.green; + bgColor = Colors.green.withValues(alpha: 0.2); + } else if (widget.channel.isHashtagChannel) { + icon = Icons.tag; + } + + final regionHeader = region != '' + ? context.l10n.channels_regionSetTo(region) + : context.l10n.channels_regionNotSet; + return Scaffold( appBar: AppBar( - title: Row( - children: [ - Icon( - widget.channel.isPublicChannel ? Icons.public : Icons.tag, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.channel.name.isEmpty - ? context.l10n.channels_channelIndex( - widget.channel.index, - ) - : widget.channel.name, - style: const TextStyle(fontSize: 16), - ), - Consumer( - builder: (context, connector, _) { - final unreadCount = connector - .getUnreadCountForChannelIndex(widget.channel.index); - final privacy = widget.channel.isPublicChannel - ? context.l10n.channels_public - : context.l10n.channels_private; - return Text( - '$privacy • ${context.l10n.chat_unread(unreadCount)}', - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ); - }, - ), - ], + title: GestureDetector( + onTap: () => openRegionSelectDialog(widget.channel), + child: Row( + children: [ + CircleAvatar( + backgroundColor: bgColor, + child: Icon(icon, color: iconColor), ), - ), - ], + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, connector, _) { + return Text( + widget.channel.name.isEmpty + ? context.l10n.channels_channelIndex( + widget.channel.index, + ) + : widget.channel.name, + style: const TextStyle(fontSize: 16), + ); + }, + ), + Consumer( + builder: (context, connector, _) { + final unreadCount = connector + .getUnreadCountForChannelIndex( + widget.channel.index, + ); + return Text( + '$regionHeader • ${context.l10n.chat_unread(unreadCount)}', + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ); + }, + ), + ], + ), + ), + ], + ), ), centerTitle: false, + titleSpacing: 0, actions: [ const RadioStatsIconButton(), PopupMenuButton( @@ -1341,6 +1384,117 @@ class _ChannelChatScreenState extends State { .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) .join(','); } + + void openRegionSelectDialog(Channel channel) async { + await showDialog( + context: context, + builder: (BuildContext context) => _RegionSelectDialog(channel: channel), + ); + if (context.mounted) { + await _connector?.loadChannelSettings(); + setState(() { + region = _connector?.getChannelRegion(channel.index) ?? ''; + }); + } + } +} + +class _RegionSelectDialog extends StatefulWidget { + final Channel channel; + + const _RegionSelectDialog({required this.channel}); + + @override + _RegionSelectDialogState createState() => _RegionSelectDialogState(); +} + +class _RegionSelectDialogState extends State<_RegionSelectDialog> { + final RegionStore regionStore = RegionStore(); + + List regions = []; + int selectedIndex = 0; + + @override + void initState() { + super.initState(); + loadRegions(); + } + + void loadRegions() { + setState(() { + regions = regionStore.loadRegions(); + Region channelRegion = context.read().getChannelRegion( + widget.channel.index, + ); + selectedIndex = regions.indexOf(channelRegion); + }); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppBar( + backgroundColor: Colors.transparent, + title: AdaptiveAppBarTitle( + context.l10n.channels_regionSelect_Title, + ), + centerTitle: true, + actions: [ + IconButton( + tooltip: context.l10n.channels_clearRegion, + icon: const Icon(Icons.backspace_outlined), + onPressed: () { + context.read().setChannelRegion( + widget.channel.index, + '', + ); + if (context.mounted) { + Navigator.pop(context); + } + }, + ), + IconButton( + tooltip: context.l10n.settings_regionSettingsSubtitle, + icon: const Icon(Icons.settings), + onPressed: () async { + await pushRegionManagementScreen(context); + loadRegions(); + }, + ), + ], + ), + SizedBox(height: 15), + Expanded( + child: ListView.builder( + itemCount: regions.length, + itemBuilder: (context, index) => ListTile( + title: Text(regions[index]), + tileColor: selectedIndex == index + ? Colors.blue.withValues(alpha: 0.2) + : null, + onTap: () { + context.read().setChannelRegion( + widget.channel.index, + regions[index], + ); + if (context.mounted) { + Navigator.pop(context); + } + }, + ), + ), + ), + ], + ), + ), + ); + } } class _SwipeReplyBubble extends StatefulWidget { diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 51d2453..7141711 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; @@ -380,10 +380,15 @@ class _ChannelsScreenState extends State isCommunityChannel && _isCommunityPublicChannel(channel, community); // Determine icon and colors based on channel type - IconData icon; - Color iconColor; - Color bgColor; - String subtitle; + IconData icon = Icons.lock; + Color iconColor = Colors.blue; + Color bgColor = Colors.blue.withValues(alpha: 0.2); + String region = connector.hasChannelRegion(channel.index) + ? context.l10n.channels_regionSetTo( + connector.getChannelRegion(channel.index), + ) + : context.l10n.channels_regionNotSet; + String subtitle = region; if (isCommunityChannel) { // Community channel styling @@ -402,17 +407,8 @@ class _ChannelsScreenState extends State icon = Icons.public; iconColor = Colors.green; bgColor = Colors.green.withValues(alpha: 0.2); - subtitle = context.l10n.channels_publicChannel; - } else if (channel.name.startsWith('#')) { + } else if (channel.isHashtagChannel) { icon = Icons.tag; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); - subtitle = context.l10n.channels_hashtagChannel; - } else { - icon = Icons.lock; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); - subtitle = context.l10n.channels_privateChannel; } return Card( diff --git a/lib/screens/region_management_screen.dart b/lib/screens/region_management_screen.dart new file mode 100644 index 0000000..5ebe632 --- /dev/null +++ b/lib/screens/region_management_screen.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/storage/region_store.dart'; +import 'package:provider/provider.dart'; + +Future pushRegionManagementScreen(BuildContext context) { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RegionManagementScreen(), + ), + ); +} + +class RegionManagementScreen extends StatefulWidget { + const RegionManagementScreen({super.key}); + + @override + State createState() => _RegionManagementScreenState(); +} + +class _RegionManagementScreenState extends State { + final RegionStore _regionStore = RegionStore(); + List _regions = []; + + String region = ''; + + @override + void initState() { + super.initState(); + final connector = context.read(); + _regionStore.setPublicKeyHex = connector.selfPublicKeyHex; + _loadRegions(); + } + + void _loadRegions() { + context.read().loadChannelSettings(); + + final regions = _regionStore.loadRegions(); + if (mounted) { + setState(() { + _regions = regions; + }); + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar( + title: Text(l10n.settings_regionManagement_screenTitle), + centerTitle: true, + actions: [ + IconButton( + tooltip: l10n.settings_regionAddRegion, + icon: const Icon(Icons.add), + onPressed: () => _showAddRegionDialog(context), + ), + ], + ), + body: ListView.builder( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88), + itemCount: _regions.length, + itemBuilder: (context, index) { + final region = _regions[index]; + return _buildRegionTile(context, region); + }, + ), + ); + } + + void _showAddRegionDialog(BuildContext context) { + final l10n = context.l10n; + final controller = TextEditingController(text: region); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.settings_regionName), + content: TextField( + controller: controller, + autofocus: true, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _handleAddRegion(controller.text, context), + decoration: InputDecoration( + hintText: l10n.settings_regionNameHint, + border: const OutlineInputBorder(), + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")), + ], + maxLength: 30, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () => _handleAddRegion(controller.text, context), + child: Text(l10n.common_add), + ), + ], + ), + ); + } + + void _handleAddRegion(Region region, BuildContext context) { + Navigator.pop(context); + _regionStore.addRegion(region); + _loadRegions(); + } + + Widget _buildRegionTile(BuildContext context, Region region) { + return Card( + key: ValueKey(region), + child: ListTile( + dense: false, + title: Text(region), + trailing: IconButton( + icon: Icon(Icons.delete_outline), + onPressed: () => _confirmDelete(context, region), + ), + ), + ); + } + + void _confirmDelete(BuildContext context, Region region) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.settings_deleteRegion), + content: Text(context.l10n.settings_deleteRegionConfirm(region)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + Navigator.pop(dialogContext); + await _regionStore.removeRegion(region); + _loadRegions(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.settings_regionDeleted)), + ); + }, + child: Text( + context.l10n.common_delete, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e9b73f8..150eaa9 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -15,6 +15,7 @@ import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; import '../widgets/radio_stats_entry.dart'; +import 'region_management_screen.dart'; /// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) /// to the UI enum range (always 5-8). @@ -287,6 +288,14 @@ class _SettingsScreenState extends State { onTap: () => _showRadioSettings(context, connector), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.landscape), + title: Text(l10n.settings_regionSettings), + subtitle: Text(l10n.settings_regionSettingsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => pushRegionManagementScreen(context), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.sensors_outlined), title: Text(l10n.radioStats_settingsTile), diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index df2822b..d415b5c 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -176,6 +176,8 @@ class BleDebugLogService extends ChangeNotifier { return 'CMD_SET_CUSTOM_VAR'; case cmdSendTracePath: return 'CMD_SEND_TRACE_PATH'; + case cmdSetFloodScope: + return 'CMD_SET_FLOOD_SCOPE'; default: return null; } diff --git a/lib/storage/channel_region_store.dart b/lib/storage/channel_region_store.dart new file mode 100644 index 0000000..e28551a --- /dev/null +++ b/lib/storage/channel_region_store.dart @@ -0,0 +1,39 @@ +import '../utils/app_logger.dart'; +import 'prefs_manager.dart'; + +class ChannelRegionStore { + static const String _keyPrefix = 'channel_region_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; + + Future loadRegion(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel settings.', + ); + return ''; + } + final prefs = PrefsManager.instance; + final key = '$keyFor$channelIndex'; + String? region = prefs.getString(key); + return region ?? ''; + } + + Future saveRegion(int channelIndex, String region) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel settings.', + ); + return ''; + } + + final prefs = PrefsManager.instance; + final key = '$keyFor$channelIndex'; + await prefs.setString(key, region); + return region; + } +} diff --git a/lib/storage/region_store.dart b/lib/storage/region_store.dart new file mode 100644 index 0000000..eead089 --- /dev/null +++ b/lib/storage/region_store.dart @@ -0,0 +1,53 @@ +import 'package:meshcore_open/storage/channel_region_store.dart'; +import 'package:meshcore_open/storage/channel_store.dart'; + +import 'prefs_manager.dart'; + +typedef Region = String; + +class RegionStore { + static const String key = 'regions'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + List loadRegions() { + final prefs = PrefsManager.instance; + List? region = prefs.getStringList(key); + return region ?? []; + } + + void saveRegions(List regions) { + final prefs = PrefsManager.instance; + + var distinctRegions = [ + ...{...regions}, + ]; + + distinctRegions.sort(); + prefs.setStringList(key, distinctRegions); + } + + void addRegion(Region region) { + final regions = loadRegions(); + regions.add(region); + saveRegions(regions); + } + + Future removeRegion(Region region) async { + final regions = loadRegions(); + final channelStore = ChannelStore(); + final channelRegionStore = ChannelRegionStore(); + channelStore.setPublicKeyHex = publicKeyHex; + channelRegionStore.setPublicKeyHex = publicKeyHex; + + for (var channel in await channelStore.loadChannels()) { + var channelRegion = await channelRegionStore.loadRegion(channel.index); + if (channelRegion == region) { + channelRegionStore.saveRegion(channel.index, ''); + } + } + regions.remove(region); + saveRegions(regions); + } +} diff --git a/untranslated.json b/untranslated.json index 1ebd9bc..d6e7a51 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,5 +1,18 @@ { "bg": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], @@ -8,62 +21,257 @@ ], "es": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "fr": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "hu": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "it": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "ja": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "ko": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "nl": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "pl": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "pt": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "ru": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "sk": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "sl": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "sv": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "uk": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ], "zh": [ + "settings_regionSettings", + "settings_regionSettingsSubtitle", + "settings_regionManagement_screenTitle", + "settings_regionNameHint", + "settings_regionAddRegion", + "settings_regionName", + "settings_regionDeleted", + "settings_deleteRegion", + "settings_deleteRegionConfirm", + "channels_regionSetTo", + "channels_regionNotSet", + "channels_regionSelect_Title", + "channels_clearRegion", "chat_sendMessage" ] }