Skip to content

Commit

Permalink
Refactor reading to improve speed with large amount of files (#278)
Browse files Browse the repository at this point in the history
Fixes #265
  • Loading branch information
knopp authored Jan 2, 2024
1 parent f0ef4e5 commit d961806
Show file tree
Hide file tree
Showing 17 changed files with 451 additions and 425 deletions.
2 changes: 1 addition & 1 deletion super_clipboard/example/lib/widget_for_reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ Future<_RepresentationWidget?> _widgetForFormat(
if (contents == null) {
return null;
} else {
final text = utf8.decode(contents);
final text = utf8.decode(contents, allowMalformed: true);
return _RepresentationWidget(
format: format,
name: 'Plain Text (utf8 file)',
Expand Down
11 changes: 6 additions & 5 deletions super_clipboard/lib/src/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ class ClipboardReadEvent {
/// such as inserting text into input or content editable elements.
Future<ClipboardReader> getClipboardReader() async {
final readerItems = await _event.getReader().getItems();
final items = await Future.wait(
readerItems.map(
(e) => ClipboardDataReader.forItem(e),
),
);
final itemInfo = await raw.DataReaderItem.getItemInfo(readerItems);
final items = itemInfo
.map(
(e) => ClipboardDataReader.forItemInfo(e),
)
.toList(growable: false);
return ClipboardReader(items);
}

Expand Down
9 changes: 5 additions & 4 deletions super_clipboard/lib/src/reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ abstract class DataReader {
/// If this reader is backed by raw DataReaderItem returns it.
raw.DataReaderItem? get rawReader => null;

static Future<DataReader> forItem(raw.DataReaderItem item) async =>
ItemDataReader.fromItem(item);
/// Creates data reader from provided item info.
static DataReader forItemInfo(raw.DataReaderItemInfo info) =>
ItemDataReader.fromItemInfo(info);
}

abstract class ClipboardDataReader extends DataReader {
Expand All @@ -146,8 +147,8 @@ abstract class ClipboardDataReader extends DataReader {
/// is not available or the data is virtual (macOS and Windows).
Future<T?> readValue<T extends Object>(ValueFormat<T> format);

static Future<ClipboardDataReader> forItem(raw.DataReaderItem item) async =>
ItemDataReader.fromItem(item);
static ClipboardDataReader forItemInfo(raw.DataReaderItemInfo item) =>
ItemDataReader.fromItemInfo(item);
}

/// Clipboard reader exposes contents of the clipboard.
Expand Down
52 changes: 8 additions & 44 deletions super_clipboard/lib/src/reader_internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,50 +62,13 @@ class ItemDataReader extends ClipboardDataReader {
required this.synthesizedFromURIFormat,
});

static Future<ClipboardDataReader> fromItem(raw.DataReaderItem item) async {
final allFormats = await item.getAvailableFormats();
final isSynthesized =
await Future.wait(allFormats.map((f) => item.isSynthesized(f)));

final virtualReceivers = (await Future.wait(
allFormats.map((f) => item.getVirtualFileReceiver(format: f))))
.whereNotNull()
.toList(growable: false);

final synthesizedFormats = allFormats
.whereIndexed((index, _) => isSynthesized[index])
.toList(growable: false);

String? synthesizedFromURIFormat;

/// If there are no virtual receivers but there is File URI, we'll
/// try to synthesize a format from it.
if (virtualReceivers.isEmpty) {
for (final format in allFormats) {
if (Formats.fileUri.canDecode(format)) {
final uri = await Formats.fileUri.decode(
format,
_PlatformDataProvider(
allFormats,
(f) => item.getDataForFormat(f).$1,
),
);
if (uri != null) {
final format = await raw.DataReader.formatForFileUri(uri);
if (format != null && !allFormats.contains(format)) {
synthesizedFromURIFormat = format;
}
}
}
}
}

static ClipboardDataReader fromItemInfo(raw.DataReaderItemInfo info) {
return ItemDataReader._(
item: item,
formats: allFormats,
synthesizedFormats: synthesizedFormats,
virtualReceivers: virtualReceivers,
synthesizedFromURIFormat: synthesizedFromURIFormat,
item: info.item,
formats: info.formats,
synthesizedFormats: info.synthesizedFormats,
virtualReceivers: info.virtualReceivers,
synthesizedFromURIFormat: info.synthesizedFromURIFormat,
);
}

Expand Down Expand Up @@ -311,7 +274,8 @@ class ItemDataReader extends ClipboardDataReader {
}) async {
final formats = format?.receiverFormats ?? await item.getAvailableFormats();
for (final format in formats) {
final receiver = await item.getVirtualFileReceiver(format: format);
final receiver = virtualReceivers
.firstWhereOrNull((element) => element.format == format);
if (receiver != null) {
return receiver;
}
Expand Down
5 changes: 3 additions & 2 deletions super_clipboard/lib/src/system_clipboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ class SystemClipboard implements ClipboardWriter {
Future<ClipboardReader> read() async {
final reader = await raw.ClipboardReader.instance.newClipboardReader();
final readerItems = await reader.getItems();
final itemInfo = await raw.DataReaderItem.getItemInfo(readerItems);
final items = <ClipboardDataReader>[];
for (final item in readerItems) {
items.add(await ClipboardDataReader.forItem(item));
for (final item in itemInfo) {
items.add(ClipboardDataReader.forItemInfo(item));
}
return ClipboardReader(items);
}
Expand Down
10 changes: 8 additions & 2 deletions super_drag_and_drop/lib/src/drop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ abstract class DropItem with Diagnosticable {
Object? get localData;

/// [DataReader] that can be used to access drag data for this item.
/// On some platforms (mobile, web) the reader is only available after
/// user actually performed the drop.
/// `dataReader` is always available in `onPerformDrop` event, but may be `null`
/// during the `onDropOver` event in following cases:
/// - on mobile and web, `dataReader` is always `null` during `onDropOver`.
/// For security reasons the data is only available after user actually
/// performed the drop.
/// - on desktop `dataReader` is gradually populated during `onDropOver` event.
/// On some platforms clipboard access can get slow with large amount of items,
/// and rather than blocking main thread the items are populated asynchronously.
DataReader? get dataReader;

/// Returns list of platform specific format identifier for this item.
Expand Down
40 changes: 28 additions & 12 deletions super_drag_and_drop/lib/src/drop_internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'package:super_native_extensions/raw_drag_drop.dart' as raw;
import 'package:super_native_extensions/raw_clipboard.dart' as raw;

// ignore: implementation_imports, // Needed for FormatExtensions
import 'package:super_clipboard/src/reader_internal.dart';
Expand Down Expand Up @@ -39,12 +40,6 @@ class _DropItem extends DropItem {
List<PlatformFormat> get platformFormats =>
_reader?.platformFormats ?? _item.formats;

Future<void> _maybeInitReader() async {
if (_reader == null && _item.readerItem != null) {
_reader = await DataReader.forItem(_item.readerItem!);
}
}

raw.DropItem _item;
DataReader? _reader;
}
Expand All @@ -59,7 +54,10 @@ class _DropSession extends DropSession {
@override
Set<raw.DropOperation> get allowedOperations => _allowedOperations;

Future<void> updateItems(List<raw.DropItem> items) async {
Future<void> updateItems(
List<raw.DropItem> items, {
required bool isDrop,
}) async {
final current = List<_DropItem>.from(_items);
_items.clear();

Expand All @@ -79,9 +77,24 @@ class _DropSession extends DropSession {
}
}

await Future.wait(_items.map(
(e) => e._maybeInitReader(),
));
final itemsNeedingReaders = _items
.where((element) =>
element._item.readerItem != null && element._reader == null)
.toList(growable: false);

if (itemsNeedingReaders.isEmpty) {
return;
}

final itemInfo = await raw.DataReaderItem.getItemInfo(
itemsNeedingReaders.map((e) => e._item.readerItem!),
timeout: isDrop ? null : const Duration(milliseconds: 10),
);

for (final (index, info) in itemInfo.indexed) {
assert(itemsNeedingReaders[index]._item.readerItem == info.item);
itemsNeedingReaders[index]._reader = DataReader.forItemInfo(info);
}
}

Future<raw.DropOperation> update({
Expand Down Expand Up @@ -261,7 +274,10 @@ class _DropContextDelegate extends raw.DropContextDelegate {
Future<raw.DropOperation> onDropUpdate(raw.DropEvent event) async {
final session =
_sessions.putIfAbsent(event.sessionId, () => _DropSession());
await session.updateItems(event.items);
await session.updateItems(
event.items,
isDrop: false,
);
return session.update(
position: event.locationInView,
allowedOperations: Set.from(event.allowedOperations),
Expand All @@ -278,7 +294,7 @@ class _DropContextDelegate extends raw.DropContextDelegate {
@override
Future<void> onPerformDrop(raw.DropEvent event) async {
final session = _sessions[event.sessionId];
await session?.updateItems(event.items);
await session?.updateItems(event.items, isDrop: true);
await session?.performDrop(
location: event.locationInView,
acceptedOperation: event.acceptedOperation!,
Expand Down
Loading

0 comments on commit d961806

Please sign in to comment.