feat(clipboard): Add pasteboard support for cross-platform image paste

This commit is contained in:
cogwheel0
2025-12-08 12:47:12 +05:30
parent fbeaebe0e8
commit 145a42b504
16 changed files with 230 additions and 56 deletions

View File

@@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform;
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'dart:math' as math;
import '../providers/chat_providers.dart';
@@ -285,6 +286,90 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
}
}
/// Handles pasting images/files from clipboard with pre-loaded image data.
///
/// This avoids a second clipboard read by using data already fetched when
/// building the context menu.
Future<void> _handleClipboardPasteWithData(Uint8List imageData) async {
if (!widget.enabled) return;
final onPasted = widget.onPastedAttachments;
if (onPasted == null) return;
PlatformUtils.lightHaptic();
final attachment = await _clipboardService.createAttachmentFromImageData(
imageData: imageData,
mimeType: 'image/png',
);
if (attachment != null) {
await onPasted([attachment]);
}
}
/// Builds a custom context menu with standard options plus "Paste Image".
///
/// The standard paste only works for text. This adds a "Paste Image"
/// option that uses the pasteboard package to read images from clipboard
/// on both iOS and Android. The option only appears when there's actually
/// an image in the clipboard.
Widget _buildContextMenu(
BuildContext context,
EditableTextState editableTextState,
) {
final List<ContextMenuButtonItem> buttonItems = List.from(
editableTextState.contextMenuButtonItems,
);
// Only add "Paste Image" if we have a callback for pasted attachments
if (widget.onPastedAttachments == null) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
}
// Check clipboard for images - the data is captured in the closure to
// avoid double-read and stale cache issues
return FutureBuilder<Uint8List?>(
future: _clipboardService.getClipboardImage(),
builder: (context, snapshot) {
final imageData = snapshot.data;
final hasImage = imageData != null && imageData.isNotEmpty;
if (hasImage) {
// Find the index of the standard Paste button to insert after it
final pasteIndex = buttonItems.indexWhere(
(item) => item.type == ContextMenuButtonType.paste,
);
// Capture imageData in closure to avoid re-reading clipboard
final pasteImageItem = ContextMenuButtonItem(
label: AppLocalizations.of(context)?.pasteImage ?? 'Paste Image',
onPressed: () {
// Close the context menu first
ContextMenuController.removeAny();
// Use the captured imageData directly
_handleClipboardPasteWithData(imageData);
},
);
// Insert after Paste if found, otherwise add at the end
if (pasteIndex >= 0) {
buttonItems.insert(pasteIndex + 1, pasteImageItem);
} else {
buttonItems.add(pasteImageItem);
}
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
);
}
void _insertNewline() {
final text = _controller.text;
TextSelection sel = _controller.selection;
@@ -1545,6 +1630,10 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
.toList(),
onContentInserted: _handleContentInserted,
),
// Custom context menu with "Paste Image" option for iOS
contextMenuBuilder: (context, editableTextState) {
return _buildContextMenu(context, editableTextState);
},
onSubmitted: (_) {
if (sendOnEnter) {
_sendMessage();