Typescript - Completed screen sharing indicator and fixed i18n usage issue

This commit is contained in:
Kiran Niranjan
2019-01-02 15:49:50 +05:30
parent 196ee2c0fe
commit c58678eeb2
22 changed files with 514 additions and 57 deletions

View File

@@ -237,7 +237,7 @@
} }
}, stream => { }, stream => {
handleStream(stream, source.display_id); handleStream(stream, source.display_id);
}, handleError) }, handleError);
}); });
}); });

12
gulpfile.js Normal file
View File

@@ -0,0 +1,12 @@
const gulp = require('gulp');
const less = require('gulp-less');
const sourcemaps = require('gulp-sourcemaps');
const path = require('path');
gulp.task('less', function () {
return gulp.src('./src/**/*.less')
.pipe(sourcemaps.init())
.pipe(less())
.pipe(sourcemaps.write())
.pipe(gulp.dest(path.join(__dirname, 'src')));
});

View File

@@ -12,7 +12,7 @@
"tsc": "git clean -xdf ./lib && npm run lint && tsc", "tsc": "git clean -xdf ./lib && npm run lint && tsc",
"lint": "tslint --project tsconfig.json", "lint": "tslint --project tsconfig.json",
"start": "npm run compile-css && npm run browserify-preload && cross-env ELECTRON_DEV=true electron .", "start": "npm run compile-css && npm run browserify-preload && cross-env ELECTRON_DEV=true electron .",
"compile-css": "lessc src/renderer/styles/main.less src/renderer/styles/main.css", "compile-css": "gulp less",
"prebuild": "npm run rebuild && npm run browserify-preload", "prebuild": "npm run rebuild && npm run browserify-preload",
"browserify-preload": "browserify -o src/renderer/_preload-main.js -x electron --insert-global-vars=__filename,__dirname src/renderer/preload-main.js", "browserify-preload": "browserify -o src/renderer/_preload-main.js -x electron --insert-global-vars=__filename,__dirname src/renderer/preload-main.js",
"rebuild": "electron-rebuild -f", "rebuild": "electron-rebuild -f",
@@ -81,6 +81,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/auto-launch": "^5.0.0", "@types/auto-launch": "^5.0.0",
"@types/ffi": "0.2.1",
"@types/lodash.omit": "^4.5.4", "@types/lodash.omit": "^4.5.4",
"@types/node": "10.11.4", "@types/node": "10.11.4",
"@types/react": "16.4.18", "@types/react": "16.4.18",
@@ -107,6 +108,9 @@
"eslint-plugin-jsx-a11y": "6.1.1", "eslint-plugin-jsx-a11y": "6.1.1",
"eslint-plugin-react": "7.11.0", "eslint-plugin-react": "7.11.0",
"glob": "7.1.3", "glob": "7.1.3",
"gulp": "4.0.0",
"gulp-less": "4.0.1",
"gulp-sourcemaps": "2.6.4",
"jest": "23.6.0", "jest": "23.6.0",
"jest-html-reporter": "2.4.2", "jest-html-reporter": "2.4.2",
"less": "3.8.1", "less": "3.8.1",

View File

@@ -126,6 +126,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
case apiCmds.closeWindow: case apiCmds.closeWindow:
windowHandler.closeWindow(arg.windowType); windowHandler.closeWindow(arg.windowType);
break; break;
case apiCmds.openScreenSharingIndicator:
const { displayId, id } = arg;
if (typeof displayId === 'string' && typeof id === 'number') {
windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id);
}
break;
default: default:
} }

View File

