mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-01 06:30:31 +10:00
Regions discovery from nearby repeaters
This commit is contained in:
@@ -264,6 +264,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Serializes path operations (setContactPath/clearContactPath) to prevent
|
// Serializes path operations (setContactPath/clearContactPath) to prevent
|
||||||
// interleaved async calls from leaving in-memory state inconsistent with device.
|
// interleaved async calls from leaving in-memory state inconsistent with device.
|
||||||
Future<void> _pathOpLock = Future.value();
|
Future<void> _pathOpLock = Future.value();
|
||||||
|
// Flood scope is a global firmware setting, so scoped channel sends must not
|
||||||
|
// overlap or a message may inherit another channel's region.
|
||||||
|
Future<void> _channelScopedSendLock = Future.value();
|
||||||
|
static const Duration _commandAckTimeout = Duration(seconds: 5);
|
||||||
Map<String, String>? _currentCustomVars;
|
Map<String, String>? _currentCustomVars;
|
||||||
|
|
||||||
/// Maps repeater pubkey-prefix hex (12 hex chars = first 6 bytes) → the
|
/// Maps repeater pubkey-prefix hex (12 hex chars = first 6 bytes) → the
|
||||||
@@ -679,7 +683,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool hasChannelRegion(int channelIndex) {
|
bool hasChannelRegion(int channelIndex) {
|
||||||
return _channelRegions[channelIndex] != '';
|
return (_channelRegions[channelIndex] ?? '').isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Region getChannelRegion(int channelIndex) {
|
Region getChannelRegion(int channelIndex) {
|
||||||
@@ -3217,19 +3221,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Send the reaction to the device (don't add as a visible message)
|
// Send the reaction to the device (don't add as a visible message)
|
||||||
final reactionQueueId = _nextReactionSendQueueId();
|
final reactionQueueId = _nextReactionSendQueueId();
|
||||||
_pendingChannelSentQueue.add(reactionQueueId);
|
_pendingChannelSentQueue.add(reactionQueueId);
|
||||||
try {
|
await _runScopedChannelSend(() async {
|
||||||
await sendFrame(
|
|
||||||
buildSetFloodScopeFrame(getChannelRegion(channel.index)),
|
|
||||||
);
|
|
||||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||||
await sendFrame(
|
await _sendFrameAndWaitForCommandAck(
|
||||||
buildSendChannelTextMsgFrame(channel.index, text),
|
buildSendChannelTextMsgFrame(channel.index, text),
|
||||||
channelSendQueueId: reactionQueueId,
|
channelSendQueueId: reactionQueueId,
|
||||||
expectsGenericAck: true,
|
expectsGenericAck: true,
|
||||||
);
|
);
|
||||||
} finally {
|
}, region: getChannelRegion(channel.index));
|
||||||
await sendFrame(buildSetFloodScopeFrame(''));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3246,16 +3245,80 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
final outboundText = prepareChannelOutboundText(channel.index, text);
|
final outboundText = prepareChannelOutboundText(channel.index, text);
|
||||||
try {
|
await _runScopedChannelSend(() async {
|
||||||
await sendFrame(buildSetFloodScopeFrame(getChannelRegion(channel.index)));
|
|
||||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||||
await sendFrame(
|
await _sendFrameAndWaitForCommandAck(
|
||||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||||
channelSendQueueId: message.messageId,
|
channelSendQueueId: message.messageId,
|
||||||
expectsGenericAck: true,
|
expectsGenericAck: true,
|
||||||
);
|
);
|
||||||
|
}, region: getChannelRegion(channel.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runScopedChannelSend(
|
||||||
|
Future<void> Function() action, {
|
||||||
|
required String region,
|
||||||
|
}) async {
|
||||||
|
final prev = _channelScopedSendLock;
|
||||||
|
final completer = Completer<void>();
|
||||||
|
_channelScopedSendLock = completer.future;
|
||||||
|
await prev;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(region));
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} finally {
|
||||||
|
if (isConnected) {
|
||||||
|
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await sendFrame(buildSetFloodScopeFrame(''));
|
completer.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendFrameAndWaitForCommandAck(
|
||||||
|
Uint8List data, {
|
||||||
|
String? channelSendQueueId,
|
||||||
|
bool expectsGenericAck = false,
|
||||||
|
}) async {
|
||||||
|
final completer = Completer<void>();
|
||||||
|
late final StreamSubscription<Uint8List> subscription;
|
||||||
|
late final Timer timeout;
|
||||||
|
|
||||||
|
void complete() {
|
||||||
|
if (!completer.isCompleted) completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void completeError(Object error) {
|
||||||
|
if (!completer.isCompleted) completer.completeError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty) return;
|
||||||
|
if (frame[0] == respCodeOk) {
|
||||||
|
complete();
|
||||||
|
} else if (frame[0] == respCodeErr) {
|
||||||
|
final errCode = frame.length > 1 ? frame[1] : -1;
|
||||||
|
completeError(Exception('Command failed with error code $errCode'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
timeout = Timer(_commandAckTimeout, () {
|
||||||
|
completeError(TimeoutException('Command ACK timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendFrame(
|
||||||
|
data,
|
||||||
|
channelSendQueueId: channelSendQueueId,
|
||||||
|
expectsGenericAck: expectsGenericAck,
|
||||||
|
);
|
||||||
|
await completer.future;
|
||||||
|
} finally {
|
||||||
|
timeout.cancel();
|
||||||
|
await subscription.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3858,6 +3921,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
case pushCodePathUpdated:
|
case pushCodePathUpdated:
|
||||||
_handlePathUpdated(frame);
|
_handlePathUpdated(frame);
|
||||||
break;
|
break;
|
||||||
|
case pushCodeControlData:
|
||||||
|
// Optional feature-specific services listen to receivedFrames directly.
|
||||||
|
break;
|
||||||
case pushCodeLoginSuccess:
|
case pushCodeLoginSuccess:
|
||||||
_handleLoginSuccess(frame);
|
_handleLoginSuccess(frame);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ const int cmdSendTelemetryReq = 39;
|
|||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
const int cmdSetCustomVar = 41;
|
const int cmdSetCustomVar = 41;
|
||||||
const int cmdSendBinaryReq = 50;
|
const int cmdSendBinaryReq = 50;
|
||||||
|
const int cmdSendControlData = 55;
|
||||||
const int cmdGetStats = 56;
|
const int cmdGetStats = 56;
|
||||||
const int cmdSendAnonReq = 57;
|
const int cmdSendAnonReq = 57;
|
||||||
const int cmdSetAutoAddConfig = 58;
|
const int cmdSetAutoAddConfig = 58;
|
||||||
@@ -226,6 +227,12 @@ const int reqTypeGetTelemetry = 0x03;
|
|||||||
const int reqTypeGetAccessList = 0x05;
|
const int reqTypeGetAccessList = 0x05;
|
||||||
const int reqTypeGetNeighbors = 0x06;
|
const int reqTypeGetNeighbors = 0x06;
|
||||||
|
|
||||||
|
const int anonReqTypeRegions = 0x01;
|
||||||
|
|
||||||
|
// Control data sub-types used by MeshCore discovery packets.
|
||||||
|
const int controlSubtypeDiscoverReq = 0x08;
|
||||||
|
const int controlSubtypeDiscoverResp = 0x09;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
|
|
||||||
@@ -268,6 +275,7 @@ const int pushCodeTraceData = 0x89;
|
|||||||
const int pushCodeNewAdvert = 0x8A;
|
const int pushCodeNewAdvert = 0x8A;
|
||||||
const int pushCodeTelemetryResponse = 0x8B;
|
const int pushCodeTelemetryResponse = 0x8B;
|
||||||
const int pushCodeBinaryResponse = 0x8C;
|
const int pushCodeBinaryResponse = 0x8C;
|
||||||
|
const int pushCodeControlData = 0x8E;
|
||||||
|
|
||||||
// Contact/advertisement types
|
// Contact/advertisement types
|
||||||
const int advTypeChat = 1;
|
const int advTypeChat = 1;
|
||||||
@@ -857,6 +865,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
|||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List buildSendControlDataFrame(Uint8List payload) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendControlData);
|
||||||
|
writer.writeBytes(payload);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List buildDiscoveryRequestPayload(
|
||||||
|
int tag, {
|
||||||
|
bool prefixOnly = false,
|
||||||
|
int typeMask = 1 << advTypeRepeater,
|
||||||
|
}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
|
||||||
|
// subtype 0x8, with the low bit selecting short/full public keys in replies.
|
||||||
|
writer.writeByte(
|
||||||
|
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
|
||||||
|
);
|
||||||
|
writer.writeByte(typeMask);
|
||||||
|
writer.writeUInt32LE(tag);
|
||||||
|
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
|
||||||
|
if (path.isEmpty) return Uint8List(0);
|
||||||
|
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||||
|
if (path.length % width != 0) {
|
||||||
|
return Uint8List.fromList(path.reversed.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
final reversed = Uint8List(path.length);
|
||||||
|
final hops = path.length ~/ width;
|
||||||
|
for (var i = 0; i < hops; i++) {
|
||||||
|
final from = (hops - 1 - i) * width;
|
||||||
|
reversed.setRange(i * width, (i + 1) * width, path, from);
|
||||||
|
}
|
||||||
|
return reversed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CMD_SEND_ANON_REQ frame.
|
||||||
|
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
|
||||||
|
Uint8List buildSendAnonReqFrame(
|
||||||
|
Uint8List repeaterPubKey, {
|
||||||
|
required int requestType,
|
||||||
|
Uint8List? replyPath,
|
||||||
|
int replyHopCount = 0,
|
||||||
|
int pathHashWidth = pathHashSize,
|
||||||
|
}) {
|
||||||
|
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||||
|
final path = replyPath ?? Uint8List(0);
|
||||||
|
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendAnonReq);
|
||||||
|
writer.writeBytes(repeaterPubKey);
|
||||||
|
writer.writeByte(requestType);
|
||||||
|
writer.writeByte(encodedPathLen);
|
||||||
|
writer.writeBytes(_reversePathByHop(path, width));
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
//Build a trace request frame
|
//Build a trace request frame
|
||||||
//[cmd][tag x4][auth x4][flag][payload]
|
//[cmd][tag x4][auth x4][flag][payload]
|
||||||
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
||||||
|
|||||||
@@ -154,6 +154,9 @@
|
|||||||
"settings_regionManagement_screenTitle": "Region Management",
|
"settings_regionManagement_screenTitle": "Region Management",
|
||||||
"settings_regionNameHint": "Enter region name",
|
"settings_regionNameHint": "Enter region name",
|
||||||
"settings_regionAddRegion": "Add region",
|
"settings_regionAddRegion": "Add region",
|
||||||
|
"settings_regionFetchRegions": "Fetch regions from repeaters",
|
||||||
|
"settings_regionFetchRegionsFail": "No regions were found",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists": "This region has already been added",
|
||||||
"settings_regionName": "Region Name",
|
"settings_regionName": "Region Name",
|
||||||
"settings_regionDeleted": "Region deleted",
|
"settings_regionDeleted": "Region deleted",
|
||||||
"settings_deleteRegion": "Delete Region",
|
"settings_deleteRegion": "Delete Region",
|
||||||
|
|||||||
@@ -760,6 +760,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Add region'**
|
/// **'Add region'**
|
||||||
String get settings_regionAddRegion;
|
String get settings_regionAddRegion;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fetch regions from repeaters'**
|
||||||
|
String get settings_regionFetchRegions;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegionsFail.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No regions were found'**
|
||||||
|
String get settings_regionFetchRegionsFail;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegionsAlreadyExists.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This region has already been added'**
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists;
|
||||||
|
|
||||||
/// No description provided for @settings_regionName.
|
/// No description provided for @settings_regionName.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -354,6 +354,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Region hinzufügen';
|
String get settings_regionAddRegion => 'Region hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Regions-Name';
|
String get settings_regionName => 'Regions-Name';
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,16 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -341,6 +341,16 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -340,6 +340,16 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -352,6 +352,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,16 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,16 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,16 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -352,6 +352,16 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -352,6 +352,16 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,16 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_regionAddRegion => 'Add region';
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_regionName => 'Region Name';
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
import 'package:meshcore_open/l10n/l10n.dart';
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
import 'package:meshcore_open/storage/region_store.dart';
|
import 'package:meshcore_open/storage/region_store.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -22,8 +27,11 @@ class RegionManagementScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
||||||
|
static final RegExp _validFetchedRegion = RegExp(r'^[a-z0-9-]{1,30}$');
|
||||||
|
|
||||||
final RegionStore _regionStore = RegionStore();
|
final RegionStore _regionStore = RegionStore();
|
||||||
List<Region> _regions = [];
|
List<Region> _regions = [];
|
||||||
|
bool _isFetchingRegions = false;
|
||||||
|
|
||||||
String region = '';
|
String region = '';
|
||||||
|
|
||||||
@@ -59,6 +67,17 @@ class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
|||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
onPressed: () => _showAddRegionDialog(context),
|
onPressed: () => _showAddRegionDialog(context),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.settings_regionFetchRegions,
|
||||||
|
icon: _isFetchingRegions
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.travel_explore),
|
||||||
|
onPressed: _isFetchingRegions ? null : _showFetchRegionsDialog,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
@@ -107,6 +126,321 @@ class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showFetchRegionsDialog() async {
|
||||||
|
if (_isFetchingRegions) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isFetchingRegions = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<Region> fetchedRegions = {};
|
||||||
|
try {
|
||||||
|
fetchedRegions = await _fetchRegionsFromRepeaters();
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isFetchingRegions = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final sortedRegions = fetchedRegions.toList()..sort();
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(l10n.settings_regionFetchRegions),
|
||||||
|
content: sortedRegions.isEmpty
|
||||||
|
? Text(l10n.settings_regionFetchRegionsFail)
|
||||||
|
: StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: sortedRegions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final fetchedRegion = sortedRegions[index];
|
||||||
|
final alreadyExists = _regions.contains(fetchedRegion);
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(fetchedRegion),
|
||||||
|
trailing: TextButton(
|
||||||
|
style: alreadyExists
|
||||||
|
? TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).disabledColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onPressed: () {
|
||||||
|
if (alreadyExists) {
|
||||||
|
_showDialogSnackBar(
|
||||||
|
context,
|
||||||
|
l10n.settings_regionFetchRegionsAlreadyExists,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_regionStore.addRegion(fetchedRegion);
|
||||||
|
_loadRegions();
|
||||||
|
setDialogState(() {});
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: Text(l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDialogSnackBar(BuildContext context, String message) {
|
||||||
|
final overlay = Overlay.maybeOf(context);
|
||||||
|
if (overlay == null) return;
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final entry = OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 32,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Material(
|
||||||
|
color: theme.colorScheme.inverseSurface,
|
||||||
|
elevation: 6,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
overlay.insert(entry);
|
||||||
|
Timer(const Duration(seconds: 3), entry.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Region>> _fetchRegionsFromRepeaters() async {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final repeaters = await _discoverNearbyRepeaters(connector);
|
||||||
|
final regions = <Region>{};
|
||||||
|
|
||||||
|
for (final repeater in repeaters) {
|
||||||
|
if (!mounted || !connector.isConnected) break;
|
||||||
|
regions.addAll(await _requestRegionsFromRepeater(connector, repeater));
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Contact>> _discoverNearbyRepeaters(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) async {
|
||||||
|
final repeaters = connector.contacts
|
||||||
|
.where((contact) => contact.type == advTypeRepeater)
|
||||||
|
.toList();
|
||||||
|
if (repeaters.isEmpty || !connector.isConnected) return <Contact>[];
|
||||||
|
|
||||||
|
StreamSubscription<Uint8List>? subscription;
|
||||||
|
Timer? timeout;
|
||||||
|
final completer = Completer<Set<String>>();
|
||||||
|
final respondingPrefixes = <String>{};
|
||||||
|
final tag = DateTime.now().microsecondsSinceEpoch & 0xFFFFFFFF;
|
||||||
|
|
||||||
|
void complete() {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
completer.complete(respondingPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty || completer.isCompleted) return;
|
||||||
|
|
||||||
|
final reader = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
if (reader.readByte() != pushCodeControlData) return;
|
||||||
|
if (reader.remaining < 9) return;
|
||||||
|
reader.skipBytes(3); // SNR, RSSI, path_len from companion firmware.
|
||||||
|
|
||||||
|
final payloadType = reader.readByte();
|
||||||
|
if (((payloadType >> 4) & 0x0F) != controlSubtypeDiscoverResp ||
|
||||||
|
(payloadType & 0x0F) != advTypeRepeater) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.skipBytes(1); // Inbound SNR reported by the responding repeater.
|
||||||
|
if (reader.readUInt32LE() != tag) return;
|
||||||
|
|
||||||
|
final publicKeyPrefix = reader.readRemainingBytes();
|
||||||
|
if (publicKeyPrefix.isEmpty) return;
|
||||||
|
respondingPrefixes.add(pubKeyToHex(publicKeyPrefix));
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore malformed discovery frames; another response may still arrive.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final payload = buildDiscoveryRequestPayload(tag, prefixOnly: true);
|
||||||
|
await connector.sendFrame(buildSendControlDataFrame(payload));
|
||||||
|
timeout = Timer(const Duration(seconds: 10), complete);
|
||||||
|
final prefixes = await completer.future;
|
||||||
|
return repeaters.where((contact) {
|
||||||
|
final contactKey = contact.publicKeyHex.toLowerCase();
|
||||||
|
return prefixes.any((prefix) => contactKey.startsWith(prefix));
|
||||||
|
}).toList();
|
||||||
|
} catch (_) {
|
||||||
|
timeout?.cancel();
|
||||||
|
await subscription.cancel();
|
||||||
|
return <Contact>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Region>> _requestRegionsFromRepeater(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
Contact repeater,
|
||||||
|
) async {
|
||||||
|
StreamSubscription<Uint8List>? subscription;
|
||||||
|
Timer? timeout;
|
||||||
|
final completer = Completer<Set<Region>>();
|
||||||
|
int? expectedTag;
|
||||||
|
final originalPath = Uint8List.fromList(repeater.path);
|
||||||
|
final originalPathLength = repeater.pathLength;
|
||||||
|
var pathChangedForRequest = false;
|
||||||
|
|
||||||
|
void complete(Set<Region> regions) {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
completer.complete(regions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restartTimeout(Duration duration) {
|
||||||
|
timeout?.cancel();
|
||||||
|
timeout = Timer(duration, () => complete(<Region>{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final replyPath = Uint8List(0);
|
||||||
|
const replyHopCount = 0;
|
||||||
|
await connector.setContactPath(repeater, replyPath, replyHopCount);
|
||||||
|
pathChangedForRequest = true;
|
||||||
|
|
||||||
|
subscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty || completer.isCompleted) return;
|
||||||
|
|
||||||
|
final reader = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
final cmd = reader.readByte();
|
||||||
|
if (cmd == respCodeSent) {
|
||||||
|
reader.skipBytes(1);
|
||||||
|
expectedTag = reader.readUInt32LE();
|
||||||
|
final estimatedTimeoutMs = reader.readUInt32LE();
|
||||||
|
restartTimeout(
|
||||||
|
Duration(
|
||||||
|
milliseconds: estimatedTimeoutMs > 0
|
||||||
|
? estimatedTimeoutMs + 2000
|
||||||
|
: 10000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd == respCodeErr) {
|
||||||
|
complete(<Region>{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd != pushCodeBinaryResponse || expectedTag == null) return;
|
||||||
|
|
||||||
|
reader.skipBytes(1);
|
||||||
|
final tag = reader.readUInt32LE();
|
||||||
|
if (tag != expectedTag) return;
|
||||||
|
|
||||||
|
complete(_parseRegionsResponse(reader.readRemainingBytes()));
|
||||||
|
} catch (_) {
|
||||||
|
complete(<Region>{});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
restartTimeout(const Duration(seconds: 10));
|
||||||
|
final frame = buildSendAnonReqFrame(
|
||||||
|
repeater.publicKey,
|
||||||
|
requestType: anonReqTypeRegions,
|
||||||
|
replyPath: replyPath,
|
||||||
|
replyHopCount: replyHopCount,
|
||||||
|
pathHashWidth: connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
await connector.sendFrame(frame);
|
||||||
|
final regions = await completer.future;
|
||||||
|
if (pathChangedForRequest && connector.isConnected) {
|
||||||
|
await _restoreRepeaterPath(
|
||||||
|
connector,
|
||||||
|
repeater,
|
||||||
|
originalPathLength,
|
||||||
|
originalPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
|
} catch (_) {
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
if (pathChangedForRequest && connector.isConnected) {
|
||||||
|
await _restoreRepeaterPath(
|
||||||
|
connector,
|
||||||
|
repeater,
|
||||||
|
originalPathLength,
|
||||||
|
originalPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Region>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restoreRepeaterPath(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
Contact repeater,
|
||||||
|
int originalPathLength,
|
||||||
|
Uint8List originalPath,
|
||||||
|
) async {
|
||||||
|
if (originalPathLength < 0) {
|
||||||
|
await connector.clearContactPath(repeater);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await connector.setContactPath(repeater, originalPath, originalPathLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Region> _parseRegionsResponse(Uint8List frame) {
|
||||||
|
if (frame.length <= 4) return <Region>{};
|
||||||
|
final names = utf8
|
||||||
|
.decode(frame.sublist(4), allowMalformed: true)
|
||||||
|
.replaceAll('\x00', '')
|
||||||
|
.split(',');
|
||||||
|
return names
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.where((name) => _validFetchedRegion.hasMatch(name))
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleAddRegion(Region region, BuildContext context) {
|
void _handleAddRegion(Region region, BuildContext context) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_regionStore.addRegion(region);
|
_regionStore.addRegion(region);
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -15,12 +18,21 @@
|
|||||||
"channels_clearRegion"
|
"channels_clearRegion"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"de": [
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists"
|
||||||
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"settings_regionSettings",
|
"settings_regionSettings",
|
||||||
"settings_regionSettingsSubtitle",
|
"settings_regionSettingsSubtitle",
|
||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -37,6 +49,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -53,6 +68,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -69,6 +87,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -85,6 +106,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -101,6 +125,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -117,6 +144,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -133,6 +163,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -149,6 +182,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -165,6 +201,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -181,6 +220,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -197,6 +239,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -213,6 +258,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -229,6 +277,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
@@ -245,6 +296,9 @@
|
|||||||
"settings_regionManagement_screenTitle",
|
"settings_regionManagement_screenTitle",
|
||||||
"settings_regionNameHint",
|
"settings_regionNameHint",
|
||||||
"settings_regionAddRegion",
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
"settings_regionName",
|
"settings_regionName",
|
||||||
"settings_regionDeleted",
|
"settings_regionDeleted",
|
||||||
"settings_deleteRegion",
|
"settings_deleteRegion",
|
||||||
|
|||||||
Reference in New Issue
Block a user