Web Inspector: Local Resource Overrides: automatically create an image/font local override when dragging content over a non-overridden resource
https://bugs.webkit.org/show_bug.cgi?id=202957

Reviewed by Joseph Pecoraro.

Since non-text resources aren't editable, some users of local resource overrides kept trying
to drag image/font files over the actual resource content view (not the local override),
which didn't do anything. Rather than having to click "Create Local Override" and then do a
drag/drop, we should support the "shorter" workflow of drag/drop over the actual resource.

* UserInterface/Base/FileUtilities.js:
(WI.FileUtilities.import): Added.
(WI.FileUtilities.importText):
(WI.FileUtilities.importJSON):
(WI.FileUtilities.importData): Added.
(WI.FileUtilities.async readText):
(WI.FileUtilities.async readJSON):
(WI.FileUtilities.async readData): Added.
(WI.FileUtilities.async _read): Added.
Create utility functions for importing non-text content as data.
Drive-by: fix a bug in the `import*` functions where the `callback` would be bound on the
          first call, meaning that since the `<input>` was cached for all calls, we'd only
          ever use the first `callback` in subsequent calls.

* UserInterface/Views/DropZoneView.js:
(WI.DropZoneView):
(WI.DropZoneView.prototype.set text): Added.
Support the text content of the drop zone changing after it's initialized.

* UserInterface/Views/FontResourceContentView.js:
(WI.FontResourceContentView):
(WI.FontResourceContentView.prototype.contentAvailable):
(WI.FontResourceContentView.prototype.dropZoneHandleDragEnter): Added.
(WI.FontResourceContentView.prototype.dropZoneHandleDrop):
(WI.FontResourceContentView.prototype._handleLocalResourceContentDidChange): Added.
* UserInterface/Views/ImageResourceContentView.js:
(WI.ImageResourceContentView):
(WI.ImageResourceContentView.prototype.contentAvailable):
(WI.ImageResourceContentView.prototype.dropZoneHandleDragEnter): Added.
(WI.ImageResourceContentView.prototype.dropZoneHandleDrop):
(WI.ImageResourceContentView.prototype._handleLocalResourceContentDidChange): Added.
Support drag/drop on non-override image/font content views, which will create/update the
local resource override for that resource.

