Skip to content

Commit

Permalink
Add ability to capture device screenshots (#505)
Browse files Browse the repository at this point in the history
* 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 <bronley@gmail.com>
  • Loading branch information
fumer-fubotv and TwitchBronBron committed Oct 12, 2023
1 parent 9494468 commit 2c9fadc
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 54 deletions.
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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)"
},
Expand Down
47 changes: 45 additions & 2 deletions src/BrightScriptCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -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: ''
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/DebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/commands/CaptureScreenshotCommand.ts
Original file line number Diff line number Diff line change
@@ -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();
132 changes: 84 additions & 48 deletions src/viewProviders/OnlineDevicesViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.TreeItem> {

public readonly id = ViewProviderId.onlineDevicesView;
Expand Down Expand Up @@ -110,65 +114,73 @@ export class OnlineDevicesViewProvider implements vscode.TreeDataProvider<vscode
const details = this.concealObject(element.details, ['udn', 'device-id', 'advertising-id', 'wifi-mac', 'ethernet-mac', 'serial-number', 'keyed-developer-id']);

for (let [key, values] of details) {

// Create a tree item for every detail property on the device
let treeItem = new DeviceInfoTreeItem(
key,
element,
vscode.TreeItemCollapsibleState.None,
key,
//if this is one of the properties that need concealed
values.value?.toString()
result.push(
this.createDeviceInfoTreeItem({
label: key,
parent: element,
collapsibleState: vscode.TreeItemCollapsibleState.None,
key: key,
//if this is one of the properties that need concealed
description: values.value?.toString(),
tooltip: 'Copy to clipboard',
// Prepare the copy to clipboard command
command: {
command: 'extension.brightscript.copyToClipboard',
title: 'Copy To Clipboard',
arguments: [values.originalValue]
}
})
);

// Prepare the copy to clipboard command
treeItem.tooltip = 'Copy to clipboard';
treeItem.command = {
command: 'extension.brightscript.copyToClipboard',
title: 'Copy To Clipboard',
arguments: [values.originalValue]
};
result.push(treeItem);
}

const device = this.findDeviceById(element.key);

if (device.deviceInfo['is-tv']) {
let changeTvInputItem = new DeviceInfoTreeItem(
'📺 Switch TV Input',
element,
vscode.TreeItemCollapsibleState.None,
'',
'click to change'
result.unshift(
this.createDeviceInfoTreeItem({
label: '📺 Switch TV Input',
parent: element,
collapsibleState: vscode.TreeItemCollapsibleState.None,
description: 'click to change',
tooltip: 'Change the current TV input',
command: {
command: 'extension.brightscript.changeTvInput',
title: 'Switch TV Input',
arguments: [device.ip]
}
})
);

// Prepare the open url command
changeTvInputItem.tooltip = 'Change the current TV input';
changeTvInputItem.command = {
command: 'extension.brightscript.changeTvInput',
title: 'Switch TV Input',
arguments: [device.ip]
};
result.unshift(changeTvInputItem);
}

let openWebpageItem = new DeviceInfoTreeItem(
'🔗 Open device web portal',
element,
vscode.TreeItemCollapsibleState.None,
'',
device.ip
result.unshift(
this.createDeviceInfoTreeItem({
label: '📷 Capture Screenshot',
parent: element,
collapsibleState: vscode.TreeItemCollapsibleState.None,
tooltip: 'Capture a screenshot',
command: {
command: 'extension.brightscript.captureScreenshot',
title: 'Capture Screenshot',
arguments: [device.ip]
}
})
);

// Prepare the open url command
openWebpageItem.tooltip = 'Open';
openWebpageItem.command = {
command: 'extension.brightscript.openUrl',
title: 'Open',
arguments: [`http://${device.ip}`]
};
result.unshift(
this.createDeviceInfoTreeItem({
label: '🔗 Open device web portal',
parent: element,
collapsibleState: vscode.TreeItemCollapsibleState.None,
tooltip: 'Open the web portal for this device',
description: device.ip,
command: {
command: 'extension.brightscript.openUrl',
title: 'Open',
arguments: [`http://${device.ip}`]
}
})
);

result.unshift(openWebpageItem);

if (semver.satisfies(element.details['software-version'], '>=11')) {
// TODO: add ECP system hooks here in the future (like registry call, etc...)
Expand All @@ -179,6 +191,30 @@ export class OnlineDevicesViewProvider implements vscode.TreeDataProvider<vscode
}
}

private createDeviceInfoTreeItem(options: {
label: string;
parent: DeviceTreeItem;
collapsibleState: vscode.TreeItemCollapsibleState;
key?: string;
description?: string;
details?: any;
command?: vscode.Command;
tooltip?: string;
}) {
const item = new DeviceInfoTreeItem(
options.label,
options.parent,
options.collapsibleState,
options.key ?? `tree-item-${treeItemKeySequence++}`,
options.description ?? '',
options.details ?? '',
options.command
);
// Prepare the open url command
item.tooltip = options.tooltip;
return item;
}

/**
* Called by VS Code to get a given element.
* Currently we don't modify this element so it is just returned back.
Expand Down

0 comments on commit 2c9fadc

Please sign in to comment.