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.
This commit is contained in:
Stephan Rodemeier 2026-04-05 21:35:39 +02:00
parent 0757c8e53a
commit 0e074fd806
33 changed files with 1653 additions and 68 deletions

View file

@ -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 |

View file

@ -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<Channel> _cachedChannels = [];
final Map<int, bool> _channelSmazEnabled = {};
final Map<int, Region> _channelRegions = {};
bool _lastSentWasCliCommand =
false; // Track if last sent message was a CLI command
final Map<String, bool> _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<void> setChannelRegion(int channelIndex, String region) async {
_channelRegions[channelIndex] = await _channelRegionStore.saveRegion(
channelIndex,
region,
);
notifyListeners();
}
Future<void> _loadChannelOrder() async {
_channelOrder = await _channelOrderStore.loadChannelOrder();
_applyChannelOrder();
@ -840,9 +860,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> 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<void> 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;

View file

@ -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]);
}

View file

@ -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"
}

View file

@ -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"
}
}

View file

@ -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:

View file

@ -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 => 'Няма съобщения.';

View file

@ -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.';

View file

@ -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';

View file

@ -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';

View file

@ -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.';

View file

@ -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.';

View file

@ -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';

View file

@ -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 => 'まだメッセージは届いていません';

View file

@ -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 => '아직 메시지가 없습니다.';

View file

@ -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.';

View file

@ -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';

View file

@ -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.';

View file

@ -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 => 'Сообщений пока нет';

View file

@ -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.';

View file

@ -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.';

View file

@ -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';

View file

@ -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 => 'Поки немає повідомлень.';

View file

@ -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 => '暂无消息';

View file

@ -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)

View file

@ -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<ChannelChatScreen> {
ChannelMessage? _replyingToMessage;
final Map<String, GlobalKey> _messageKeys = {};
bool _isLoadingOlder = false;
Region region = '';
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
@ -60,12 +64,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages;
region = context.read<MeshCoreConnector>().getChannelRegion(
widget.channel.index,
);
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().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<ChannelChatScreen> {
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
// 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<MeshCoreConnector>(
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<MeshCoreConnector>(
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<MeshCoreConnector>(
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<String>(
@ -1341,6 +1384,117 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
.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<Region> regions = [];
int selectedIndex = 0;
@override
void initState() {
super.initState();
loadRegions();
}
void loadRegions() {
setState(() {
regions = regionStore.loadRegions();
Region channelRegion = context.read<MeshCoreConnector>().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<MeshCoreConnector>().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<MeshCoreConnector>().setChannelRegion(
widget.channel.index,
regions[index],
);
if (context.mounted) {
Navigator.pop(context);
}
},
),
),
),
],
),
),
);
}
}
class _SwipeReplyBubble extends StatefulWidget {

View file

@ -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<ChannelsScreen>
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<ChannelsScreen>
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(

View file

@ -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<void> pushRegionManagementScreen(BuildContext context) {
return Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const RegionManagementScreen(),
),
);
}
class RegionManagementScreen extends StatefulWidget {
const RegionManagementScreen({super.key});
@override
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
}
class _RegionManagementScreenState extends State<RegionManagementScreen> {
final RegionStore _regionStore = RegionStore();
List<Region> _regions = [];
String region = '';
@override
void initState() {
super.initState();
final connector = context.read<MeshCoreConnector>();
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
_loadRegions();
}
void _loadRegions() {
context.read<MeshCoreConnector>().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: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
],
maxLength: 30,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () => _handleAddRegion(controller.text, context),
child: Text(l10n.common_add),
),
],
),
);
}
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),
),
),
],
),
);
}
}

View file

@ -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<SettingsScreen> {
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),

View file

@ -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;
}

View file

@ -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<String> 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<String> 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;
}
}

View file

@ -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<Region> loadRegions() {
final prefs = PrefsManager.instance;
List<Region>? region = prefs.getStringList(key);
return region ?? [];
}
void saveRegions(List<Region> 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<void> 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);
}
}

View file

@ -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"
]
}