* UserInterface/Views/ResourceContentView.js:
(WI.ResourceContentView):
(WI.ResourceContentView.prototype.get navigationItems):
(WI.ResourceContentView.prototype._handleImportLocalResourceOverride): Added.
When viewing a local resource override, add an "Import" navigation item for non-drag/drop
updating of the contents of the local resource override. This is also exposed for text-based
local resource overrides, since drag/drop inserts the contents of the file (if it's text),
which attempts to determine whether the dropped file is text or data based on the MIME type.

* UserInterface/Views/AuditNavigationSidebarPanel.js:
(WI.AuditNavigationSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
* UserInterface/Views/CanvasOverviewContentView.js:
(WI.CanvasOverviewContentView.prototype._handleImportButtonNavigationItemClicked):
* UserInterface/Views/CanvasSidebarPanel.js:
(WI.CanvasSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
* UserInterface/Views/HeapAllocationsTimelineView.js:
(WI.HeapAllocationsTimelineView.prototype._importButtonNavigationItemClicked):
* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView.prototype._importHAR):
* UserInterface/Views/TimelineRecordingContentView.js:
(WI.TimelineRecordingContentView.prototype._importButtonNavigationItemClicked):
Explicitly allow multiple files to be imported at the same time.

* UserInterface/Base/BlobUtilities.js:
(WI.BlobUtilities.blobForContent):
Drive-by: remove extra `base64Encoded` argument when calling `decodeBase64ToBlob`, which
          caused SVG-based image local resource overrides to show a broken image when
          closing and reopening Web Inspector.

* Localizations/en.lproj/localizedStrings.js:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251144 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog
index 0552085..9fa2e7a 100644
--- a/Source/WebInspectorUI/ChangeLog
+++ b/Source/WebInspectorUI/ChangeLog
@@ -1,3 +1,80 @@
+2019-10-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Local Resource Overrides: automatically create an image/font local override when dragging content over a non-overridden resource
+        https://bugs.webkit.org/show_bug.cgi?id=202957
+
+        Reviewed by Joseph Pecoraro.
+
+        Since non-text resources aren't editable, some users of local resource overrides kept trying
+        to drag image/font files over the actual resource content view (not the local override),
+        which didn't do anything. Rather than having to click "Create Local Override" and then do a
+        drag/drop, we should support the "shorter" workflow of drag/drop over the actual resource.
+
+        * UserInterface/Base/FileUtilities.js:
+        (WI.FileUtilities.import): Added.
+        (WI.FileUtilities.importText):
+        (WI.FileUtilities.importJSON):
+        (WI.FileUtilities.importData): Added.
+        (WI.FileUtilities.async readText):
+        (WI.FileUtilities.async readJSON):
+        (WI.FileUtilities.async readData): Added.
+        (WI.FileUtilities.async _read): Added.
+        Create utility functions for importing non-text content as data.
+        Drive-by: fix a bug in the `import*` functions where the `callback` would be bound on the
+                  first call, meaning that since the `<input>` was cached for all calls, we'd only
+                  ever use the first `callback` in subsequent calls.
+
+        * UserInterface/Views/DropZoneView.js:
+        (WI.DropZoneView):
+        (WI.DropZoneView.prototype.set text): Added.
+        Support the text content of the drop zone changing after it's initialized.
+
+        * UserInterface/Views/FontResourceContentView.js:
+        (WI.FontResourceContentView):
+        (WI.FontResourceContentView.prototype.contentAvailable):
+        (WI.FontResourceContentView.prototype.dropZoneHandleDragEnter): Added.
+        (WI.FontResourceContentView.prototype.dropZoneHandleDrop):
+        (WI.FontResourceContentView.prototype._handleLocalResourceContentDidChange): Added.
+        * UserInterface/Views/ImageResourceContentView.js:
+        (WI.ImageResourceContentView):
+        (WI.ImageResourceContentView.prototype.contentAvailable):
+        (WI.ImageResourceContentView.prototype.dropZoneHandleDragEnter): Added.
+        (WI.ImageResourceContentView.prototype.dropZoneHandleDrop):
+        (WI.ImageResourceContentView.prototype._handleLocalResourceContentDidChange): Added.
+        Support drag/drop on non-override image/font content views, which will create/update the
+        local resource override for that resource.
+
+        * UserInterface/Views/ResourceContentView.js:
+        (WI.ResourceContentView):
+        (WI.ResourceContentView.prototype.get navigationItems):
+        (WI.ResourceContentView.prototype._handleImportLocalResourceOverride): Added.
+        When viewing a local resource override, add an "Import" navigation item for non-drag/drop
+        updating of the contents of the local resource override. This is also exposed for text-based
+        local resource overrides, since drag/drop inserts the contents of the file (if it's text),
+        which attempts to determine whether the dropped file is text or data based on the MIME type.
+
+        * UserInterface/Views/AuditNavigationSidebarPanel.js:
+        (WI.AuditNavigationSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
+        * UserInterface/Views/CanvasOverviewContentView.js:
+        (WI.CanvasOverviewContentView.prototype._handleImportButtonNavigationItemClicked):
+        * UserInterface/Views/CanvasSidebarPanel.js:
+        (WI.CanvasSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
+        * UserInterface/Views/HeapAllocationsTimelineView.js:
+        (WI.HeapAllocationsTimelineView.prototype._importButtonNavigationItemClicked):
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView.prototype._importHAR):
+        * UserInterface/Views/TimelineRecordingContentView.js:
+        (WI.TimelineRecordingContentView.prototype._importButtonNavigationItemClicked):
+        Explicitly allow multiple files to be imported at the same time.
+
+        * UserInterface/Base/BlobUtilities.js:
+        (WI.BlobUtilities.blobForContent):
+        Drive-by: remove extra `base64Encoded` argument when calling `decodeBase64ToBlob`, which
+                  caused SVG-based image local resource overrides to show a broken image when
+                  closing and reopening Web Inspector.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
 2019-10-14  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: REGRESSION(r250991): Sources: local resource overrides should be enabled when not in tests
diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
index 78a889b..f19f154 100644
--- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
+++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
@@ -382,8 +382,6 @@
 localizedStrings["Done"] = "Done";
 localizedStrings["Download"] = "Download";
 localizedStrings["Download Web Archive"] = "Download Web Archive";
-localizedStrings["Drop Font"] = "Drop Font";
-localizedStrings["Drop Image"] = "Drop Image";
 localizedStrings["Dropped Element"] = "Dropped Element";
 localizedStrings["Dropped Node"] = "Dropped Node";
 localizedStrings["Duplicate Selector"] = "Duplicate Selector";
@@ -1217,6 +1215,9 @@
 localizedStrings["Unsupported property name"] = "Unsupported property name";
 localizedStrings["Unsupported property value"] = "Unsupported property value";
 localizedStrings["Untitled"] = "Untitled";
+localizedStrings["Update Font"] = "Update Font";
+localizedStrings["Update Image"] = "Update Image";
+localizedStrings["Update Local Override"] = "Update Local Override";
 localizedStrings["Usage: %s"] = "Usage: %s";
 localizedStrings["Use Default Appearance"] = "Use Default Appearance";
 localizedStrings["Use Mock Capture Devices"] = "Use Mock Capture Devices";
diff --git a/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js b/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js
index 08c60b0..da6f055 100644
--- a/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js
@@ -27,7 +27,7 @@
     static blobForContent(content, base64Encoded, mimeType)
     {
         if (base64Encoded)
-            return BlobUtilities.decodeBase64ToBlob(content, base64Encoded, mimeType);
+            return BlobUtilities.decodeBase64ToBlob(content, mimeType);
         return BlobUtilities.textToBlob(content, mimeType);
     }
 
diff --git a/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js b/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js
index ff7d8d0..68a0275 100644
--- a/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js
@@ -91,38 +91,105 @@
         fileReader.readAsDataURL(saveData.content);
     }
 
-    static importText(callback)
+    static import(callback, {multiple} = {})
     {
-        if (!FileUtilities._importTextInputElement) {
-            let inputElement = FileUtilities._importTextInputElement = document.createElement("input");
-            inputElement.type = "file";
-            inputElement.multiple = true;
-            inputElement.addEventListener("change", (event) => {
-                WI.FileUtilities.readText(inputElement.files, callback);
-            });
-        }
+        let inputElement = document.createElement("input");
+        inputElement.type = "file";
+        inputElement.value = null;
+        inputElement.multiple = !!multiple;
+        inputElement.addEventListener("change", (event) => {
+            callback(inputElement.files);
+        });
 
-        FileUtilities._importTextInputElement.value = null;
-        FileUtilities._importTextInputElement.click();
+        inputElement.click();
+
+        // Cache the last used import element so that it doesn't get GCd while the native file
+        // picker is shown, which would prevent the "change" event listener from firing.
+        FileUtilities.importInputElement = inputElement;
     }
 
-    static importJSON(callback)
+    static importText(callback, options = {})
     {
-        if (!FileUtilities._importJSONInputElement) {
-            let inputElement = FileUtilities._importJSONInputElement = document.createElement("input");
-            inputElement.type = "file";
-            inputElement.multiple = true;
-            inputElement.addEventListener("change", (event) => {
-                WI.FileUtilities.readJSON(inputElement.files, callback);
-            });
-        }
+        FileUtilities.import((files) => {
+            FileUtilities.readText(files, callback);
+        }, options);
+    }
 
-        FileUtilities._importJSONInputElement.value = null;
-        FileUtilities._importJSONInputElement.click();
+    static importJSON(callback, options = {})
+    {
+        FileUtilities.import((files) => {
+            FileUtilities.readJSON(files, callback);
+        }, options);
+    }
+
+    static importData(callback, options = {})
+    {
+        FileUtilities.import((files) => {
+            FileUtilities.readData(files, callback);
+        }, options);
     }
 
     static async readText(fileOrList, callback)
     {
+        await FileUtilities._read(fileOrList, async (file, result) => {
+            await new Promise((resolve, reject) => {
+                let reader = new FileReader;
+                reader.addEventListener("loadend", (event) => {
+                    result.text = reader.result;
+                    resolve(event);
+                });
+                reader.addEventListener("error", reject);
+                reader.readAsText(file);
+            });
+        }, callback);
+    }
+
+    static async readJSON(fileOrList, callback)
+    {
+        await WI.FileUtilities.readText(fileOrList, async (result) => {
+            if (result.text && !result.error) {
+                try {
+                    result.json = JSON.parse(result.text);
+                } catch (e) {
+                    result.error = e;
+                }
+            }
+
+            await callback(result);
+        });
+    }
+
+    static async readData(fileOrList, callback)
+    {
+        await FileUtilities._read(fileOrList, async (file, result) => {
+            await new Promise((resolve, reject) => {
+                let reader = new FileReader;
+                reader.addEventListener("loadend", (event) => {
+                    let {mimeType, base64, data} = parseDataURL(reader.result);
+
+                    // In case no mime type was determined, try to derive one from the file extension.
+                    if (!mimeType || mimeType === "text/plain") {
+                        let extension = WI.fileExtensionForFilename(result.filename);
+                        if (extension)
+                            mimeType = WI.mimeTypeForFileExtension(extension);
+                    }
+
+                    result.mimeType = mimeType;
+                    result.base64Encoded = base64;
+                    result.content = data;
+
+                    resolve(event);
+                });
+                reader.addEventListener("error", reject);
+                reader.readAsDataURL(file);
+            });
+        }, callback);
+    }
+
+    // Private
+
+    static async _read(fileOrList, operation, callback)
+    {
         console.assert(fileOrList instanceof File || fileOrList instanceof FileList);
 
         let files = [];
@@ -137,37 +204,12 @@
             };
 
             try {
-                await new Promise((resolve, reject) => {
-                    let reader = new FileReader;
-                    reader.addEventListener("loadend", (event) => {
-                        result.text = reader.result;
-                        resolve(event);
-                    });
-                    reader.addEventListener("error", reject);
-                    reader.readAsText(file);
-                });
+                await operation(file, result);
             } catch (e) {
                 result.error = e;
             }
 
-            let promise = callback(result);
-            if (promise instanceof Promise)
-                await promise;
+            await callback(result);
         }
     }
-
-    static async readJSON(fileOrList, callback)
-    {
-        return WI.FileUtilities.readText(fileOrList, (result) => {
-            if (result.text && !result.error) {
-                try {
-                    result.json = JSON.parse(result.text);
-                } catch (e) {
-                    result.error = e;
-                }
-            }
-
-            return callback(result);
-        });
-    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
index b5c196d..65c9336 100644
--- a/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
+++ b/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
@@ -315,7 +315,7 @@
 
     _handleImportButtonNavigationItemClicked(event)
     {
-        WI.FileUtilities.importJSON((result) => WI.auditManager.processJSON(result));
+        WI.FileUtilities.importJSON((result) => WI.auditManager.processJSON(result), {multiple: true});
     }
 
     _handleEditButtonNavigationItemClicked(event)
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js b/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
index 964cc5f..959aa94 100644
--- a/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
@@ -336,7 +336,7 @@
 
     _handleImportButtonNavigationItemClicked(event)
     {
-        WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result));
+        WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result), {multiple: true});
     }
 
     _handleRecordingSaved(event)
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
index 3159f03..c2bbdc0 100644
--- a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
+++ b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
@@ -322,7 +322,7 @@
 
     _handleImportButtonNavigationItemClicked(event)
     {
-        WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result));
+        WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result), {multiple: true});
     }
 
     _treeSelectionDidChange(event)
