import classNames from 'classnames'; import { ipcRenderer } from 'electron'; import * as React from 'react'; import { apiCmds, apiName } from '../../common/api-interface'; import { isLinux, isMac, isWindowsOS } from '../../common/env'; import { i18n } from '../../common/i18n-preload'; const screenRegExp = new RegExp(/^screen \d+$/gim); const SCREEN_PICKER_NAMESPACE = 'ScreenPicker'; const ENTIRE_SCREEN = 'entire screen'; interface IState { sources: ICustomDesktopCapturerSource[]; selectedSource: ICustomDesktopCapturerSource | undefined; selectedTab: tabs; } interface ICustomDesktopCapturerSource extends Electron.DesktopCapturerSource { fileName: string | null; } type tabs = 'screens' | 'applications'; const enum keyCode { pageDown = 34, rightArrow = 39, pageUp = 33, leftArrow = 37, homeKey = 36, upArrow = 38, endKey = 35, arrowDown = 40, enterKey = 13, escapeKey = 27, } type inputChangeEvent = React.ChangeEvent; export default class ScreenPicker extends React.Component<{}, IState> { private isScreensAvailable: boolean; private isApplicationsAvailable: boolean; private readonly eventHandlers = { onSelect: (src: ICustomDesktopCapturerSource) => this.select(src), onToggle: (tab: tabs) => (_event: inputChangeEvent) => this.toggle(tab), onClose: () => this.close(), onSubmit: () => this.submit(), }; private currentIndex: number; constructor(props) { super(props); this.state = { sources: [], selectedSource: undefined, selectedTab: 'screens', }; this.currentIndex = 0; this.isScreensAvailable = false; this.isApplicationsAvailable = false; this.updateState = this.updateState.bind(this); this.handleKeyUpPress = this.handleKeyUpPress.bind(this); this.renderTabTitles = this.renderTabTitles.bind(this); } /** * Callback to handle event when a component is mounted */ public componentDidMount(): void { ipcRenderer.on('screen-picker-data', this.updateState); document.addEventListener('keyup', this.handleKeyUpPress, true); if (isWindowsOS) { document.body.classList.add('ScreenPicker-window-border'); } } /** * Callback to handle event when a component is unmounted */ public componentWillUnmount(): void { ipcRenderer.removeListener('screen-picker-data', this.updateState); document.removeEventListener('keyup', this.handleKeyUpPress, true); } /** * Renders the component */ public render(): JSX.Element { const { sources, selectedSource } = this.state; return (
{i18n.t( `Choose what you'd like to share`, SCREEN_PICKER_NAMESPACE, )()}
{this.renderSources(sources)}
); } /** * Renders the sources by separating screens and applications * * @param sources {DesktopCapturerSource} */ private renderSources(sources: ICustomDesktopCapturerSource[]): JSX.Element { const screens: JSX.Element[] = []; const applications: JSX.Element[] = []; sources.map((source: ICustomDesktopCapturerSource) => { screenRegExp.lastIndex = 0; const shouldHighlight: string = classNames( 'ScreenPicker-item-container', { 'ScreenPicker-selected': this.shouldHighlight(source.id) }, ); const sourceName = source.name.toLocaleLowerCase(); if ( ((isMac || isLinux) && source.display_id !== '') || (isWindowsOS && (sourceName === ENTIRE_SCREEN || screenRegExp.exec(sourceName))) ) { source.fileName = 'fullscreen'; let screenName; if (sourceName === ENTIRE_SCREEN) { screenName = i18n.t('Entire screen', SCREEN_PICKER_NAMESPACE)(); } else { const screenNumber = source.name.substr(7, source.name.length); screenName = i18n.t( 'Screen {number}', SCREEN_PICKER_NAMESPACE, )({ number: screenNumber }); } screens.push(
this.eventHandlers.onSelect(source)} >
thumbnail image
{screenName}
, ); } else { source.fileName = null; applications.push(
this.eventHandlers.onSelect(source)} >
thumbnail image
{source.name}
, ); } }); this.isScreensAvailable = screens.length > 0; this.isApplicationsAvailable = applications.length > 0; if (!this.isScreensAvailable && !this.isApplicationsAvailable) { return (
{i18n.t( 'No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE, )()}
); } return (
{this.renderTabTitles()}
{screens}
{applications}
); } /** * Renders the screen and application tab section */ private renderTabTitles(): JSX.Element[] | undefined { const { selectedTab } = this.state; if (this.isScreensAvailable && this.isApplicationsAvailable) { return [ , , , , ]; } if (this.isScreensAvailable) { return [ , , ]; } if (this.isApplicationsAvailable) { return [ , , ]; } return; } /** * Updates the selected state * * @param id {string} */ private shouldHighlight(id: string): boolean { const { selectedSource } = this.state; return selectedSource ? id === selectedSource.id : false; } /** * updates the state when the source is selected * * @param selectedSource {DesktopCapturerSource} */ private select(selectedSource: ICustomDesktopCapturerSource): void { this.setState({ selectedSource }); if (selectedSource) { ipcRenderer.send('screen-source-select', selectedSource); } } /** * Updates the screen picker tabs * * @param selectedTab */ private toggle(selectedTab: tabs): void { this.setState({ selectedTab }); } /** * Closes the screen picker window */ private close(): void { // setting null will clean up listeners ipcRenderer.send('screen-source-selected', null); ipcRenderer.send(apiName.symphonyApi, { cmd: apiCmds.closeWindow, windowType: 'screen-picker', }); } /** * Sends the selected source to the main process * and closes the screen picker window */ private submit(): void { const { selectedSource } = this.state; if (selectedSource) { ipcRenderer.send('screen-source-selected', selectedSource); } } /** * Method handles used key up event * @param e */ private handleKeyUpPress(e): void { const key = e.keyCode || e.which; const { sources } = this.state; switch (key) { case keyCode.pageDown: case keyCode.rightArrow: this.updateSelectedSource(1); break; case keyCode.pageUp: case keyCode.leftArrow: this.updateSelectedSource(-1); break; case keyCode.homeKey: if (this.currentIndex !== 0) { this.updateSelectedSource(0); } break; case keyCode.upArrow: this.updateSelectedSource(-2); break; case keyCode.endKey: if (this.currentIndex !== sources.length - 1) { this.updateSelectedSource(sources.length - 1); } break; case keyCode.arrowDown: this.updateSelectedSource(2); break; case keyCode.enterKey: this.eventHandlers.onSubmit(); break; case keyCode.escapeKey: this.eventHandlers.onClose(); break; default: break; } } /** * Updated the UI selected state based on * the selected source state * * @param index */ private updateSelectedSource(index) { const { sources, selectedSource } = this.state; if (selectedSource) { this.currentIndex = sources.findIndex((source) => { return source.id === selectedSource.id; }); } // Find the next item to be selected const nextIndex = (this.currentIndex + index + sources.length) % sources.length; if (sources[nextIndex] && sources[nextIndex].id) { // Updates the selected source this.setState({ selectedSource: sources[nextIndex] }); } } /** * Sets the component state * * @param _event * @param data {Object} */ private updateState(_event, data): void { this.setState(data as IState); } }