mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-01 16:15:56 +10:00
Merge pull request #479 from MrSurly/478-implement-right-click
Issue 478: Implement right click for desktop use
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -646,6 +646,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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+118
-120
@@ -429,142 +429,140 @@ 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 (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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user