mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-11 11:07:04 +10:00
Merge pull request #1 from ericszimmermann/ez_snr_multibyte2
Multibyte Support for SNR-Indicator, channel message_bubble and Setting
This commit is contained in:
@@ -13,6 +13,7 @@ import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/cyr2lat.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
@@ -422,6 +423,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: (message.pathVariants.isNotEmpty
|
||||
? message.pathVariants.first
|
||||
: Uint8List(0));
|
||||
final displayPathHashWidth =
|
||||
message.pathHashWidth ??
|
||||
context.read<MeshCoreConnector>().pathHashByteWidth;
|
||||
|
||||
const maxSwipeOffset = 64.0;
|
||||
const replySwipeThreshold = 64.0;
|
||||
@@ -608,7 +612,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(displayPath),
|
||||
_formatPathPrefixes(
|
||||
displayPath,
|
||||
displayPathHashWidth,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
@@ -1432,10 +1439,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
String _formatPathPrefixes(Uint8List pathBytes, int pathHashByteWidth) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -488,11 +488,17 @@ class _ChannelMessagePathMapScreenState
|
||||
? Uint8List.fromList(selectedPathTmp.reversed.toList())
|
||||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final width = (widget.message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final hops = _buildPathHops(selectedPath, connector, context.l10n, width);
|
||||
final width =
|
||||
(widget.message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final selectedIndex = _indexForPath(selectedPathTmp, observedPaths);
|
||||
final hops = _buildPathHops(
|
||||
selectedPath,
|
||||
connector,
|
||||
context.l10n,
|
||||
width,
|
||||
);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
@@ -610,14 +616,18 @@ class _ChannelMessagePathMapScreenState
|
||||
bounds: bounds,
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||
index,
|
||||
) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
_focusedHopIndex = null;
|
||||
});
|
||||
}),
|
||||
_buildPathSelector(
|
||||
context,
|
||||
observedPaths,
|
||||
selectedIndex,
|
||||
width,
|
||||
(index) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
_focusedHopIndex = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (points.isEmpty)
|
||||
Center(
|
||||
child: Card(
|
||||
@@ -643,13 +653,11 @@ class _ChannelMessagePathMapScreenState
|
||||
BuildContext context,
|
||||
List<_ObservedPath> paths,
|
||||
int selectedIndex,
|
||||
int hashByteWidth,
|
||||
ValueChanged<int> onSelected,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final width = context.read<MeshCoreConnector>().pathHashByteWidth.clamp(
|
||||
1,
|
||||
4,
|
||||
);
|
||||
final width = hashByteWidth.clamp(1, 4);
|
||||
final selectedPath = paths[selectedIndex];
|
||||
final label = selectedPath.isPrimary
|
||||
? l10n.channelPath_primaryPath(selectedIndex + 1)
|
||||
|
||||
@@ -805,16 +805,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
pathsWithRepeaters = paths.map((path) {
|
||||
final isDirectRepeater =
|
||||
directRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
directRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
directRepeater.matchesPathStart(
|
||||
path.pathBytes,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
final isSecondDirectRepeater =
|
||||
secondDirectRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
secondDirectRepeater.matchesPathStart(
|
||||
path.pathBytes,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
final isThirdDirectRepeater =
|
||||
thirdDirectRepeater != null &&
|
||||
path.pathBytes.isNotEmpty &&
|
||||
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||
thirdDirectRepeater.matchesPathStart(
|
||||
path.pathBytes,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
|
||||
int ranking = -1;
|
||||
Color color = Colors.grey;
|
||||
|
||||
@@ -1266,7 +1266,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPing,
|
||||
path: Uint8List.fromList([contact.publicKey.first]),
|
||||
path: _contactPathPrefix(contact, hw),
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1299,7 +1299,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
: context.l10n.contacts_roomPing,
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: Uint8List.fromList([contact.publicKey.first]),
|
||||
: _contactPathPrefix(contact, hw),
|
||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
@@ -1416,6 +1416,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List _contactPathPrefix(Contact contact, int hashByteWidth) {
|
||||
if (contact.publicKey.isEmpty) return Uint8List(0);
|
||||
final width = hashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt()
|
||||
.clamp(1, contact.publicKey.length)
|
||||
.toInt();
|
||||
return Uint8List.fromList(contact.publicKey.sublist(0, width));
|
||||
}
|
||||
|
||||
void _confirmDelete(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
|
||||
@@ -728,7 +728,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
for (final repeater in withLocation) {
|
||||
if (repeater.type != advTypeRepeater) continue;
|
||||
if (repeater.publicKey.length < lastHop.length) continue;
|
||||
if (!listEquals(repeater.publicKey.sublist(0, lastHop.length), lastHop)) {
|
||||
if (!listEquals(
|
||||
repeater.publicKey.sublist(0, lastHop.length),
|
||||
lastHop,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
|
||||
@@ -986,11 +989,21 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
if (settings.mapShowOverlaps) {
|
||||
final hopWidth = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt();
|
||||
final hasOverlap = contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKeyHex != contact.publicKeyHex &&
|
||||
c.publicKey.first == contact.publicKey.first &&
|
||||
c.publicKey.length >= hopWidth &&
|
||||
contact.publicKey.length >= hopWidth &&
|
||||
listEquals(
|
||||
c.publicKey.sublist(0, hopWidth),
|
||||
contact.publicKey.sublist(0, hopWidth),
|
||||
) &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
(contact.type == advTypeRepeater ||
|
||||
contact.type == advTypeRoom),
|
||||
|
||||
@@ -298,6 +298,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.route_outlined),
|
||||
title: Text(l10n.repeater_pathHashMode),
|
||||
subtitle: Text(_pathHashModeSubtitle(connector.pathHashByteWidth)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
enabled: connector.isConnected,
|
||||
onTap: () => _editPathHashMode(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on_outlined),
|
||||
title: Text(l10n.settings_location),
|
||||
@@ -338,6 +347,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _pathHashModeSubtitle(int pathHashByteWidth) {
|
||||
final width = pathHashByteWidth.clamp(1, 3).toInt();
|
||||
final mode = width - 1;
|
||||
final unit = width == 1 ? 'byte' : 'bytes';
|
||||
return 'Mode $mode - $width $unit per hop';
|
||||
}
|
||||
|
||||
Widget _buildActionsCard(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
@@ -545,6 +561,74 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _editPathHashMode(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
var selectedMode = (connector.pathHashByteWidth - 1).clamp(0, 2).toInt();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.repeater_pathHashMode),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: selectedMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pathHashMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 0, child: Text('0 - 1 byte')),
|
||||
DropdownMenuItem(value: 1, child: Text('1 - 2 bytes')),
|
||||
DropdownMenuItem(value: 2, child: Text('2 - 3 bytes')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setDialogState(() => selectedMode = value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.repeater_pathHashModeHelper,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
try {
|
||||
await connector.setPathHashMode(selectedMode);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_settingsSaved),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_error(e.toString())),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editLocation(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
final latController = TextEditingController();
|
||||
|
||||
Reference in New Issue
Block a user