diff --git a/client/lib/ui/explorer/explorer_actions.dart b/client/lib/ui/explorer/explorer_actions.dart index ab036733..2ad253d4 100644 --- a/client/lib/ui/explorer/explorer_actions.dart +++ b/client/lib/ui/explorer/explorer_actions.dart @@ -6,7 +6,7 @@ import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/name_conflict.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/explorer/page.dart'; -import 'package:phylum/ui/explorer/paste_action_handler.dart'; +import 'package:phylum/ui/explorer/paste_helpers.dart'; import 'package:phylum/ui/explorer/selection_mode.dart'; import 'package:phylum/ui/preview/resource_preview.dart'; import 'package:phylum/util/dialogs.dart'; @@ -31,7 +31,21 @@ class ExplorerActions extends StatelessWidget { PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), RefreshIntent: CallbackAction(onInvoke: (i) => refreshKey.currentState?.show()), PasteFromClipboardIntent: CallbackAction( - onInvoke: (PasteFromClipboardIntent i) => handlePasteAction(i, context), + onInvoke: (PasteFromClipboardIntent i) async { + final folderId = context.read().folderId; + if (folderId == null) return; + + final clipboard = SystemClipboard.instance; + if (clipboard == null) return; + + final reader = await clipboard.read(); + + if (!context.mounted) return; + processPasteItems(context, folderId, reader.items); + + await SystemClipboard.instance?.write([]); + return null; + }, ), SelectAllIntent: CallbackAction( onInvoke: (i) => context.read().updateSelection((i) => i, SelectionMode.all, true), diff --git a/client/lib/ui/explorer/explorer_view.dart b/client/lib/ui/explorer/explorer_view.dart index 5993bbe9..c70c0fa5 100644 --- a/client/lib/ui/explorer/explorer_view.dart +++ b/client/lib/ui/explorer/explorer_view.dart @@ -8,8 +8,8 @@ import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/destination_picker/destination_picker.dart'; import 'package:phylum/ui/explorer/explorer_actions.dart'; import 'package:phylum/ui/explorer/explorer_gesture_handler.dart'; +import 'package:phylum/ui/explorer/paste_helpers.dart'; import 'package:phylum/ui/explorer/path_view.dart'; -import 'package:phylum/ui/explorer/resource_drop_and_drop.dart'; import 'package:phylum/util/dialogs.dart'; import 'package:phylum/util/upload_utils.dart'; import 'package:path/path.dart' as p; @@ -66,17 +66,8 @@ class _ExplorerViewState extends State { return; } final reader = await event.getClipboardReader(); - final items = reader.items; - for (final item in items) { - final uri = await item.readValue(Formats.uri); - print('Value read: ${uri?.uri}'); - if (4 < 5) return; - - if (!mounted) return; - final err = await processDataReader(context, folderId, item); - if (!mounted || err == null) return; - showAlertDialog(context, title: 'Error uploading file', message: err, barrierDismissible: true); - } + if (!mounted) return; + processPasteItems(context, folderId, reader.items); } Future handleShareIntent(List files) async { diff --git a/client/lib/ui/explorer/paste_action_handler.dart b/client/lib/ui/explorer/paste_action_handler.dart deleted file mode 100644 index 7d0501bf..00000000 --- a/client/lib/ui/explorer/paste_action_handler.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:phylum/ui/app/app_shortcuts.dart'; -import 'package:phylum/libphylum/db/db.dart'; -import 'package:phylum/libphylum/db/resource_helpers.dart'; -import 'package:phylum/libphylum/name_conflict.dart'; -import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/ui/explorer/explorer_controller.dart'; -import 'package:phylum/util/upload_utils.dart'; -import 'package:provider/provider.dart'; -import 'package:super_clipboard/super_clipboard.dart'; - -void handlePasteAction(PasteFromClipboardIntent i, BuildContext context) async { - final account = context.read(); - final folderId = context.read().folderId; - if (folderId == null) { - return; - } - final openUri = kIsWeb ? Uri.base.replace(path: 'open') : account.apiClient.createUri('/open'); - final clipboard = SystemClipboard.instance; - if (clipboard == null) { - return; - } - final reader = await clipboard.read(); - final paths = []; - final ids = []; - var copyFn = account.resourceRepository.copy; - - for (final item in reader.items) { - final filePath = - await item.readValue(Formats.fileUri).then((value) => value?.toFilePath(windows: Platform.isWindows)); - if (filePath != null) { - assert(ids.isEmpty); - paths.add(filePath); - } else { - assert(paths.isEmpty); - final uri = await item.readValue(Formats.uri).then((value) => value?.uri); - if (uri != null) { - if (uri.authority == openUri.authority && uri.path == openUri.path && uri.queryParameters.containsKey('id')) { - ids.add(uri.queryParameters['id']!); - if (uri.queryParameters.containsKey('cut')) { - copyFn = account.resourceRepository.move; - } - } - } - } - } - if (!context.mounted) return; - if (paths.isNotEmpty) { - uploadRecursive(context, folderId, paths); - } - final resources = - await account.db.getResources(ids).then((resources) => resources.whereType().toList(growable: false)); - if (!context.mounted) return; - for (final r in resources) { - handleNameConflict( - context, - r.name, - overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => copyFn( - resource: r, - name: name, - parent: folderId, - conflictResolution: conflictResolution, - ), - ); - } - - await SystemClipboard.instance?.write([]); - - return null; -} diff --git a/client/lib/ui/explorer/paste_helpers.dart b/client/lib/ui/explorer/paste_helpers.dart new file mode 100644 index 00000000..247c33b7 --- /dev/null +++ b/client/lib/ui/explorer/paste_helpers.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/db/resource_helpers.dart'; +import 'package:phylum/libphylum/name_conflict.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/util/upload_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class PasteInfo { + final Iterable _items; + Iterable get files => _items.map((info) => info.file).whereType(); + Iterable get paths => _items.map((info) => info.path).whereType(); + Iterable get resourceIds => _items.map((info) => info.resourceId).whereType(); + Iterable get errors => _items.map((info) => info.error).whereType(); + + PasteInfo(this._items); + + Future> getResources(PhylumAccount account) async => + account.db.getResources(resourceIds).then((resources) => resources.whereType().toList(growable: false)); +} + +class PasteItem { + final XFile? file; + final String? path; + final String? resourceId; + final String? error; + + PasteItem.withError({required this.error}) + : file = null, + path = null, + resourceId = null; + + PasteItem.withXFile({required this.file}) + : path = null, + resourceId = null, + error = null; + + PasteItem.withPath({required this.path}) + : file = null, + resourceId = null, + error = null; + + PasteItem.withResourceId({required this.resourceId}) + : file = null, + path = null, + error = null; +} + +Future extractPasteItem(PhylumAccount account, DataReader reader) async { + final uri = await reader.readValue(Formats.uri).then((value) => value?.uri); + if (uri != null && (uri.isScheme('http') || uri.isScheme('https'))) { + final openUri = kIsWeb ? Uri.base.replace(path: 'open') : account.apiClient.createUri('/open'); + if (uri.authority == openUri.authority && uri.path == openUri.path && uri.queryParameters.containsKey('id')) { + final id = uri.queryParameters['id']!; + return PasteItem.withResourceId(resourceId: id); + } + } + + final xfileCompleter = Completer(); + reader.getFile(null, (file) async { + try { + final data = await file.readAll(); + xfileCompleter.complete(XFile.fromData( + data, + path: file.fileName, + name: file.fileName, + length: data.length, + )); + } catch (e) { + xfileCompleter.complete(null); + } + }); + final xfile = await xfileCompleter.future; + if (xfile != null) return PasteItem.withXFile(file: xfile); + + if (!kIsWeb) { + final path = + await reader.readValue(Formats.fileUri).then((value) => value?.toFilePath(windows: Platform.isWindows)); + if (path != null) { + return PasteItem.withPath(path: path); + } + } + + return PasteItem.withError(error: await reader.getSuggestedName() ?? 'Unkonwn'); +} + +Future extractPasteInfo(PhylumAccount account, Iterable data) async { + final items = await Future.wait(data.map((item) => extractPasteItem(account, item))); + return PasteInfo(items); +} + +void processPasteItems(BuildContext context, String folderId, Iterable data) async { + final account = context.read(); + final info = await extractPasteInfo(account, data); + final resources = await info.getResources(account); + + if (!context.mounted) return; + uploadXFiles(context, folderId, info.files); + uploadPath(context, folderId, info.paths); + + var copyFn = account.resourceRepository.copy; + for (final r in resources) { + handleNameConflict( + context, + r.name, + overwriteFn: (r) => nameConflictDelete, + (name, conflictResolution) => copyFn( + resource: r, + name: name, + parent: folderId, + conflictResolution: conflictResolution, + ), + ); + } +} + +extension on DataReader { + Future readValue(ValueFormat format) { + final c = Completer(); + final value = getValue( + format, + (value) => c.complete(value), + onError: (err) => c.completeError(err), + ); + if (value == null) { + return SynchronousFuture(null); + } + return c.future; + } +} diff --git a/client/lib/ui/explorer/resource_drop_and_drop.dart b/client/lib/ui/explorer/resource_drop_and_drop.dart index 536499cb..f674efa3 100644 --- a/client/lib/ui/explorer/resource_drop_and_drop.dart +++ b/client/lib/ui/explorer/resource_drop_and_drop.dart @@ -1,21 +1,15 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/name_conflict.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/explorer/explorer_controller.dart'; +import 'package:phylum/ui/explorer/paste_helpers.dart'; import 'package:phylum/ui/explorer/resource_details_row.dart'; import 'package:phylum/ui/explorer/resource_icon_extension.dart'; import 'package:phylum/ui/explorer/selection_mode.dart'; -import 'package:phylum/util/dialogs.dart'; -import 'package:phylum/util/logging.dart'; import 'package:phylum/util/upload_utils.dart'; import 'package:provider/provider.dart'; -import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_drag_and_drop/super_drag_and_drop.dart'; @pragma('vm:platform-const') @@ -76,56 +70,6 @@ class _ResourceDragTargetState extends State { } } -Future processDataReader(BuildContext context, String folderId, DataReader reader) async { - final uploadError = Completer(); - reader.getFile(null, (file) async { - try { - final data = await file.readAll(); - if (context.mounted) { - await uploadDirect(context, folderId, [ - XFile.fromData( - data, - path: file.fileName, - name: file.fileName, - length: data.length, - ) - ]); - } - uploadError.complete(null); - } catch (e) { - logger.d('Error reading file data', error: e); - uploadError.complete(e.toString()); - } - }, onError: (err) { - logger.e('Error calling getFile', error: err); - }); - final err = await uploadError.future; - if (err == null) { - return null; - } - if (kIsWeb) { - return err; - } - - // Try reading paths from filesystem - final c = Completer(); - reader.getValue( - Formats.fileUri, - (value) { - final path = value?.toFilePath(windows: Platform.isWindows); - if (path == null) { - c.complete('Unable to get file path'); - } else { - if (context.mounted) { - uploadRecursive(context, folderId, [path]).then((value) => c.complete(null)); - } - } - }, - onError: (value) => c.complete(value.toString()), - ); - return c.future; -} - class ExternalDropRegion extends StatefulWidget { final DropWidgetBuilder buildItem; @@ -162,15 +106,7 @@ class _ExternalDropRegionState extends State { onPerformDrop: (event) async { final folderId = context.read().folderId; if (folderId == null) return; - for (final item in event.session.items) { - final reader = item.dataReader; - if (reader == null) continue; - - if (!context.mounted) return; - final err = await processDataReader(context, folderId, reader); - if (!context.mounted) return; - showAlertDialog(context, title: 'Error uploading file', message: err, barrierDismissible: true); - } + processPasteItems(context, folderId, event.session.items.map((item) => item.dataReader).whereType()); setState(() => dropTargetActive = false); }, ); diff --git a/client/lib/util/upload_utils.dart b/client/lib/util/upload_utils.dart index d6ad5d71..72be9a7f 100644 --- a/client/lib/util/upload_utils.dart +++ b/client/lib/util/upload_utils.dart @@ -48,10 +48,10 @@ Future createDirectory(BuildContext context, String folderId, {String? pre Future pickAndUploadFiles(BuildContext context, String folderId) async { final files = await openFiles(); if (!context.mounted) return; - return uploadDirect(context, folderId, files); + return uploadXFiles(context, folderId, files); } -Future uploadDirect(BuildContext context, String folderId, List files) async { +Future uploadXFiles(BuildContext context, String folderId, Iterable files) async { final account = context.read(); for (final file in files) { String? id; @@ -81,10 +81,10 @@ Future uploadDirect(BuildContext context, String folderId, List fil Future pickAndUploadDirectory(BuildContext context, String folderId) async { final path = await getDirectoryPath(); if (path == null || !context.mounted) return; - return uploadRecursive(context, folderId, [path]); + return uploadPath(context, folderId, [path]); } -Future uploadRecursive(BuildContext context, String folderId, Iterable paths) async { +Future uploadPath(BuildContext context, String folderId, Iterable paths) async { final account = context.read(); showProgressDialog(context, message: 'Counting Files'); DirTree? tree;