feat(desktop-app): add viewport screenshot option

This commit is contained in:
Johnny Zabala 2020-07-28 16:41:46 -04:00
parent 8e70d87497
commit 797c951d3c
6 changed files with 273 additions and 143 deletions

View file

@ -5,7 +5,7 @@ import Grid from '@material-ui/core/Grid';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import ScrollDownIcon from '../icons/ScrollDown'; import ScrollDownIcon from '../icons/ScrollDown';
import ScrollUpIcon from '../icons/ScrollUp'; import ScrollUpIcon from '../icons/ScrollUp';
import ScreenshotIcon from '../icons/Screenshot'; import ScreenshotIcon from '../icons/FullScreenshot';
import DeviceRotateIcon from '../icons/DeviceRotate'; import DeviceRotateIcon from '../icons/DeviceRotate';
import InspectElementIcon from '../icons/InspectElement'; import InspectElementIcon from '../icons/InspectElement';
import MutedIcon from '../icons/Muted'; import MutedIcon from '../icons/Muted';
@ -68,9 +68,19 @@ const ScrollControls = ({
</Tooltip> </Tooltip>
</Grid> </Grid>
<Grid item className={cx(commonStyles.icons, commonStyles.enabled)}> <Grid item className={cx(commonStyles.icons, commonStyles.enabled)}>
<Tooltip title={browser.allDevicesMuted? "Unmute all devices": "Mute all devices"}> <Tooltip
title={
browser.allDevicesMuted
? 'Unmute all devices'
: 'Mute all devices'
}
>
<div onClick={onAllDevicesMutedChange}> <div onClick={onAllDevicesMutedChange}>
{ browser.allDevicesMuted ? (<MutedIcon {...iconProps} />):(<UnmutedIcon {...iconProps} />) } {browser.allDevicesMuted ? (
<MutedIcon {...iconProps} />
) : (
<UnmutedIcon {...iconProps} />
)}
</div> </div>
</Tooltip> </Tooltip>
</Grid> </Grid>

View file

@ -10,6 +10,7 @@ import console from 'electron-timber';
import BugIcon from '../icons/Bug'; import BugIcon from '../icons/Bug';
import MutedIcon from '../icons/Muted'; import MutedIcon from '../icons/Muted';
import UnmutedIcon from '../icons/Unmuted'; import UnmutedIcon from '../icons/Unmuted';
import FullScreenshotIcon from '../icons/FullScreenshot';
import ScreenshotIcon from '../icons/Screenshot'; import ScreenshotIcon from '../icons/Screenshot';
import DeviceRotateIcon from '../icons/DeviceRotate'; import DeviceRotateIcon from '../icons/DeviceRotate';
import {iconsColor} from '../../constants/colors'; import {iconsColor} from '../../constants/colors';
@ -34,7 +35,7 @@ import {CAPABILITIES} from '../../constants/devices';
import styles from './style.module.css'; import styles from './style.module.css';
import commonStyles from '../common.styles.css'; import commonStyles from '../common.styles.css';
import UnplugIcon from '../icons/Unplug'; import UnplugIcon from '../icons/Unplug';
import {captureFullPage} from './screenshotUtil'; import {captureScreenshot} from './screenshotUtil';
import { import {
DEVTOOLS_MODES, DEVTOOLS_MODES,
INDIVIDUAL_LAYOUT, INDIVIDUAL_LAYOUT,
@ -352,15 +353,22 @@ class WebView extends Component {
this.webviewRef.current.send('scrollUpMessage'); this.webviewRef.current.send('scrollUpMessage');
}; };
processScreenshotEvent = async ({now}) => { processScreenshotEvent = async ({
now,
fullScreen = true,
}: {
now?: Date,
fullScreen?: boolean,
}) => {
this.setState({screenshotInProgress: true}); this.setState({screenshotInProgress: true});
await captureFullPage( await captureScreenshot({
this.props.browser.address, address: this.props.browser.address,
this.props.device, device: this.props.device,
this.webviewRef.current, webView: this.webviewRef.current,
now != null, createSeparateDir: now != null,
now fullScreen,
); now,
});
this.setState({screenshotInProgress: false}); this.setState({screenshotInProgress: false});
}; };
@ -779,11 +787,23 @@ class WebView extends Component {
commonStyles.icons, commonStyles.icons,
commonStyles.enabled commonStyles.enabled
)} )}
onClick={() => this.processScreenshotEvent({})} onClick={() => this.processScreenshotEvent({fullScreen: false})}
> >
<ScreenshotIcon height={18} color={iconsColor} /> <ScreenshotIcon height={18} color={iconsColor} />
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title="Take Full Size Screenshot">
<div
className={cx(
styles.webViewToolbarIcons,
commonStyles.icons,
commonStyles.enabled
)}
onClick={this.processScreenshotEvent}
>
<FullScreenshotIcon height={18} color={iconsColor} />
</div>
</Tooltip>
<Tooltip title="Tilt Device"> <Tooltip title="Tilt Device">
<div <div
className={cx(styles.webViewToolbarIcons, commonStyles.icons, { className={cx(styles.webViewToolbarIcons, commonStyles.icons, {

View file

@ -11,16 +11,25 @@ import fs from 'fs-extra';
import PromiseWorker from 'promise-worker'; import PromiseWorker from 'promise-worker';
import NotificationMessage from '../NotificationMessage'; import NotificationMessage from '../NotificationMessage';
import {userPreferenceSettings} from '../../settings/userPreferenceSettings'; import {userPreferenceSettings} from '../../settings/userPreferenceSettings';
import {type Device} from '../../constants/devices';
const mergeImg = Promise.promisifyAll(_mergeImg); const mergeImg = Promise.promisifyAll(_mergeImg);
export const captureFullPage = async ( const captureScreenshot = async ({
address, address,
device, device,
webView, webView,
createSeparateDir, createSeparateDir,
now now,
) => { fullScreen = false,
}: {
address: string,
device: Device,
webView: WebviewElement,
createSeparateDir: boolean,
now?: Date,
fullScreen: boolean,
}) => {
const worker = new Worker('./imageWorker.js'); const worker = new Worker('./imageWorker.js');
const promiseWorker = new PromiseWorker(worker); const promiseWorker = new PromiseWorker(worker);
const toastId = toast.info( const toastId = toast.info(
@ -30,96 +39,14 @@ export const captureFullPage = async (
/>, />,
{autoClose: false} {autoClose: false}
); );
// Hiding scrollbars in the screenshot const webViewUtils = new WebViewUtils(webView);
await webView.insertCSS(` const insertedCSSKey = await webViewUtils.hideScrollbarAndFixedPositionedElements();
.responsivelyApp__ScreenshotInProgress::-webkit-scrollbar {
display: none;
}
.responsivelyApp__HiddenForScreenshot { const images = fullScreen
display: none !important; ? await webViewUtils.getFullScreenImages(promiseWorker)
} : [await webViewUtils.getViewportImage(promiseWorker)];
`);
// Get the windows's scroll details await webViewUtils.unHideScrollbarAndFixedPositionedElements(insertedCSSKey);
let scrollX = 0;
let scrollY = 0;
const pageX = 0;
const pageY = 0;
const {
previousScrollPosition,
scrollHeight,
viewPortHeight,
scrollWidth,
viewPortWidth,
} = await webView.executeJavaScript(`
document.body.classList.add('responsivelyApp__ScreenshotInProgress');
responsivelyApp.screenshotVar = {
previousScrollPosition : {
left: window.scrollX,
top: window.scrollY,
},
scrollHeight: document.body.scrollHeight,
scrollWidth: document.body.scrollWidth,
viewPortHeight: document.documentElement.clientHeight,
viewPortWidth: document.documentElement.clientWidth,
};
responsivelyApp.screenshotVar;
`);
const images = [];
for (
let pageY = 0;
scrollY < scrollHeight;
pageY++, scrollY = viewPortHeight * pageY
) {
scrollX = 0;
const columnImages = [];
for (
let pageX = 0;
scrollX < scrollWidth;
pageX++, scrollX = viewPortWidth * pageX
) {
await webView.executeJavaScript(`
window.scrollTo(${scrollX}, ${scrollY})
responsivelyApp.hideFixedPositionElementsForScreenshot();
`);
await _delay(200);
const options = {
x: 0,
y: 0,
width: viewPortWidth,
height: viewPortHeight,
};
if (scrollX + viewPortWidth > scrollWidth) {
options.width = scrollWidth - scrollX;
options.x = viewPortWidth - options.width;
}
if (scrollY + viewPortHeight > scrollHeight) {
options.height = scrollHeight - scrollY;
options.y = viewPortHeight - options.height;
}
const image = await _takeSnapshot(webView, options);
columnImages.push(image);
}
const pngs = columnImages.map(img => img.toPNG());
images.push(
await promiseWorker.postMessage(
{
images: pngs,
direction: 'horizontal',
},
[...pngs]
)
);
}
webView.executeJavaScript(`
window.scrollTo(${JSON.stringify(previousScrollPosition)});
document.body.classList.remove('responsivelyApp__ScreenshotInProgress');
responsivelyApp.unHideElementsHiddenForScreenshot();
`);
toast.update(toastId, { toast.update(toastId, {
render: ( render: (
@ -130,17 +57,20 @@ export const captureFullPage = async (
), ),
type: toast.TYPE.INFO, type: toast.TYPE.INFO,
}); });
const resultFilename = _getScreenshotFileName( const resultFilename = _getScreenshotFileName(
address, address,
device, device,
now, now,
createSeparateDir createSeparateDir
); );
const mergedImage = await promiseWorker.postMessage({ const mergedImage = await promiseWorker.postMessage({
images, images,
direction: 'vertical', direction: 'vertical',
resultFilename, resultFilename,
}); });
toast.update(toastId, { toast.update(toastId, {
render: ( render: (
<NotificationMessage tick message={`${device.name} screenshot taken!`} /> <NotificationMessage tick message={`${device.name} screenshot taken!`} />
@ -148,18 +78,157 @@ export const captureFullPage = async (
type: toast.TYPE.INFO, type: toast.TYPE.INFO,
autoClose: 2000, autoClose: 2000,
}); });
await _delay(250); await _delay(250);
shell.showItemInFolder(path.join(resultFilename.dir, resultFilename.file)); shell.showItemInFolder(path.join(resultFilename.dir, resultFilename.file));
}; };
class WebViewUtils {
webView: WebviewElement;
constructor(webView) {
this.webView = webView;
}
getWindowSizeAndScrollDetails(): Promise {
return this.webView.executeJavaScript(`
responsivelyApp.screenshotVar = {
previousScrollPosition : {
left: window.scrollX,
top: window.scrollY,
},
scrollHeight: document.body.scrollHeight,
scrollWidth: document.body.scrollWidth,
viewPortHeight: document.documentElement.clientHeight,
viewPortWidth: document.documentElement.clientWidth,
};
responsivelyApp.screenshotVar;
`);
}
async scrollTo(scrollX: number, scrollY: number): Promise {
await this.webView.executeJavaScript(`
window.scrollTo(${scrollX}, ${scrollY})
`);
// wait a little for the scroll to take effect.
await _delay(200);
}
async hideScrollbarAndFixedPositionedElements(): Promise<string> {
const key = await this.webView.insertCSS(`
.responsivelyApp__ScreenshotInProgress::-webkit-scrollbar {
display: none;
}
.responsivelyApp__HiddenForScreenshot {
display: none !important;
}
`);
await this.webView.executeJavaScript(`
document.body.classList.add('responsivelyApp__ScreenshotInProgress');
responsivelyApp.hideFixedPositionElementsForScreenshot();
`);
// wait a little for the 'hide' effect to take place.
await _delay(200);
return key;
}
async unHideScrollbarAndFixedPositionedElements(insertedCSSKey): Promise {
await this.webView.removeInsertedCSS(insertedCSSKey);
return this.webView.executeJavaScript(`
document.body.classList.remove('responsivelyApp__ScreenshotInProgress');
responsivelyApp.unHideElementsHiddenForScreenshot();
`);
}
async getFullScreenImages(promiseWorker: PromiseWorker): Promise {
const {
previousScrollPosition,
scrollHeight,
viewPortHeight,
scrollWidth,
viewPortWidth,
} = await this.getWindowSizeAndScrollDetails();
const images = [];
let scrollX = 0;
let scrollY = 0;
for (
let pageY = 0;
scrollY < scrollHeight;
pageY++, scrollY = viewPortHeight * pageY
) {
scrollX = 0;
const columnImages = [];
for (
let pageX = 0;
scrollX < scrollWidth;
pageX++, scrollX = viewPortWidth * pageX
) {
await this.scrollTo(scrollX, scrollY);
const options = {
x: 0,
y: 0,
width: viewPortWidth,
height: viewPortHeight,
};
if (scrollX + viewPortWidth > scrollWidth) {
options.width = scrollWidth - scrollX;
options.x = viewPortWidth - options.width;
}
if (scrollY + viewPortHeight > scrollHeight) {
options.height = scrollHeight - scrollY;
options.y = viewPortHeight - options.height;
}
const image = await this.takeSnapshot(options);
columnImages.push(image);
}
const pngs = columnImages.map(img => img.toPNG());
images.push(
await promiseWorker.postMessage(
{
images: pngs,
direction: 'horizontal',
},
[...pngs]
)
);
}
this.scrollTo(previousScrollPosition.left, previousScrollPosition.top);
return images;
}
async getViewportImage(promiseWorker: PromiseWorker): Promise {
const image = await this.takeSnapshot();
const png = image.toPNG();
return promiseWorker.postMessage(
{
images: [png],
direction: 'horizontal',
},
[png]
);
}
takeSnapshot(options): Promise {
return remote.webContents
.fromId(this.webView.getWebContentsId())
.capturePage(options);
}
}
const _delay = ms => const _delay = ms =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms); setTimeout(() => resolve(), ms);
}); });
const _takeSnapshot = (webView, options) =>
remote.webContents.fromId(webView.getWebContentsId()).capturePage(options);
function _getScreenshotFileName( function _getScreenshotFileName(
address, address,
device, device,
@ -189,7 +258,7 @@ function _getScreenshotFileName(
}; };
} }
export const getWebsiteName = address => { const getWebsiteName = (address: string) => {
let domain = ''; let domain = '';
if (address.startsWith('file://')) { if (address.startsWith('file://')) {
const fileNameStartingIndex = address.lastIndexOf('/') + 1; const fileNameStartingIndex = address.lastIndexOf('/') + 1;
@ -208,3 +277,5 @@ export const getWebsiteName = address => {
} }
return domain.charAt(0).toUpperCase() + domain.slice(1); return domain.charAt(0).toUpperCase() + domain.slice(1);
}; };
export {getWebsiteName, captureScreenshot};

View file

@ -0,0 +1,41 @@
import React, {Fragment} from 'react';
export default ({width, height, color, padding, margin}) => (
<Fragment>
{/* <svg
width={width}
height={height}
fill={color}
style={{padding, margin}}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path d="M20,10.5a1,1,0,0,0-1,1v7a1,1,0,0,1-1,1H4a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1H6a1,1,0,0,0,1-.68l.54-1.64a1,1,0,0,1,.95-.68H14a1,1,0,0,0,0-2H8.44A3,3,0,0,0,5.6,6.55l-.32,1H4a3,3,0,0,0-3,3v8a3,3,0,0,0,3,3H18a3,3,0,0,0,3-3v-7A1,1,0,0,0,20,10.5Zm-9-1a4,4,0,1,0,4,4A4,4,0,0,0,11,9.5Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,11,15.5Zm11-11H21v-1a1,1,0,0,0-2,0v1H18a1,1,0,0,0,0,2h1v1a1,1,0,0,0,2,0v-1h1a1,1,0,0,0,0-2Z" />
</svg> */}
<svg
width={width}
height={height}
fill={color}
style={{padding, margin}}
xmlns="http://www.w3.org/2000/svg"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
className="screenshotIcon"
>
<g transform="translate(0,-952.36218)">
<path
d="m 83,958.36218 c -1.1046,0 -2,0.89543 -2,2 l 0,7 -7,0 c -1.10457,0 -2,0.8954 -2,2 0,1.1046 0.89543,2 2,2 l 7,0 0,7 c 0,1.10457 0.8954,2 2,2 1.1046,0 2,-0.89543 2,-2 l 0,-7 7,0 c 1.10457,0 2,-0.8954 2,-2 0,-1.1046 -0.89543,-2 -2,-2 l -7,0 0,-7 c 0,-1.10457 -0.8954,-2 -2,-2 z m -48,22 c -0.78068,0.007 -1.3909,0.40265 -1.71875,0.96875 l -6.4375,11.03125 -12.84375,0 c -4.3973999,0 -8,3.6026 -8,8.00002 l 0,38 c 0,4.3974 3.6026001,8 8,8 l 62,0 c 4.3974,0 8,-3.6026 8,-8 l 0,-38 c 0,-4.39742 -3.6026,-8.00002 -8,-8.00002 l -12.84375,0 -6.4375,-11.03125 C 56.3641,980.74168 55.68774,980.36046 55,980.36218 l -20,0 z m 1.125,4 17.75,0 6.40625,11 c 0.34687,0.60075 1.02507,0.99534 1.71875,1 l 14,0 c 2.25056,0 4,1.74944 4,4.00002 l 0,38 c 0,2.2505 -1.74944,4 -4,4 l -62,0 c -2.25056,0 -4,-1.7495 -4,-4 l 0,-38 c 0,-2.25058 1.74944,-4.00002 4,-4.00002 l 14,0 c 0.69368,-0.005 1.37188,-0.39925 1.71875,-1 l 6.40625,-11 z M 45,1002.3622 c -8.81287,0 -16,7.1871 -16,16 0,8.8128 7.18713,16 16,16 8.81286,0 16,-7.1872 16,-16 0,-8.8129 -7.18714,-16 -16,-16 z m 0,4 c 6.6511,0 12,5.3489 12,12 0,6.6511 -5.3489,12 -12,12 -6.65111,0 -12,-5.3489 -12,-12 0,-6.6511 5.34889,-12 12,-12 z"
fill={color}
fillOpacity="1"
stroke="none"
marker="none"
visibility="visible"
display="inline"
overflow="visible"
/>
</g>
</svg>
</Fragment>
);

View file

@ -1,41 +1,23 @@
import React, {Fragment} from 'react'; import React, {Fragment} from 'react';
export default ({width, height, color, padding, margin}) => ( export default ({width, height, color, padding, margin}) => (
<Fragment> <svg
{/* <svg width={width}
width={width} height={height}
height={height} fill={color}
viewBox="0 0 78 91"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color} fill={color}
style={{padding, margin}} d="M29 25C28.2193 25.007 27.6091 25.4026 27.2812 25.9687L20.8438 37H8C3.6026 37 0 40.6026 0 45V83C0 87.3974 3.6026 91 8 91H70C74.3974 91 78 87.3974 78 83V45C78 40.6026 74.3974 37 70 37H57.1562L50.7188 25.9687C50.3641 25.3795 49.6877 24.9983 49 25H29ZM30.125 29H47.875L54.2812 40C54.6281 40.6007 55.3063 40.9953 56 41H70C72.2506 41 74 42.7494 74 45V83C74 85.2505 72.2506 87 70 87H8C5.74944 87 4 85.2505 4 83V45C4 42.7494 5.74944 41 8 41H22C22.6937 40.995 23.3719 40.6007 23.7188 40L30.125 29ZM39 47C30.1871 47 23 54.1871 23 63C23 71.8128 30.1871 79 39 79C47.8129 79 55 71.8128 55 63C55 54.1871 47.8129 47 39 47ZM39 51C45.6511 51 51 56.3489 51 63C51 69.6511 45.6511 75 39 75C32.3489 75 27 69.6511 27 63C27 56.3489 32.3489 51 39 51Z"
xmlns="http://www.w3.org/2000/svg" />
viewBox="0 0 24 24" <path
>
<path d="M20,10.5a1,1,0,0,0-1,1v7a1,1,0,0,1-1,1H4a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1H6a1,1,0,0,0,1-.68l.54-1.64a1,1,0,0,1,.95-.68H14a1,1,0,0,0,0-2H8.44A3,3,0,0,0,5.6,6.55l-.32,1H4a3,3,0,0,0-3,3v8a3,3,0,0,0,3,3H18a3,3,0,0,0,3-3v-7A1,1,0,0,0,20,10.5Zm-9-1a4,4,0,1,0,4,4A4,4,0,0,0,11,9.5Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,11,15.5Zm11-11H21v-1a1,1,0,0,0-2,0v1H18a1,1,0,0,0,0,2h1v1a1,1,0,0,0,2,0v-1h1a1,1,0,0,0,0-2Z" />
</svg> */}
<svg
width={width}
height={height}
fill={color} fill={color}
style={{padding, margin}} d="M60 15.5L65 1H76.5L69.5 13H74.5L60 30.5L65 15.5H60Z"
xmlns="http://www.w3.org/2000/svg" stroke="black"
version="1.1" strokeWidth="2"
x="0px" strokeLinejoin="round"
y="0px" />
viewBox="0 0 100 100" </svg>
className="screenshotIcon"
>
<g transform="translate(0,-952.36218)">
<path
d="m 83,958.36218 c -1.1046,0 -2,0.89543 -2,2 l 0,7 -7,0 c -1.10457,0 -2,0.8954 -2,2 0,1.1046 0.89543,2 2,2 l 7,0 0,7 c 0,1.10457 0.8954,2 2,2 1.1046,0 2,-0.89543 2,-2 l 0,-7 7,0 c 1.10457,0 2,-0.8954 2,-2 0,-1.1046 -0.89543,-2 -2,-2 l -7,0 0,-7 c 0,-1.10457 -0.8954,-2 -2,-2 z m -48,22 c -0.78068,0.007 -1.3909,0.40265 -1.71875,0.96875 l -6.4375,11.03125 -12.84375,0 c -4.3973999,0 -8,3.6026 -8,8.00002 l 0,38 c 0,4.3974 3.6026001,8 8,8 l 62,0 c 4.3974,0 8,-3.6026 8,-8 l 0,-38 c 0,-4.39742 -3.6026,-8.00002 -8,-8.00002 l -12.84375,0 -6.4375,-11.03125 C 56.3641,980.74168 55.68774,980.36046 55,980.36218 l -20,0 z m 1.125,4 17.75,0 6.40625,11 c 0.34687,0.60075 1.02507,0.99534 1.71875,1 l 14,0 c 2.25056,0 4,1.74944 4,4.00002 l 0,38 c 0,2.2505 -1.74944,4 -4,4 l -62,0 c -2.25056,0 -4,-1.7495 -4,-4 l 0,-38 c 0,-2.25058 1.74944,-4.00002 4,-4.00002 l 14,0 c 0.69368,-0.005 1.37188,-0.39925 1.71875,-1 l 6.40625,-11 z M 45,1002.3622 c -8.81287,0 -16,7.1871 -16,16 0,8.8128 7.18713,16 16,16 8.81286,0 16,-7.1872 16,-16 0,-8.8129 -7.18714,-16 -16,-16 z m 0,4 c 6.6511,0 12,5.3489 12,12 0,6.6511 -5.3489,12 -12,12 -6.65111,0 -12,-5.3489 -12,-12 0,-6.6511 5.34889,-12 12,-12 z"
fill={color}
fillOpacity="1"
stroke="none"
marker="none"
visibility="visible"
display="inline"
overflow="visible"
/>
</g>
</svg>
</Fragment>
); );

6
desktop-app/flow-typed/electron.js vendored Normal file
View file

@ -0,0 +1,6 @@
declare class WebviewElement extends HTMLElement {
insertCSS: string => Promise<string>;
executeJavaScript: string => Promise<any>;
getWebContentsId: () => number;
removeInsertedCSS: number => Promise<void>;
}