diff --git a/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js
index ea35d7d..a772dd7 100644
--- a/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js
@@ -30,7 +30,7 @@
 
 WI.DropZoneView = class DropZoneView extends WI.View
 {
-    constructor(delegate, {text} = {})
+    constructor(delegate)
     {
         console.assert(delegate);
         console.assert(typeof delegate.dropZoneShouldAppearForDragEvent === "function");
@@ -41,9 +41,6 @@
         this._targetElement = null;
         this._activelyHandlingDrag = false;
 
-        if (text)
-            this.element.textContent = text;
-
         this.element.classList.add("drop-zone");
     }
 
@@ -75,6 +72,11 @@
             this._targetElement.addEventListener("dragenter", this._boundHandleDragEnter);
     }
 
+    set text(text)
+    {
+        this.element.textContent = text;
+    }
+
     // Protected
 
     initialLayout()
diff --git a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
index 112d98f..8993e5e 100644
--- a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
@@ -32,9 +32,6 @@
         this._styleElement = null;
         this._previewElement = null;
         this._previewContainer = null;
-
-        if (this.showingLocalResourceOverride)
-            this._dropZoneView = new WI.DropZoneView(this, {text: WI.UIString("Drop Font")});
     }
 
     // Public
@@ -68,9 +65,13 @@
 
         this._updatePreviewElement();
 
