From 2c9fadc90205f8bbf53761102847bb67824e7af0 Mon Sep 17 00:00:00 2001 From: fumer-fubotv <89787347+fumer-fubotv@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:51:14 -0700 Subject: [PATCH] Add ability to capture device screenshots (#505) * Add ability to capture device screenshots * Add ability to capture device screenshots 1. Added support to show preview of screenshot 2. Fixed a linter issue * Simplify tree item creation. Change order of screenshot entry. * Return values for host/password/workspace getters * Store remotePassword after each debug launch * Better password handling. Use default temp screenshot dir * Tweak the device view command titles --------- Co-authored-by: Bronley Plumb --- package.json | 14 +- src/BrightScriptCommands.ts | 47 ++++++- src/DebugConfigurationProvider.ts | 2 + src/commands/CaptureScreenshotCommand.ts | 55 ++++++++ .../OnlineDevicesViewProvider.ts | 132 +++++++++++------- 5 files changed, 196 insertions(+), 54 deletions(-) create mode 100644 src/commands/CaptureScreenshotCommand.ts diff --git a/package.json b/package.json index e063a474..d422a926 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,8 @@ "onCommand:extension.brightscript.changeTvInput", "onCommand:extension.brightscript.sendRemoteText", "onCommand:brighterscript.showPreview", - "onCommand:brighterscript.showPreviewToSide" + "onCommand:brighterscript.showPreviewToSide", + "onCommand:extension.brightscript.captureScreenshot" ], "contributes": { "viewsContainers": { @@ -2770,21 +2771,26 @@ "category": "BrighterScript", "icon": "./images/icons/inspect-active.svg" }, + { + "command": "extension.brightscript.captureScreenshot", + "title": "Capture Screenshot", + "category": "BrighterScript" + }, { "command": "extension.brightscript.rokuDeviceView.pauseScreenshotCapture", - "title": "Pause Screenshot Capture", + "title": "Device View: Pause Screenshot Capture", "category": "BrighterScript", "icon": "$(debug-pause)" }, { "command": "extension.brightscript.rokuDeviceView.resumeScreenshotCapture", - "title": "Resume Screenshot Capture", + "title": "Device View: Resume Screenshot Capture", "category": "BrighterScript", "icon": "$(debug-start)" }, { "command": "extension.brightscript.rokuDeviceView.refreshScreenshot", - "title": "Refresh Screenshot", + "title": "Device View: Refresh Screenshot", "category": "BrighterScript", "icon": "$(refresh)" }, diff --git a/src/BrightScriptCommands.ts b/src/BrightScriptCommands.ts index b80a8374..035b9922 100644 --- a/src/BrightScriptCommands.ts +++ b/src/BrightScriptCommands.ts @@ -3,6 +3,7 @@ import * as vscode from 'vscode'; import BrightScriptFileUtils from './BrightScriptFileUtils'; import { GlobalStateManager } from './GlobalStateManager'; import { brighterScriptPreviewCommand } from './commands/BrighterScriptPreviewCommand'; +import { captureScreenshotCommand } from './commands/CaptureScreenshotCommand'; import { languageServerInfoCommand } from './commands/LanguageServerInfoCommand'; import { util } from './util'; import { util as rokuDebugUtil } from 'roku-debug/dist/util'; @@ -22,13 +23,16 @@ export class BrightScriptCommands { } private fileUtils: BrightScriptFileUtils; - private host: string; + public host: string; + public password: string; + public workspacePath: string; private keypressNotifiers = [] as ((key: string, literalCharacter: boolean) => void)[]; public registerCommands() { brighterScriptPreviewCommand.register(this.context); languageServerInfoCommand.register(this.context); + captureScreenshotCommand.register(this.context, this); this.registerGeneralCommands(); @@ -343,7 +347,7 @@ export class BrightScriptCommands { let config = vscode.workspace.getConfiguration('brightscript.remoteControl', null); this.host = config.get('host'); // eslint-disable-next-line no-template-curly-in-string - if (this.host === '${promptForHost}') { + if (!this.host || this.host === '${promptForHost}') { this.host = await vscode.window.showInputBox({ placeHolder: 'The IP address of your Roku device', value: '' @@ -363,6 +367,45 @@ export class BrightScriptCommands { console.error('Error doing dns lookup for host ', this.host, e); } } + return this.host; + } + + public async getRemotePassword() { + this.password = await this.context.workspaceState.get('remotePassword'); + if (!this.password) { + let config = vscode.workspace.getConfiguration('brightscript.remoteControl', null); + this.password = config.get('password'); + // eslint-disable-next-line no-template-curly-in-string + if (!this.password || this.password === '${promptForPassword}') { + this.password = await vscode.window.showInputBox({ + placeHolder: 'The developer account password for your Roku device', + value: '' + }); + } + } + if (!this.password) { + throw new Error(`Can't send command: password is required.`); + } else { + await this.context.workspaceState.update('remotePassword', this.password); + } + return this.password; + } + + public async getWorkspacePath() { + this.workspacePath = await this.context.workspaceState.get('workspacePath'); + //let folderUri: vscode.Uri; + if (!this.workspacePath) { + if (vscode.workspace.workspaceFolders.length === 1) { + this.workspacePath = vscode.workspace.workspaceFolders[0].uri.fsPath; + } else { + //there are multiple workspaces, ask the user to specify which one they want to use + let workspaceFolder = await vscode.window.showWorkspaceFolderPick(); + if (workspaceFolder) { + this.workspacePath = workspaceFolder.uri.fsPath; + } + } + } + return this.workspacePath; } public registerKeypressNotifier(notifier: (key: string, literalCharacter: boolean) => void) { diff --git a/src/DebugConfigurationProvider.ts b/src/DebugConfigurationProvider.ts index 8c3d4d9a..c4d681a4 100644 --- a/src/DebugConfigurationProvider.ts +++ b/src/DebugConfigurationProvider.ts @@ -477,6 +477,8 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio config.password = await this.openInputBox('The developer account password for your Roku device.'); if (!config.password) { throw new Error('Debug session terminated: password is required.'); + } else { + await this.context.workspaceState.update('remotePassword', config.password); } } diff --git a/src/commands/CaptureScreenshotCommand.ts b/src/commands/CaptureScreenshotCommand.ts new file mode 100644 index 00000000..58d00a6f --- /dev/null +++ b/src/commands/CaptureScreenshotCommand.ts @@ -0,0 +1,55 @@ +import * as vscode from 'vscode'; +import * as fsExtra from 'fs-extra'; +import * as rokuDeploy from 'roku-deploy'; +import type { BrightScriptCommands } from '../BrightScriptCommands'; + +export const FILE_SCHEME = 'bs-captureScreenshot'; + +export class CaptureScreenshotCommand { + + public register(context: vscode.ExtensionContext, BrightScriptCommandsInstance: BrightScriptCommands) { + context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.captureScreenshot', async (hostParam?: string) => { + let host: string; + let password: string; + + //if a hostParam was not provided, then go the normal flow for getting info + if (!hostParam) { + host = await BrightScriptCommandsInstance.getRemoteHost(); + password = await BrightScriptCommandsInstance.getRemotePassword(); + + //the host was provided, probably by clicking the "capture screenshot" link in the tree view. Do we have a password stored as well? If not, prompt for one + } else { + host = hostParam; + let remoteHost = await context.workspaceState.get('remoteHost'); + if (host === remoteHost) { + password = context.workspaceState.get('remotePassword'); + } else { + password = await vscode.window.showInputBox({ + placeHolder: `Please enter the developer password for host '${host}'`, + value: '' + }); + } + } + + await vscode.window.withProgress({ + title: `Capturing screenshot from '${host}'`, + location: vscode.ProgressLocation.Notification + }, async () => { + try { + let screenshotPath = await rokuDeploy.takeScreenshot({ + host: host, + password: password + }); + if (screenshotPath) { + void vscode.window.showInformationMessage(`Screenshot saved at: ` + screenshotPath); + void vscode.commands.executeCommand('vscode.open', vscode.Uri.file(screenshotPath)); + } + } catch (e) { + void vscode.window.showErrorMessage('Could not capture screenshot'); + } + }); + })); + } +} + +export const captureScreenshotCommand = new CaptureScreenshotCommand(); diff --git a/src/viewProviders/OnlineDevicesViewProvider.ts b/src/viewProviders/OnlineDevicesViewProvider.ts index 5e1e22b8..ba15c511 100644 --- a/src/viewProviders/OnlineDevicesViewProvider.ts +++ b/src/viewProviders/OnlineDevicesViewProvider.ts @@ -3,10 +3,14 @@ import * as semver from 'semver'; import type { ActiveDeviceManager, RokuDeviceDetails } from '../ActiveDeviceManager'; import { icons } from '../icons'; import { firstBy } from 'thenby'; -import { Cache } from 'brighterscript/dist/Cache'; import { util } from '../util'; import { ViewProviderId } from './ViewProviderId'; +/** + * A sequence used to generate unique IDs for tree items that don't care about having a key + */ +let treeItemKeySequence = 0; + export class OnlineDevicesViewProvider implements vscode.TreeDataProvider { public readonly id = ViewProviderId.onlineDevicesView; @@ -110,65 +114,73 @@ export class OnlineDevicesViewProvider implements vscode.TreeDataProvider=11')) { // TODO: add ECP system hooks here in the future (like registry call, etc...) @@ -179,6 +191,30 @@ export class OnlineDevicesViewProvider implements vscode.TreeDataProvider