提交 7be726fd 编写于 作者: R Rob Lourens

Support downloading blob URLs and data URLs when clicked in a notebook webview

Fix #98101
上级 0780b5ef
......@@ -18,12 +18,15 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewService, WebviewElement, IDataLinkClickEvent } from 'vs/workbench/contrib/webview/browser/webview';
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { dirname } from 'vs/base/common/resources';
import { dirname, joinPath } from 'vs/base/common/resources';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
export interface WebviewIntialized {
__vscode_notebook_message: boolean;
......@@ -160,6 +163,8 @@ export class BackLayerWebView extends Disposable {
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IFileService private readonly fileService: IFileService,
) {
super();
this.element = document.createElement('div');
......@@ -516,6 +521,10 @@ ${loaderJs}
}
}));
this._register(this.webview.onDidClickDataLink(event => {
this._onDidClickDataLink(event);
}));
this._register(this.webview.onDidReload(() => {
this.preloadsCache.clear();
for (const [output, inset] of this.insetMapping.entries()) {
......@@ -586,6 +595,33 @@ ${loaderJs}
}));
}
private async _onDidClickDataLink(event: IDataLinkClickEvent): Promise<void> {
const defaultDir = dirname(this.documentUri);
const defaultUri = joinPath(defaultDir, event.downloadName || 'download.png');
const newFileUri = await this.fileDialogService.showSaveDialog({
defaultUri
});
if (!newFileUri) {
return;
}
const splitData = event.dataURL.split(';base64,')[1];
if (!splitData) {
return;
}
const decoded = atob(splitData);
const typedArray = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) {
typedArray[i] = decoded.charCodeAt(i);
}
const buff = VSBuffer.wrap(typedArray);
await this.fileService.writeFile(newFileUri, buff);
await this.openerService.open(newFileUri);
}
async waitForInitialization() {
await this._initalized;
}
......
......@@ -6,12 +6,12 @@
import { addClass } from 'vs/base/browser/dom';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WebviewExtensionDescription, WebviewOptions, WebviewContentOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IDataLinkClickEvent, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
......@@ -26,6 +26,7 @@ export const enum WebviewMessageChannels {
doUpdateState = 'do-update-state',
doReload = 'do-reload',
loadResource = 'load-resource',
saveResource = 'save-resource',
loadLocalhost = 'load-localhost',
webviewReady = 'webview-ready',
wheel = 'did-scroll-wheel'
......@@ -136,6 +137,10 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this.handleKeyDown(data);
}));
this._register(this.on(WebviewMessageChannels.saveResource, (event: IDataLinkClickEvent) => {
this._onDidClickDataLink.fire(event);
}));
this.style();
this._register(webviewThemeDataProvider.onThemeDataChanged(this.style, this));
}
......@@ -155,6 +160,9 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
private readonly _onDidClickLink = this._register(new Emitter<string>());
public readonly onDidClickLink = this._onDidClickLink.event;
private readonly _onDidClickDataLink = this._register(new Emitter<IDataLinkClickEvent>());
public readonly onDidClickDataLink = this._onDidClickDataLink.event;
private readonly _onDidReload = this._register(new Emitter<void>());
public readonly onDidReload = this._onDidReload.event;
......
......@@ -240,6 +240,10 @@
if (scrollTarget) {
scrollTarget.scrollIntoView();
}
} else if (node.href.startsWith('blob:')) {
handleBlobUrlClick(node.href, node.download);
} else if (node.href.startsWith('data:')) {
handleDataUrlClick(node.href, node.download);
} else {
host.postMessage('did-click-link', node.href.baseVal || node.href);
}
......@@ -250,6 +254,31 @@
}
};
const handleDataUrlClick = async (url, downloadName) => {
host.postMessage('save-resource', {
data: url,
downloadName
});
};
const handleBlobUrlClick = async (url, downloadName) => {
try {
const response = await fetch(url);
const blob = await response.blob();
const reader = new FileReader();
reader.addEventListener('load', () => {
const data = reader.result;
host.postMessage('save-resource', {
data,
downloadName
});
});
reader.readAsDataURL(blob);
} catch (e) {
console.error(e.message);
}
};
/**
* @param {MouseEvent} event
*/
......
......@@ -73,6 +73,11 @@ export interface WebviewExtensionDescription {
readonly id: ExtensionIdentifier;
}
export interface IDataLinkClickEvent {
dataURL: string;
downloadName?: string;
}
export interface Webview extends IDisposable {
html: string;
contentOptions: WebviewContentOptions;
......@@ -84,6 +89,7 @@ export interface Webview extends IDisposable {
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidClickLink: Event<string>;
readonly onDidClickDataLink: Event<IDataLinkClickEvent>;
readonly onDidScroll: Event<{ scrollYPercentage: number }>;
readonly onDidWheel: Event<IMouseWheelEvent>;
readonly onDidUpdateState: Event<string | undefined>;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册