Merge branch 'dev' into update-pacox-multibyte

This commit is contained in:
HDDen
2026-06-12 13:39:09 +03:00
95 changed files with 14166 additions and 6756 deletions
+221 -170
View File
@@ -185,17 +185,17 @@ class _MapScreenState extends State<MapScreen> {
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: () => _mapController.move(center, zoom),
),
],
@@ -420,61 +420,79 @@ class _MapScreenState extends State<MapScreen> {
automaticallyImplyLeading: false,
bottom: const SyncProgressAppBarBottom(),
actions: [
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
IconButton(
icon: const LosIcon(),
onPressed: () {
final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null &&
connector.selfLongitude != null) {
candidates.add(
LineOfSightEndpoint(
label: context.l10n.pathTrace_you,
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
color: Colors.teal,
icon: Icons.person_pin_circle,
),
);
}
for (final c in contactsWithLocation) {
candidates.add(
LineOfSightEndpoint(
label: c.name,
point: LatLng(c.latitude!, c.longitude!),
color: _getNodeColor(c.type),
icon: _getNodeIcon(c.type),
),
);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LineOfSightMapScreen(
title: context.l10n.map_losScreenTitle,
candidates: candidates,
),
),
);
},
tooltip: context.l10n.map_lineOfSight,
),
PopupMenuButton(
itemBuilder: (context) => [
if (!_isBuildingPathTrace &&
connector.selfLatitude != null &&
connector.selfLongitude != null)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.radar),
const SizedBox(width: 8),
Text(context.l10n.contacts_pathTrace),
],
),
onTap: () => _startPath(
LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
),
),
if (!_isBuildingPathTrace)
PopupMenuItem(
child: Row(
children: [
const LosIcon(),
const SizedBox(width: 8),
Text(context.l10n.map_lineOfSight),
],
),
onTap: () {
final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null &&
connector.selfLongitude != null) {
candidates.add(
LineOfSightEndpoint(
label: context.l10n.pathTrace_you,
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
color: Colors.teal,
icon: Icons.person_pin_circle,
),
);
}
for (final c in contactsWithLocation) {
candidates.add(
LineOfSightEndpoint(
label: c.name,
point: LatLng(c.latitude!, c.longitude!),
color: _getNodeColor(c.type),
icon: _getNodeIcon(c.type),
),
);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LineOfSightMapScreen(
title: context.l10n.map_losScreenTitle,
candidates: candidates,
),
),
);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
@@ -907,8 +925,8 @@ class _MapScreenState extends State<MapScreen> {
final color = _getNodeColor(guess.contact.type);
final marker = Marker(
point: guess.position,
width: 35,
height: 35,
width: 48,
height: 48,
child: GestureDetector(
onLongPress: () => _isBuildingPathTrace
? _showNodeInfo(context, guess.contact)
@@ -920,26 +938,28 @@ class _MapScreenState extends State<MapScreen> {
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(
alpha: guess.highConfidence ? 0.55 : 0.30,
),
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),
child: Center(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(
alpha: guess.highConfidence ? 0.55 : 0.30,
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
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),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
),
),
),
@@ -1041,39 +1061,37 @@ class _MapScreenState extends State<MapScreen> {
for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
height: 35,
width: 48,
height: 48,
child: GestureDetector(
onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, contact)
: _showNodeInfo(context, contact),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: settings.mapShowOverlaps && !_isBuildingPathTrace
? Colors.red
: _getNodeColor(contact.type),
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),
),
],
),
child: Icon(
_getNodeIcon(contact.type),
color: Colors.white,
size: 20,
),
child: Center(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: settings.mapShowOverlaps && !_isBuildingPathTrace
? Colors.red
: _getNodeColor(contact.type),
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),
),
],
),
],
child: Icon(
_getNodeIcon(contact.type),
color: Colors.white,
size: 20,
),
),
),
),
);
@@ -1219,7 +1237,9 @@ class _MapScreenState extends State<MapScreen> {
Icon(
Icons.location_on,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": $nodeCount",
@@ -1232,10 +1252,12 @@ class _MapScreenState extends State<MapScreen> {
),
Row(
children: [
const Icon(
Icon(
Icons.wrong_location,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": ${nodeCountAll - nodeCount}",
@@ -1248,10 +1270,12 @@ class _MapScreenState extends State<MapScreen> {
),
Row(
children: [
const Icon(
Icon(
Icons.add_outlined,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": $nodeCountAll",
@@ -1547,56 +1571,14 @@ class _MapScreenState extends State<MapScreen> {
LatLng? guessedPosition,
}) {
final connector = context.read<MeshCoreConnector>();
showDialog(
showModalBottomSheet(
context: context,
builder: (dialogContext) => AlertDialog(
title: Row(
children: [
Icon(
_getNodeIcon(contact.type),
color: _getNodeColor(contact.type),
),
const SizedBox(width: 8),
Expanded(child: SelectableText(contact.name)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
context.l10n.map_type,
contact.typeLabel(context.l10n),
),
_buildInfoRow(
context.l10n.map_path,
contact.pathLabel(context.l10n),
),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.map_location,
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
context.l10n.map_estLocation,
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
),
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_close),
),
if (contact.type ==
advTypeChat) // Only show chat button for chat nodes
TextButton(
showDragHandle: true,
builder: (sheetContext) {
final actions = <Widget>[];
if (contact.type == advTypeChat) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
@@ -1604,7 +1586,7 @@ class _MapScreenState extends State<MapScreen> {
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(dialogContext);
Navigator.pop(sheetContext);
Navigator.push(
context,
MaterialPageRoute(
@@ -1617,30 +1599,93 @@ class _MapScreenState extends State<MapScreen> {
},
child: Text(context.l10n.contacts_openChat),
),
if (contact.type == advTypeRepeater)
TextButton(
);
}
if (contact.type == advTypeRepeater) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
Navigator.pop(sheetContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
);
}
if (contact.type == advTypeRoom) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
],
),
);
}
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getNodeIcon(contact.type),
color: _getNodeColor(contact.type),
),
const SizedBox(width: 8),
Expanded(child: SelectableText(contact.name)),
],
),
const SizedBox(height: 8),
_buildInfoRow(
context.l10n.map_type,
contact.typeLabel(context.l10n),
),
_buildInfoRow(
context.l10n.map_path,
contact.pathLabel(context.l10n),
),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.map_location,
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
context.l10n.map_estLocation,
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
),
_buildInfoRow(
context.l10n.map_publicKey,
contact.publicKeyHex,
),
const SizedBox(height: 16),
...actions,
TextButton(
onPressed: () => Navigator.pop(sheetContext),
child: Text(context.l10n.common_close),
),
],
),
),
),
);
},
);
}
@@ -1725,6 +1770,9 @@ class _MapScreenState extends State<MapScreen> {
child: Text(context.l10n.common_hide),
),
TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
onPressed: () async {
setState(() {
_hiddenMarkerIds.add(marker.id);
@@ -1756,7 +1804,7 @@ class _MapScreenState extends State<MapScreen> {
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -1821,10 +1869,7 @@ class _MapScreenState extends State<MapScreen> {
);
await connector.refreshDeviceInfo();
if (!mounted) return;
showDismissibleSnackBar(
messenger.context,
content: Text(successMsg),
);
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
},
),
ListTile(
@@ -2213,7 +2258,10 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 8),
Text(
_getTimeFilterLabel(settings.mapTimeFilterHours),
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Slider(
value: _hoursToSliderValue(settings.mapTimeFilterHours),
@@ -2390,7 +2438,10 @@ class _MapScreenState extends State<MapScreen> {
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
SelectableText(
PathHelper.splitPathBytes(