mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-01 17:19:34 -06:00
[client] Further streamline paste and drop
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
136
client/lib/ui/explorer/paste_helpers.dart
Normal file
136
client/lib/ui/explorer/paste_helpers.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user