|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import * as vscode from 'vscode'; |
| 7 | +import * as nls from 'vscode-nls'; |
| 8 | +import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; |
| 9 | +import { Disposable } from './util/dispose'; |
| 10 | +import { escapeAttribute, getNonce } from './util/dom'; |
| 11 | + |
| 12 | +const localize = nls.loadMessageBundle(); |
| 13 | + |
| 14 | +class AudioPreviewProvider implements vscode.CustomReadonlyEditorProvider { |
| 15 | + |
| 16 | + public static readonly viewType = 'vscode.mediaPreview.audioView'; |
| 17 | + |
| 18 | + constructor( |
| 19 | + private readonly extensionRoot: vscode.Uri, |
| 20 | + private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, |
| 21 | + ) { } |
| 22 | + |
| 23 | + public async openCustomDocument(uri: vscode.Uri) { |
| 24 | + return { uri, dispose: () => { } }; |
| 25 | + } |
| 26 | + |
| 27 | + public async resolveCustomEditor(document: vscode.CustomDocument, webviewEditor: vscode.WebviewPanel): Promise<void> { |
| 28 | + new AudioPreview(this.extensionRoot, document.uri, webviewEditor, this.binarySizeStatusBarEntry); |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +const enum PreviewState { |
| 33 | + Disposed, |
| 34 | + Visible, |
| 35 | + Active, |
| 36 | +} |
| 37 | + |
| 38 | +class AudioPreview extends Disposable { |
| 39 | + |
| 40 | + private readonly id: string = `${Date.now()}-${Math.random().toString()}`; |
| 41 | + |
| 42 | + private _previewState = PreviewState.Visible; |
| 43 | + private _binarySize: number | undefined; |
| 44 | + |
| 45 | + private readonly emptyAudioDataUri = 'data:audio/wav;base64,'; |
| 46 | + |
| 47 | + constructor( |
| 48 | + private readonly extensionRoot: vscode.Uri, |
| 49 | + private readonly resource: vscode.Uri, |
| 50 | + private readonly webviewEditor: vscode.WebviewPanel, |
| 51 | + private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, |
| 52 | + ) { |
| 53 | + super(); |
| 54 | + |
| 55 | + const resourceRoot = resource.with({ |
| 56 | + path: resource.path.replace(/\/[^\/]+?\.\w+$/, '/'), |
| 57 | + }); |
| 58 | + |
| 59 | + webviewEditor.webview.options = { |
| 60 | + enableScripts: true, |
| 61 | + enableForms: false, |
| 62 | + localResourceRoots: [ |
| 63 | + resourceRoot, |
| 64 | + extensionRoot, |
| 65 | + ] |
| 66 | + }; |
| 67 | + |
| 68 | + this._register(webviewEditor.webview.onDidReceiveMessage(message => { |
| 69 | + switch (message.type) { |
| 70 | + case 'reopen-as-text': { |
| 71 | + vscode.commands.executeCommand('vscode.openWith', resource, 'default', webviewEditor.viewColumn); |
| 72 | + break; |
| 73 | + } |
| 74 | + } |
| 75 | + })); |
| 76 | + |
| 77 | + this._register(webviewEditor.onDidChangeViewState(() => { |
| 78 | + this.update(); |
| 79 | + })); |
| 80 | + |
| 81 | + this._register(webviewEditor.onDidDispose(() => { |
| 82 | + if (this._previewState === PreviewState.Active) { |
| 83 | + this.binarySizeStatusBarEntry.hide(this.id); |
| 84 | + } |
| 85 | + this._previewState = PreviewState.Disposed; |
| 86 | + })); |
| 87 | + |
| 88 | + const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); |
| 89 | + this._register(watcher.onDidChange(e => { |
| 90 | + if (e.toString() === this.resource.toString()) { |
| 91 | + this.render(); |
| 92 | + } |
| 93 | + })); |
| 94 | + this._register(watcher.onDidDelete(e => { |
| 95 | + if (e.toString() === this.resource.toString()) { |
| 96 | + this.webviewEditor.dispose(); |
| 97 | + } |
| 98 | + })); |
| 99 | + |
| 100 | + vscode.workspace.fs.stat(resource).then(({ size }) => { |
| 101 | + this._binarySize = size; |
| 102 | + this.update(); |
| 103 | + }); |
| 104 | + |
| 105 | + this.render(); |
| 106 | + this.update(); |
| 107 | + } |
| 108 | + |
| 109 | + private async render() { |
| 110 | + if (this._previewState === PreviewState.Disposed) { |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + const content = await this.getWebviewContents(); |
| 115 | + if (this._previewState as PreviewState === PreviewState.Disposed) { |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + this.webviewEditor.webview.html = content; |
| 120 | + } |
| 121 | + |
| 122 | + private update() { |
| 123 | + if (this._previewState === PreviewState.Disposed) { |
| 124 | + return; |
| 125 | + } |
| 126 | + |
| 127 | + if (this.webviewEditor.active) { |
| 128 | + this._previewState = PreviewState.Active; |
| 129 | + this.binarySizeStatusBarEntry.show(this.id, this._binarySize); |
| 130 | + } else { |
| 131 | + if (this._previewState === PreviewState.Active) { |
| 132 | + this.binarySizeStatusBarEntry.hide(this.id); |
| 133 | + } |
| 134 | + this._previewState = PreviewState.Visible; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + private async getWebviewContents(): Promise<string> { |
| 139 | + const version = Date.now().toString(); |
| 140 | + const settings = { |
| 141 | + src: await this.getResourcePath(this.webviewEditor, this.resource, version), |
| 142 | + }; |
| 143 | + |
| 144 | + const nonce = getNonce(); |
| 145 | + |
| 146 | + const cspSource = this.webviewEditor.webview.cspSource; |
| 147 | + return /* html */`<!DOCTYPE html> |
| 148 | +<html lang="en"> |
| 149 | +<head> |
| 150 | + <meta charset="UTF-8"> |
| 151 | +
|
| 152 | + <!-- Disable pinch zooming --> |
| 153 | + <meta name="viewport" |
| 154 | + content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> |
| 155 | +
|
| 156 | + <title>Audio Preview</title> |
| 157 | +
|
| 158 | + <link rel="stylesheet" href="${escapeAttribute(this.extensionResource('media', 'audioPreview.css'))}" type="text/css" media="screen" nonce="${nonce}"> |
| 159 | +
|
| 160 | + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: ${cspSource}; media-src ${cspSource}; script-src 'nonce-${nonce}'; style-src ${cspSource} 'nonce-${nonce}';"> |
| 161 | + <meta id="settings" data-settings="${escapeAttribute(JSON.stringify(settings))}"> |
| 162 | +</head> |
| 163 | +<body class="container loading"> |
| 164 | + <div class="loading-indicator"></div> |
| 165 | + <div class="loading-error"> |
| 166 | + <p>${localize('preview.audioLoadError', "An error occurred while loading the audio file.")}</p> |
| 167 | + <a href="#" class="open-file-link">${localize('preview.audioLoadErrorLink', "Open file using VS Code's standard text/binary editor?")}</a> |
| 168 | + </div> |
| 169 | + <script src="${escapeAttribute(this.extensionResource('media', 'audioPreview.js'))}" nonce="${nonce}"></script> |
| 170 | +</body> |
| 171 | +</html>`; |
| 172 | + } |
| 173 | + |
| 174 | + private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise<string> { |
| 175 | + if (resource.scheme === 'git') { |
| 176 | + const stat = await vscode.workspace.fs.stat(resource); |
| 177 | + if (stat.size === 0) { |
| 178 | + return this.emptyAudioDataUri; |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + // Avoid adding cache busting if there is already a query string |
| 183 | + if (resource.query) { |
| 184 | + return webviewEditor.webview.asWebviewUri(resource).toString(); |
| 185 | + } |
| 186 | + return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(); |
| 187 | + } |
| 188 | + |
| 189 | + private extensionResource(...parts: string[]) { |
| 190 | + return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +export function registerAudioPreviewSupport(context: vscode.ExtensionContext, binarySizeStatusBarEntry: BinarySizeStatusBarEntry): vscode.Disposable { |
| 195 | + const provider = new AudioPreviewProvider(context.extensionUri, binarySizeStatusBarEntry); |
| 196 | + return vscode.window.registerCustomEditorProvider(AudioPreviewProvider.viewType, provider, { |
| 197 | + supportsMultipleEditorsPerDocument: true, |
| 198 | + webviewOptions: { |
| 199 | + retainContextWhenHidden: true, |
| 200 | + } |
| 201 | + }); |
| 202 | +} |
0 commit comments