Merge pull request #1 from ericszimmermann/ez_snr_multibyte2

Multibyte Support for SNR-Indicator, channel message_bubble and Setting
This commit is contained in:
PacoX
2026-05-19 10:14:03 +02:00
committed by GitHub
47 changed files with 438 additions and 119 deletions
+13 -5
View File
@@ -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(',');
}
}
+25 -17
View File
@@ -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)
+12 -6
View File
@@ -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;
+12 -2
View File
@@ -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,
+15 -2
View File
@@ -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),
+84
View File
@@ -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();