mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-20 01:15:35 +10:00
Compare commits
24 Commits
PRE-BETA-9.5
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 5670ab0067 | |||
| 98a9197cb1 | |||
| 55da952bfd | |||
| 9b4742951d | |||
| 43f3d439ba | |||
| 5f54a2cd1b | |||
| e1536c49b1 | |||
| bfa62523df | |||
| 6813a72767 | |||
| dbd3a40bdc | |||
| 3009036565 | |||
| 80f4fd5218 | |||
| 321d4b9775 | |||
| 1c183d7e67 | |||
| a1e6f6967c | |||
| f34c2a92c3 | |||
| f1478722b0 | |||
| c867225073 | |||
| 06285a02ff | |||
| 9d1251f21e | |||
| 0ca96ce5b1 | |||
| b72ade1387 | |||
| 3fc038682c | |||
| 0e074fd806 |
@@ -120,6 +120,8 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||||
|
| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) |
|
||||||
|
| 55 | CMD_SEND_CONTROL_DATA | Send control data (e.g. zero-hop discovery, v8+) |
|
||||||
| 56 | CMD_GET_STATS | Request companion radio stats |
|
| 56 | CMD_GET_STATS | Request companion radio stats |
|
||||||
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||||
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||||
@@ -162,6 +164,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
||||||
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
||||||
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
||||||
|
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
|
||||||
|
|
||||||
## Data Models
|
## Data Models
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
|
import 'package:meshcore_open/storage/region_store.dart';
|
||||||
import 'package:pointycastle/export.dart';
|
import 'package:pointycastle/export.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
@@ -35,6 +36,7 @@ import 'meshcore_connector_tcp.dart';
|
|||||||
import '../storage/channel_message_store.dart';
|
import '../storage/channel_message_store.dart';
|
||||||
import '../storage/channel_order_store.dart';
|
import '../storage/channel_order_store.dart';
|
||||||
import '../storage/channel_settings_store.dart';
|
import '../storage/channel_settings_store.dart';
|
||||||
|
import '../storage/channel_region_store.dart';
|
||||||
import '../storage/channel_store.dart';
|
import '../storage/channel_store.dart';
|
||||||
import '../storage/contact_discovery_store.dart';
|
import '../storage/contact_discovery_store.dart';
|
||||||
import '../storage/contact_settings_store.dart';
|
import '../storage/contact_settings_store.dart';
|
||||||
@@ -266,6 +268,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
|
||||||
@@ -297,6 +303,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final MessageStore _messageStore = MessageStore();
|
final MessageStore _messageStore = MessageStore();
|
||||||
final ChannelOrderStore _channelOrderStore = ChannelOrderStore();
|
final ChannelOrderStore _channelOrderStore = ChannelOrderStore();
|
||||||
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
||||||
|
final ChannelRegionStore _channelRegionStore = ChannelRegionStore();
|
||||||
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
||||||
final ContactStore _contactStore = ContactStore();
|
final ContactStore _contactStore = ContactStore();
|
||||||
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
|
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
|
||||||
@@ -306,6 +313,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final Map<int, bool> _channelSmazEnabled = {};
|
final Map<int, bool> _channelSmazEnabled = {};
|
||||||
final Map<int, bool> _channelCyr2LatEnabled = {};
|
final Map<int, bool> _channelCyr2LatEnabled = {};
|
||||||
final Map<int, String?> _channelCyr2LatProfileId = {};
|
final Map<int, String?> _channelCyr2LatProfileId = {};
|
||||||
|
final Map<int, Region> _channelRegions = {};
|
||||||
bool _lastSentWasCliCommand =
|
bool _lastSentWasCliCommand =
|
||||||
false; // Track if last sent message was a CLI command
|
false; // Track if last sent message was a CLI command
|
||||||
final Map<String, bool> _contactSmazEnabled = {};
|
final Map<String, bool> _contactSmazEnabled = {};
|
||||||
@@ -704,6 +712,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return _contactSmazEnabled[contactKeyHex] ?? false;
|
return _contactSmazEnabled[contactKeyHex] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasChannelRegion(int channelIndex) {
|
||||||
|
return (_channelRegions[channelIndex] ?? '').isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
Region getChannelRegion(int channelIndex) {
|
||||||
|
return _channelRegions[channelIndex] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
void ensureContactSmazSettingLoaded(String contactKeyHex) {
|
void ensureContactSmazSettingLoaded(String contactKeyHex) {
|
||||||
_ensureContactSmazSettingLoaded(contactKeyHex);
|
_ensureContactSmazSettingLoaded(contactKeyHex);
|
||||||
}
|
}
|
||||||
@@ -857,6 +873,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setChannelRegion(int channelIndex, String region) async {
|
||||||
|
// Update in-memory state and notify synchronously so the UI reflects the
|
||||||
|
// change immediately; persistence happens in the background.
|
||||||
|
_channelRegions[channelIndex] = region;
|
||||||
|
notifyListeners();
|
||||||
|
await _channelRegionStore.saveRegion(channelIndex, region);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadChannelOrder() async {
|
Future<void> _loadChannelOrder() async {
|
||||||
_channelOrder = await _channelOrderStore.loadChannelOrder();
|
_channelOrder = await _channelOrderStore.loadChannelOrder();
|
||||||
_applyChannelOrder();
|
_applyChannelOrder();
|
||||||
@@ -1015,11 +1039,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
Future<void> loadChannelSettings({int? maxChannels}) async {
|
Future<void> loadChannelSettings({int? maxChannels}) async {
|
||||||
_channelSmazEnabled.clear();
|
_channelSmazEnabled.clear();
|
||||||
_channelCyr2LatEnabled.clear();
|
_channelCyr2LatEnabled.clear();
|
||||||
|
_channelRegions.clear();
|
||||||
final channelCount = maxChannels ?? _maxChannels;
|
final channelCount = maxChannels ?? _maxChannels;
|
||||||
for (int i = 0; i < channelCount; i++) {
|
for (int i = 0; i < channelCount; i++) {
|
||||||
_channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i);
|
_channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i);
|
||||||
_channelCyr2LatEnabled[i] = await _channelSettingsStore
|
_channelCyr2LatEnabled[i] = await _channelSettingsStore
|
||||||
.loadCyr2LatEnabled(i);
|
.loadCyr2LatEnabled(i);
|
||||||
|
_channelRegions[i] = await _channelRegionStore.loadRegion(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3383,12 +3409,15 @@ 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);
|
||||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
await _runScopedChannelSend(() async {
|
||||||
await sendFrame(
|
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||||
buildSendChannelTextMsgFrame(channel.index, text),
|
await _sendFrameAndWaitForCommandAck(
|
||||||
channelSendQueueId: reactionQueueId,
|
buildSendChannelTextMsgFrame(channel.index, text),
|
||||||
expectsGenericAck: true,
|
channelSendQueueId: reactionQueueId,
|
||||||
);
|
expectsGenericAck: true,
|
||||||
|
successCode: respCodeSent,
|
||||||
|
);
|
||||||
|
}, region: getChannelRegion(channel.index));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3405,12 +3434,97 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
final outboundText = prepareChannelOutboundText(channel.index, text);
|
final outboundText = prepareChannelOutboundText(channel.index, text);
|
||||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
await _runScopedChannelSend(() async {
|
||||||
await sendFrame(
|
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
await _sendFrameAndWaitForCommandAck(
|
||||||
channelSendQueueId: message.messageId,
|
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||||
expectsGenericAck: true,
|
channelSendQueueId: message.messageId,
|
||||||
);
|
expectsGenericAck: true,
|
||||||
|
successCode: respCodeSent,
|
||||||
|
);
|
||||||
|
}, region: getChannelRegion(channel.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runScopedChannelSend(
|
||||||
|
Future<void> Function() action, {
|
||||||
|
required String region,
|
||||||
|
}) async {
|
||||||
|
final prev = _channelScopedSendLock;
|
||||||
|
final completer = Completer<void>();
|
||||||
|
_channelScopedSendLock = completer.future;
|
||||||
|
await prev;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Only touch the global flood scope for region-scoped channels. Plain
|
||||||
|
// channels send exactly as before, which also stays compatible with
|
||||||
|
// firmware that predates CMD_SET_FLOOD_SCOPE. The lock is still held so an
|
||||||
|
// unscoped send can't interleave with (and inherit the scope of) a
|
||||||
|
// concurrent scoped send.
|
||||||
|
if (region.isEmpty) {
|
||||||
|
await action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(region));
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} finally {
|
||||||
|
if (isConnected) {
|
||||||
|
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends [data] and resolves once the device replies. [successCode] is the
|
||||||
|
// response code that signals success for this frame: SET_FLOOD_SCOPE replies
|
||||||
|
// with RESP_CODE_OK, whereas a channel text send replies with RESP_CODE_SENT.
|
||||||
|
// Waiting for the text send's RESP_CODE_SENT before the scope is reset
|
||||||
|
// guarantees the firmware has already built the packet with the active scope.
|
||||||
|
Future<void> _sendFrameAndWaitForCommandAck(
|
||||||
|
Uint8List data, {
|
||||||
|
String? channelSendQueueId,
|
||||||
|
bool expectsGenericAck = false,
|
||||||
|
int successCode = respCodeOk,
|
||||||
|
}) async {
|
||||||
|
final completer = Completer<void>();
|
||||||
|
late final StreamSubscription<Uint8List> subscription;
|
||||||
|
late final Timer timeout;
|
||||||
|
|
||||||
|
void complete() {
|
||||||
|
if (!completer.isCompleted) completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void completeError(Object error) {
|
||||||
|
if (!completer.isCompleted) completer.completeError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty) return;
|
||||||
|
if (frame[0] == successCode) {
|
||||||
|
complete();
|
||||||
|
} else if (frame[0] == respCodeErr) {
|
||||||
|
final errCode = frame.length > 1 ? frame[1] : -1;
|
||||||
|
completeError(Exception('Command failed with error code $errCode'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
timeout = Timer(_commandAckTimeout, () {
|
||||||
|
completeError(TimeoutException('Command ACK timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendFrame(
|
||||||
|
data,
|
||||||
|
channelSendQueueId: channelSendQueueId,
|
||||||
|
expectsGenericAck: expectsGenericAck,
|
||||||
|
);
|
||||||
|
await completer.future;
|
||||||
|
} finally {
|
||||||
|
timeout.cancel();
|
||||||
|
await subscription.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeContact(Contact contact) async {
|
Future<void> removeContact(Contact contact) async {
|
||||||
@@ -4017,6 +4131,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;
|
||||||
@@ -4163,6 +4280,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_messageStore.setPublicKeyHex = selfPublicKeyHex;
|
_messageStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
|
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_channelRegionStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
_contactStore.setPublicKeyHex = selfPublicKeyHex;
|
_contactStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
_channelStore.setPublicKeyHex = selfPublicKeyHex;
|
_channelStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||||
@@ -206,6 +207,8 @@ 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 cmdSetFloodScope = 54;
|
||||||
|
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;
|
||||||
@@ -230,6 +233,12 @@ Uint8List buildTelemetryBinaryPayload() {
|
|||||||
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int anonReqTypeRegions = 0x01;
|
||||||
|
|
||||||
|
// Control data sub-types used by MeshCore discovery packets.
|
||||||
|
const int controlSubtypeDiscoverReq = 0x08;
|
||||||
|
const int controlSubtypeDiscoverResp = 0x09;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
|
|
||||||
@@ -272,6 +281,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;
|
||||||
@@ -866,6 +876,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}) {
|
||||||
@@ -960,3 +1031,18 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
|||||||
}
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build CMD_SET_FLOOD_SCOPE
|
||||||
|
// Format: [cmd][scope]
|
||||||
|
Uint8List buildSetFloodScopeFrame(String region) {
|
||||||
|
if (region == '') {
|
||||||
|
// reset scope
|
||||||
|
return Uint8List.fromList([cmdSetFloodScope, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = region.startsWith('#') ? region : '#$region';
|
||||||
|
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
||||||
|
final scope = Uint8List.fromList(hash.sublist(0, 16));
|
||||||
|
|
||||||
|
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -20,6 +21,7 @@ class LinkHandler {
|
|||||||
required String text,
|
required String text,
|
||||||
required TextStyle style,
|
required TextStyle style,
|
||||||
TextStyle? linkStyle,
|
TextStyle? linkStyle,
|
||||||
|
VoidCallback? onSecondaryTap,
|
||||||
}) {
|
}) {
|
||||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||||
@@ -27,7 +29,7 @@ class LinkHandler {
|
|||||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||||
|
|
||||||
if (PlatformInfo.isDesktop) {
|
if (PlatformInfo.isDesktop) {
|
||||||
return SelectableLinkify(
|
final linkify = SelectableLinkify(
|
||||||
text: text,
|
text: text,
|
||||||
style: style,
|
style: style,
|
||||||
linkStyle: effectiveLinkStyle,
|
linkStyle: effectiveLinkStyle,
|
||||||
@@ -35,6 +37,14 @@ class LinkHandler {
|
|||||||
linkifiers: linkifiers,
|
linkifiers: linkifiers,
|
||||||
onOpen: onOpen,
|
onOpen: onOpen,
|
||||||
);
|
);
|
||||||
|
if (onSecondaryTap == null) return linkify;
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
|
||||||
|
},
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: linkify,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Linkify(
|
return Linkify(
|
||||||
text: text,
|
text: text,
|
||||||
|
|||||||
@@ -2488,6 +2488,13 @@
|
|||||||
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
|
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
|
||||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
|
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
|
||||||
"repeater_chanUtil": "Nutzung des Kanals",
|
"repeater_chanUtil": "Nutzung des Kanals",
|
||||||
|
"@settings_deleteRegionConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"region": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@routing_lastWorked": {
|
"@routing_lastWorked": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"when": {
|
"when": {
|
||||||
@@ -2495,6 +2502,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@channels_regionSetTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"region": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "de-mitte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_regionSettings": "Regionen",
|
||||||
|
"settings_regionSettingsSubtitle": "Gespeicherte Regionen verwalten",
|
||||||
|
"settings_regionManagement_screenTitle": "Regions-Verwaltung",
|
||||||
|
"settings_regionNameHint": "Regions-Namen eingeben",
|
||||||
|
"settings_regionAddRegion": "Region hinzufügen",
|
||||||
|
"settings_regionDeleted": "Region entfernt",
|
||||||
|
"settings_regionName": "Regions-Name",
|
||||||
|
"settings_deleteRegion": "Region entfernen",
|
||||||
|
"settings_deleteRegionConfirm": "Region \"{region}\" aus der Liste entfernen?",
|
||||||
|
"channels_regionNotSet": "Region: keine",
|
||||||
|
"channels_regionSetTo": "Region: {region}",
|
||||||
|
"channels_regionSelect_Title": "Region auswählen",
|
||||||
|
"channels_clearRegion": "Region zurücksetzen",
|
||||||
"@routing_deliveryCounts": {
|
"@routing_deliveryCounts": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"successes": {
|
"successes": {
|
||||||
|
|||||||
@@ -158,6 +158,25 @@
|
|||||||
"settings_radioSettings": "Radio Settings",
|
"settings_radioSettings": "Radio Settings",
|
||||||
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
|
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
|
||||||
"settings_radioSettingsUpdated": "Radio settings updated",
|
"settings_radioSettingsUpdated": "Radio settings updated",
|
||||||
|
"settings_regionSettings": "Regions",
|
||||||
|
"settings_regionSettingsSubtitle": "Manage stored regions",
|
||||||
|
"settings_regionManagement_screenTitle": "Region Management",
|
||||||
|
"settings_regionNameHint": "Enter region name",
|
||||||
|
"settings_regionAddRegion": "Add region",
|
||||||
|
"settings_regionFetchRegions": "Fetch regions from repeaters",
|
||||||
|
"settings_regionFetchRegionsFail": "No regions were found",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists": "This region has already been added",
|
||||||
|
"settings_regionName": "Region Name",
|
||||||
|
"settings_regionDeleted": "Region deleted",
|
||||||
|
"settings_deleteRegion": "Delete Region",
|
||||||
|
"settings_deleteRegionConfirm": "Remove \"{region}\" from region list?",
|
||||||
|
"@settings_deleteRegionConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"region": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings_location": "Location",
|
"settings_location": "Location",
|
||||||
"settings_locationSubtitle": "GPS coordinates",
|
"settings_locationSubtitle": "GPS coordinates",
|
||||||
"settings_locationUpdated": "Location and GPS settings updated",
|
"settings_locationUpdated": "Location and GPS settings updated",
|
||||||
@@ -635,6 +654,18 @@
|
|||||||
"channels_scanQrCodeComingSoon": "Coming soon",
|
"channels_scanQrCodeComingSoon": "Coming soon",
|
||||||
"channels_enterHashtag": "Enter hashtag",
|
"channels_enterHashtag": "Enter hashtag",
|
||||||
"channels_hashtagHint": "e.g. #team",
|
"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_noMessages": "No messages yet",
|
||||||
"chat_sendMessage": "Send message",
|
"chat_sendMessage": "Send message",
|
||||||
"chat_sendMessageTo": "Send a message to {contactName}",
|
"chat_sendMessageTo": "Send a message to {contactName}",
|
||||||
|
|||||||
@@ -784,6 +784,78 @@ abstract class AppLocalizations {
|
|||||||
/// **'Radio settings updated'**
|
/// **'Radio settings updated'**
|
||||||
String get settings_radioSettingsUpdated;
|
String get settings_radioSettingsUpdated;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Regions'**
|
||||||
|
String get settings_regionSettings;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionSettingsSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Manage stored regions'**
|
||||||
|
String get settings_regionSettingsSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionManagement_screenTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region Management'**
|
||||||
|
String get settings_regionManagement_screenTitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionNameHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter region name'**
|
||||||
|
String get settings_regionNameHint;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionAddRegion.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add region'**
|
||||||
|
String get settings_regionAddRegion;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fetch regions from repeaters'**
|
||||||
|
String get settings_regionFetchRegions;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegionsFail.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No regions were found'**
|
||||||
|
String get settings_regionFetchRegionsFail;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionFetchRegionsAlreadyExists.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This region has already been added'**
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region Name'**
|
||||||
|
String get settings_regionName;
|
||||||
|
|
||||||
|
/// No description provided for @settings_regionDeleted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region deleted'**
|
||||||
|
String get settings_regionDeleted;
|
||||||
|
|
||||||
|
/// No description provided for @settings_deleteRegion.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete Region'**
|
||||||
|
String get settings_deleteRegion;
|
||||||
|
|
||||||
|
/// No description provided for @settings_deleteRegionConfirm.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove \"{region}\" from region list?'**
|
||||||
|
String settings_deleteRegionConfirm(String region);
|
||||||
|
|
||||||
/// No description provided for @settings_location.
|
/// No description provided for @settings_location.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2458,6 +2530,30 @@ abstract class AppLocalizations {
|
|||||||
/// **'e.g. #team'**
|
/// **'e.g. #team'**
|
||||||
String get channels_hashtagHint;
|
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.
|
/// No description provided for @chat_noMessages.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -367,6 +367,45 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get settings_radioSettingsUpdated =>
|
String get settings_radioSettingsUpdated =>
|
||||||
'Радио настройките са актуализирани';
|
'Радио настройките са актуализирани';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Местоположение';
|
String get settings_location => 'Местоположение';
|
||||||
|
|
||||||
@@ -1330,6 +1369,20 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'напр. #отбор';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Няма съобщения.';
|
String get chat_noMessages => 'Няма съобщения.';
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,46 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert';
|
String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regionen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle =>
|
||||||
|
'Gespeicherte Regionen verwalten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Regions-Verwaltung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Regions-Namen eingeben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Region hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Regions-Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region entfernt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Region entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Region \"$region\" aus der Liste entfernen?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Ort';
|
String get settings_location => 'Ort';
|
||||||
|
|
||||||
@@ -1329,6 +1369,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'z.B. #team';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,45 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Radio settings updated';
|
String get settings_radioSettingsUpdated => 'Radio settings updated';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Location';
|
String get settings_location => 'Location';
|
||||||
|
|
||||||
@@ -1300,6 +1339,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'e.g. #team';
|
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
|
@override
|
||||||
String get chat_noMessages => 'No messages yet';
|
String get chat_noMessages => 'No messages yet';
|
||||||
|
|
||||||
|
|||||||
@@ -367,6 +367,45 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados';
|
String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Ubicación';
|
String get settings_location => 'Ubicación';
|
||||||
|
|
||||||
@@ -1327,6 +1366,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'ej. #equipo';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Aún no hay mensajes';
|
String get chat_noMessages => 'Aún no hay mensajes';
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,45 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour';
|
String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Emplacement';
|
String get settings_location => 'Emplacement';
|
||||||
|
|
||||||
@@ -1329,6 +1368,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'ex. #equipe';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Aucun message pour le moment';
|
String get chat_noMessages => 'Aucun message pour le moment';
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,45 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve';
|
String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'hely';
|
String get settings_location => 'hely';
|
||||||
|
|
||||||
@@ -1322,6 +1361,20 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'például #csapat';
|
String get channels_hashtagHint => 'például #csapat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_regionSetTo(String region) {
|
||||||
|
return 'Region: $region';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_regionNotSet => 'Region: none';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_regionSelect_Title => 'Select a region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_clearRegion => 'Clear region';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Még nincsenek üzenetek';
|
String get chat_noMessages => 'Még nincsenek üzenetek';
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,45 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate';
|
String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Posizione';
|
String get settings_location => 'Posizione';
|
||||||
|
|
||||||
@@ -1328,6 +1367,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'es. #team';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,45 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました';
|
String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => '場所';
|
String get settings_location => '場所';
|
||||||
|
|
||||||
@@ -1259,6 +1298,20 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => '例:#チーム';
|
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
|
@override
|
||||||
String get chat_noMessages => 'まだメッセージは届いていません';
|
String get chat_noMessages => 'まだメッセージは届いていません';
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,45 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.';
|
String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => '위치';
|
String get settings_location => '위치';
|
||||||
|
|
||||||
@@ -1261,6 +1300,20 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => '예: #팀';
|
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
|
@override
|
||||||
String get chat_noMessages => '아직 메시지가 없습니다.';
|
String get chat_noMessages => '아직 메시지가 없습니다.';
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,45 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt';
|
String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Locatie';
|
String get settings_location => 'Locatie';
|
||||||
|
|
||||||
@@ -1316,6 +1355,20 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'bijv. #team';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Nog geen berichten.';
|
String get chat_noMessages => 'Nog geen berichten.';
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,45 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get settings_radioSettingsUpdated =>
|
String get settings_radioSettingsUpdated =>
|
||||||
'Ustawienia radia zostały zaktualizowane';
|
'Ustawienia radia zostały zaktualizowane';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Lokalizacja';
|
String get settings_location => 'Lokalizacja';
|
||||||
|
|
||||||
@@ -1338,6 +1377,20 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'np. #zespół';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,45 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get settings_radioSettingsUpdated =>
|
String get settings_radioSettingsUpdated =>
|
||||||
'Configurações de rádio atualizadas';
|
'Configurações de rádio atualizadas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Localização';
|
String get settings_location => 'Localização';
|
||||||
|
|
||||||
@@ -1326,6 +1365,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'ex. #equipe';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||||
|
|
||||||
|
|||||||
@@ -368,6 +368,45 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Настройки радио обновлены';
|
String get settings_radioSettingsUpdated => 'Настройки радио обновлены';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Позиция';
|
String get settings_location => 'Позиция';
|
||||||
|
|
||||||
@@ -1327,6 +1366,20 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'например, #команда';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Сообщений пока нет';
|
String get chat_noMessages => 'Сообщений пока нет';
|
||||||
|
|
||||||
|
|||||||
@@ -367,6 +367,45 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované';
|
String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Lokalita';
|
String get settings_location => 'Lokalita';
|
||||||
|
|
||||||
@@ -1316,6 +1355,20 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'napr. #tím';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||||
|
|
||||||
|
|||||||
@@ -366,6 +366,45 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene';
|
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Lokacija';
|
String get settings_location => 'Lokacija';
|
||||||
|
|
||||||
@@ -1314,6 +1353,20 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'npr. #ekipa';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Še ni sporočil.';
|
String get chat_noMessages => 'Še ni sporočil.';
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,45 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get settings_radioSettingsUpdated =>
|
String get settings_radioSettingsUpdated =>
|
||||||
'Radioinställningarna har uppdaterats';
|
'Radioinställningarna har uppdaterats';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Plats';
|
String get settings_location => 'Plats';
|
||||||
|
|
||||||
@@ -1306,6 +1345,20 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 't.ex. #team';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||||
|
|
||||||
|
|||||||
@@ -367,6 +367,45 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено';
|
String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => 'Геопозиція';
|
String get settings_location => 'Геопозиція';
|
||||||
|
|
||||||
@@ -1320,6 +1359,20 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => 'напр. #команда';
|
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
|
@override
|
||||||
String get chat_noMessages => 'Поки немає повідомлень.';
|
String get chat_noMessages => 'Поки немає повідомлень.';
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,45 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_radioSettingsUpdated => '无线电设置已更新';
|
String get settings_radioSettingsUpdated => '无线电设置已更新';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettings => 'Regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionNameHint => 'Enter region name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionAddRegion => 'Add region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionFetchRegionsAlreadyExists =>
|
||||||
|
'This region has already been added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionName => 'Region Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_regionDeleted => 'Region deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_deleteRegion => 'Delete Region';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_deleteRegionConfirm(String region) {
|
||||||
|
return 'Remove \"$region\" from region list?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_location => '位置';
|
String get settings_location => '位置';
|
||||||
|
|
||||||
@@ -1245,6 +1284,20 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_hashtagHint => '例如:#团队';
|
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
|
@override
|
||||||
String get chat_noMessages => '暂无消息';
|
String get chat_noMessages => '暂无消息';
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class Channel {
|
|||||||
|
|
||||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||||
|
|
||||||
|
bool get isHashtagChannel => name.startsWith('#');
|
||||||
|
|
||||||
|
bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel;
|
||||||
|
|
||||||
static Channel? fromFrame(Uint8List frame) {
|
static Channel? fromFrame(Uint8List frame) {
|
||||||
// CHANNEL_INFO format:
|
// CHANNEL_INFO format:
|
||||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||||
|
|||||||
@@ -110,20 +110,32 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
final time =
|
final time =
|
||||||
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
||||||
return GestureDetector(
|
Future<void> copyHex() async {
|
||||||
onLongPress: () async {
|
await Clipboard.setData(
|
||||||
await Clipboard.setData(
|
ClipboardData(
|
||||||
ClipboardData(
|
text: entry.payload
|
||||||
text: entry.payload
|
.map(
|
||||||
.map(
|
(b) => b
|
||||||
(b) => b
|
.toRadixString(16)
|
||||||
.toRadixString(16)
|
.padLeft(2, '0'),
|
||||||
.padLeft(2, '0'),
|
)
|
||||||
)
|
.join(''),
|
||||||
.join(''),
|
),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
showDismissibleSnackBar(
|
||||||
|
context,
|
||||||
|
content: Text(
|
||||||
|
context.l10n.debugLog_bleCopied,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: copyHex,
|
||||||
|
onLongPress: copyHex,
|
||||||
|
onSecondaryTap: copyHex,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: MeshPalette.bg,
|
color: MeshPalette.bg,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import '../theme/mesh_theme.dart';
|
|||||||
import '../widgets/mesh_ui.dart';
|
import '../widgets/mesh_ui.dart';
|
||||||
import 'channel_message_path_screen.dart';
|
import 'channel_message_path_screen.dart';
|
||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
|
import 'region_management_screen.dart';
|
||||||
|
import '../storage/region_store.dart';
|
||||||
|
|
||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
@@ -273,46 +275,63 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Row(
|
title: GestureDetector(
|
||||||
children: [
|
behavior: HitTestBehavior.opaque,
|
||||||
_channelIcon(widget.channel),
|
onTap: () => openRegionSelectDialog(widget.channel),
|
||||||
const SizedBox(width: 8),
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Column(
|
_channelIcon(widget.channel),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
const SizedBox(width: 8),
|
||||||
children: [
|
Expanded(
|
||||||
Text(
|
child: Column(
|
||||||
widget.channel.name.isEmpty
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? context.l10n.channels_channelIndex(
|
children: [
|
||||||
widget.channel.index,
|
Text(
|
||||||
)
|
widget.channel.name.isEmpty
|
||||||
: widget.channel.name,
|
? context.l10n.channels_channelIndex(
|
||||||
style: const TextStyle(fontSize: 16),
|
widget.channel.index,
|
||||||
maxLines: 1,
|
)
|
||||||
overflow: TextOverflow.ellipsis,
|
: widget.channel.name,
|
||||||
),
|
style: const TextStyle(fontSize: 16),
|
||||||
Consumer<MeshCoreConnector>(
|
maxLines: 1,
|
||||||
builder: (context, connector, _) {
|
overflow: TextOverflow.ellipsis,
|
||||||
final unreadCount = connector
|
),
|
||||||
.getUnreadCountForChannelIndex(widget.channel.index);
|
Consumer<MeshCoreConnector>(
|
||||||
final privacy = widget.channel.isPublicChannel
|
builder: (context, connector, _) {
|
||||||
? context.l10n.channels_public
|
final unreadCount = connector
|
||||||
: context.l10n.channels_private;
|
.getUnreadCountForChannelIndex(
|
||||||
return Text(
|
widget.channel.index,
|
||||||
'$privacy • ${context.l10n.chat_unread(unreadCount)}',
|
);
|
||||||
overflow: TextOverflow.ellipsis,
|
final privacy = widget.channel.isPublicChannel
|
||||||
style: const TextStyle(fontSize: 12),
|
? context.l10n.channels_public
|
||||||
);
|
: context.l10n.channels_private;
|
||||||
},
|
final region = connector.getChannelRegion(
|
||||||
),
|
widget.channel.index,
|
||||||
],
|
);
|
||||||
|
final regionText = region.isNotEmpty
|
||||||
|
? ' • ${context.l10n.channels_regionSetTo(region)}'
|
||||||
|
: '';
|
||||||
|
return Text(
|
||||||
|
'$privacy • ${context.l10n.chat_unread(unreadCount)}$regionText',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
bottom: const SyncProgressAppBarBottom(),
|
bottom: const SyncProgressAppBarBottom(),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.l10n.channels_regionSelect_Title,
|
||||||
|
icon: const Icon(Icons.landscape),
|
||||||
|
onPressed: () => openRegionSelectDialog(widget.channel),
|
||||||
|
),
|
||||||
const RadioStatsIconButton(),
|
const RadioStatsIconButton(),
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
@@ -646,6 +665,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
color: textColor.withValues(alpha: 0.72),
|
color: textColor.withValues(alpha: 0.72),
|
||||||
),
|
),
|
||||||
|
onSecondaryTap: PlatformInfo.isDesktop
|
||||||
|
? () => _showMessageActions(message)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -658,6 +680,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
: EdgeInsets.zero,
|
: EdgeInsets.zero,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
RouteChip(
|
RouteChip(
|
||||||
isDirect: (message.pathLength ?? -1) >= 0,
|
isDirect: (message.pathLength ?? -1) >= 0,
|
||||||
@@ -666,13 +689,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Flexible(
|
||||||
context.l10n.channels_via(
|
child: Text(
|
||||||
_formatPathPrefixes(displayPath),
|
context.l10n.channels_via(
|
||||||
),
|
_formatPathPrefixes(displayPath),
|
||||||
style: MeshTheme.mono(
|
),
|
||||||
fontSize: 9.5 * textScale,
|
style: MeshTheme.mono(
|
||||||
color: metaColor,
|
fontSize: 9.5 * textScale,
|
||||||
|
color: metaColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1572,6 +1597,118 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
}
|
}
|
||||||
return hues[h % hues.length];
|
return hues[h % hues.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> openRegionSelectDialog(Channel channel) async {
|
||||||
|
// The AppBar subtitle reads the region from the connector inside a
|
||||||
|
// Consumer, so setChannelRegion's notifyListeners refreshes it directly —
|
||||||
|
// no post-dialog setState needed.
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => _RegionSelectDialog(channel: channel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegionSelectDialog extends StatefulWidget {
|
||||||
|
final Channel channel;
|
||||||
|
|
||||||
|
const _RegionSelectDialog({required this.channel});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RegionSelectDialog> createState() => _RegionSelectDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegionSelectDialogState extends State<_RegionSelectDialog> {
|
||||||
|
final RegionStore regionStore = RegionStore();
|
||||||
|
|
||||||
|
List<Region> regions = [];
|
||||||
|
int selectedIndex = -1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loadRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadRegions() {
|
||||||
|
setState(() {
|
||||||
|
regions = regionStore.loadRegions();
|
||||||
|
final channelRegion = context.read<MeshCoreConnector>().getChannelRegion(
|
||||||
|
widget.channel.index,
|
||||||
|
);
|
||||||
|
selectedIndex = regions.indexOf(channelRegion);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
title: Text(context.l10n.channels_regionSelect_Title),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.l10n.channels_clearRegion,
|
||||||
|
icon: const Icon(Icons.backspace_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<MeshCoreConnector>().setChannelRegion(
|
||||||
|
widget.channel.index,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.l10n.settings_regionSettingsSubtitle,
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: () async {
|
||||||
|
await pushRegionManagementScreen(context);
|
||||||
|
if (!mounted) return;
|
||||||
|
loadRegions();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: regions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final selected = selectedIndex == index;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.landscape,
|
||||||
|
color: selected ? MeshPalette.blue : null,
|
||||||
|
),
|
||||||
|
title: Text(regions[index]),
|
||||||
|
trailing: selected
|
||||||
|
? const Icon(Icons.check, color: MeshPalette.blue)
|
||||||
|
: null,
|
||||||
|
tileColor: selected ? MeshPalette.blueBg : null,
|
||||||
|
onTap: () {
|
||||||
|
// Tapping the already-selected region clears it.
|
||||||
|
context.read<MeshCoreConnector>().setChannelRegion(
|
||||||
|
widget.channel.index,
|
||||||
|
selected ? '' : regions[index],
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SwipeReplyBubble extends StatefulWidget {
|
class _SwipeReplyBubble extends StatefulWidget {
|
||||||
|
|||||||
+146
-120
@@ -374,13 +374,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
_communityIndex,
|
_communityIndex,
|
||||||
);
|
);
|
||||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||||
|
final community = isCommunityChannel
|
||||||
|
? _communityIndex.getCommunityForChannel(channel)
|
||||||
|
: null;
|
||||||
|
// Only flood-routed channels carry a region; show it when one is set.
|
||||||
|
String subtitle = connector.hasChannelRegion(channel.index)
|
||||||
|
? context.l10n.channels_regionSetTo(
|
||||||
|
connector.getChannelRegion(channel.index),
|
||||||
|
)
|
||||||
|
: '';
|
||||||
switch (channelType) {
|
switch (channelType) {
|
||||||
case ChannelType.communityPublic:
|
case ChannelType.communityPublic:
|
||||||
icon = Icons.groups;
|
icon = Icons.groups;
|
||||||
iconColor = MeshPalette.magenta;
|
iconColor = MeshPalette.magenta;
|
||||||
|
if (community != null) {
|
||||||
|
subtitle =
|
||||||
|
'${context.l10n.community_publicChannel} • ${community.name}';
|
||||||
|
}
|
||||||
case ChannelType.communityHashtag:
|
case ChannelType.communityHashtag:
|
||||||
icon = Icons.groups;
|
icon = Icons.groups;
|
||||||
iconColor = MeshPalette.magenta;
|
iconColor = MeshPalette.magenta;
|
||||||
|
if (community != null) {
|
||||||
|
subtitle =
|
||||||
|
'${context.l10n.community_hashtagChannel} • ${community.name}';
|
||||||
|
}
|
||||||
case ChannelType.public:
|
case ChannelType.public:
|
||||||
icon = Icons.public;
|
icon = Icons.public;
|
||||||
iconColor = MeshPalette.signal;
|
iconColor = MeshPalette.signal;
|
||||||
@@ -429,142 +446,151 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
channelMessageStore,
|
channelMessageStore,
|
||||||
channel,
|
channel,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
onSecondaryTap: PlatformInfo.isDesktop
|
||||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
? () => _showChannelActions(
|
||||||
? (_) => _showChannelActions(
|
this.context,
|
||||||
this.context,
|
connector,
|
||||||
connector,
|
channelMessageStore,
|
||||||
channelMessageStore,
|
channel,
|
||||||
channel,
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
child: Row(
|
||||||
child: Row(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
children: [
|
||||||
children: [
|
// Leading avatar with optional community badge
|
||||||
// Leading avatar with optional community badge
|
Stack(
|
||||||
Stack(
|
clipBehavior: Clip.none,
|
||||||
clipBehavior: Clip.none,
|
children: [
|
||||||
children: [
|
AvatarCircle(
|
||||||
AvatarCircle(
|
name: channelLabel,
|
||||||
name: channelLabel,
|
size: 42,
|
||||||
size: 42,
|
color: iconColor,
|
||||||
color: iconColor,
|
icon: icon,
|
||||||
icon: icon,
|
|
||||||
),
|
|
||||||
if (isCommunityChannel)
|
|
||||||
Positioned(
|
|
||||||
right: -2,
|
|
||||||
bottom: -2,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: MeshPalette.magenta,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainerLow,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.people,
|
|
||||||
size: 8,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
// Title + subtitle + ch chip
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
channelLabel,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium
|
|
||||||
?.copyWith(fontWeight: FontWeight.w500),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
StatusChip(
|
|
||||||
label: 'CH ${channel.index}',
|
|
||||||
color: MeshPalette.blue,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (lastPreview.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
lastPreview,
|
|
||||||
style: MeshTheme.mono(
|
|
||||||
fontSize: 11.5,
|
|
||||||
color: scheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
if (isCommunityChannel)
|
||||||
const SizedBox(width: 8),
|
Positioned(
|
||||||
// Right side: time + unread badge + muted + drag handle
|
right: -2,
|
||||||
Column(
|
bottom: -2,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MeshPalette.magenta,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerLow,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.people,
|
||||||
|
size: 8,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Title + subtitle + ch chip
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (lastTime != null)
|
|
||||||
Text(
|
|
||||||
_relativeTime(lastTime),
|
|
||||||
style: MeshTheme.mono(
|
|
||||||
fontSize: 11,
|
|
||||||
color: scheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isMuted) ...[
|
Expanded(
|
||||||
Icon(
|
child: Text(
|
||||||
Icons.notifications_off,
|
channelLabel,
|
||||||
size: 14,
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
color: scheme.onSurfaceVariant,
|
?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
],
|
const SizedBox(width: 6),
|
||||||
if (unreadCount > 0) UnreadBadge(count: unreadCount),
|
StatusChip(
|
||||||
|
label: 'CH ${channel.index}',
|
||||||
|
color: MeshPalette.blue,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (subtitle.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (lastPreview.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
lastPreview,
|
||||||
|
style: MeshTheme.mono(
|
||||||
|
fontSize: 11.5,
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (showDragHandle && dragIndex != null) ...[
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 8),
|
||||||
ReorderableDragStartListener(
|
// Right side: time + unread badge + muted + drag handle
|
||||||
index: dragIndex,
|
Column(
|
||||||
child: Padding(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
padding: const EdgeInsets.all(8),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Icon(
|
children: [
|
||||||
Icons.drag_handle,
|
if (lastTime != null)
|
||||||
|
Text(
|
||||||
|
_relativeTime(lastTime),
|
||||||
|
style: MeshTheme.mono(
|
||||||
|
fontSize: 11,
|
||||||
color: scheme.onSurfaceVariant,
|
color: scheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isMuted) ...[
|
||||||
|
Icon(
|
||||||
|
Icons.notifications_off,
|
||||||
|
size: 14,
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
if (unreadCount > 0) UnreadBadge(count: unreadCount),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (showDragHandle && dragIndex != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: dragIndex,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.drag_handle,
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1435,6 +1435,9 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
color: textColor.withValues(alpha: 0.72),
|
color: textColor.withValues(alpha: 0.72),
|
||||||
fontSize: bodyFontSize * textScale,
|
fontSize: bodyFontSize * textScale,
|
||||||
),
|
),
|
||||||
|
onSecondaryTap: PlatformInfo.isDesktop
|
||||||
|
? onLongPress
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -147,13 +147,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
connector,
|
connector,
|
||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
if (PlatformInfo.isDesktop) {
|
|
||||||
return GestureDetector(
|
|
||||||
onSecondaryTapUp: (_) =>
|
|
||||||
_showContactContextMenu(contact, connector),
|
|
||||||
child: tile,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return tile;
|
return tile;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -204,6 +197,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () => _showContactContextMenu(contact, connector),
|
onLongPress: () => _showContactContextMenu(contact, connector),
|
||||||
|
onSecondaryTap: PlatformInfo.isDesktop
|
||||||
|
? () => _showContactContextMenu(contact, connector)
|
||||||
|
: null,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
minZoom: _mapMinZoom,
|
minZoom: _mapMinZoom,
|
||||||
maxZoom: _mapMaxZoom,
|
maxZoom: _mapMaxZoom,
|
||||||
onLongPress: (_, point) => _addCustomPoint(point),
|
onLongPress: (_, point) => _addCustomPoint(point),
|
||||||
|
onSecondaryTap: (_, point) => _addCustomPoint(point),
|
||||||
onPositionChanged: (camera, hasGesture) {
|
onPositionChanged: (camera, hasGesture) {
|
||||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
if (!_didReceivePositionUpdate ||
|
if (!_didReceivePositionUpdate ||
|
||||||
|
|||||||
+41
-23
@@ -285,6 +285,31 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleMapContextPress(
|
||||||
|
BuildContext context,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
LatLng latLng,
|
||||||
|
) {
|
||||||
|
if (_isSelectingPoi) {
|
||||||
|
setState(() {
|
||||||
|
_isSelectingPoi = false;
|
||||||
|
});
|
||||||
|
_shareMarker(
|
||||||
|
context: context,
|
||||||
|
connector: connector,
|
||||||
|
position: latLng,
|
||||||
|
defaultLabel: context.l10n.map_pointOfInterest,
|
||||||
|
flags: 'poi',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showShareMarkerAtPositionSheet(
|
||||||
|
context: context,
|
||||||
|
connector: connector,
|
||||||
|
position: latLng,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Builder(
|
return Builder(
|
||||||
@@ -708,24 +733,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: (_, latLng) {
|
onLongPress: (_, latLng) {
|
||||||
if (_isSelectingPoi) {
|
_handleMapContextPress(context, connector, latLng);
|
||||||
setState(() {
|
},
|
||||||
_isSelectingPoi = false;
|
onSecondaryTap: (_, latLng) {
|
||||||
});
|
_handleMapContextPress(context, connector, latLng);
|
||||||
_shareMarker(
|
|
||||||
context: context,
|
|
||||||
connector: connector,
|
|
||||||
position: latLng,
|
|
||||||
defaultLabel: context.l10n.map_pointOfInterest,
|
|
||||||
flags: 'poi',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_showShareMarkerAtPositionSheet(
|
|
||||||
context: context,
|
|
||||||
connector: connector,
|
|
||||||
position: latLng,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onPositionChanged: (camera, hasGesture) {
|
onPositionChanged: (camera, hasGesture) {
|
||||||
// Track zoom in half-step buckets so cluster/marker
|
// Track zoom in half-step buckets so cluster/marker
|
||||||
@@ -1181,9 +1192,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onLongPress: () => _isBuildingPathTrace
|
onLongPress: () {
|
||||||
? _showNodeInfo(context, guess.contact)
|
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||||
: null,
|
},
|
||||||
|
onSecondaryTap: () {
|
||||||
|
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||||
|
},
|
||||||
onTap: () => _isBuildingPathTrace
|
onTap: () => _isBuildingPathTrace
|
||||||
? _addToPath(context, guess.contact, position: guess.position)
|
? _addToPath(context, guess.contact, position: guess.position)
|
||||||
: _selectNode(guess.contact, guessedPosition: guess.position),
|
: _selectNode(guess.contact, guessedPosition: guess.position),
|
||||||
@@ -1383,8 +1397,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
height: size,
|
height: size,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onLongPress: () =>
|
onLongPress: () {
|
||||||
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||||
|
},
|
||||||
|
onSecondaryTap: () {
|
||||||
|
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||||
|
},
|
||||||
onTap: () => _isBuildingPathTrace
|
onTap: () => _isBuildingPathTrace
|
||||||
? _addToPath(context, contact)
|
? _addToPath(context, contact)
|
||||||
: _selectNode(contact),
|
: _selectNode(contact),
|
||||||
|
|||||||
@@ -0,0 +1,527 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
|
import 'package:meshcore_open/storage/region_store.dart';
|
||||||
|
import 'package:meshcore_open/theme/mesh_theme.dart';
|
||||||
|
import 'package:meshcore_open/widgets/mesh_ui.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
Future<void> pushRegionManagementScreen(BuildContext context) {
|
||||||
|
return Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (context) => const RegionManagementScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegionManagementScreen extends StatefulWidget {
|
||||||
|
const RegionManagementScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
||||||
|
static final RegExp _validFetchedRegion = RegExp(r'^[a-z0-9-]{1,30}$');
|
||||||
|
|
||||||
|
final RegionStore _regionStore = RegionStore();
|
||||||
|
List<Region> _regions = [];
|
||||||
|
bool _isFetchingRegions = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
_loadRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadRegions() {
|
||||||
|
final regions = _regionStore.loadRegions();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_regions = regions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(l10n.settings_regionManagement_screenTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.settings_regionAddRegion,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () => _showAddRegionDialog(context),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.settings_regionFetchRegions,
|
||||||
|
icon: _isFetchingRegions
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.travel_explore),
|
||||||
|
onPressed: _isFetchingRegions ? null : _showFetchRegionsDialog,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
|
||||||
|
itemCount: _regions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final region = _regions[index];
|
||||||
|
return _buildRegionTile(context, region);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddRegionDialog(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.settings_regionName),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
onSubmitted: (_) => _handleAddRegion(controller.text, context),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: l10n.settings_regionNameHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
inputFormatters: <TextInputFormatter>[
|
||||||
|
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
|
||||||
|
],
|
||||||
|
maxLength: 30,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _handleAddRegion(controller.text, context),
|
||||||
|
child: Text(l10n.common_add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showFetchRegionsDialog() async {
|
||||||
|
if (_isFetchingRegions) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isFetchingRegions = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<Region> fetchedRegions = {};
|
||||||
|
try {
|
||||||
|
fetchedRegions = await _fetchRegionsFromRepeaters();
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isFetchingRegions = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final sortedRegions = fetchedRegions.toList()..sort();
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(l10n.settings_regionFetchRegions),
|
||||||
|
content: sortedRegions.isEmpty
|
||||||
|
? Text(l10n.settings_regionFetchRegionsFail)
|
||||||
|
: StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: sortedRegions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final fetchedRegion = sortedRegions[index];
|
||||||
|
final alreadyExists = _regions.contains(fetchedRegion);
|
||||||
|
return MeshCard(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.landscape,
|
||||||
|
color: MeshPalette.blue,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
fetchedRegion,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: alreadyExists
|
||||||
|
? TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).disabledColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onPressed: () {
|
||||||
|
if (alreadyExists) {
|
||||||
|
_showDialogSnackBar(
|
||||||
|
context,
|
||||||
|
l10n.settings_regionFetchRegionsAlreadyExists,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_regionStore.addRegion(fetchedRegion);
|
||||||
|
_loadRegions();
|
||||||
|
setDialogState(() {});
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: Text(l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDialogSnackBar(BuildContext context, String message) {
|
||||||
|
final overlay = Overlay.maybeOf(context);
|
||||||
|
if (overlay == null) return;
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final entry = OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 32,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Material(
|
||||||
|
color: theme.colorScheme.inverseSurface,
|
||||||
|
elevation: 6,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
overlay.insert(entry);
|
||||||
|
Timer(const Duration(seconds: 3), entry.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Region>> _fetchRegionsFromRepeaters() async {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final repeaters = await _discoverNearbyRepeaters(connector);
|
||||||
|
final regions = <Region>{};
|
||||||
|
|
||||||
|
for (final repeater in repeaters) {
|
||||||
|
if (!mounted || !connector.isConnected) break;
|
||||||
|
regions.addAll(await _requestRegionsFromRepeater(connector, repeater));
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Contact>> _discoverNearbyRepeaters(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) async {
|
||||||
|
final repeaters = connector.contacts
|
||||||
|
.where((contact) => contact.type == advTypeRepeater)
|
||||||
|
.toList();
|
||||||
|
if (repeaters.isEmpty || !connector.isConnected) return <Contact>[];
|
||||||
|
|
||||||
|
StreamSubscription<Uint8List>? subscription;
|
||||||
|
Timer? timeout;
|
||||||
|
final completer = Completer<Set<String>>();
|
||||||
|
final respondingPrefixes = <String>{};
|
||||||
|
final tag = DateTime.now().microsecondsSinceEpoch & 0xFFFFFFFF;
|
||||||
|
|
||||||
|
void complete() {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
completer.complete(respondingPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty || completer.isCompleted) return;
|
||||||
|
|
||||||
|
final reader = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
if (reader.readByte() != pushCodeControlData) return;
|
||||||
|
if (reader.remaining < 9) return;
|
||||||
|
reader.skipBytes(3); // SNR, RSSI, path_len from companion firmware.
|
||||||
|
|
||||||
|
final payloadType = reader.readByte();
|
||||||
|
if (((payloadType >> 4) & 0x0F) != controlSubtypeDiscoverResp ||
|
||||||
|
(payloadType & 0x0F) != advTypeRepeater) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.skipBytes(1); // Inbound SNR reported by the responding repeater.
|
||||||
|
if (reader.readUInt32LE() != tag) return;
|
||||||
|
|
||||||
|
final publicKeyPrefix = reader.readRemainingBytes();
|
||||||
|
if (publicKeyPrefix.isEmpty) return;
|
||||||
|
respondingPrefixes.add(pubKeyToHex(publicKeyPrefix));
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore malformed discovery frames; another response may still arrive.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final payload = buildDiscoveryRequestPayload(tag, prefixOnly: true);
|
||||||
|
await connector.sendFrame(buildSendControlDataFrame(payload));
|
||||||
|
timeout = Timer(const Duration(seconds: 10), complete);
|
||||||
|
final prefixes = await completer.future;
|
||||||
|
return repeaters.where((contact) {
|
||||||
|
final contactKey = contact.publicKeyHex.toLowerCase();
|
||||||
|
return prefixes.any((prefix) => contactKey.startsWith(prefix));
|
||||||
|
}).toList();
|
||||||
|
} catch (_) {
|
||||||
|
timeout?.cancel();
|
||||||
|
await subscription.cancel();
|
||||||
|
return <Contact>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Region>> _requestRegionsFromRepeater(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
Contact repeater,
|
||||||
|
) async {
|
||||||
|
StreamSubscription<Uint8List>? subscription;
|
||||||
|
Timer? timeout;
|
||||||
|
final completer = Completer<Set<Region>>();
|
||||||
|
int? expectedTag;
|
||||||
|
final originalPath = Uint8List.fromList(repeater.path);
|
||||||
|
final originalPathLength = repeater.pathLength;
|
||||||
|
var pathChangedForRequest = false;
|
||||||
|
|
||||||
|
void complete(Set<Region> regions) {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
completer.complete(regions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restartTimeout(Duration duration) {
|
||||||
|
timeout?.cancel();
|
||||||
|
timeout = Timer(duration, () => complete(<Region>{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final replyPath = Uint8List(0);
|
||||||
|
const replyHopCount = 0;
|
||||||
|
await connector.setContactPath(repeater, replyPath, replyHopCount);
|
||||||
|
pathChangedForRequest = true;
|
||||||
|
|
||||||
|
subscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty || completer.isCompleted) return;
|
||||||
|
|
||||||
|
final reader = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
final cmd = reader.readByte();
|
||||||
|
if (cmd == respCodeSent) {
|
||||||
|
reader.skipBytes(1);
|
||||||
|
expectedTag = reader.readUInt32LE();
|
||||||
|
final estimatedTimeoutMs = reader.readUInt32LE();
|
||||||
|
restartTimeout(
|
||||||
|
Duration(
|
||||||
|
milliseconds: estimatedTimeoutMs > 0
|
||||||
|
? estimatedTimeoutMs + 2000
|
||||||
|
: 10000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd == respCodeErr) {
|
||||||
|
complete(<Region>{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd != pushCodeBinaryResponse || expectedTag == null) return;
|
||||||
|
|
||||||
|
reader.skipBytes(1);
|
||||||
|
final tag = reader.readUInt32LE();
|
||||||
|
if (tag != expectedTag) return;
|
||||||
|
|
||||||
|
complete(_parseRegionsResponse(reader.readRemainingBytes()));
|
||||||
|
} catch (_) {
|
||||||
|
complete(<Region>{});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
restartTimeout(const Duration(seconds: 10));
|
||||||
|
final frame = buildSendAnonReqFrame(
|
||||||
|
repeater.publicKey,
|
||||||
|
requestType: anonReqTypeRegions,
|
||||||
|
replyPath: replyPath,
|
||||||
|
replyHopCount: replyHopCount,
|
||||||
|
pathHashWidth: connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
await connector.sendFrame(frame);
|
||||||
|
final regions = await completer.future;
|
||||||
|
if (pathChangedForRequest && connector.isConnected) {
|
||||||
|
await _restoreRepeaterPath(
|
||||||
|
connector,
|
||||||
|
repeater,
|
||||||
|
originalPathLength,
|
||||||
|
originalPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
|
} catch (_) {
|
||||||
|
timeout?.cancel();
|
||||||
|
subscription?.cancel();
|
||||||
|
if (pathChangedForRequest && connector.isConnected) {
|
||||||
|
await _restoreRepeaterPath(
|
||||||
|
connector,
|
||||||
|
repeater,
|
||||||
|
originalPathLength,
|
||||||
|
originalPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Region>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restoreRepeaterPath(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
Contact repeater,
|
||||||
|
int originalPathLength,
|
||||||
|
Uint8List originalPath,
|
||||||
|
) async {
|
||||||
|
if (originalPathLength < 0) {
|
||||||
|
await connector.clearContactPath(repeater);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await connector.setContactPath(repeater, originalPath, originalPathLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Region> _parseRegionsResponse(Uint8List frame) {
|
||||||
|
if (frame.length <= 4) return <Region>{};
|
||||||
|
final names = utf8
|
||||||
|
.decode(frame.sublist(4), allowMalformed: true)
|
||||||
|
.replaceAll('\x00', '')
|
||||||
|
.split(',');
|
||||||
|
return names
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.where((name) => _validFetchedRegion.hasMatch(name))
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddRegion(Region region, BuildContext context) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_regionStore.addRegion(region);
|
||||||
|
_loadRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRegionTile(BuildContext context, Region region) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return MeshCard(
|
||||||
|
key: ValueKey(region),
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.landscape, color: MeshPalette.blue),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
region,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.l10n.settings_deleteRegion,
|
||||||
|
icon: Icon(Icons.delete_outline, color: scheme.error),
|
||||||
|
onPressed: () => _confirmDelete(context, region),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmDelete(BuildContext context, Region region) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(context.l10n.settings_deleteRegion),
|
||||||
|
content: Text(context.l10n.settings_deleteRegionConfirm(region)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: Text(context.l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
await _regionStore.removeRegion(region);
|
||||||
|
// Deleting a region clears it from any channels that used it;
|
||||||
|
// refresh the connector's in-memory channel regions to match.
|
||||||
|
await connector.loadChannelSettings();
|
||||||
|
_loadRegions();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.settings_regionDeleted)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.l10n.common_delete,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(dialogContext).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import 'app_debug_log_screen.dart';
|
|||||||
import 'ble_debug_log_screen.dart';
|
import 'ble_debug_log_screen.dart';
|
||||||
import '../widgets/radio_stats_entry.dart';
|
import '../widgets/radio_stats_entry.dart';
|
||||||
import '../widgets/sync_progress_overlay.dart';
|
import '../widgets/sync_progress_overlay.dart';
|
||||||
|
import 'region_management_screen.dart';
|
||||||
|
|
||||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||||
/// to the UI enum range (always 5-8).
|
/// to the UI enum range (always 5-8).
|
||||||
@@ -449,6 +450,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
onTap: () => _showRadioSettings(context, connector),
|
onTap: () => _showRadioSettings(context, connector),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, indent: 16),
|
const Divider(height: 1, indent: 16),
|
||||||
|
_tappableTile(
|
||||||
|
context,
|
||||||
|
icon: Icons.landscape,
|
||||||
|
title: l10n.settings_regionSettings,
|
||||||
|
subtitle: l10n.settings_regionSettingsSubtitle,
|
||||||
|
onTap: () => pushRegionManagementScreen(context),
|
||||||
|
),
|
||||||
|
const Divider(height: 1, indent: 16),
|
||||||
_tappableTile(
|
_tappableTile(
|
||||||
context,
|
context,
|
||||||
icon: Icons.sensors_outlined,
|
icon: Icons.sensors_outlined,
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
return 'CMD_SET_CUSTOM_VAR';
|
return 'CMD_SET_CUSTOM_VAR';
|
||||||
case cmdSendTracePath:
|
case cmdSendTracePath:
|
||||||
return 'CMD_SEND_TRACE_PATH';
|
return 'CMD_SEND_TRACE_PATH';
|
||||||
|
case cmdSetFloodScope:
|
||||||
|
return 'CMD_SET_FLOOD_SCOPE';
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
await channelRegionStore.saveRegion(channel.index, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regions.remove(region);
|
||||||
|
saveRegions(regions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ class MeshCard extends StatelessWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
|
final VoidCallback? onSecondaryTap;
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final EdgeInsetsGeometry margin;
|
final EdgeInsetsGeometry margin;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
@@ -61,6 +62,7 @@ class MeshCard extends StatelessWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
|
this.onSecondaryTap,
|
||||||
this.padding = const EdgeInsets.all(14),
|
this.padding = const EdgeInsets.all(14),
|
||||||
this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
this.color,
|
this.color,
|
||||||
@@ -89,6 +91,7 @@ class MeshCard extends StatelessWidget {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
onLongPress!();
|
onLongPress!();
|
||||||
},
|
},
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
child: Padding(padding: padding, child: child),
|
child: Padding(padding: padding, child: child),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import '../helpers/path_helper.dart';
|
import '../helpers/path_helper.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
@@ -534,56 +535,67 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> {
|
|||||||
l10n.routing_deliveryCounts(record.successCount, record.failureCount),
|
l10n.routing_deliveryCounts(record.successCount, record.failureCount),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Card(
|
return GestureDetector(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
behavior: HitTestBehavior.opaque,
|
||||||
child: ListTile(
|
onSecondaryTapUp: PlatformInfo.isDesktop && hasBytes
|
||||||
enabled: hasBytes,
|
? (_) =>
|
||||||
leading: CircleAvatar(
|
_showPathDetail(context, connector, contact, record.pathBytes)
|
||||||
radius: 18,
|
: null,
|
||||||
backgroundColor: bg,
|
child: Card(
|
||||||
child: Icon(
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
_qualityIcon(quality),
|
child: ListTile(
|
||||||
size: 18,
|
enabled: hasBytes,
|
||||||
color: fg,
|
leading: CircleAvatar(
|
||||||
semanticLabel: _qualityLabel(context, quality),
|
radius: 18,
|
||||||
|
backgroundColor: bg,
|
||||||
|
child: Icon(
|
||||||
|
_qualityIcon(quality),
|
||||||
|
size: 18,
|
||||||
|
color: fg,
|
||||||
|
semanticLabel: _qualityLabel(context, quality),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
subtitle: Text(
|
||||||
subtitle: Text(
|
'$line1\n${line2Parts.join(' • ')}',
|
||||||
'$line1\n${line2Parts.join(' • ')}',
|
style: const TextStyle(fontSize: 11),
|
||||||
style: const TextStyle(fontSize: 11),
|
),
|
||||||
),
|
isThreeLine: true,
|
||||||
isThreeLine: true,
|
trailing: Row(
|
||||||
trailing: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
if (inUse)
|
||||||
if (inUse)
|
Tooltip(
|
||||||
Tooltip(
|
message: l10n.routing_inUse,
|
||||||
message: l10n.routing_inUse,
|
child: Icon(
|
||||||
child: Icon(
|
Icons.check_circle,
|
||||||
Icons.check_circle,
|
color: scheme.primary,
|
||||||
color: scheme.primary,
|
semanticLabel: l10n.routing_inUse,
|
||||||
semanticLabel: l10n.routing_inUse,
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 20),
|
||||||
|
tooltip: l10n.chat_removePath,
|
||||||
|
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||||
|
onPressed: () => pathService.removePathRecord(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
record.pathBytes,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
],
|
||||||
icon: const Icon(Icons.delete_outline, size: 20),
|
),
|
||||||
tooltip: l10n.chat_removePath,
|
onTap: hasBytes && !inUse
|
||||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
? () => _applyHistoryPath(connector, contact, record)
|
||||||
onPressed: () => pathService.removePathRecord(
|
: null,
|
||||||
contact.publicKeyHex,
|
onLongPress: hasBytes
|
||||||
record.pathBytes,
|
? () => _showPathDetail(
|
||||||
),
|
context,
|
||||||
),
|
connector,
|
||||||
],
|
contact,
|
||||||
|
record.pathBytes,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
onTap: hasBytes && !inUse
|
|
||||||
? () => _applyHistoryPath(connector, contact, record)
|
|
||||||
: null,
|
|
||||||
onLongPress: hasBytes
|
|
||||||
? () =>
|
|
||||||
_showPathDetail(context, connector, contact, record.pathBytes)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class TranslatedMessageContent extends StatelessWidget {
|
|||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final TextStyle? originalStyle;
|
final TextStyle? originalStyle;
|
||||||
final bool showOriginalFirst;
|
final bool showOriginalFirst;
|
||||||
|
final VoidCallback? onSecondaryTap;
|
||||||
|
|
||||||
const TranslatedMessageContent({
|
const TranslatedMessageContent({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -16,6 +17,7 @@ class TranslatedMessageContent extends StatelessWidget {
|
|||||||
this.originalText,
|
this.originalText,
|
||||||
this.originalStyle,
|
this.originalStyle,
|
||||||
this.showOriginalFirst = true,
|
this.showOriginalFirst = true,
|
||||||
|
this.onSecondaryTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,12 +38,14 @@ class TranslatedMessageContent extends StatelessWidget {
|
|||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
fontSize: style.fontSize,
|
fontSize: style.fontSize,
|
||||||
),
|
),
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
final translatedWidget = LinkHandler.buildLinkifyText(
|
final translatedWidget = LinkHandler.buildLinkifyText(
|
||||||
context: context,
|
context: context,
|
||||||
text: trimmedDisplay,
|
text: trimmedDisplay,
|
||||||
style: style,
|
style: style,
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!shouldShowOriginal) {
|
if (!shouldShowOriginal) {
|
||||||
|
|||||||
+311
-1
@@ -1 +1,311 @@
|
|||||||
{}
|
{
|
||||||
|
"bg": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"de": [
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists"
|
||||||
|
],
|
||||||
|
|
||||||
|
"es": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"hu": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ja": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"nl": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pl": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pt": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ru": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sk": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sl": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sv": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"uk": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"settings_regionSettings",
|
||||||
|
"settings_regionSettingsSubtitle",
|
||||||
|
"settings_regionManagement_screenTitle",
|
||||||
|
"settings_regionNameHint",
|
||||||
|
"settings_regionAddRegion",
|
||||||
|
"settings_regionFetchRegions",
|
||||||
|
"settings_regionFetchRegionsFail",
|
||||||
|
"settings_regionFetchRegionsAlreadyExists",
|
||||||
|
"settings_regionName",
|
||||||
|
"settings_regionDeleted",
|
||||||
|
"settings_deleteRegion",
|
||||||
|
"settings_deleteRegionConfirm",
|
||||||
|
"channels_regionSetTo",
|
||||||
|
"channels_regionNotSet",
|
||||||
|
"channels_regionSelect_Title",
|
||||||
|
"channels_clearRegion"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user