feat: add contact UI helpers and path editor for routing management

- Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types.
- Created colorForName and firstCharacterOrEmoji functions to enhance contact display.
- Developed PathEditorSheet widget for managing contact paths with a user-friendly interface.
- Introduced RoutingSheet for managing contact routing modes and displaying path history.
- Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation.
This commit is contained in:
zjs81
2026-06-11 00:07:12 -07:00
parent 743ef7f124
commit cba1e5950c
86 changed files with 8149 additions and 6379 deletions
+136 -78
View File
@@ -103,6 +103,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
Contact? _targetContact;
// Live path resolved at trace time; used by the response handler for
// endpoint inference so it matches the path that was actually traced.
Uint8List _tracedPath = Uint8List(0);
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
@@ -168,17 +171,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: _resetMapView,
),
],
@@ -228,6 +231,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
return traceBytes;
}
/// Resolves the path bytes to trace. When tracing a specific contact's
/// route (flipPathAround), re-read that contact's live forced/auto path from
/// the connector so a path the user just changed (force flood / set path /
/// reset to auto) is honored immediately, instead of the value captured when
/// this screen was first pushed.
Uint8List _resolveLivePath(MeshCoreConnector connector) {
final target = widget.targetContact;
if (!widget.flipPathAround || target == null) {
return widget.path;
}
final live = connector.allContactsUnfiltered.firstWhere(
(c) => c.publicKeyHex == target.publicKeyHex,
orElse: () => target,
);
return live.pathBytesForDisplay;
}
Future<void> _doPathTrace() async {
if (mounted) {
setState(() {
@@ -236,9 +256,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
});
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final livePath = _resolveLivePath(connector);
_tracedPath = livePath;
final pathTmp = widget.reversePathAround
? Uint8List.fromList(widget.path.reversed.toList())
: widget.path;
? Uint8List.fromList(livePath.reversed.toList())
: livePath;
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
@@ -248,7 +272,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
noNotify: !mounted,
);
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
0, //flags
@@ -414,13 +437,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final tc = _targetContact!;
if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!);
} else if (widget.path.length > 1) {
} else if (_tracedPath.length > 1) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
// sits in the middle of the symmetric sequence; .last is the local side.
final lastHop = widget.reversePathAround
? widget.path.first
: widget.path.last;
? _tracedPath.first
: _tracedPath.last;
final peers = connector.allContacts
.where(
@@ -593,7 +616,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
!_failed2Loaded)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
@@ -640,31 +663,35 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: point,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
hasGps ? label : '~$label',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
],
),
alignment: Alignment.center,
child: Text(
hasGps ? label : '~$label',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
@@ -689,29 +716,33 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: selfPoint,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
],
),
alignment: Alignment.center,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
@@ -735,26 +766,30 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: targetPos,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
? Colors.purple.withValues(alpha: 0.55)
: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
? Colors.purple.withValues(alpha: 0.55)
: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: const Icon(Icons.person, color: Colors.white, size: 18),
),
alignment: Alignment.center,
child: const Icon(Icons.person, color: Colors.white, size: 18),
),
),
);
@@ -927,6 +962,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
);
}
Widget _colorDot(Color color) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
Widget _buildLegendCard(
BuildContext context,
PathTraceData pathTraceData,
@@ -949,9 +990,26 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Row(
children: [
_colorDot(Colors.green),
const SizedBox(width: 4),
Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)),
const SizedBox(width: 12),
_colorDot(Colors.orange),
const SizedBox(width: 4),
Text(l10n.pathTrace_legendInferred, style: const TextStyle(fontSize: 11)),
],
),
],
),
),
const Divider(height: 1),