[client] Further streamline paste and drop

This commit is contained in:
Abhishek Shroff
2025-05-04 02:15:03 +05:30
parent 1bdb42ab92
commit 30ae383d99
6 changed files with 161 additions and 158 deletions

View File

@@ -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<PreviousFocusIntent>(onInvoke: (i) => null),
RefreshIntent: CallbackAction<RefreshIntent>(onInvoke: (i) => refreshKey.currentState?.show()),
PasteFromClipboardIntent: CallbackAction<PasteFromClipboardIntent>(
onInvoke: (PasteFromClipboardIntent i) => handlePasteAction(i, context),
onInvoke: (PasteFromClipboardIntent i) async {
final folderId = context.read<ExplorerState>().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<SelectAllIntent>(
onInvoke: (i) => context.read<ExplorerController>().updateSelection((i) => i, SelectionMode.all, true),

View File

@@ -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<ExplorerView> {
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<void> handleShareIntent(List<SharedMediaFile> files) async {

View File

@@ -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<PhylumAccount>();
final folderId = context.read<ExplorerState>().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 = <String>[];
final ids = <String>[];
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<Resource>().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;
}

View File

@@ -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<PasteItem> _items;
Iterable<XFile> get files => _items.map((info) => info.file).whereType<XFile>();
Iterable<String> get paths => _items.map((info) => info.path).whereType<String>();
Iterable<String> get resourceIds => _items.map((info) => info.resourceId).whereType<String>();
Iterable<String> get errors => _items.map((info) => info.error).whereType<String>();
PasteInfo(this._items);
Future<Iterable<Resource>> getResources(PhylumAccount account) async =>
account.db.getResources(resourceIds).then((resources) => resources.whereType<Resource>().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<PasteItem> 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<XFile?>();
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<PasteInfo> extractPasteInfo(PhylumAccount account, Iterable<DataReader> data) async {
final items = await Future.wait(data.map((item) => extractPasteItem(account, item)));
return PasteInfo(items);
}
void processPasteItems(BuildContext context, String folderId, Iterable<DataReader> data) async {
final account = context.read<PhylumAccount>();
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<T?> readValue<T extends Object>(ValueFormat<T> format) {
final c = Completer<T?>();
final value = getValue(
format,
(value) => c.complete(value),
onError: (err) => c.completeError(err),
);
if (value == null) {
return SynchronousFuture(null);
}
return c.future;
}
}

View File

@@ -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<ResourceDragTarget> {
}
}
Future<String?> processDataReader(BuildContext context, String folderId, DataReader reader) async {
final uploadError = Completer<String?>();
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<String?>();
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<ExternalDropRegion> {
onPerformDrop: (event) async {
final folderId = context.read<ExplorerState>().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);
},
);

View File

@@ -48,10 +48,10 @@ Future<void> createDirectory(BuildContext context, String folderId, {String? pre
Future<void> 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<void> uploadDirect(BuildContext context, String folderId, List<XFile> files) async {
Future<void> uploadXFiles(BuildContext context, String folderId, Iterable<XFile> files) async {
final account = context.read<PhylumAccount>();
for (final file in files) {
String? id;
@@ -81,10 +81,10 @@ Future<void> uploadDirect(BuildContext context, String folderId, List<XFile> fil
Future<void> 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<void> uploadRecursive(BuildContext context, String folderId, Iterable<String> paths) async {
Future<void> uploadPath(BuildContext context, String folderId, Iterable<String> paths) async {
final account = context.read<PhylumAccount>();
showProgressDialog(context, message: 'Counting Files');
DirTree? tree;