-        if (this._dropZoneView) {
-            this._dropZoneView.targetElement = this._previewContainer;
-            this.addSubview(this._dropZoneView);
+        if (WI.NetworkManager.supportsLocalResourceOverrides()) {
+            let dropZoneView = new WI.DropZoneView(this);
+            dropZoneView.targetElement = this._previewContainer;
+            this.addSubview(dropZoneView);
+
+            if (this.showingLocalResourceOverride)
+                this.resource.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleLocalResourceContentDidChange, this);
         }
     }
 
@@ -118,38 +119,39 @@
         return event.dataTransfer.types.includes("Files");
     }
 
+    dropZoneHandleDragEnter(dropZone, event)
+    {
+        if (this.showingLocalResourceOverride)
+            dropZone.text = WI.UIString("Update Font");
+        else if (WI.networkManager.localResourceOverrideForURL(this.resource.url))
+            dropZone.text = WI.UIString("Update Local Override");
+        else
+            dropZone.text = WI.UIString("Create Local Override");
+    }
+
     dropZoneHandleDrop(dropZone, event)
     {
         let files = event.dataTransfer.files;
-        let file = files.length === 1 ? files[0] : null;
-        if (!file) {
+        if (files.length !== 1) {
             InspectorFrontendHost.beep();
             return;
         }
 
-        let fileReader = new FileReader;
-        fileReader.addEventListener("loadend", (event) => {
+        WI.FileUtilities.readData(files, async ({dataURL, mimeType, base64Encoded, content}) => {
             let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
-            if (!localResourceOverride)
-                return;
-
-            let dataURL = fileReader.result;
-            let {base64, data, mimeType} = parseDataURL(dataURL);
-
-            // In case no mime type was determined, try to derive one from the file extension.
-            if (!mimeType || mimeType === "text/plain") {
-                let extension = WI.fileExtensionForFilename(file.name);
-                if (extension)
-                    mimeType = WI.mimeTypeForFileExtension(extension);
+            if (!localResourceOverride && !this.showingLocalResourceOverride) {
+                localResourceOverride = await this.resource.createLocalResourceOverride();
+                WI.networkManager.addLocalResourceOverride(localResourceOverride);
             }
 
-            let revision = localResourceOverride.localResource.currentRevision;
-            revision.updateRevisionContent(data, {base64Encoded: base64, mimeType});
+            console.assert(localResourceOverride);
 
-            this._fontObjectURL = this.resource.createObjectURL();
-            this._updatePreviewElement();
+            let revision = localResourceOverride.localResource.currentRevision;
+            revision.updateRevisionContent(content, {base64Encoded, mimeType});
+
+            if (!this.showingLocalResourceOverride)
+                WI.showLocalResourceOverride(localResourceOverride);
         });
-        fileReader.readAsDataURL(file);
     }
 
     // Private
@@ -208,6 +210,12 @@
 
         this.sizeToFit();
     }
+
+    _handleLocalResourceContentDidChange(event)
+    {
+        this._fontObjectURL = this.resource.createObjectURL();
+        this._updatePreviewElement();
+    }
 };
 
 WI.FontResourceContentView._uniqueFontIdentifier = 0;
diff --git a/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js b/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js
index d1f32dc..ec5145a 100644
--- a/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js
@@ -403,7 +403,7 @@
                 WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
                 this.dispatchEventToListeners(WI.TimelineView.Event.NeedsEntireSelectedRange);
             });
