From 29e5924fa2da5e0d840482157839adba8b310e21 Mon Sep 17 00:00:00 2001 From: James Orson Date: Thu, 10 Sep 2026 13:09:55 -0700 Subject: [PATCH] fix: accept web drops whose items have no FileSystemEntry The web ondrop handler called webkitGetAsEntry()! on every item in the drag. That API returns null for string items, and a drag routinely carries them alongside its files -- a drag from the browser's downloads menu sends text/uri-list and text/plain next to the file. The null assert threw synchronously inside List.generate, before the catchError was attached, so performOperation_web was never invoked: no DropDoneEvent reached the app, the file never uploaded, and the drop target's highlight stayed stuck until a rebuild. Null-guard the entry and fall back to getAsFile(), which returns the file for exactly the items the entry API cannot describe. Skip items that yield neither. Also notify with the resulting list even when it is empty. The event is what resets DropTarget's status, so swallowing it on a file-less drag (a dragged link, selected text) leaves the highlight stuck -- the same symptom, one step further along. Refs MixinNetwork/flutter-plugins#456, MixinNetwork/flutter-plugins#459 Signed-off-by: James Orson --- .../desktop_drop/lib/desktop_drop_web.dart | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/desktop_drop/lib/desktop_drop_web.dart b/packages/desktop_drop/lib/desktop_drop_web.dart index e628572e..77d219ea 100644 --- a/packages/desktop_drop/lib/desktop_drop_web.dart +++ b/packages/desktop_drop/lib/desktop_drop_web.dart @@ -83,12 +83,21 @@ class DesktopDropWeb { final web.File file = await fileCompleter.future; + return _fileToWebDropItem(file, relativePath: entry.fullPath); + } + + /// Builds a [WebDropItem] straight from a [web.File]. + /// + /// Used for drag sources that expose a file but no FileSystemEntry, such as + /// the browser's own downloads menu. Those items have no path relative to a + /// dropped folder, so [relativePath] is null. + WebDropItem _fileToWebDropItem(web.File file, {String? relativePath}) { return WebDropItem( uri: web.URL.createObjectURL(file), name: file.name, size: file.size, lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified), - relativePath: entry.fullPath, + relativePath: relativePath, type: file.type, children: [], ); @@ -100,14 +109,29 @@ class DesktopDropWeb { final items = event.dataTransfer!.items; - Future.wait(List.generate(items.length, (index) { + Future.wait(List>.generate(items.length, (index) { final item = items[index]; - final entry = item.webkitGetAsEntry()!; - return _entryToWebDropItem(entry); + // A drag can carry string items alongside its files -- the browser's + // downloads menu sends text/uri-list and text/plain next to the file. + // webkitGetAsEntry() returns null for those, and for file items that + // have no FileSystemEntry backing them, so fall back to getAsFile() + // rather than asserting. Anything that yields neither is skipped. + final entry = item.webkitGetAsEntry(); + if (entry != null) { + return _entryToWebDropItem(entry); + } + final file = item.getAsFile(); + if (file != null) { + return Future.value(_fileToWebDropItem(file)); + } + return Future.value(null); })).then((webItems) { + // Always notify, even with nothing usable: the event is what resets + // the drop target's state, so swallowing it leaves the drag highlight + // stuck until the page is rebuilt. channel.invokeMethod( "performOperation_web", - webItems.map((e) => e.toJson()).toList(), + webItems.whereType().map((e) => e.toJson()).toList(), ); }).catchError((e, s) { debugPrint('desktop_drop_web: $e $s');