@@ -1,5 +1,5 @@
import * as electron from 'electron'; import * as electron from 'electron';
import { BrowserWindow, crashReporter, ipcMain, webContents } from 'electron'; import { BrowserWindow, crashReporter, ipcMain } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { format, parse } from 'url'; import { format, parse } from 'url';
@@ -8,6 +8,7 @@ import { buildNumber, clientVersion, version } from '../../package.json';
import DesktopCapturerSource = Electron.DesktopCapturerSource; import DesktopCapturerSource = Electron.DesktopCapturerSource;
import { apiName, WindowTypes } from '../common/api-interface'; import { apiName, WindowTypes } from '../common/api-interface';
import { isMac, isWindowsOS } from '../common/env'; import { isMac, isWindowsOS } from '../common/env';
import { i18n } from '../common/i18n';
import { getCommandLineArgs, getGuid } from '../common/utils'; import { getCommandLineArgs, getGuid } from '../common/utils';
import { AppMenu } from './app-menu'; import { AppMenu } from './app-menu';
import { config, IConfig } from './config-handler'; import { config, IConfig } from './config-handler';
@@ -45,6 +46,11 @@ export class WindowHandler {
show: false, show: false,
title: 'Symphony', title: 'Symphony',
width: 400, width: 400,
webPreferences: {
sandbox: true,
nodeIntegration: false,
devTools: false,
},
}; };
} }
@@ -69,6 +75,30 @@ export class WindowHandler {
}; };
} }
/**
* Screen sharing indicator window opts
*/
private static getScreenSharingIndicatorOpts(): ICustomBrowserWindowConstructorOpts {
return {
width: 592,
height: 48,
show: false,
modal: true,
frame: false,
focusable: false,
transparent: true,
autoHideMenuBar: true,
resizable: false,
alwaysOnTop: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
devTools: false,
},
winKey: getGuid(),
};
}
/** /**
* Verifies if the url is valid and * Verifies if the url is valid and
* forcefully appends https if not present * forcefully appends https if not present
@@ -97,11 +127,12 @@ export class WindowHandler {
private readonly windows: object; private readonly windows: object;
private readonly isCustomTitleBarAndWindowOS: boolean; private readonly isCustomTitleBarAndWindowOS: boolean;
private mainWindow: ICustomBrowserWindow | null; private mainWindow: ICustomBrowserWindow | null = null;
private loadingWindow: Electron.BrowserWindow | null; private loadingWindow: Electron.BrowserWindow | null = null;
private aboutAppWindow: Electron.BrowserWindow | null; private aboutAppWindow: Electron.BrowserWindow | null = null;
private moreInfoWindow: Electron.BrowserWindow | null; private moreInfoWindow: Electron.BrowserWindow | null = null;
private screenPickerWindow: Electron.BrowserWindow | null; private screenPickerWindow: Electron.BrowserWindow | null = null;
private screenSharingIndicatorWindow: Electron.BrowserWindow | null = null;
constructor(opts?: Electron.BrowserViewConstructorOptions) { constructor(opts?: Electron.BrowserViewConstructorOptions) {
// Settings // Settings
@@ -115,12 +146,6 @@ export class WindowHandler {
this.isCustomTitleBarAndWindowOS = isWindowsOS && this.config.isCustomTitleBar; this.isCustomTitleBarAndWindowOS = isWindowsOS && this.config.isCustomTitleBar;
this.appMenu = null; this.appMenu = null;
// Window references
this.mainWindow = null;
this.loadingWindow = null;
this.aboutAppWindow = null;
this.moreInfoWindow = null;
this.screenPickerWindow = null;
try { try {
const extra = { podUrl: this.globalConfig.url, process: 'main' }; const extra = { podUrl: this.globalConfig.url, process: 'main' };
@@ -174,7 +199,7 @@ export class WindowHandler {
this.mainWindow.webContents.insertCSS( this.mainWindow.webContents.insertCSS(
fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/snack-bar.css'), 'utf8').toString(), fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/snack-bar.css'), 'utf8').toString(),
); );
this.mainWindow.webContents.send('page-load', { isWindowsOS }); this.mainWindow.webContents.send('page-load', { isWindowsOS, resources: i18n.loadedResources });
this.appMenu = new AppMenu(); this.appMenu = new AppMenu();
this.monitorWindowActions(); this.monitorWindowActions();
// Ready to show the window // Ready to show the window
@@ -211,7 +236,7 @@ export class WindowHandler {
* *
* @param windowType * @param windowType
*/ */
public closeWindow(windowType: WindowTypes) { public closeWindow(windowType: WindowTypes): void {
switch (windowType) { switch (windowType) {
case 'screen-picker': case 'screen-picker':
if (this.screenPickerWindow && !this.screenPickerWindow.isDestroyed()) this.screenPickerWindow.close(); if (this.screenPickerWindow && !this.screenPickerWindow.isDestroyed()) this.screenPickerWindow.close();
@@ -224,7 +249,7 @@ export class WindowHandler {
* *
* @param shouldAutoReload {boolean} * @param shouldAutoReload {boolean}
*/ */
public setIsAutoReload(shouldAutoReload: boolean) { public setIsAutoReload(shouldAutoReload: boolean): void {
this.isAutoReload = shouldAutoReload; this.isAutoReload = shouldAutoReload;
} }
@@ -243,7 +268,7 @@ export class WindowHandler {
* Displays a loading window until the main * Displays a loading window until the main
* application is loaded * application is loaded
*/ */
public showLoadingScreen() { public showLoadingScreen(): void {
this.loadingWindow = createComponentWindow('loading-screen', WindowHandler.getLoadingWindowOpts()); this.loadingWindow = createComponentWindow('loading-screen', WindowHandler.getLoadingWindowOpts());
this.loadingWindow.webContents.once('did-finish-load', () => { this.loadingWindow.webContents.once('did-finish-load', () => {
if (this.loadingWindow) { if (this.loadingWindow) {
@@ -257,7 +282,7 @@ export class WindowHandler {
/** /**
* Creates a about app window * Creates a about app window
*/ */
public createAboutAppWindow() { public createAboutAppWindow(): void {
this.aboutAppWindow = createComponentWindow('about-app'); this.aboutAppWindow = createComponentWindow('about-app');
this.aboutAppWindow.webContents.once('did-finish-load', () => { this.aboutAppWindow.webContents.once('did-finish-load', () => {
if (this.aboutAppWindow) { if (this.aboutAppWindow) {
@@ -269,8 +294,8 @@ export class WindowHandler {
/** /**
* Creates a more info window * Creates a more info window
*/ */
public createMoreInfoWindow() { public createMoreInfoWindow(): void {
this.moreInfoWindow = createComponentWindow('more-info-window'); this.moreInfoWindow = createComponentWindow('more-info');
this.moreInfoWindow.webContents.once('did-finish-load', () => { this.moreInfoWindow.webContents.once('did-finish-load', () => {
if (this.aboutAppWindow) { if (this.aboutAppWindow) {
this.aboutAppWindow.webContents.send('more-info-data'); this.aboutAppWindow.webContents.send('more-info-data');
@@ -280,10 +305,14 @@ export class WindowHandler {
/** /**
* Creates a screen picker window * Creates a screen picker window
*
* @param win
* @param sources
* @param id
*/ */
public createScreenPickerWindow(win: webContents, sources: DesktopCapturerSource[], id: number) { public createScreenPickerWindow(win: Electron.WebContents, sources: DesktopCapturerSource[], id: number): void {
const opts = WindowHandler.getScreenPickerWindowOpts(); const opts = WindowHandler.getScreenPickerWindowOpts();
this.screenPickerWindow = createComponentWindow('screen-picker-window', opts); this.screenPickerWindow = createComponentWindow('screen-picker', opts);
this.screenPickerWindow.webContents.once('did-finish-load', () => { this.screenPickerWindow.webContents.once('did-finish-load', () => {
if (this.screenPickerWindow) { if (this.screenPickerWindow) {
this.screenPickerWindow.webContents.send('screen-picker-data', { sources, id }); this.screenPickerWindow.webContents.send('screen-picker-data', { sources, id });
@@ -300,6 +329,53 @@ export class WindowHandler {
}); });
} }
/**
* Creates a screen sharing indicator whenever uses start
* sharing the screen
*
* @param screenSharingWebContents {Electron.webContents}
* @param displayId {string}
* @param id {number}
*/
public createScreenSharingIndicatorWindow(screenSharingWebContents: Electron.webContents, displayId: string, id: number): void {
const indicatorScreen = (displayId && electron.screen.getAllDisplays().filter((d) => displayId.includes(d.id.toString()))[0]) || electron.screen.getPrimaryDisplay();
const screenRect = indicatorScreen.workArea;
let opts = WindowHandler.getScreenSharingIndicatorOpts();
if (opts.width && opts.height) {
opts = Object.assign({}, opts, {
x: screenRect.x + Math.round((screenRect.width - opts.width) / 2),
y: screenRect.y + screenRect.height - opts.height,
});
}
this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts);
this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true);
this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => {
if (this.screenSharingIndicatorWindow) {
this.screenSharingIndicatorWindow.webContents.send('screen-sharing-indicator-data', { id });
const stopScreenSharing = (_event, indicatorId) => {
if (id === indicatorId) {
screenSharingWebContents.send('screen-sharing-stopped', id);
}
};
const destroyScreenSharingIndicator = (_event, indicatorId) => {
if (id === indicatorId && this.screenSharingIndicatorWindow && !this.screenSharingIndicatorWindow.isDestroyed()) {
this.screenSharingIndicatorWindow.close();
}
};
this.screenSharingIndicatorWindow.once('close', () => {
ipcMain.removeListener('stop-screen-sharing', stopScreenSharing);
ipcMain.removeListener('destroy-screen-sharing-indicator', destroyScreenSharingIndicator);
});
ipcMain.once('stop-screen-sharing', stopScreenSharing);
ipcMain.once('destroy-screen-sharing-indicator', destroyScreenSharingIndicator);
}
});
}
/** /**
* Opens an external url in the system's default browser * Opens an external url in the system's default browser
* *

View File

@@ -46,15 +46,14 @@ export const preventWindowNavigation = (browserWindow: Electron.BrowserWindow, i
*/ */
export const createComponentWindow = ( export const createComponentWindow = (
componentName: string, componentName: string,
opts?: Electron.BrowserWindowConstructorOptions): BrowserWindow => { opts?: Electron.BrowserWindowConstructorOptions,
): BrowserWindow => {
const parent = windowHandler.getMainWindow() || undefined; const options: Electron.BrowserWindowConstructorOptions = {
const options = {
center: true, center: true,
height: 300, height: 300,
maximizable: false, maximizable: false,
minimizable: false, minimizable: false,
parent,
resizable: false, resizable: false,
show: false, show: false,
width: 300, width: 300,
@@ -71,7 +70,10 @@ export const createComponentWindow = (
const targetUrl = url.format({ const targetUrl = url.format({
pathname: require.resolve('../renderer/react-window.html'), pathname: require.resolve('../renderer/react-window.html'),
protocol: 'file', protocol: 'file',
query: { componentName }, query: {
componentName,
locale: i18n.getLocale(),
},
slashes: true, slashes: true,
}); });

View File

@@ -19,6 +19,7 @@ export enum apiCmds {
openScreenSnippet = 'open-screen-snippet', openScreenSnippet = 'open-screen-snippet',
keyPress = 'key-press', keyPress = 'key-press',
closeWindow = 'close-window', closeWindow = 'close-window',
openScreenSharingIndicator = 'open-screen-sharing-indicator',
} }
export enum apiName { export enum apiName {
@@ -41,6 +42,7 @@ export interface IApiArgs {
locale: string; locale: string;
keyCode: number; keyCode: number;
windowType: WindowTypes; windowType: WindowTypes;
displayId: string;
} }
export type WindowTypes = 'screen-picker'; export type WindowTypes = 'screen-picker';
@@ -70,6 +72,14 @@ export interface IBoundsChange extends Electron.Rectangle {
windowName: string; windowName: string;
} }
/**
* Screen sharing indicator
*/
export interface IScreenSharingIndicator {
type: string;
reason?: string;
}
export enum KeyCodes { export enum KeyCodes {
Esc = 27, Esc = 27,
Alt = 18, Alt = 18,

View File

@@ -0,0 +1,86 @@
import { formatString } from './format-string';
const localeCodeRegex = /^([a-z]{2})-([A-Z]{2})$/;
export type LocaleType = 'en-US' | 'ja-JP';
type formaterFunction = (...args: any[]) => string;
class Translation {
/**
* Returns translated string with respect to value, resource & name space
*
* @param value {string} key field in the resources
* @param resource {string} current locale resource
* @param namespace {string} name space in the resource
*/
private static translate(value: string, resource: JSON | null, namespace: string | undefined): string {
return resource ? Translation.getResource(resource, namespace)[value] : null;
}
private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource;
private locale: LocaleType = 'en-US';
private loadedResources: object = {};
/**
* Apply the locale for translation
*
* @param locale
*/
public setLocale(locale: LocaleType): void {
const localeMatch: string[] | null = locale.match(localeCodeRegex);
if (!locale && (!localeMatch || localeMatch.length < 1)) {
return;
}
this.locale = locale;
}
/**
* Gets the current locale
*/
public getLocale(): LocaleType {
return this.locale;
}
/**
* fetches and returns the translated value
*
* @param value {string}
* @param namespace {string}
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
* @returns translate and formats string
*/
public t(value: string, namespace?: string): formaterFunction {
return (...args: any[]): string => {
if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(Translation.translate(value, this.loadedResources[this.locale], namespace), args);
}
const resource = this.loadResource(this.locale);
return formatString(Translation.translate(value, resource, namespace) || value, args);
};
}
/**
* Keeps ref of loaded resources from the main process
*
* @param resource
*/
public setResource(resource: JSON): void {
this.loadedResources = resource;
}
/**
* Reads the resources dir and returns the data
*
* @param locale
*/
private loadResource(locale: LocaleType): JSON | null {
return this.loadedResources[locale];
}
}
const i18n = new Translation();
export { i18n };

View File

@@ -21,8 +21,8 @@ class Translation {
return resource ? Translation.getResource(resource, namespace)[value] : null; return resource ? Translation.getResource(resource, namespace)[value] : null;
} }
private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource; private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource;
public loadedResources: object = {};
private locale: LocaleType = 'en-US'; private locale: LocaleType = 'en-US';
private loadedResource: object = {};
/** /**
* Apply the locale for translation * Apply the locale for translation
@@ -55,8 +55,8 @@ class Translation {
*/ */
public t(value: string, namespace?: string): formaterFunction { public t(value: string, namespace?: string): formaterFunction {
return (...args: any[]): string => { return (...args: any[]): string => {
if (this.loadedResource && this.loadedResource[this.locale]) { if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(Translation.translate(value, this.loadedResource[this.locale], namespace), args); return formatString(Translation.translate(value, this.loadedResources[this.locale], namespace), args);
} }
const resource = this.loadResource(this.locale); const resource = this.loadResource(this.locale);
return formatString(Translation.translate(value, resource, namespace) || value, args); return formatString(Translation.translate(value, resource, namespace) || value, args);
@@ -68,14 +68,14 @@ class Translation {
* *
* @param locale * @param locale
*/ */
public loadResource(locale: LocaleType): JSON | null { private loadResource(locale: LocaleType): JSON | null {
const resourcePath = path.resolve(__dirname, '..', 'locale', `${locale}.json`); const resourcePath = path.resolve(__dirname, '..', 'locale', `${locale}.json`);
if (!fs.existsSync(resourcePath)) { if (!fs.existsSync(resourcePath)) {
return null; return null;
} }
return this.loadedResource[this.locale] = require(resourcePath); return this.loadedResources[this.locale] = require(resourcePath);
} }
} }

View File

@@ -110,6 +110,11 @@
"Select Screen": "Select Screen", "Select Screen": "Select Screen",
"Share": "Share" "Share": "Share"
}, },
"ScreenSharingIndicator": {
"You are sharing your screen on ": "You are sharing your screen on ",
"Stop sharing": "Stop sharing",
"Hide": "Hide"
},
"ScreenSnippet": { "ScreenSnippet": {
"Done": "Done", "Done": "Done",
"Erase": "Erase", "Erase": "Erase",

View File

@@ -0,0 +1,86 @@
import classNames from 'classnames';
import { ipcRenderer, remote } from 'electron';
import * as React from 'react';
import { isMac } from '../../common/env';
import { i18n } from '../../common/i18n';
interface IState {
id: number;
}
/**
* Window that display a banner when the users starting sharing screen
*/
export default class ScreenSharingIndicator extends React.Component<{}, IState> {
private readonly eventHandlers = {
onStopScreenSharing: (id) => this.stopScreenShare(id),
onClose: (id) => this.close(id),
};
constructor(props) {
super(props);
this.state = {
id: 0,
};
this.updateState = this.updateState.bind(this);
}
/**
* main render function
*/
public render(): JSX.Element {
const { id } = this.state;
const namespace = 'ScreenSharingIndicator';
return (
<div className={classNames('ScreenSharingIndicator', { mac: isMac })}>
<span className='drag-area'/>
<span className='text-label'>{i18n.t(`You are sharing your screen on `, namespace)()}</span>
<span className='text-label'><b>{remote.app.getName()}</b></span>
<span className='buttons'>
<a className='hide-button' href='#' onClick={() => this.eventHandlers.onClose(id)}>{i18n.t('Hide', namespace)()}</a>
<button className='stop-sharing-button' onClick={() => this.eventHandlers.onStopScreenSharing(id)}>
{i18n.t('Stop sharing', namespace)()}
</button>
</span>
</div>
);
}
public componentDidMount(): void {
ipcRenderer.on('screen-sharing-indicator-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('screen-sharing-indicator-data', this.updateState);
}
/**
* Stops sharing screen
*
* @param id
*/
private stopScreenShare(id: number): void {
ipcRenderer.send('stop-screen-sharing', id);
}
/**
* Closes the screen sharing indicator window
*
* @param id
*/
private close(id): void {
ipcRenderer.send('destroy-screen-sharing-indicator', id);
}
/**
* Sets the About app state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron';
import * as React from 'react'; import * as React from 'react';
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import { i18n } from '../../common/i18n'; import { i18n } from '../../common/i18n-preload';
interface IState { interface IState {
show: boolean; show: boolean;

View File

@@ -2,7 +2,7 @@ import { ipcRenderer, remote } from 'electron';
import * as React from 'react'; import * as React from 'react';
import { apiCmds, apiName } from '../../common/api-interface'; import { apiCmds, apiName } from '../../common/api-interface';
import { i18n } from '../../common/i18n'; import { i18n } from '../../common/i18n-preload';
interface IState { interface IState {
isMaximized: boolean; isMaximized: boolean;

View File

@@ -1,10 +1,28 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { i18n, LocaleType } from '../common/i18n';
import AboutBox from './components/about-app'; import AboutBox from './components/about-app';
import LoadingScreen from './components/loading-screen'; import LoadingScreen from './components/loading-screen';
import MoreInfo from './components/more-info'; import MoreInfo from './components/more-info';
import ScreenPicker from './components/screen-picker'; import ScreenPicker from './components/screen-picker';
import ScreenSharingIndicator from './components/screen-sharing-indicator';
const enum components {
aboutApp = 'about-app',
loadingScreen = 'loading-screen',
moreInfo = 'more-info',
screenPicker = 'screen-picker',
screenSharingIndicator = 'screen-sharing-indicator',
}
const loadStyle = (style) => {
const styles = document.createElement('link');
styles.rel = 'stylesheet';
styles.type = 'text/css';
styles.href = `./styles/${style}.css`;
document.getElementsByTagName('head')[0].appendChild(styles);
};
/** /**
* Loads the appropriate component * Loads the appropriate component
@@ -12,21 +30,31 @@ import ScreenPicker from './components/screen-picker';
const load = () => { const load = () => {
const query = new URL(window.location.href).searchParams; const query = new URL(window.location.href).searchParams;
const componentName = query.get('componentName'); const componentName = query.get('componentName');
const locale = query.get('locale');
i18n.setLocale(locale as LocaleType);
let component; let component;
switch (componentName) { switch (componentName) {
case 'about-app': case components.aboutApp:
loadStyle(components.aboutApp);
component = AboutBox; component = AboutBox;
break; break;
case 'loading-screen': case components.loadingScreen:
loadStyle(components.loadingScreen);
component = LoadingScreen; component = LoadingScreen;
break; break;
case 'more-info-window': case components.moreInfo:
loadStyle(components.moreInfo);
component = MoreInfo; component = MoreInfo;
break; break;
case 'screen-picker-window': case components.screenPicker:
loadStyle(components.screenPicker);
component = ScreenPicker; component = ScreenPicker;
break; break;
case components.screenSharingIndicator:
loadStyle(components.screenSharingIndicator);
component = ScreenSharingIndicator;
break;
} }
const element = React.createElement(component); const element = React.createElement(component);
ReactDOM.render(element, document.getElementById('Root')); ReactDOM.render(element, document.getElementById('Root'));

View File

@@ -2,6 +2,7 @@ import { ipcRenderer } from 'electron';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { i18n } from '../common/i18n-preload';
import SnackBar from './components/snack-bar'; import SnackBar from './components/snack-bar';
import WindowsTitleBar from './components/windows-title-bar'; import WindowsTitleBar from './components/windows-title-bar';
import { SSFApi } from './ssf-api'; import { SSFApi } from './ssf-api';
@@ -37,7 +38,10 @@ const createAPI = () => {
createAPI(); createAPI();
// When the window is completely loaded // When the window is completely loaded
ipcRenderer.on('page-load', (_event, { isWindowsOS }) => { ipcRenderer.on('page-load', (_event, { isWindowsOS, resources }) => {
i18n.setResource(resources);
if (isWindowsOS) { if (isWindowsOS) {
// injects custom window title bar // injects custom window title bar
const titleBar = React.createElement(WindowsTitleBar); const titleBar = React.createElement(WindowsTitleBar);

View File

@@ -17,10 +17,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="./styles/main.css" />
</head> </head>
<body style="overflow: hidden; background-color: white; margin: 0; height: 100%"> <body>
<div id="Root"></div> <div id="Root"></div>
</body> </body>
</html> </html>

View File

@@ -6,21 +6,23 @@ import {
apiName, apiName,
IActivityDetection, IActivityDetection,
IBadgeCount, IBadgeCount,
IBoundsChange, IBoundsChange, IScreenSharingIndicator,
IScreenSnippet, KeyCodes, IScreenSnippet, KeyCodes,
} from '../common/api-interface'; } from '../common/api-interface';
import { i18n, LocaleType } from '../common/i18n'; import { i18n, LocaleType } from '../common/i18n-preload';
import { throttle } from '../common/throttle'; import { throttle } from '../common/throttle';
import { getSource } from './desktop-capturer'; import { getSource } from './desktop-capturer';
let isAltKey = false; let isAltKey: boolean = false;
let isMenuOpen = false; let isMenuOpen: boolean = false;
let nextId = 0;
interface ILocalObject { interface ILocalObject {
ipcRenderer; ipcRenderer;
activityDetectionCallback?: (arg: IActivityDetection) => void; activityDetectionCallback?: (arg: IActivityDetection) => void;
screenSnippetCallback?: (arg: IScreenSnippet) => void; screenSnippetCallback?: (arg: IScreenSnippet) => void;
boundsChangeCallback?: (arg: IBoundsChange) => void; boundsChangeCallback?: (arg: IBoundsChange) => void;
screenSharingIndicatorCallback?: (arg: IScreenSharingIndicator) => void;
} }
const local: ILocalObject = { const local: ILocalObject = {
@@ -141,6 +143,41 @@ export class SSFApi {
} }
} }
/**
* Shows a banner that informs user that the screen is being shared.
*
* @param options object with following fields:
* - stream https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/MediaStream object.
* The indicator automatically destroys itself when stream becomes inactive (see MediaStream.active).
* - displayId id of the display that is being shared or that contains the shared app
* @param callback callback function that will be called to handle events.
* Callback receives event object { type: string }. Types:
* - 'error' - error occured. Event object contains 'reason' field.
* - 'stopRequested' - user clicked "Stop Sharing" button.
*/
public showScreenSharingIndicator(options, callback): void {
const { stream, displayId } = options;
if (typeof callback === 'function') {
if (!stream || !stream.active || stream.getVideoTracks().length !== 1) {
callback({ type: 'error', reason: 'bad stream' });
return;
}
if (displayId && typeof (displayId) !== 'string') {
callback({ type: 'error', reason: 'bad displayId' });
return;
}
local.screenSharingIndicatorCallback = callback;
const id = ++nextId;
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenSharingIndicator,
displayId: options.displayId,
id,
});
}
}
} }
/** /**
@@ -213,6 +250,12 @@ local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => {
} }
}); });
local.ipcRenderer.on('screen-sharing-stopped', () => {
if (typeof local.screenSharingIndicatorCallback === 'function') {
local.screenSharingIndicatorCallback({ type: 'stopRequested' });
}
});
// Invoked whenever the app is reloaded/navigated // Invoked whenever the app is reloaded/navigated
const sanitize = (): void => { const sanitize = (): void => {
if (window.name === apiName.mainWindowName) { if (window.name === apiName.mainWindowName) {

View File

@@ -4,6 +4,13 @@
@font-family: sans-serif; @font-family: sans-serif;
@text-padding: 10px; @text-padding: 10px;
body {
overflow: hidden;
background-color: white;
margin: 0;
height: 100%;
}
.AboutApp { .AboutApp {
text-align: center; text-align: center;
display: flex; display: flex;

View File

@@ -1,6 +1,13 @@
@font-family: sans-serif; @font-family: sans-serif;
@text-padding: 10px; @text-padding: 10px;
body {
overflow: hidden;
background-color: white;
margin: 0;
height: 100%;
}
.LoadingScreen { .LoadingScreen {
margin: 0; margin: 0;
height: 200px; height: 200px;

View File

@@ -1,9 +0,0 @@
@import "about-app";
@import "loading-screen";
@import "screen-picker";
html, body {
margin: 0;
height: 100%;
font-family: @font-family;
}

View File

@@ -20,6 +20,12 @@
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma"); src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
} }
body {
overflow: hidden;
background-color: white;
margin: 0;
height: 100%;
}
.ScreenPicker { .ScreenPicker {

View File

@@ -0,0 +1,86 @@
@white: rgba(255,255,255, 1);
@background: #6b717c;
@button-color: #303237;
@stop-sharing-background: #107ccc;
@font: "system";
@font-face {
font-family: system;
font-style: normal;
src: local(".SFNSText"), local(".HelveticaNeueDeskInterface"), local("Ubuntu Light"), local("Segoe UI"), local("Roboto"), local("Tahoma");
}
body {
font-family: @font;
font-size: 13px;
padding: 0;
margin: 10px;
user-select: none;
-webkit-app-region: drag;
background: rgba(255, 255, 255, 0.9);
}
.ScreenSharingIndicator {
.window-border {
border: 2px solid rgba(68, 68, 68, 1);
}
.buttons {
float: right;
font-size: 12px;
font-weight: bold;
}
.stop-sharing-button {
text-transform: uppercase;
background: @stop-sharing-background;
color: @white;
border: none;
font-size: inherit;
border-radius: 14px;
height: 28px;
padding: 6px 14px;
outline: none;
cursor: pointer;
-webkit-app-region: no-drag;
}
.hide-button {
text-decoration: none;
text-transform: uppercase;
color: @background;
margin-right: 29px;
-webkit-app-region: no-drag;
}
.text-label {
position: relative;
top: -2px;
left: 16px;
font-size: 14px;
}
.drag-area {
display: inline-block;
width: 8px;
height: 22px;
position: relative;
cursor: move;
top: 3px;
text-transform: uppercase;
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 1px, rgba(0, 0, 0, 0.12) 1px, rgba(0, 0, 0, 0.12) 2px);
}
.mac {
.hide-button {
color: @button-color;
}
.stop-sharing-button {
padding: 6px 18px;
}
}
}