-        });
+        }, {multiple: true});
     }
 
     _takeHeapSnapshotClicked()
diff --git a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
index 58f499b..a302b17 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
@@ -40,9 +40,6 @@
         this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
-
-        if (this.showingLocalResourceOverride)
-            this._dropZoneView = new WI.DropZoneView(this, {text: WI.UIString("Drop Image")});
     }
 
     // Public
@@ -89,9 +86,13 @@
             this._draggingInternalImageElement = false;
         });
 
-        if (this._dropZoneView) {
-            this._dropZoneView.targetElement = imageContainer;
-            this.addSubview(this._dropZoneView);
+        if (WI.NetworkManager.supportsLocalResourceOverrides()) {
+            let dropZoneView = new WI.DropZoneView(this);
+            dropZoneView.targetElement = imageContainer;
+            this.addSubview(dropZoneView);
+
+            if (this.showingLocalResourceOverride)
+                this.resource.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleLocalResourceContentDidChange, this);
         }
     }
 
@@ -132,37 +133,39 @@
         return event.dataTransfer.types.includes("Files");
     }
 
+    dropZoneHandleDragEnter(dropZone, event)
+    {
+        if (this.showingLocalResourceOverride)
+            dropZone.text = WI.UIString("Update Image");
+        else if (WI.networkManager.localResourceOverrideForURL(this.resource.url))
+            dropZone.text = WI.UIString("Update Local Override");
+        else
+            dropZone.text = WI.UIString("Create Local Override");
+    }
+
     dropZoneHandleDrop(dropZone, event)
     {
         let files = event.dataTransfer.files;
-        let file = files.length === 1 ? files[0] : null;
-        if (!file) {
+        if (files.length !== 1) {
             InspectorFrontendHost.beep();
             return;
         }
 
-        let fileReader = new FileReader;
-        fileReader.addEventListener("loadend", (event) => {
+        WI.FileUtilities.readData(files, async ({dataURL, mimeType, base64Encoded, content}) => {
             let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
-            if (!localResourceOverride)
-                return;
-
-            let dataURL = fileReader.result;
-            this._imageElement.src = dataURL;
-
-            let {base64, data, mimeType} = parseDataURL(dataURL);
-
-            // In case no mime type was determined, try to derive one from the file extension.
-            if (!mimeType || mimeType === "text/plain") {
-                let extension = WI.fileExtensionForFilename(file.name);
-                if (extension)
-                    mimeType = WI.mimeTypeForFileExtension(extension);
+            if (!localResourceOverride && !this.showingLocalResourceOverride) {
+                localResourceOverride = await this.resource.createLocalResourceOverride();
+                WI.networkManager.addLocalResourceOverride(localResourceOverride);
             }
 
+            console.assert(localResourceOverride);
+
             let revision = localResourceOverride.localResource.currentRevision;
-            revision.updateRevisionContent(data, {base64Encoded: base64, mimeType});
+            revision.updateRevisionContent(content, {base64Encoded, mimeType});
+
+            if (!this.showingLocalResourceOverride)
+                WI.showLocalResourceOverride(localResourceOverride);
         });
-        fileReader.readAsDataURL(file);
     }
 
     // Private
@@ -183,4 +186,9 @@
 
         this._updateImageGrid();
     }
+
+    _handleLocalResourceContentDidChange(event)
+    {
+        this._imageElement.src = this.resource.createObjectURL();
+    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js b/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
index 2796713..b8d7a29 100644
--- a/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
@@ -2150,7 +2150,7 @@
 
     _importHAR()
     {
-        WI.FileUtilities.importJSON((result) => this.processHAR(result));
+        WI.FileUtilities.importJSON((result) => this.processHAR(result), {multiple: true});
     }
 
     _waterfallPopoverContent()
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
index 82b361b..2e7b2ce 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
@@ -66,9 +66,13 @@
 
                 this._localResourceOverrideBannerView = new WI.LocalResourceOverrideLabelView(resource);
 
+                this._importLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("import-local-resource-override", WI.UIString("Import"), "Images/Import.svg", 15, 15);
+                this._importLocalResourceOverrideButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+                this._importLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+                this._importLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportLocalResourceOverride, this);
+
                 this._removeLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("remove-local-resource-override", WI.UIString("Remove Local Override"), "Images/NavigationItemTrash.svg", 15, 15);
                 this._removeLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleRemoveLocalResourceOverride, this);
-                this._removeLocalResourceOverrideButtonNavigationItem.enabled = true;
                 this._removeLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
             } else {
                 this._localResourceOverrideBannerView = new WI.LocalResourceOverrideWarningView(resource);
@@ -93,6 +97,8 @@
     {
         let items = [];
 
+        if (this._importLocalResourceOverrideButtonNavigationItem)
+            items.push(this._importLocalResourceOverrideButtonNavigationItem, new WI.DividerNavigationItem);
         if (this._removeLocalResourceOverrideButtonNavigationItem)
             items.push(this._removeLocalResourceOverrideButtonNavigationItem);
         if (this._createLocalResourceOverrideButtonNavigationItem)
@@ -232,6 +238,35 @@
         WI.showLocalResourceOverride(localResourceOverride);
     }
 
+    _handleImportLocalResourceOverride(event)
+    {
+        console.assert(this._showingLocalResourceOverride);
+
+        WI.FileUtilities.import(async (fileList) => {
+            console.assert(fileList.length === 1);
+
+            let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
+            console.assert(localResourceOverride);
+
+            let revision = localResourceOverride.localResource.currentRevision;
+
+            let file = fileList[0];
+            let mimeType = file.type || WI.mimeTypeForFileExtension(WI.fileExtensionForFilename(file.name));
+            if (WI.shouldTreatMIMETypeAsText(mimeType)) {
+                await WI.FileUtilities.readText(file, ({text}) => {
+                    revision.updateRevisionContent(text, {base64Encoded: false, mimeType});
+                });
+            } else {
+                await WI.FileUtilities.readData(file, ({dataURL, mimeType, base64Encoded, content}) => {
+                    revision.updateRevisionContent(content, {base64Encoded, mimeType});
+                });
+            }
+
+            if (!this.showingLocalResourceOverride)
+                WI.showLocalResourceOverride(localResourceOverride);
+        });
+    }
+
     _handleRemoveLocalResourceOverride(event)
     {
         console.assert(this._showingLocalResourceOverride);
diff --git a/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
index 7c76711..1cecfff 100644
--- a/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
@@ -621,7 +621,7 @@
 
     _importButtonNavigationItemClicked(event)
     {
-        WI.FileUtilities.importJSON((result) => WI.timelineManager.processJSON(result));
+        WI.FileUtilities.importJSON((result) => WI.timelineManager.processJSON(result), {multiple: true});
     }
 
     _clearTimeline(event)