Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[desktop_drop] Drag and drop folder on web now returns the folder's content #288

Merged
merged 2 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 34 additions & 2 deletions packages/desktop_drop/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,39 @@
# This file should be version controlled and should not be manually edited.

version:
revision: 01e10e793e2459e9e563d5f928e40452aecd80cc
channel: master
revision: "2524052335ec76bb03e04ede244b071f1b86d190"
channel: "stable"

project_type: plugin

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190
- platform: android
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190
- platform: linux
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190
- platform: macos
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190
- platform: web
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190
- platform: windows
create_revision: 2524052335ec76bb03e04ede244b071f1b86d190
base_revision: 2524052335ec76bb03e04ede244b071f1b86d190

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
21 changes: 15 additions & 6 deletions packages/desktop_drop/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ class _ExampleDragTargetState extends State<ExampleDragTarget> {

Offset? offset;

Future<void> printFiles(List<DropItem> files, [int depth = 0]) async {
debugPrint(' |' * depth);
for (final file in files) {
debugPrint(' |' * depth +
'> ${file.path} ${file.name}'
' ${await file.lastModified()}'
' ${await file.length()}'
' ${file.mimeType}');
if (file is DropItemDirectory) {
printFiles(file.children, depth + 1);
}
}
}

@override
Widget build(BuildContext context) {
return DropTarget(
Expand All @@ -57,12 +71,7 @@ class _ExampleDragTargetState extends State<ExampleDragTarget> {
});

debugPrint('onDragDone:');
for (final file in detail.files) {
debugPrint(' ${file.path} ${file.name}'
' ${await file.lastModified()}'
' ${await file.length()}'
' ${file.mimeType}');
}
await printFiles(detail.files);
},
onDragUpdated: (details) {
setState(() {
Expand Down
83 changes: 19 additions & 64 deletions packages/desktop_drop/example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<!-- <base href="$FLUTTER_BASE_HREF">-->
<base href="$FLUTTER_BASE_HREF">

<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
Expand All @@ -31,74 +31,29 @@

<title>desktop_drop_example</title>
<link rel="manifest" href="manifest.json">

<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}

if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});

// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
}
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
});
</script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/desktop_drop/lib/desktop_drop.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'src/drop_target.dart';
export 'src/drop_item.dart';
94 changes: 67 additions & 27 deletions packages/desktop_drop/lib/desktop_drop_web.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'dart:async';
import 'dart:html' as html show window, Url;
import 'dart:html' as html;
import 'dart:js_util' as js_util;

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'src/drop_item.dart';
import 'src/web_drop_item.dart';

/// A web implementation of the DesktopDrop plugin.
class DesktopDropWeb {
Expand All @@ -25,38 +26,77 @@ class DesktopDropWeb {
pluginInstance._registerEvents();
}

Future<WebDropItem> _entryToWebDropItem(dynamic entry) async {
if (entry.isDirectory == true) {
final reader = js_util.callMethod(entry, 'createReader', []);
final entriesCompleter = Completer<List>();
final metadataCompleter = Completer();
entry.getMetadata((value) {
metadataCompleter.complete(value);
}, (error) {
metadataCompleter.completeError(error);
});
final metaData = await metadataCompleter.future;
reader.readEntries((values) {
entriesCompleter.complete(List.from(values));
}, (error) {
entriesCompleter.completeError(error);
});
final entries = await entriesCompleter.future;
final modificationTime = js_util.dartify(metaData.modificationTime);
final children = await Future.wait(
entries.map((e) => _entryToWebDropItem(e)),
)
..removeWhere((element) => element.name == '.DS_Store' && element.type == '');
return WebDropItem(
uri: html.Url.createObjectUrlFromBlob(html.Blob([], 'directory')),
name: entry.name ?? '',
size: metaData.size ?? 0,
lastModified: modificationTime != null && modificationTime is DateTime
? modificationTime
: DateTime.now(),
relativePath: entry.fullPath,
type: 'directory',
children: children,
);
}
final fileCompleter = Completer<html.File>();
entry.file((file) {
fileCompleter.complete(file);
}, (error) {
fileCompleter.completeError(error);
});
final file = await fileCompleter.future;
return WebDropItem(
uri: html.Url.createObjectUrl(file),
children: [],
name: file.name,
size: file.size,
type: file.type,
relativePath: file.relativePath,
lastModified: file.lastModified != null
? DateTime.fromMillisecondsSinceEpoch(file.lastModified!)
: file.lastModifiedDate,
);
}

void _registerEvents() {
html.window.onDrop.listen((event) {
event.preventDefault();

final results = <WebDropItem>[];

try {
final items = event.dataTransfer.files;
if (items != null) {
for (final item in items) {
results.add(
WebDropItem(
uri: html.Url.createObjectUrl(item),
name: item.name,
size: item.size,
type: item.type,
relativePath: item.relativePath,
lastModified: item.lastModified != null
? DateTime.fromMillisecondsSinceEpoch(item.lastModified!)
: item.lastModifiedDate,
),
);
}
}
} catch (e, s) {
debugPrint('desktop_drop_web: $e $s');
} finally {
final items = event.dataTransfer.items;
Future.wait(List.generate(items?.length ?? 0, (index) {
final item = items![index];
final entry = item.getAsEntry();
return _entryToWebDropItem(entry);
})).then((webItems) {
channel.invokeMethod(
"performOperation_web",
results.map((e) => e.toJson()).toList(),
webItems.map((e) => e.toJson()).toList(),
);
}
}).catchError((e, s) {
debugPrint('desktop_drop_web: $e $s');
});
});

html.window.onDragEnter.listen((event) {
Expand Down
16 changes: 5 additions & 11 deletions packages/desktop_drop/lib/src/channel.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'dart:convert';

import 'package:cross_file/cross_file.dart';
import 'package:desktop_drop/src/drop_item.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'drop_item.dart';
import 'events.dart';
import 'utils/platform.dart' if (dart.library.html) 'utils/platform_web.dart';
import 'web_drop_item.dart';

typedef RawDropListener = void Function(DropEvent);

Expand Down Expand Up @@ -64,7 +64,7 @@ class DesktopDrop {
_notifyEvent(
DropDoneEvent(
location: _offset ?? Offset.zero,
files: paths.map((e) => XFile(e)).toList(),
files: paths.map((e) => DropItemFile(e)).toList(),
),
);
_offset = null;
Expand All @@ -83,20 +83,14 @@ class DesktopDrop {
}).where((e) => e.isNotEmpty);
_notifyEvent(DropDoneEvent(
location: Offset(offset[0], offset[1]),
files: paths.map((e) => XFile(e)).toList(),
files: paths.map((e) => DropItemFile(e)).toList(),
));
break;
case "performOperation_web":
final results = (call.arguments as List)
.cast<Map>()
.map((e) => WebDropItem.fromJson(e.cast<String, dynamic>()))
.map((e) => XFile(
e.uri,
name: e.name,
length: e.size,
lastModified: e.lastModified,
mimeType: e.type,
))
.map((e) => e.toDropItem())
.toList();
_notifyEvent(
DropDoneEvent(location: _offset ?? Offset.zero, files: results),
Expand Down