mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
refactor code and remove old JS code
This commit is contained in:
parent
2c356ab641
commit
9e347bab77
@ -1,8 +0,0 @@
|
||||
build
|
||||
config
|
||||
coverage
|
||||
dist
|
||||
installer
|
||||
node_modules
|
||||
js/preload/_*.js
|
||||
tests/**
|
31
.eslintrc
31
.eslintrc
@ -1,31 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"airbnb-base/rules/best-practices",
|
||||
"airbnb-base/rules/errors",
|
||||
"airbnb-base/rules/node",
|
||||
"airbnb-base/rules/strict",
|
||||
"airbnb-base/rules/variables"
|
||||
],
|
||||
"rules": {
|
||||
"comma-dangle": 0,
|
||||
"vars-on-top": 0,
|
||||
"one-var": 0,
|
||||
"indent": [ 2, 4, {
|
||||
"SwitchCase": 1
|
||||
} ],
|
||||
"space-in-parens": 0,
|
||||
"func-names" : 0,
|
||||
"eol-last": 0,
|
||||
"no-use-before-define": 0,
|
||||
"strict": 0
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"SYM_API": true,
|
||||
"ssf": true
|
||||
}
|
||||
}
|
2
NOTICE
2
NOTICE
@ -1,2 +1,2 @@
|
||||
SymphonyElectron
|
||||
Copyright 2016 Symphony Software Foundation
|
||||
Copyright 2019 Symphony Software Foundation
|
||||
|
@ -7,7 +7,7 @@ Describe the problem or feature in addition to a link to the
|
||||
Describe the approach you've taken to implement this change / resolve the issue
|
||||
|
||||
## Related PRs
|
||||
List related PRs against other branches / repositories:
|
||||
List related PRs against other branches/repositories:
|
||||
|
||||
branch | PR
|
||||
------ | ------
|
||||
|
@ -1,52 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>About Symphony</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.name {
|
||||
flex: 1;
|
||||
font-size: 1.3em;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
flex: 1;
|
||||
font-size: 1em;
|
||||
color: #2f2f2f;
|
||||
}
|
||||
|
||||
.copyright-text {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
font-size: 0.6em;
|
||||
color: #7f7f7f;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 20px
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<img class="logo" src="symphony-logo.png">
|
||||
<span id="app-name" class="name">Symphony</span>
|
||||
<span id="version" class="version-text"></span>
|
||||
<span id="copyright" class="copyright-text"></span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,144 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { version, clientVersion, buildNumber } = require('../../package.json');
|
||||
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
const { isMac } = require('../utils/misc');
|
||||
|
||||
let aboutWindow;
|
||||
|
||||
let windowConfig = {
|
||||
width: 350,
|
||||
height: 260,
|
||||
show: false,
|
||||
modal: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: true,
|
||||
resizable: false,
|
||||
fullscreenable: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'renderer.js'),
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* method to get the HTML template path
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'about-app.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'about-window: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
return 'file://' + templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the about application window for a specific window
|
||||
* @param {String} windowName - name of the window upon
|
||||
* which this window should show
|
||||
*/
|
||||
function openAboutWindow(windowName) {
|
||||
|
||||
// This prevents creating multiple instances of the
|
||||
// about window
|
||||
if (aboutWindow) {
|
||||
if (aboutWindow.isMinimized()) {
|
||||
aboutWindow.restore();
|
||||
}
|
||||
aboutWindow.focus();
|
||||
return;
|
||||
}
|
||||
let allWindows = BrowserWindow.getAllWindows();
|
||||
allWindows = allWindows.find((window) => { return window.winName === windowName });
|
||||
|
||||
// if we couldn't find any window matching the window name
|
||||
// it will render as a new window
|
||||
if (allWindows) {
|
||||
windowConfig.parent = allWindows;
|
||||
}
|
||||
|
||||
aboutWindow = new BrowserWindow(windowConfig);
|
||||
aboutWindow.setVisibleOnAllWorkspaces(true);
|
||||
aboutWindow.loadURL(getTemplatePath());
|
||||
|
||||
// sets the AlwaysOnTop property for the about window
|
||||
// if the main window's AlwaysOnTop is true
|
||||
let focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.isAlwaysOnTop()) {
|
||||
aboutWindow.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
aboutWindow.once('ready-to-show', () => {
|
||||
aboutWindow.show();
|
||||
});
|
||||
|
||||
aboutWindow.webContents.on('did-finish-load', () => {
|
||||
// initialize crash reporter
|
||||
initCrashReporterMain({ process: 'about app window' });
|
||||
initCrashReporterRenderer(aboutWindow, { process: 'render | about app window' });
|
||||
aboutWindow.webContents.send('versionInfo', {
|
||||
version,
|
||||
clientVersion,
|
||||
buildNumber,
|
||||
i18n: i18n.getMessageFor('AboutSymphony')
|
||||
});
|
||||
if (!isMac) {
|
||||
// prevents from displaying menu items when "alt" key is pressed
|
||||
aboutWindow.setMenu(null);
|
||||
}
|
||||
});
|
||||
|
||||
aboutWindow.webContents.on('crashed', function (event, killed) {
|
||||
|
||||
log.send(logLevels.INFO, `About Window crashed! Killed? ${killed}`);
|
||||
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Renderer Process Crashed'),
|
||||
message: i18n.getMessageFor('Oops! Looks like we have had a crash.'),
|
||||
buttons: ['Close']
|
||||
};
|
||||
|
||||
electron.dialog.showMessageBox(options, function () {
|
||||
if (aboutWindow && !aboutWindow.isDestroyed()) {
|
||||
aboutWindow.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
aboutWindow.on('close', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
aboutWindow.on('closed', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a window
|
||||
*/
|
||||
function destroyWindow() {
|
||||
aboutWindow = null;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
openAboutWindow: openAboutWindow
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
'use strict';
|
||||
const { remote, ipcRenderer, crashReporter } = require('electron');
|
||||
|
||||
renderDom();
|
||||
|
||||
/**
|
||||
* Method that renders application data
|
||||
*/
|
||||
function renderDom() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const applicationName = remote.app.getName() || 'Symphony';
|
||||
let appName = document.getElementById('app-name');
|
||||
let copyright = document.getElementById('copyright');
|
||||
|
||||
appName.innerHTML = applicationName;
|
||||
copyright.innerHTML = `Copyright © ${new Date().getFullYear()} ${applicationName}`
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('versionInfo', (event, versionInfo) => {
|
||||
const versionText = document.getElementById('version');
|
||||
const { version, clientVersion, buildNumber, i18n } = versionInfo;
|
||||
|
||||
document.title = i18n['About Symphony'] || 'About Symphony';
|
||||
|
||||
if (versionText) {
|
||||
versionText.innerHTML = version ? `${i18n.Version || 'Version'} ${clientVersion}-${version} (${buildNumber})` : 'N/A';
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
// note: this is a workaround until
|
||||
// https://github.com/electron/electron/issues/8841
|
||||
// is fixed on the electron. where 'will-navigate'
|
||||
// is never fired in sandbox mode
|
||||
//
|
||||
// This is required in order to prevent from loading
|
||||
// dropped content
|
||||
window.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
@ -1,126 +0,0 @@
|
||||
'use strict';
|
||||
const electron = require('electron');
|
||||
const { app } = electron;
|
||||
const throttle = require('../utils/throttle');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
let setIsAutoReloadFnc;
|
||||
let getIsOnlineFnc;
|
||||
if (!process.env.ELECTRON_QA) {
|
||||
/* eslint-disable global-require */
|
||||
const { getIsOnline, setIsAutoReload } = require('../windowMgr');
|
||||
getIsOnlineFnc = getIsOnline;
|
||||
setIsAutoReloadFnc = setIsAutoReload;
|
||||
/* eslint-enable global-require */
|
||||
}
|
||||
|
||||
let maxIdleTime;
|
||||
let activityWindow;
|
||||
let intervalId;
|
||||
let throttleActivity;
|
||||
/**
|
||||
* Check if the user is idle
|
||||
*/
|
||||
function activityDetection() {
|
||||
|
||||
if (app.isReady()) {
|
||||
|
||||
electron.powerMonitor.querySystemIdleTime((time) => {
|
||||
// sent Idle time in milliseconds
|
||||
let idleTime = time * 1000 + 1; //ensuring that zero wont be sent
|
||||
if (idleTime != null && idleTime !== undefined) {
|
||||
|
||||
if (idleTime < maxIdleTime) {
|
||||
|
||||
let systemActivity = { isUserIdle: false, systemIdleTime: idleTime };
|
||||
if (systemActivity && !systemActivity.isUserIdle && typeof systemActivity.systemIdleTime === 'number') {
|
||||
return self.send({ systemIdleTime: systemActivity.systemIdleTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
// If idle for more than 4 mins, monitor system idle status every second
|
||||
if (!intervalId) {
|
||||
self.monitorUserActivity();
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring user activity status.
|
||||
* Run every 4 mins to check user idle status
|
||||
*/
|
||||
function initiateActivityDetection() {
|
||||
|
||||
if (!throttleActivity) {
|
||||
throttleActivity = throttle(maxIdleTime, activityDetection);
|
||||
setInterval(throttleActivity, maxIdleTime);
|
||||
}
|
||||
self.activityDetection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor system idle status every second
|
||||
*/
|
||||
function monitorUserActivity() {
|
||||
intervalId = setInterval(monitor, 1000);
|
||||
|
||||
function monitor() {
|
||||
if (app.isReady()) {
|
||||
electron.powerMonitor.querySystemIdleTime((time) => {
|
||||
// sent Idle time in milliseconds
|
||||
let idleTime = time * 1000 + 1; //ensuring that zero wont be sent
|
||||
if (idleTime != null && idleTime !== undefined) {
|
||||
if (idleTime < maxIdleTime && typeof getIsOnlineFnc === 'function' && getIsOnlineFnc()) {
|
||||
// If system is active, send an update to the app bridge and clear the timer
|
||||
self.activityDetection();
|
||||
if (typeof setIsAutoReloadFnc === 'function') {
|
||||
setIsAutoReloadFnc(false);
|
||||
}
|
||||
clearInterval(intervalId);
|
||||
intervalId = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends user activity status from main process to activity detection hosted by
|
||||
* renderer process. Allows main process to use activity detection
|
||||
* provided by JS.
|
||||
* @param {object} data - data as object
|
||||
*/
|
||||
function send(data) {
|
||||
if (activityWindow && data) {
|
||||
log.send(logLevels.INFO, 'activity occurred at time= ' + new Date().toUTCString());
|
||||
activityWindow.send('activity', {
|
||||
systemIdleTime: data.systemIdleTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the activity's window
|
||||
* @param period
|
||||
* @param win
|
||||
*/
|
||||
function setActivityWindow(period, win) {
|
||||
maxIdleTime = period;
|
||||
activityWindow = win;
|
||||
// Initiate activity detection to monitor user activity status
|
||||
initiateActivityDetection();
|
||||
}
|
||||
|
||||
// Exporting this for unit tests
|
||||
const self = {
|
||||
send: send,
|
||||
setActivityWindow: setActivityWindow,
|
||||
activityDetection: activityDetection,
|
||||
monitorUserActivity: monitorUserActivity,
|
||||
};
|
||||
module.exports = self;
|
@ -1,60 +0,0 @@
|
||||
const AutoLaunch = require('auto-launch');
|
||||
|
||||
// Local Dependencies
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { readConfigFileSync } = require('../config.js');
|
||||
const { isMac } = require('../utils/misc.js');
|
||||
|
||||
const globalConfigData = readConfigFileSync();
|
||||
const props = isMac ? {
|
||||
name: 'Symphony',
|
||||
mac: {
|
||||
useLaunchAgent: true,
|
||||
},
|
||||
path: process.execPath,
|
||||
} : {
|
||||
name: 'Symphony',
|
||||
path: getAutoLaunchPath() || process.execPath,
|
||||
};
|
||||
|
||||
class AutoLaunchController extends AutoLaunch {
|
||||
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable auto launch
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
enableAutoLaunch() {
|
||||
log.send(logLevels.INFO, `Enabling auto launch!`);
|
||||
return this.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto launch
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
disableAutoLaunch() {
|
||||
log.send(logLevels.INFO, `Disabling auto launch!`);
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace forward slash in the path to backward slash
|
||||
* @return {any}
|
||||
*/
|
||||
function getAutoLaunchPath() {
|
||||
const autoLaunchPath = globalConfigData && globalConfigData.autoLaunchPath || null;
|
||||
return autoLaunchPath ? autoLaunchPath.replace(/\//g, '\\') : null;
|
||||
}
|
||||
|
||||
const autoLaunchInstance = new AutoLaunchController(props);
|
||||
|
||||
module.exports = {
|
||||
enable: autoLaunchInstance.enableAutoLaunch.bind(autoLaunchInstance),
|
||||
disable: autoLaunchInstance.disableAutoLaunch.bind(autoLaunchInstance)
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const nativeImage = electron.nativeImage;
|
||||
|
||||
const { isMac } = require('./utils/misc.js');
|
||||
const windowMgr = require('./windowMgr.js');
|
||||
const maxCount = 1e8;
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
/**
|
||||
* Shows the badge count
|
||||
* @param count
|
||||
*/
|
||||
function show(count) {
|
||||
if (typeof count !== 'number') {
|
||||
log.send(logLevels.WARN, 'badgeCount: invalid func arg, must be a number: ' + count);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
// too big of a number here and setBadgeCount crashes
|
||||
app.setBadgeCount(Math.min(maxCount, count));
|
||||
return;
|
||||
}
|
||||
|
||||
// handle ms windows...
|
||||
const mainWindow = windowMgr.getMainWindow();
|
||||
|
||||
if (mainWindow) {
|
||||
if (count > 0) {
|
||||
// get badge img from renderer process, will return
|
||||
// img dataUrl in setDataUrl func.
|
||||
mainWindow.send('createBadgeDataUrl', { count: count });
|
||||
} else {
|
||||
// clear badge count icon
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data url
|
||||
* @param dataUrl
|
||||
* @param count
|
||||
*/
|
||||
function setDataUrl(dataUrl, count) {
|
||||
const mainWindow = windowMgr.getMainWindow();
|
||||
if (mainWindow && dataUrl && count) {
|
||||
let img = nativeImage.createFromDataURL(dataUrl);
|
||||
// for accessibility screen readers
|
||||
const desc = 'Symphony has ' + count + ' unread messages';
|
||||
mainWindow.setOverlayIcon(img, desc);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
show: show,
|
||||
setDataUrl: setDataUrl
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title data-i18n-text="Authentication Request"></title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 20px
|
||||
}
|
||||
|
||||
span {
|
||||
padding-top: 10px;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.hostname {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.credentials-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
form {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<span data-i18n-text="Please provide your login credentials for:"></span>
|
||||
<span id="hostname" class="hostname" data-i18n-text="hostname"></span>
|
||||
<span id="credentialsError" class="credentials-error" data-i18n-text="Invalid user name/password"></span>
|
||||
<form id="basicAuth" name="Basic Auth" action="Login">
|
||||
<table class="form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="username-text" data-i18n-text="User name:"></td>
|
||||
<td>
|
||||
<input id="username" name="username" title="Username" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="password-text" data-i18n-text="Password:"></td>
|
||||
<td>
|
||||
<input id="password" type="password" title="Password" required>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<div class="button-container">
|
||||
<button type="submit" id="login" data-i18n-text="Log In"></button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button type="button" id="cancel" data-i18n-text="Cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,177 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const ipc = electron.ipcMain;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { isMac } = require('../utils/misc');
|
||||
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
|
||||
let basicAuthWindow;
|
||||
|
||||
const local = {};
|
||||
|
||||
let windowConfig = {
|
||||
width: 360,
|
||||
height: isMac ? 270 : 295,
|
||||
show: false,
|
||||
modal: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: true,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'renderer.js'),
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* method to get the HTML template path
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'basic-auth.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'basic-auth: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
return 'file://' + templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the basic auth window for authentication
|
||||
* @param {String} windowName - name of the window upon which this window should show
|
||||
* @param {String} hostname - name of the website that requires authentication
|
||||
* @param {boolean} isValidCredentials - false if invalid username or password
|
||||
* @param {Function} clearSettings
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function openBasicAuthWindow(windowName, hostname, isValidCredentials, clearSettings, callback) {
|
||||
|
||||
// Register callback function
|
||||
if (typeof callback === 'function') {
|
||||
local.authCallback = callback;
|
||||
}
|
||||
// Register close function
|
||||
if (typeof clearSettings === 'function') {
|
||||
local.clearSettings = clearSettings;
|
||||
}
|
||||
|
||||
// This prevents creating multiple instances of the
|
||||
// basic auth window
|
||||
if (basicAuthWindow) {
|
||||
if (basicAuthWindow.isMinimized()) {
|
||||
basicAuthWindow.restore();
|
||||
}
|
||||
basicAuthWindow.focus();
|
||||
return;
|
||||
}
|
||||
let allWindows = BrowserWindow.getAllWindows();
|
||||
allWindows = allWindows.find((window) => { return window.winName === windowName });
|
||||
|
||||
// if we couldn't find any window matching the window name
|
||||
// it will render as a new window
|
||||
if (allWindows) {
|
||||
windowConfig.parent = allWindows;
|
||||
}
|
||||
|
||||
basicAuthWindow = new BrowserWindow(windowConfig);
|
||||
basicAuthWindow.setVisibleOnAllWorkspaces(true);
|
||||
basicAuthWindow.loadURL(getTemplatePath());
|
||||
|
||||
// sets the AlwaysOnTop property for the basic auth window
|
||||
// if the main window's AlwaysOnTop is true
|
||||
let focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.isAlwaysOnTop()) {
|
||||
basicAuthWindow.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
basicAuthWindow.once('ready-to-show', () => {
|
||||
basicAuthWindow.show();
|
||||
});
|
||||
|
||||
basicAuthWindow.webContents.on('did-finish-load', () => {
|
||||
const basicAuthContent = i18n.getMessageFor('BasicAuth');
|
||||
basicAuthWindow.webContents.send('i18n-basic-auth', basicAuthContent);
|
||||
// initialize crash reporter
|
||||
initCrashReporterMain({ process: 'basic auth window' });
|
||||
initCrashReporterRenderer(basicAuthWindow, { process: 'render | basic auth window' });
|
||||
basicAuthWindow.webContents.send('hostname', hostname);
|
||||
basicAuthWindow.webContents.send('isValidCredentials', isValidCredentials);
|
||||
if (!isMac) {
|
||||
// prevents from displaying menu items when "alt" key is pressed
|
||||
basicAuthWindow.setMenu(null);
|
||||
}
|
||||
});
|
||||
|
||||
basicAuthWindow.webContents.on('crashed', function (event, killed) {
|
||||
|
||||
log.send(logLevels.INFO, `Basic Auth Window crashed! Killed? ${killed}`);
|
||||
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Renderer Process Crashed'),
|
||||
message: i18n.getMessageFor('Oops! Looks like we have had a crash.'),
|
||||
buttons: ['Close']
|
||||
};
|
||||
|
||||
electron.dialog.showMessageBox(options, function () {
|
||||
closeAuthWindow(true);
|
||||
});
|
||||
});
|
||||
|
||||
basicAuthWindow.on('close', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
basicAuthWindow.on('closed', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
}
|
||||
|
||||
ipc.on('login', (event, args) => {
|
||||
if (typeof args === 'object' && typeof local.authCallback === 'function') {
|
||||
local.authCallback(args.username, args.password);
|
||||
closeAuthWindow(false);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('close-basic-auth', () => {
|
||||
closeAuthWindow(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Destroys a window
|
||||
*/
|
||||
function destroyWindow() {
|
||||
basicAuthWindow = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to close the auth window
|
||||
* @param {boolean} clearSettings - Whether to clear the auth settings
|
||||
*/
|
||||
function closeAuthWindow(clearSettings) {
|
||||
if (clearSettings && typeof local.clearSettings === 'function') {
|
||||
local.clearSettings();
|
||||
}
|
||||
|
||||
if (basicAuthWindow) {
|
||||
basicAuthWindow.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openBasicAuthWindow: openBasicAuthWindow
|
||||
};
|
@ -1,83 +0,0 @@
|
||||
'use strict';
|
||||
const { ipcRenderer, crashReporter } = require('electron');
|
||||
|
||||
renderDom();
|
||||
|
||||
/**
|
||||
* Method that renders application data
|
||||
*/
|
||||
function renderDom() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadContent();
|
||||
});
|
||||
}
|
||||
|
||||
function loadContent() {
|
||||
let basicAuth = document.getElementById('basicAuth');
|
||||
let cancel = document.getElementById('cancel');
|
||||
|
||||
if (basicAuth) {
|
||||
basicAuth.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
submitForm();
|
||||
};
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
cancel.addEventListener('click', () => {
|
||||
ipcRenderer.send('close-basic-auth');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that gets invoked on submitting the form
|
||||
*/
|
||||
function submitForm() {
|
||||
let username = document.getElementById('username').value;
|
||||
let password = document.getElementById('password').value;
|
||||
|
||||
if (username && password) {
|
||||
ipcRenderer.send('login', { username, password });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hosts name
|
||||
*/
|
||||
ipcRenderer.on('hostname', (event, host) => {
|
||||
let hostname = document.getElementById('hostname');
|
||||
|
||||
if (hostname){
|
||||
hostname.innerHTML = host || 'unknown';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Triggered if user credentials are invalid
|
||||
*/
|
||||
ipcRenderer.on('isValidCredentials', (event, isValidCredentials) => {
|
||||
let credentialsError = document.getElementById('credentialsError');
|
||||
|
||||
if (credentialsError){
|
||||
credentialsError.style.display = isValidCredentials ? 'none' : 'block'
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('i18n-basic-auth', (event, content) => {
|
||||
if (content && typeof content === 'object') {
|
||||
const i18nNodes = document.querySelectorAll('[data-i18n-text]');
|
||||
|
||||
for (let node of i18nNodes) {
|
||||
if (node.attributes['data-i18n-text'] && node.attributes['data-i18n-text'].value) {
|
||||
node.innerText = content[node.attributes['data-i18n-text'].value] || node.attributes['data-i18n-text'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const windowMgr = require('./windowMgr.js');
|
||||
const { getConfigField } = require('./config.js');
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
/**
|
||||
* Method that checks if user has enabled the bring to front feature
|
||||
* if so then activates the main window
|
||||
* @param {String} windowName - Name of the window to activate
|
||||
* @param {String} reason - The reason for which the window is to be activated
|
||||
*/
|
||||
function bringToFront(windowName, reason) {
|
||||
|
||||
getConfigField('bringToFront')
|
||||
.then((bringToFrontSetting) => {
|
||||
if (typeof bringToFrontSetting === 'boolean' && bringToFrontSetting) {
|
||||
log.send(logLevels.INFO, 'Window has been activated for: ' + reason);
|
||||
windowMgr.activate(windowName || 'main', false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
log.send(logLevels.ERROR, 'Could not read bringToFront field from config error= ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
bringToFront: bringToFront
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const nodePath = require('path');
|
||||
const electron = require('electron');
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
const cacheCheckFilename = 'CacheCheck';
|
||||
const cacheCheckFilePath = nodePath.join(electron.app.getPath('userData'), cacheCheckFilename);
|
||||
|
||||
function handleCacheFailureCheckOnStartup() {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
if (fs.existsSync(cacheCheckFilePath)) {
|
||||
log.send(logLevels.INFO, `Cache check file exists, so not clearing cache!`);
|
||||
fs.unlinkSync(cacheCheckFilePath);
|
||||
resolve();
|
||||
} else {
|
||||
log.send(logLevels.INFO, `Cache check file does not exist, we are clearing the cache!`);
|
||||
electron.session.defaultSession.clearCache(() => {
|
||||
log.send(logLevels.INFO, `Cleared cache!`);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function handleCacheFailureCheckOnExit() {
|
||||
log.send(logLevels.INFO, `Clean exit! Creating cache check file!`);
|
||||
fs.writeFileSync(cacheCheckFilePath, "");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleCacheFailureCheckOnStartup: handleCacheFailureCheckOnStartup,
|
||||
handleCacheFailureCheckOnExit: handleCacheFailureCheckOnExit
|
||||
};
|
420
js/config.js
420
js/config.js
@ -1,420 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const omit = require('lodash.omit');
|
||||
const pick = require('lodash.pick');
|
||||
const difference = require('lodash.difference');
|
||||
|
||||
const isDevEnv = require('./utils/misc.js').isDevEnv;
|
||||
const isMac = require('./utils/misc.js').isMac;
|
||||
const getRegistry = require('./utils/getRegistry.js');
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
const { buildNumber } = require('../package.json');
|
||||
|
||||
const configFileName = 'Symphony.config';
|
||||
|
||||
// cached config when first reading files. initially undefined and will be
|
||||
// updated when read from disk.
|
||||
let userConfig;
|
||||
let globalConfig;
|
||||
|
||||
let ignoreSettings = [
|
||||
'minimizeOnClose',
|
||||
'launchOnStartup',
|
||||
'alwaysOnTop',
|
||||
'url',
|
||||
'memoryRefresh',
|
||||
'bringToFront',
|
||||
'isCustomTitleBar'
|
||||
];
|
||||
|
||||
/**
|
||||
* Tries to read given field from user config file, if field doesn't exist
|
||||
* then tries reading from global config. User config is stord in directory:
|
||||
* app.getPath('userData') and file called Symphony.config. Global config is
|
||||
* stored in file Symphony.config in directory where executable gets installed.
|
||||
*
|
||||
* Config is a flat key/value file.
|
||||
* e.g. { url: 'https://my.symphony.com', }
|
||||
*
|
||||
* @param {String} fieldName Name of field to try fetching
|
||||
* @return {Promise} Returns promise that will succeed with field
|
||||
* value if found in either user or global config. Otherwise will fail promise.
|
||||
*/
|
||||
function getConfigField(fieldName) {
|
||||
return getUserConfigField(fieldName)
|
||||
.then((value) => {
|
||||
// got value from user config
|
||||
return value;
|
||||
}, () => {
|
||||
// failed to get value from user config, so try global config
|
||||
return getGlobalConfigField(fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific user config value for a field
|
||||
* @param fieldName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function getUserConfigField(fieldName) {
|
||||
return readUserConfig().then((config) => {
|
||||
if (typeof fieldName === 'string' && fieldName in config) {
|
||||
return config[fieldName];
|
||||
}
|
||||
|
||||
throw new Error('field does not exist in user config: ' + fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user config file and returns all the attributes
|
||||
* @param customConfigPath
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function readUserConfig(customConfigPath) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (userConfig) {
|
||||
resolve(userConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
let configPath = customConfigPath;
|
||||
|
||||
if (!configPath) {
|
||||
configPath = path.join(app.getPath('userData'), configFileName);
|
||||
}
|
||||
|
||||
log.send(logLevels.INFO, `config path ${configPath}`);
|
||||
|
||||
fs.readFile(configPath, 'utf8', (err, data) => {
|
||||
|
||||
if (err) {
|
||||
log.send(logLevels.INFO, `cannot open user config file ${configPath}, error is ${err}`);
|
||||
reject(new Error(`cannot open user config file ${configPath}, error is ${err}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// data is the contents of the text file we just read
|
||||
userConfig = JSON.parse(data);
|
||||
resolve(userConfig);
|
||||
} catch (e) {
|
||||
log.send(logLevels.INFO, `cannot parse user config data ${data}, error is ${e}`);
|
||||
reject(new Error(`cannot parse user config data ${data}, error is ${e}`));
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific global config value for a field
|
||||
* @param fieldName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function getGlobalConfigField(fieldName) {
|
||||
return readGlobalConfig().then((config) => {
|
||||
if (typeof fieldName === 'string' && fieldName in config) {
|
||||
return config[fieldName];
|
||||
}
|
||||
|
||||
throw new Error('field does not exist in global config: ' + fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* reads global configuration file: config/Symphony.config. this file is
|
||||
* hold items (such as the start url) that are intended to be used as
|
||||
* global (or default) values for all users running this app. for production
|
||||
* this file is located relative to the executable - it is placed there by
|
||||
* the installer. this makes the file easily modifable by admin (or person who
|
||||
* installed app). for dev env, the file is read directly from packed asar file.
|
||||
*/
|
||||
function readGlobalConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (globalConfig) {
|
||||
resolve(globalConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
let configPath;
|
||||
let globalConfigFileName = path.join('config', configFileName);
|
||||
if (isDevEnv) {
|
||||
// for dev env, get config file from asar
|
||||
configPath = path.join(app.getAppPath(), globalConfigFileName);
|
||||
} else {
|
||||
// for non-dev, config file is placed by installer relative to exe.
|
||||
// this is so the config can be easily be changed post install.
|
||||
let execPath = path.dirname(app.getPath('exe'));
|
||||
// for mac exec is stored in subdir, for linux/windows config
|
||||
// dir is in the same location.
|
||||
configPath = path.join(execPath, isMac ? '..' : '', globalConfigFileName);
|
||||
}
|
||||
|
||||
fs.readFile(configPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(new Error('cannot open global config file: ' + configPath + ', error: ' + err));
|
||||
} else {
|
||||
try {
|
||||
// data is the contents of the text file we just read
|
||||
globalConfig = JSON.parse(data);
|
||||
} catch (e) {
|
||||
reject(new Error('can not parse config file data: ' + data + ', error: ' + err));
|
||||
}
|
||||
getRegistry('PodUrl')
|
||||
.then((url) => {
|
||||
globalConfig.url = url;
|
||||
resolve(globalConfig);
|
||||
}).catch(() => {
|
||||
resolve(globalConfig);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user config with given field with new value
|
||||
* @param {String} fieldName Name of field in config to be added/changed.
|
||||
* @param {Object} newValue Object to replace given value
|
||||
* @return {Promise} Promise that resolves/rejects when file write is complete.
|
||||
*/
|
||||
function updateConfigField(fieldName, newValue) {
|
||||
return readUserConfig()
|
||||
.then((config) => {
|
||||
return saveUserConfig(fieldName, newValue, config);
|
||||
}, () => {
|
||||
// in case config doesn't exist, can't read or is corrupted.
|
||||
// add configBuildNumber - just in case in future we need to provide
|
||||
// upgrade capabilities.
|
||||
return saveUserConfig(fieldName, newValue, {
|
||||
configBuildNumber: buildNumber || '0',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an updated value to the user config
|
||||
* @param fieldName
|
||||
* @param newValue
|
||||
* @param oldConfig
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function saveUserConfig(fieldName, newValue, oldConfig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let configPath = path.join(app.getPath('userData'), configFileName);
|
||||
|
||||
if (!oldConfig || !fieldName) {
|
||||
reject(new Error('can not save config, invalid input'));
|
||||
return;
|
||||
}
|
||||
|
||||
// clone and set new value
|
||||
let newConfig = Object.assign({}, oldConfig);
|
||||
newConfig[fieldName] = newValue;
|
||||
|
||||
let jsonNewConfig = JSON.stringify(newConfig, null, ' ');
|
||||
|
||||
fs.writeFile(configPath, jsonNewConfig, 'utf8', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
userConfig = newConfig;
|
||||
resolve(newConfig);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the existing user config settings by removing
|
||||
* 'minimizeOnClose', 'launchOnStartup', 'url' and 'alwaysOnTop'
|
||||
* @param {Object} oldUserConfig the old user config object
|
||||
*/
|
||||
function updateUserConfig(oldUserConfig) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// create a new object from the old user config
|
||||
// by ommitting the user related settings from
|
||||
// the old user config
|
||||
log.send(logLevels.INFO, `old user config string ${JSON.stringify(oldUserConfig)}`);
|
||||
let newUserConfig = omit(oldUserConfig, ignoreSettings);
|
||||
let newUserConfigString = JSON.stringify(newUserConfig, null, 2);
|
||||
|
||||
log.send(logLevels.INFO, `new config string ${newUserConfigString}`);
|
||||
|
||||
// get the user config path
|
||||
let userConfigFile;
|
||||
userConfigFile = path.join(app.getPath('userData'), configFileName);
|
||||
|
||||
if (!userConfigFile) {
|
||||
reject(new Error(`user config file doesn't exist`));
|
||||
return;
|
||||
}
|
||||
|
||||
// write the new user config changes to the user config file
|
||||
fs.writeFile(userConfigFile, newUserConfigString, 'utf-8', (err) => {
|
||||
if (err) {
|
||||
reject(new Error(`Failed to update user config error: ${err}`));
|
||||
return;
|
||||
}
|
||||
userConfig = newUserConfig;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates user config on first time launch
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function updateUserConfigOnLaunch(resolve, reject) {
|
||||
// we get the user config path using electron
|
||||
const userConfigFile = path.join(app.getPath('userData'), configFileName);
|
||||
|
||||
// In case the file exists, we remove it so that all the
|
||||
// values are fetched from the global config
|
||||
// https://perzoinc.atlassian.net/browse/ELECTRON-126
|
||||
return readUserConfig(userConfigFile).then((data) => {
|
||||
// Add build number info to the user config data
|
||||
const updatedData = Object.assign(data || {}, { configBuildNumber: buildNumber || '0' });
|
||||
|
||||
updateUserConfig(updatedData)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}).catch((err) => {
|
||||
return reject(err);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that tries to grab multiple config field from user config
|
||||
* if field doesn't exist tries reading from global config
|
||||
*
|
||||
* @param {Array} fieldNames - array of config filed names
|
||||
* @returns {Promise} - object all the config data from user and global config
|
||||
*/
|
||||
function getMultipleConfigField(fieldNames) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let userConfigData;
|
||||
|
||||
if (!fieldNames && fieldNames.length < 0) {
|
||||
reject(new Error('cannot read config file, invalid fields'));
|
||||
return;
|
||||
}
|
||||
|
||||
// reads user config data
|
||||
readUserConfig().then((config) => {
|
||||
userConfigData = pick(config, fieldNames);
|
||||
let userConfigKeys = userConfigData ? Object.keys(userConfigData) : undefined;
|
||||
|
||||
/**
|
||||
* Condition to validate data from user config,
|
||||
* if all the required fields are not present
|
||||
* this tries to fetch the remaining fields from global config
|
||||
*/
|
||||
if (!userConfigKeys || userConfigKeys.length < fieldNames.length) {
|
||||
|
||||
// remainingConfig - config field that are not present in the user config
|
||||
let remainingConfig = difference(fieldNames, userConfigKeys);
|
||||
|
||||
if (remainingConfig && Object.keys(remainingConfig).length > 0) {
|
||||
readGlobalConfig().then((globalConfigData) => {
|
||||
// assigns the remaining fields from global config to the user config
|
||||
userConfigData = Object.assign(userConfigData, pick(globalConfigData, remainingConfig));
|
||||
resolve(userConfigData);
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
resolve(userConfigData);
|
||||
}
|
||||
}).catch(() => {
|
||||
// This reads global config if there was any
|
||||
// error while reading user config
|
||||
readGlobalConfig().then((config) => {
|
||||
userConfigData = pick(config, fieldNames);
|
||||
resolve(userConfigData);
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the cached config
|
||||
*/
|
||||
function clearCachedConfigs() {
|
||||
userConfig = null;
|
||||
globalConfig = null;
|
||||
}
|
||||
|
||||
function readConfigFileSync() {
|
||||
|
||||
let configPath;
|
||||
let globalConfigFileName = path.join('config', configFileName);
|
||||
if (isDevEnv) {
|
||||
// for dev env, get config file from asar
|
||||
configPath = path.join(app.getAppPath(), globalConfigFileName);
|
||||
} else {
|
||||
// for non-dev, config file is placed by installer relative to exe.
|
||||
// this is so the config can be easily be changed post install.
|
||||
let execPath = path.dirname(app.getPath('exe'));
|
||||
// for mac exec is stored in subdir, for linux/windows config
|
||||
// dir is in the same location.
|
||||
configPath = path.join(execPath, isMac ? '..' : '', globalConfigFileName);
|
||||
}
|
||||
|
||||
let data = fs.readFileSync(configPath);
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'config: parsing config failed: ' + err);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
const readConfigFromFile = (key) => {
|
||||
let data = readConfigFileSync();
|
||||
return data[key];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
configFileName,
|
||||
|
||||
getConfigField,
|
||||
|
||||
updateConfigField,
|
||||
updateUserConfigOnLaunch,
|
||||
|
||||
getMultipleConfigField,
|
||||
|
||||
// items below here are only exported for testing, do NOT use!
|
||||
saveUserConfig,
|
||||
clearCachedConfigs,
|
||||
|
||||
readConfigFileSync,
|
||||
readConfigFromFile,
|
||||
|
||||
// use only if you specifically need to read global config fields
|
||||
getGlobalConfigField,
|
||||
getUserConfigField
|
||||
};
|
@ -1,85 +0,0 @@
|
||||
const { crashReporter } = require('electron');
|
||||
const { getMultipleConfigField } = require('./config.js');
|
||||
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
const configFields = ['url', 'crashReporter'];
|
||||
let crashReporterData;
|
||||
|
||||
/**
|
||||
* Method that returns all the required field for crash reporter
|
||||
*
|
||||
* @param extras {object}
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
function getCrashReporterConfig(extras) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (crashReporterData && crashReporterData.companyName) {
|
||||
crashReporterData.extra = Object.assign(crashReporterData.extra, extras);
|
||||
resolve(crashReporterData);
|
||||
return;
|
||||
}
|
||||
|
||||
getMultipleConfigField(configFields)
|
||||
.then((data) => {
|
||||
|
||||
if (!data && !data.crashReporter && !data.crashReporter.companyName) {
|
||||
reject(new Error('Company name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
crashReporterData = {
|
||||
companyName: data.crashReporter.companyName,
|
||||
submitURL: data.crashReporter.submitURL,
|
||||
uploadToServer: data.crashReporter.uploadToServer,
|
||||
extra: Object.assign(
|
||||
{podUrl: data.url},
|
||||
extras
|
||||
)
|
||||
};
|
||||
resolve(crashReporterData);
|
||||
})
|
||||
.catch((err) => log.send(
|
||||
logLevels.ERROR,
|
||||
'Unable to initialize crash reporter failed to read config file. Error is -> ' + err
|
||||
));
|
||||
})
|
||||
}
|
||||
|
||||
function initCrashReporterMain(extras) {
|
||||
getCrashReporterConfig(extras).then((mainCrashReporterData) => {
|
||||
try {
|
||||
crashReporter.start(mainCrashReporterData);
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'Failed to start crash reporter main process. Error is -> ' + err);
|
||||
}
|
||||
}).catch((err) => log.send(
|
||||
logLevels.ERROR,
|
||||
'Unable to initialize crash reporter for main process. Error is -> ' + err
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method to initialize crash reporter for renderer process
|
||||
*
|
||||
* @param browserWindow {Electron.BrowserWindow}
|
||||
* @param extras {Object}
|
||||
*/
|
||||
function initCrashReporterRenderer(browserWindow, extras) {
|
||||
if (browserWindow && browserWindow.webContents && !browserWindow.isDestroyed()) {
|
||||
getCrashReporterConfig(extras).then((rendererCrashReporterData) => {
|
||||
browserWindow.webContents.send('register-crash-reporter', rendererCrashReporterData);
|
||||
}).catch((err) => log.send(
|
||||
logLevels.ERROR,
|
||||
'Unable to initialize crash reporter for renderer process. Error is -> ' + err
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initCrashReporterMain,
|
||||
initCrashReporterRenderer,
|
||||
};
|
132
js/cryptoLib.js
132
js/cryptoLib.js
@ -1,132 +0,0 @@
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const ffi = require('ffi');
|
||||
const ref = require('ref');
|
||||
const path = require('path');
|
||||
const execPath = path.dirname(app.getPath('exe'));
|
||||
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
const { isMac, isDevEnv } = require('../js/utils/misc');
|
||||
|
||||
const TAG_LENGTH = 16;
|
||||
const arch = process.arch === 'ia32';
|
||||
const winLibraryPath = isDevEnv ? path.join(__dirname, '..', 'library') : path.join(execPath, 'library');
|
||||
const macLibraryPath = isDevEnv ? path.join(__dirname, '..', 'library') : path.join(execPath, '..', 'library');
|
||||
|
||||
const cryptoLibPath = isMac ?
|
||||
path.join(macLibraryPath, 'cryptoLib.dylib') :
|
||||
(arch ? path.join(winLibraryPath, 'libsymphonysearch-x86.dll') : path.join(winLibraryPath, 'libsymphonysearch-x64.dll'));
|
||||
|
||||
const library = new ffi.Library((cryptoLibPath), {
|
||||
|
||||
AESEncryptGCM: [ref.types.int32, [
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.int32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.int32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.uint32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.uint32,
|
||||
]],
|
||||
|
||||
AESDecryptGCM: [ref.types.int32, [
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.int32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.int32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.uint32,
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.refType(ref.types.uchar),
|
||||
ref.types.uint32,
|
||||
ref.refType(ref.types.uchar),
|
||||
]],
|
||||
|
||||
getVersion: [ref.types.CString, []],
|
||||
});
|
||||
|
||||
/**
|
||||
* Method to decrypt content
|
||||
* @param Base64IV
|
||||
* @param Base64AAD
|
||||
* @param Base64Key
|
||||
* @param Base64In
|
||||
* @return {*}
|
||||
* @constructor
|
||||
*/
|
||||
const AESGCMEncrypt = function (Base64IV, Base64AAD, Base64Key, Base64In) {
|
||||
return EncryptDecrypt('AESGCMEncrypt', Base64IV, Base64AAD, Base64Key, Base64In);
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to decrypt content
|
||||
* @param Base64IV
|
||||
* @param Base64AAD
|
||||
* @param Base64Key
|
||||
* @param Base64In
|
||||
* @return {*}
|
||||
* @constructor
|
||||
*/
|
||||
const AESGCMDecrypt = function (Base64IV, Base64AAD, Base64Key, Base64In) {
|
||||
return EncryptDecrypt('AESGCMDecrypt', Base64IV, Base64AAD, Base64Key, Base64In);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encrypt / Decrypt
|
||||
* @param name {String} - Method name
|
||||
* @param Base64IV {String} base64
|
||||
* @param Base64AAD {String} base64
|
||||
* @param Base64Key {String} base64
|
||||
* @param Base64In {String} base64
|
||||
* @return {*}
|
||||
* @constructor
|
||||
*/
|
||||
const EncryptDecrypt = function (name, Base64IV, Base64AAD, Base64Key, Base64In) {
|
||||
let base64In = Base64In;
|
||||
|
||||
if (!base64In) {
|
||||
base64In = "";
|
||||
}
|
||||
|
||||
const IV = Buffer.from(Base64IV, 'base64');
|
||||
const AAD = Buffer.from(Base64AAD, 'base64');
|
||||
const Key = Buffer.from(Base64Key, 'base64');
|
||||
const In = Buffer.from(base64In, 'base64');
|
||||
|
||||
if (name === 'AESGCMEncrypt') {
|
||||
const OutPtr = Buffer.alloc(In.length);
|
||||
const Tag = Buffer.alloc(TAG_LENGTH);
|
||||
|
||||
const resultCode = library.AESEncryptGCM(In, In.length, AAD, AAD.length, Key, IV, IV.length, OutPtr, Tag, TAG_LENGTH);
|
||||
|
||||
if (resultCode < 0) {
|
||||
log.send(logLevels.ERROR, `AESEncryptGCM, Failed to encrypt with exit code ${resultCode}`);
|
||||
}
|
||||
const bufferArray = [OutPtr, Tag];
|
||||
return Buffer.concat(bufferArray).toString('base64');
|
||||
}
|
||||
|
||||
if (name === 'AESGCMDecrypt') {
|
||||
const CipherTextLen = In.length - TAG_LENGTH;
|
||||
const Tag = Buffer.from(In.slice(In.length - 16, In.length));
|
||||
const OutPtr = Buffer.alloc(In.length - TAG_LENGTH);
|
||||
|
||||
const resultCode = library.AESDecryptGCM(In, CipherTextLen, AAD, AAD.length, Tag, TAG_LENGTH, Key, IV, IV.length, OutPtr);
|
||||
|
||||
if (resultCode < 0) {
|
||||
log.send(logLevels.ERROR, `AESDecryptGCM, Failed to decrypt with exit code ${resultCode}`);
|
||||
}
|
||||
return OutPtr.toString('base64');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AESGCMEncrypt: AESGCMEncrypt,
|
||||
AESGCMDecrypt: AESGCMDecrypt,
|
||||
};
|
@ -1,161 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// This code provides equivalent of desktopCapturer.getSources that works in
|
||||
// a sandbox renderer. see: https://electron.atom.io/docs/api/desktop-capturer/
|
||||
//
|
||||
// The code here is not entirely kosher/stable as it is using private ipc
|
||||
// events. The code was take directly from electron.asar file provided in
|
||||
// prebuilt node module. Note: the slight difference here is the thumbnail
|
||||
// returns a base64 encoded image rather than a electron nativeImage.
|
||||
//
|
||||
// Until electron provides access to desktopCapturer in a sandboxed
|
||||
// renderer process, this will have to do. See github issue posted here to
|
||||
// electron: https://github.com/electron/electron/issues/9312
|
||||
|
||||
const { ipcRenderer, remote, desktopCapturer } = require('electron');
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
const { isWindowsOS } = require('../utils/misc');
|
||||
const USER_CANCELLED = 'User Cancelled';
|
||||
|
||||
let nextId = 0;
|
||||
let includes = [].includes;
|
||||
let screenShareArgv;
|
||||
let isScreenShareEnabled = false;
|
||||
let dialogContent;
|
||||
|
||||
function getNextId() {
|
||||
return ++nextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the options and their types are valid
|
||||
* @param options |options.type| can not be empty and has to include 'window' or 'screen'.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValid(options) {
|
||||
return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sources for capturing screens / windows
|
||||
* @param options
|
||||
* @param callback
|
||||
* @returns {*}
|
||||
*/
|
||||
function getSource(options, callback) {
|
||||
let captureScreen, captureWindow, id;
|
||||
let sourceTypes = [];
|
||||
if (!isValid(options)) {
|
||||
callback(new Error('Invalid options'));
|
||||
return;
|
||||
}
|
||||
captureWindow = includes.call(options.types, 'window');
|
||||
captureScreen = includes.call(options.types, 'screen');
|
||||
|
||||
let updatedOptions = options;
|
||||
if (!updatedOptions.thumbnailSize) {
|
||||
updatedOptions.thumbnailSize = {
|
||||
width: 150,
|
||||
height: 150
|
||||
};
|
||||
}
|
||||
|
||||
if (isWindowsOS && captureWindow) {
|
||||
/**
|
||||
* Sets the captureWindow to false if Desktop composition
|
||||
* is disabled otherwise true
|
||||
*
|
||||
* Setting captureWindow to false returns only screen sources
|
||||
* @type {boolean}
|
||||
*/
|
||||
captureWindow = remote.systemPreferences.isAeroGlassEnabled();
|
||||
}
|
||||
|
||||
if (captureWindow) {
|
||||
sourceTypes.push('window');
|
||||
}
|
||||
if (captureScreen) {
|
||||
sourceTypes.push('screen');
|
||||
}
|
||||
|
||||
// displays a dialog if media permissions are disable
|
||||
if (!isScreenShareEnabled) {
|
||||
let focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
remote.dialog.showMessageBox(focusedWindow, dialogContent ||
|
||||
{
|
||||
type: 'error',
|
||||
title: 'Permission Denied!',
|
||||
message: 'Your administrator has disabled screen share. Please contact your admin for help'
|
||||
});
|
||||
callback(new Error('Permission Denied'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
id = getNextId();
|
||||
desktopCapturer.getSources({ types: sourceTypes, thumbnailSize: updatedOptions.thumbnailSize }, (event, sources) => {
|
||||
|
||||
if (screenShareArgv) {
|
||||
const title = screenShareArgv.substr(screenShareArgv.indexOf('=') + 1);
|
||||
const filteredSource = sources.filter(source => source.name === title);
|
||||
|
||||
if (Array.isArray(filteredSource) && filteredSource.length > 0) {
|
||||
return callback(null, filteredSource[0]);
|
||||
}
|
||||
|
||||
if (typeof filteredSource === 'object' && filteredSource.name) {
|
||||
return callback(null, filteredSource);
|
||||
}
|
||||
|
||||
if (sources.length > 0) {
|
||||
return callback(null, sources[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const updatedSources = sources.map(source => {
|
||||
return Object.assign({}, source, {
|
||||
thumbnail: source.thumbnail.toDataURL()
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.openScreenPickerWindow,
|
||||
sources: updatedSources,
|
||||
id: id
|
||||
});
|
||||
|
||||
function successCallback(e, source) {
|
||||
// Cleaning up the event listener to prevent memory leaks
|
||||
if (!source) {
|
||||
ipcRenderer.removeListener('start-share' + id, func);
|
||||
return callback(new Error(USER_CANCELLED));
|
||||
}
|
||||
return callback(null, source);
|
||||
}
|
||||
|
||||
const func = successCallback.bind(this);
|
||||
ipcRenderer.once('start-share' + id, func);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// event that updates screen share argv
|
||||
ipcRenderer.once('screen-share-argv', (event, arg) => {
|
||||
if (typeof arg === 'string') {
|
||||
screenShareArgv = arg;
|
||||
}
|
||||
});
|
||||
|
||||
// event that updates screen share permission
|
||||
ipcRenderer.on('is-screen-share-enabled', (event, screenShare, content) => {
|
||||
dialogContent = content;
|
||||
if (typeof screenShare === 'boolean' && screenShare) {
|
||||
isScreenShareEnabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = getSource;
|
@ -1,146 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// This code provides equivalent of desktopCapturer.getSources that works in
|
||||
// a sandbox renderer. see: https://electron.atom.io/docs/api/desktop-capturer/
|
||||
//
|
||||
// The code here is not entirely kosher/stable as it is using private ipc
|
||||
// events. The code was take directly from electron.asar file provided in
|
||||
// prebuilt node module. Note: the slight difference here is the thumbnail
|
||||
// returns a base64 encoded image rather than a electron nativeImage.
|
||||
//
|
||||
// Until electron provides access to desktopCapturer in a sandboxed
|
||||
// renderer process, this will have to do. See github issue posted here to
|
||||
// electron: https://github.com/electron/electron/issues/9312
|
||||
|
||||
const { remote, desktopCapturer, ipcRenderer } = require('electron');
|
||||
const { isWindowsOS } = require('../utils/misc');
|
||||
|
||||
let includes = [].includes;
|
||||
let screenShareArgv;
|
||||
let isScreenShareEnabled = false;
|
||||
let dialogContent;
|
||||
|
||||
/**
|
||||
* Checks if the options and their types are valid
|
||||
* @param options |options.type| can not be empty and has to include 'window' or 'screen'.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValid(options) {
|
||||
return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sources for capturing screens / windows
|
||||
* @param options
|
||||
* @param callback
|
||||
* @returns {*}
|
||||
*/
|
||||
function getSources(options, callback) {
|
||||
let captureScreen, captureWindow;
|
||||
let sourceTypes = [];
|
||||
if (!isValid(options)) {
|
||||
callback(new Error('Invalid options'));
|
||||
return;
|
||||
}
|
||||
captureWindow = includes.call(options.types, 'window');
|
||||
captureScreen = includes.call(options.types, 'screen');
|
||||
|
||||
let updatedOptions = options;
|
||||
if (!updatedOptions.thumbnailSize) {
|
||||
updatedOptions.thumbnailSize = {
|
||||
width: 150,
|
||||
height: 150
|
||||
};
|
||||
}
|
||||
|
||||
if (isWindowsOS && captureWindow) {
|
||||
/**
|
||||
* Sets the captureWindow to false if Desktop composition
|
||||
* is disabled otherwise true
|
||||
*
|
||||
* Setting captureWindow to false returns only screen sources
|
||||
* @type {boolean}
|
||||
*/
|
||||
captureWindow = remote.systemPreferences.isAeroGlassEnabled();
|
||||
}
|
||||
if (captureWindow) {
|
||||
sourceTypes.push('window');
|
||||
}
|
||||
if (captureScreen) {
|
||||
sourceTypes.push('screen');
|
||||
}
|
||||
|
||||
// displays a dialog if media permissions are disable
|
||||
if (!isScreenShareEnabled) {
|
||||
let focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
remote.dialog.showMessageBox(focusedWindow, dialogContent ||
|
||||
{
|
||||
type: 'error',
|
||||
title: 'Permission Denied!',
|
||||
message: 'Your administrator has disabled screen share. Please contact your admin for help'
|
||||
});
|
||||
callback(new Error('Permission Denied'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
desktopCapturer.getSources({ types: sourceTypes, thumbnailSize: updatedOptions.thumbnailSize }, (event, sources) => {
|
||||
|
||||
if (screenShareArgv) {
|
||||
const title = screenShareArgv.substr(screenShareArgv.indexOf('=') + 1);
|
||||
const filteredSource = sources.filter(source => source.name === title);
|
||||
|
||||
if (Array.isArray(filteredSource) && filteredSource.length > 0) {
|
||||
return callback(null, filteredSource[0]);
|
||||
}
|
||||
|
||||
if (typeof filteredSource === 'object' && filteredSource.name) {
|
||||
return callback(null, filteredSource);
|
||||
}
|
||||
|
||||
if (sources.length > 0) {
|
||||
return callback(null, sources[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let source;
|
||||
return callback(null, (function() {
|
||||
let i, len, results;
|
||||
results = [];
|
||||
for (i = 0, len = sources.length; i < len; i++) {
|
||||
source = sources[i];
|
||||
results.push({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL()
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
}()));
|
||||
});
|
||||
}
|
||||
|
||||
// event that updates screen share argv
|
||||
ipcRenderer.once('screen-share-argv', (event, arg) => {
|
||||
if (typeof arg === 'string') {
|
||||
screenShareArgv = arg;
|
||||
}
|
||||
});
|
||||
|
||||
// event that updates screen share permission
|
||||
ipcRenderer.on('is-screen-share-enabled', (event, screenShare, content) => {
|
||||
dialogContent = content;
|
||||
if (typeof screenShare === 'boolean' && screenShare) {
|
||||
isScreenShareEnabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated instead use getSource
|
||||
* @type {getSources}
|
||||
*/
|
||||
module.exports = getSources;
|
@ -1,174 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const ipc = electron.ipcMain;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { isMac, isWindowsOS } = require('./../utils/misc.js');
|
||||
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
|
||||
let screenPickerWindow;
|
||||
let preloadWindow;
|
||||
let eventId;
|
||||
|
||||
let windowConfig = {
|
||||
width: 580,
|
||||
height: isMac ? 519 : 523,
|
||||
show: false,
|
||||
modal: true,
|
||||
frame: false,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'renderer.js'),
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* method to get the HTML template path
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'screen-picker.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'screen-picker: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
return 'file://' + templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the screen picker window
|
||||
* @param eventSender {RTCRtpSender} - event sender window object
|
||||
* @param sources {Array} - list of object which has screens and applications
|
||||
* @param id {Number} - event emitter id
|
||||
*/
|
||||
function openScreenPickerWindow(eventSender, sources, id) {
|
||||
|
||||
// prevent a new window from being opened if there is an
|
||||
// existing window / there is no event sender
|
||||
if (!eventSender || screenPickerWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Screen picker will always be placed on top of the focused window
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
// As screen picker is an independent window this will make sure
|
||||
// it will open screen picker window center of the focused window
|
||||
if (focusedWindow) {
|
||||
const { x, y, width, height } = focusedWindow.getBounds();
|
||||
|
||||
if (x !== undefined && y !== undefined) {
|
||||
const windowWidth = Math.round(width * 0.5);
|
||||
const windowHeight = Math.round(height * 0.5);
|
||||
|
||||
// Calculating the center of the parent window
|
||||
// to place the configuration window
|
||||
const centerX = x + width / 2.0;
|
||||
const centerY = y + height / 2.0;
|
||||
windowConfig.x = Math.round(centerX - (windowWidth / 2.0));
|
||||
windowConfig.y = Math.round(centerY - (windowHeight / 2.0));
|
||||
}
|
||||
|
||||
if (isWindowsOS) {
|
||||
windowConfig.parent = focusedWindow;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the window ref to send event
|
||||
preloadWindow = eventSender;
|
||||
eventId = id;
|
||||
|
||||
screenPickerWindow = new BrowserWindow(windowConfig);
|
||||
screenPickerWindow.setVisibleOnAllWorkspaces(true);
|
||||
screenPickerWindow.loadURL(getTemplatePath());
|
||||
|
||||
screenPickerWindow.once('ready-to-show', () => {
|
||||
screenPickerWindow.show();
|
||||
});
|
||||
|
||||
screenPickerWindow.webContents.on('did-finish-load', () => {
|
||||
const screenPickerContent = i18n.getMessageFor('ScreenPicker');
|
||||
screenPickerWindow.webContents.send('i18n-screen-picker', screenPickerContent);
|
||||
// initialize crash reporter
|
||||
initCrashReporterMain({ process: 'desktop capture window' });
|
||||
initCrashReporterRenderer(screenPickerWindow, { process: 'render | desktop capture window' });
|
||||
screenPickerWindow.webContents.send('desktop-capturer-sources', sources, isWindowsOS);
|
||||
});
|
||||
|
||||
screenPickerWindow.webContents.on('crashed', function (event, killed) {
|
||||
|
||||
log.send(logLevels.INFO, `Screen Picker Window crashed! Killed? ${killed}`);
|
||||
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Renderer Process Crashed'),
|
||||
message: i18n.getMessageFor('Oops! Looks like we have had a crash.'),
|
||||
buttons: ['Close']
|
||||
};
|
||||
|
||||
electron.dialog.showMessageBox(options, function () {
|
||||
if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
|
||||
screenPickerWindow.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
screenPickerWindow.on('close', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
screenPickerWindow.on('closed', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a window
|
||||
*/
|
||||
function destroyWindow() {
|
||||
// sending null will clean up the event listener
|
||||
startScreenShare(null);
|
||||
screenPickerWindow = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to a specific with the selected source
|
||||
* @param source {Object} - User selected source
|
||||
*/
|
||||
function startScreenShare(source) {
|
||||
if (preloadWindow && !preloadWindow.isDestroyed()) {
|
||||
preloadWindow.send('start-share' + eventId, source);
|
||||
}
|
||||
}
|
||||
|
||||
// Emitted when user has selected a source and press the share button
|
||||
ipc.on('share-selected-source', (event, source) => {
|
||||
startScreenShare(source);
|
||||
});
|
||||
|
||||
// Emitted when user closes the screen picker window
|
||||
ipc.on('close-screen-picker', () => {
|
||||
if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
|
||||
screenPickerWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
openScreenPickerWindow
|
||||
};
|
@ -1,319 +0,0 @@
|
||||
'use strict';
|
||||
const { ipcRenderer, crashReporter } = require('electron');
|
||||
|
||||
const screenRegExp = new RegExp(/^Screen \d+$/gmi);
|
||||
|
||||
// All the required Keyboard keyCode events
|
||||
const keyCodeEnum = Object.freeze({
|
||||
pageDown: 34,
|
||||
rightArrow: 39,
|
||||
pageUp: 33,
|
||||
leftArrow: 37,
|
||||
homeKey: 36,
|
||||
upArrow: 38,
|
||||
endKey: 35,
|
||||
arrowDown: 40,
|
||||
enterKey: 13,
|
||||
escapeKey: 27
|
||||
});
|
||||
|
||||
let availableSources;
|
||||
let selectedSource;
|
||||
let currentIndex = -1;
|
||||
let localizedContent;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderDom();
|
||||
});
|
||||
|
||||
/**
|
||||
* Method that renders application data
|
||||
*/
|
||||
function renderDom() {
|
||||
const applicationTab = document.getElementById('application-tab');
|
||||
const screenTab = document.getElementById('screen-tab');
|
||||
const share = document.getElementById('share');
|
||||
const cancel = document.getElementById('cancel');
|
||||
const xButton = document.getElementById('x-button');
|
||||
|
||||
// Event listeners
|
||||
xButton.addEventListener('click', () => {
|
||||
closeScreenPickerWindow();
|
||||
}, false);
|
||||
|
||||
share.addEventListener('click', () => {
|
||||
startShare();
|
||||
}, false);
|
||||
|
||||
cancel.addEventListener('click', () => {
|
||||
closeScreenPickerWindow();
|
||||
}, false);
|
||||
|
||||
screenTab.addEventListener('click', () => {
|
||||
updateShareButtonText(localizedContent && localizedContent[ 'Select Screen' ] ?
|
||||
localizedContent[ 'Select Screen' ] :
|
||||
'Select Screen');
|
||||
}, false);
|
||||
|
||||
applicationTab.addEventListener('click', () => {
|
||||
updateShareButtonText(localizedContent && localizedContent[ 'Select Application' ] ?
|
||||
localizedContent[ 'Select Application' ] :
|
||||
'Select Application');
|
||||
}, false);
|
||||
|
||||
document.addEventListener('keyup', handleKeyUpPress.bind(this), true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted by main process with a list of available
|
||||
* Screens and Applications
|
||||
*/
|
||||
ipcRenderer.on('desktop-capturer-sources', (event, sources, isWindowsOS) => {
|
||||
|
||||
if (!Array.isArray(sources) && typeof isWindowsOS !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
availableSources = sources;
|
||||
|
||||
if (isWindowsOS) {
|
||||
document.body.classList.add('window-border');
|
||||
}
|
||||
|
||||
const screenContent = document.getElementById('screen-contents');
|
||||
const applicationContent = document.getElementById('application-contents');
|
||||
const applicationTab = document.getElementById('applications');
|
||||
const screenTab = document.getElementById('screens');
|
||||
|
||||
let hasScreens = false;
|
||||
let hasApplications = false;
|
||||
|
||||
for (let source of sources) {
|
||||
screenRegExp.lastIndex = 0;
|
||||
if (source.display_id !== "") {
|
||||
source.fileName = 'fullscreen';
|
||||
if (source.name === 'Entire screen') {
|
||||
const screenName = localizedContent['Entire screen'] || 'Entire screen';
|
||||
screenContent.appendChild(createItem(source, screenName));
|
||||
} else {
|
||||
const screenName = localizedContent['Screen {number}'] || 'Screen {number}';
|
||||
const screenNumber = source.name.substr(7, source.name.length);
|
||||
const localisedScreenString = screenName.replace('{number}', screenNumber);
|
||||
screenContent.appendChild(createItem(source, localisedScreenString));
|
||||
}
|
||||
hasScreens = true;
|
||||
} else {
|
||||
source.fileName = null;
|
||||
applicationContent.appendChild(createItem(source, source.name));
|
||||
hasApplications = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasScreens && !hasApplications) {
|
||||
const errorContent = document.getElementById('error-content');
|
||||
const mainContent = document.getElementById('main-content');
|
||||
|
||||
errorContent.style.display = 'block';
|
||||
mainContent.style.display = 'none';
|
||||
}
|
||||
|
||||
if (hasApplications) {
|
||||
applicationTab.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (hasScreens) {
|
||||
screenTab.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function startShare() {
|
||||
if (selectedSource && selectedSource.id) {
|
||||
ipcRenderer.send('share-selected-source', selectedSource);
|
||||
closeScreenPickerWindow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DOM elements and injects data
|
||||
* @param source Screen source
|
||||
* @param name Name of the source
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
function createItem(source, name) {
|
||||
const itemContainer = document.createElement("div");
|
||||
const sectionBox = document.createElement("div");
|
||||
const imageTag = document.createElement("img");
|
||||
const titleContainer = document.createElement("div");
|
||||
|
||||
// Added class names to the dom elements
|
||||
itemContainer.classList.add('item-container');
|
||||
sectionBox.classList.add('screen-section-box');
|
||||
imageTag.classList.add('img-wrapper');
|
||||
titleContainer.classList.add('screen-source-title');
|
||||
|
||||
// Inject data to the dom element
|
||||
imageTag.src = source.thumbnail;
|
||||
titleContainer.innerText = name;
|
||||
|
||||
sectionBox.appendChild(imageTag);
|
||||
itemContainer.id = source.id;
|
||||
itemContainer.appendChild(sectionBox);
|
||||
itemContainer.appendChild(titleContainer);
|
||||
|
||||
itemContainer.addEventListener('click', updateUI.bind(this, source, itemContainer), false);
|
||||
|
||||
return itemContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* When ever user select a source store it and update the UI
|
||||
* @param source
|
||||
* @param itemContainer
|
||||
*/
|
||||
function updateUI(source, itemContainer) {
|
||||
selectedSource = source;
|
||||
|
||||
let shareButton = document.getElementById('share');
|
||||
shareButton.className = 'share-button';
|
||||
|
||||
highlightSelectedSource();
|
||||
itemContainer.classList.add('selected');
|
||||
shareButton.innerText = localizedContent && localizedContent.Share ? localizedContent.Share : 'Share';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through the items and removes
|
||||
* the selected class property
|
||||
*/
|
||||
function highlightSelectedSource() {
|
||||
let items = document.getElementsByClassName('item-container');
|
||||
for (const item of items) {
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the share button
|
||||
* text based on the content type
|
||||
* @param text
|
||||
*/
|
||||
function updateShareButtonText(text) {
|
||||
let shareButton = document.getElementById('share');
|
||||
|
||||
if (shareButton && shareButton.classList[0] === 'share-button-disable') {
|
||||
shareButton.innerText = text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method handles used key up event
|
||||
* @param e
|
||||
*/
|
||||
function handleKeyUpPress(e) {
|
||||
const keyCode = e.keyCode || e.which;
|
||||
|
||||
switch (keyCode) {
|
||||
case keyCodeEnum.pageDown:
|
||||
case keyCodeEnum.rightArrow:
|
||||
updateSelectedSource(1);
|
||||
break;
|
||||
case keyCodeEnum.pageUp:
|
||||
case keyCodeEnum.leftArrow:
|
||||
updateSelectedSource(-1);
|
||||
break;
|
||||
case keyCodeEnum.homeKey:
|
||||
if (currentIndex !== 0) {
|
||||
updateSelectedSource(0);
|
||||
}
|
||||
break;
|
||||
case keyCodeEnum.upArrow:
|
||||
updateSelectedSource(-2);
|
||||
break;
|
||||
case keyCodeEnum.endKey:
|
||||
if (currentIndex !== availableSources.length - 1) {
|
||||
updateSelectedSource(availableSources.length - 1);
|
||||
}
|
||||
break;
|
||||
case keyCodeEnum.arrowDown:
|
||||
updateSelectedSource(2);
|
||||
break;
|
||||
case keyCodeEnum.enterKey:
|
||||
startShare();
|
||||
break;
|
||||
case keyCodeEnum.escapeKey:
|
||||
closeScreenPickerWindow();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates UI based on the key press
|
||||
* @param index
|
||||
*/
|
||||
function updateSelectedSource(index) {
|
||||
|
||||
let selectedElement = document.getElementsByClassName('selected')[0];
|
||||
if (selectedElement) {
|
||||
currentIndex = availableSources.findIndex((source) => {
|
||||
return source.id === selectedElement.id
|
||||
});
|
||||
}
|
||||
|
||||
// Find the next item to be selected
|
||||
let nextIndex = (currentIndex + index + availableSources.length) % availableSources.length;
|
||||
if (availableSources[nextIndex] && availableSources[nextIndex].id) {
|
||||
let item = document.getElementById(availableSources[nextIndex] ? availableSources[nextIndex].id : "");
|
||||
|
||||
if (item) {
|
||||
// Method that stores and update the selected source
|
||||
updateUI(availableSources[nextIndex], item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the screen picker window
|
||||
*/
|
||||
function closeScreenPickerWindow() {
|
||||
document.removeEventListener('keyUp', handleKeyUpPress.bind(this), true);
|
||||
ipcRenderer.send('close-screen-picker');
|
||||
}
|
||||
|
||||
ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('i18n-screen-picker', (event, content) => {
|
||||
localizedContent = content;
|
||||
if (content && typeof content === 'object') {
|
||||
const i18nNodes = document.querySelectorAll('[data-i18n-text]');
|
||||
|
||||
for (let node of i18nNodes) {
|
||||
if (node.attributes['data-i18n-text'] && node.attributes['data-i18n-text'].value) {
|
||||
node.innerText = content[node.attributes['data-i18n-text'].value] || node.attributes['data-i18n-text'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// note: this is a workaround until
|
||||
// https://github.com/electron/electron/issues/8841
|
||||
// is fixed on the electron. where 'will-navigate'
|
||||
// is never fired in sandbox mode
|
||||
//
|
||||
// This is required in order to prevent from loading
|
||||
// dropped content
|
||||
window.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
@ -1,246 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title data-i18n-text="Screen Picker"></title>
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: system;
|
||||
font-style: normal;
|
||||
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
font-family: "system";
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-border {
|
||||
border: 2px solid rgba(68,68,68, 1);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
background: rgba(249,249,250, 1);
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-window-title {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 30px;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3;
|
||||
text-align: left;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
#x-button {
|
||||
width: 20px;
|
||||
color: rgba(221, 221, 221, 1);
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
font-style: normal;
|
||||
margin: 0 0 0 4px;
|
||||
font-size: 1.2rem;
|
||||
min-height: 13px;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
padding: 5px 25px;
|
||||
font-size: 0.8em;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
color: rgba(187,187,187, 1);
|
||||
text-transform: uppercase;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
height: 331px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 0 0;
|
||||
border-top: 1px solid rgba(221, 221, 221, 1);
|
||||
flex-flow: row wrap;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
color: rgba(61,162,253, 1);
|
||||
border-bottom: 4px solid rgba(61,162,253, 1);
|
||||
}
|
||||
|
||||
#screen-tab:checked ~ #screen-contents,
|
||||
#application-tab:checked ~ #application-contents {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
background: rgba(255,255,255, 1);
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
height: 150px;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
width: 40%;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.item-container:hover {
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, .1), 0 5px 5px rgba(0, 0, 0, .1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.screen-section-box {
|
||||
-webkit-transition: all 1.0s ease;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
max-height: 200px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.screen-source-title {
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
overflow: hidden;
|
||||
font-size: 0.8rem;
|
||||
color: grey;
|
||||
margin: auto;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-color: rgba(61,162,253, 1);
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
button {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
padding: 8px 32px;
|
||||
margin: 8px 8px 8px 0;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
text-transform: uppercase;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.share-button {
|
||||
background-color: rgba(61,162,253, 1);
|
||||
color: rgba(255,255,255, 1);
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.share-button-disable {
|
||||
background-color: rgba(221, 221, 221, 1);
|
||||
color: rgba(148, 148, 148, 1);
|
||||
text-transform: uppercase;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#error-content {
|
||||
display: none;
|
||||
padding: 180px 0 180px;
|
||||
border-top: 1px solid rgba(221, 221, 221, 1);
|
||||
}
|
||||
|
||||
#error-content span {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="custom-window-title">
|
||||
<span data-i18n-text="Choose what you'd like to share"></span>
|
||||
<div id="x-button">
|
||||
<div class="content-button">
|
||||
<i>
|
||||
<svg viewBox="0 0 48 48" fill="grey">
|
||||
<path d="M39.4,33.8L31,25.4c-0.4-0.4-0.9-0.9-1.4-1.4c0.5-0.5,1-1,1.4-1.4l8.4-8.4c0.8-0.8,0.8-2,0-2.8l-2.8-2.8 c-0.8-0.8-2-0.8-2.8,0L25.4,17c-0.4,0.4-0.9,0.9-1.4,1.4c-0.5-0.5-1-1-1.4-1.4l-8.4-8.4c-0.8-0.8-2-0.8-2.8,0l-2.8,2.8 c-0.8,0.8-0.8,2,0,2.8l8.4,8.4c0.4,0.4,0.9,0.9,1.4,1.4c-0.5,0.5-1,1-1.4,1.4l-8.4,8.4c-0.8,0.8-0.8,2,0,2.8l2.8,2.8 c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.4-0.4,0.9-0.9,1.4-1.4c0.5,0.5,1,1,1.4,1.4l8.4,8.4c0.8,0.8,2,0.8,2.8,0l2.8-2.8 C40.2,35.8,40.2,34.6,39.4,33.8z"></path>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="error-content">
|
||||
<span id="error-message" data-i18n-text="No screens or applications are currently available."></span>
|
||||
</div>
|
||||
<div id="main-content">
|
||||
<input id="screen-tab" type="radio" name="tabs" checked>
|
||||
<label id="screens" for="screen-tab" class="hidden" data-i18n-text="Screens"></label>
|
||||
<input id="application-tab" type="radio" name="tabs">
|
||||
<label id="applications" for="application-tab" class="hidden" data-i18n-text="Applications"></label>
|
||||
<section id="screen-contents">
|
||||
</section>
|
||||
<section id="application-contents">
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button id="cancel" class="cancel-button" data-i18n-text="Cancel"></button>
|
||||
<button id="share" class="share-button-disable" data-i18n-text="Select Screen"></button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,46 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const basicAuth = require('../basicAuth');
|
||||
let currentAuthURL;
|
||||
let tries = 0;
|
||||
|
||||
/**
|
||||
* Having a proxy or hosts that requires authentication will allow user to
|
||||
* enter their credentials 'username' & 'password'
|
||||
*/
|
||||
electron.app.on('login', (event, webContents, request, authInfo, callback) => {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// This check is to determine whether the request is for the same
|
||||
// host if so then increase the login tries from which we can
|
||||
// display invalid credentials
|
||||
if (currentAuthURL !== request.url) {
|
||||
currentAuthURL = request.url;
|
||||
} else {
|
||||
tries++
|
||||
}
|
||||
|
||||
// name of the host to display
|
||||
let hostname = authInfo.host || authInfo.realm;
|
||||
let browserWin = electron.BrowserWindow.fromWebContents(webContents);
|
||||
let windowName = browserWin.winName || '';
|
||||
|
||||
/**
|
||||
* Method that resets currentAuthURL and tries
|
||||
* if user closes the auth window
|
||||
*/
|
||||
function clearSettings() {
|
||||
callback();
|
||||
currentAuthURL = '';
|
||||
tries = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an electron modal window in which
|
||||
* user can enter credentials fot the host
|
||||
*/
|
||||
basicAuth.openBasicAuthWindow(windowName, hostname, tries === 0, clearSettings, callback);
|
||||
});
|
@ -1,48 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
|
||||
let ignoreAllCertErrors = false;
|
||||
|
||||
/**
|
||||
* If certificate error occurs allow user to deny or allow particular certificate
|
||||
* error. If user selects 'Ignore All', then all subsequent certificate errors
|
||||
* will ignored during this session.
|
||||
*
|
||||
* Note: the dialog is synchronous so further processing is blocked until
|
||||
* user provides a response.
|
||||
*/
|
||||
electron.app.on('certificate-error', function(event, webContents, url, error,
|
||||
certificate, callback) {
|
||||
|
||||
if (ignoreAllCertErrors) {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
log.send(logLevels.WARNING, 'Certificate error: ' + error + ' for url: ' + url);
|
||||
|
||||
const browserWin = electron.BrowserWindow.fromWebContents(webContents);
|
||||
const buttonId = electron.dialog.showMessageBox(browserWin, {
|
||||
type: 'warning',
|
||||
buttons: [ 'Allow', 'Deny', 'Ignore All' ],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
noLink: true,
|
||||
title: i18n.getMessageFor('Certificate Error'),
|
||||
message: i18n.getMessageFor('Certificate Error') + `: ${error}\nURL: ${url}`,
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (buttonId === 2) {
|
||||
ignoreAllCertErrors = true;
|
||||
}
|
||||
|
||||
callback(buttonId !== 1);
|
||||
});
|
@ -1,67 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
|
||||
/**
|
||||
* Show dialog pinned to given window when loading error occurs
|
||||
* @param {BrowserWindow} win Window to host dialog
|
||||
* @param {String} url Url that failed
|
||||
* @param {String} errorDesc Description of error
|
||||
* @param {Number} errorCode Error code
|
||||
* @param {function} retryCallback Callback when user clicks reload
|
||||
* @param {Boolean} showDialog Indicates if a dialog need to be show to a user
|
||||
*/
|
||||
function showLoadFailure(win, url, errorDesc, errorCode, retryCallback, showDialog) {
|
||||
let msg;
|
||||
if (url) {
|
||||
msg = i18n.getMessageFor('Error loading URL') + `:\n${url}`;
|
||||
} else {
|
||||
msg = i18n.getMessageFor('Error loading window');
|
||||
}
|
||||
if (errorDesc) {
|
||||
msg += '\n\n' + errorDesc;
|
||||
}
|
||||
if (errorCode) {
|
||||
msg += '\n\nError Code: ' + errorCode;
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
electron.dialog.showMessageBox(win, {
|
||||
type: 'error',
|
||||
buttons: [i18n.getMessageFor('Reload'), i18n.getMessageFor('Ignore')],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
noLink: true,
|
||||
title: i18n.getMessageFor('Loading Error'),
|
||||
message: msg
|
||||
}, response);
|
||||
}
|
||||
|
||||
log.send(logLevels.WARNING, 'Load failure msg: ' + errorDesc +
|
||||
' errorCode: ' + errorCode + ' for url:' + url);
|
||||
|
||||
// async handle of user input
|
||||
function response(buttonId) {
|
||||
// retry if hitting butotn index 0 (i.e., reload)
|
||||
if (buttonId === 0 && typeof retryCallback === 'function') {
|
||||
retryCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show message indicating network connectivity has been lost.
|
||||
* @param {BrowserWindow} win Window to host dialog
|
||||
* @param {String} url Url that failed
|
||||
* @param {function} retryCallback Callback when user clicks reload
|
||||
*/
|
||||
function showNetworkConnectivityError(win, url, retryCallback) {
|
||||
let errorDesc = i18n.getMessageFor('Network connectivity has been lost. Check your internet connection.');
|
||||
showLoadFailure(win, url, errorDesc, 0, retryCallback, true);
|
||||
}
|
||||
|
||||
module.exports = { showLoadFailure, showNetworkConnectivityError };
|
@ -1,253 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
|
||||
const local = {
|
||||
ipcRenderer: ipcRenderer,
|
||||
downloadItems: []
|
||||
};
|
||||
|
||||
let showInFolderText = 'Show in Folder';
|
||||
let openText = 'Open';
|
||||
let downloadedText = 'Downloaded';
|
||||
let fileNotFoundTitle = 'File not Found';
|
||||
let fileNotFoundMessage = 'The file you are trying to open cannot be found in the specified path.';
|
||||
|
||||
// listen for file download complete event
|
||||
local.ipcRenderer.on('downloadCompleted', (event, arg) => {
|
||||
createDOM(arg);
|
||||
});
|
||||
|
||||
// listen for file download progress event
|
||||
local.ipcRenderer.on('downloadProgress', () => {
|
||||
initiate();
|
||||
});
|
||||
|
||||
// listen for locale change and update
|
||||
local.ipcRenderer.on('locale-changed', (event, data) => {
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
|
||||
if (data.downloadManager) {
|
||||
openText = data.downloadManager.Open;
|
||||
downloadedText = data.downloadManager.Downloaded;
|
||||
|
||||
showInFolderText = data.downloadManager['Show in Folder'];
|
||||
fileNotFoundTitle = data.downloadManager['File not Found'];
|
||||
fileNotFoundMessage = data.downloadManager['The file you are trying to open cannot be found in the specified path.'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Open file in default app.
|
||||
* @param id
|
||||
*/
|
||||
function openFile(id) {
|
||||
let fileIndex = local.downloadItems.findIndex((item) => {
|
||||
return item._id === id;
|
||||
});
|
||||
if (fileIndex !== -1) {
|
||||
let openResponse = remote.shell.openExternal(`file:///${local.downloadItems[fileIndex].savedPath}`);
|
||||
let focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
if (!openResponse && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
remote.dialog.showMessageBox(focusedWindow, {type: 'error', title: fileNotFoundTitle, message: fileNotFoundMessage});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show downloaded file in explorer or finder.
|
||||
* @param id
|
||||
*/
|
||||
function showInFinder(id) {
|
||||
let showFileIndex = local.downloadItems.findIndex((item) => {
|
||||
return item._id === id;
|
||||
});
|
||||
if (showFileIndex !== -1) {
|
||||
let showResponse = remote.shell.showItemInFolder(local.downloadItems[showFileIndex].savedPath);
|
||||
let focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
if (!showResponse && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
remote.dialog.showMessageBox(focusedWindow, {type: 'error', title: fileNotFoundTitle, message: fileNotFoundMessage});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the document object model
|
||||
* @param arg
|
||||
*/
|
||||
function createDOM(arg) {
|
||||
|
||||
if (arg && arg._id) {
|
||||
let fileDisplayName = getFileDisplayName(arg.fileName);
|
||||
let downloadItemKey = arg._id;
|
||||
|
||||
local.downloadItems.push(arg);
|
||||
|
||||
let ul = document.getElementById('download-main');
|
||||
if (ul) {
|
||||
let li = document.createElement('li');
|
||||
li.id = downloadItemKey;
|
||||
li.classList.add('download-element');
|
||||
ul.insertBefore(li, ul.childNodes[0]);
|
||||
|
||||
let itemDiv = document.createElement('div');
|
||||
itemDiv.classList.add('download-item');
|
||||
itemDiv.id = 'dl-item';
|
||||
li.appendChild(itemDiv);
|
||||
let openMainFile = document.getElementById('dl-item');
|
||||
openMainFile.addEventListener('click', () => {
|
||||
let id = openMainFile.parentNode.id;
|
||||
openFile(id);
|
||||
});
|
||||
|
||||
let fileDetails = document.createElement('div');
|
||||
fileDetails.classList.add('file');
|
||||
itemDiv.appendChild(fileDetails);
|
||||
|
||||
let downProgress = document.createElement('div');
|
||||
downProgress.id = 'download-progress';
|
||||
downProgress.classList.add('download-complete');
|
||||
downProgress.classList.add('flash');
|
||||
setTimeout(() => {
|
||||
downProgress.classList.remove('flash');
|
||||
}, 4000);
|
||||
fileDetails.appendChild(downProgress);
|
||||
|
||||
let fileIcon = document.createElement('span');
|
||||
fileIcon.classList.add('tempo-icon');
|
||||
fileIcon.classList.add('tempo-icon--download');
|
||||
fileIcon.classList.add('download-complete-color');
|
||||
setTimeout(() => {
|
||||
fileIcon.classList.remove('download-complete-color');
|
||||
fileIcon.classList.remove('tempo-icon--download');
|
||||
fileIcon.classList.add('tempo-icon--document');
|
||||
}, 4000);
|
||||
downProgress.appendChild(fileIcon);
|
||||
|
||||
let fileNameDiv = document.createElement('div');
|
||||
fileNameDiv.classList.add('downloaded-filename');
|
||||
itemDiv.appendChild(fileNameDiv);
|
||||
|
||||
let h2FileName = document.createElement('h2');
|
||||
h2FileName.classList.add('text-cutoff');
|
||||
h2FileName.innerHTML = fileDisplayName;
|
||||
h2FileName.title = fileDisplayName;
|
||||
fileNameDiv.appendChild(h2FileName);
|
||||
|
||||
let fileProgressTitle = document.createElement('span');
|
||||
fileProgressTitle.id = 'per';
|
||||
fileProgressTitle.innerHTML = `${arg.total} ${downloadedText}`;
|
||||
fileNameDiv.appendChild(fileProgressTitle);
|
||||
|
||||
let caret = document.createElement('div');
|
||||
caret.id = 'menu';
|
||||
caret.classList.add('caret');
|
||||
caret.classList.add('tempo-icon');
|
||||
caret.classList.add('tempo-icon--dropdown');
|
||||
li.appendChild(caret);
|
||||
|
||||
let actionMenu = document.createElement('div');
|
||||
actionMenu.id = 'download-action-menu';
|
||||
actionMenu.classList.add('download-action-menu');
|
||||
caret.appendChild(actionMenu);
|
||||
|
||||
let caretUL = document.createElement('ul');
|
||||
caretUL.id = downloadItemKey;
|
||||
actionMenu.appendChild(caretUL);
|
||||
|
||||
let caretLiOpen = document.createElement('li');
|
||||
caretLiOpen.id = 'download-open';
|
||||
caretLiOpen.innerHTML = openText;
|
||||
caretUL.appendChild(caretLiOpen);
|
||||
let openFileDocument = document.getElementById('download-open');
|
||||
openFileDocument.addEventListener('click', () => {
|
||||
let id = openFileDocument.parentNode.id;
|
||||
openFile(id);
|
||||
});
|
||||
|
||||
let caretLiShow = document.createElement('li');
|
||||
caretLiShow.id = 'download-show-in-folder';
|
||||
caretLiShow.innerHTML = showInFolderText;
|
||||
caretUL.appendChild(caretLiShow);
|
||||
let showInFinderDocument = document.getElementById('download-show-in-folder');
|
||||
showInFinderDocument.addEventListener('click', () => {
|
||||
let id = showInFinderDocument.parentNode.id;
|
||||
showInFinder(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the download manager
|
||||
*/
|
||||
function initiate() {
|
||||
let mainFooter = document.getElementById('footer');
|
||||
let mainDownloadDiv = document.getElementById('download-manager-footer');
|
||||
|
||||
if (mainDownloadDiv) {
|
||||
|
||||
mainFooter.classList.remove('hidden');
|
||||
|
||||
let ulFind = document.getElementById('download-main');
|
||||
|
||||
if (!ulFind) {
|
||||
let uList = document.createElement('ul');
|
||||
uList.id = 'download-main';
|
||||
mainDownloadDiv.appendChild(uList);
|
||||
}
|
||||
|
||||
let closeSpanFind = document.getElementById('close-download-bar');
|
||||
|
||||
if (!closeSpanFind) {
|
||||
let closeSpan = document.createElement('span');
|
||||
closeSpan.id = 'close-download-bar';
|
||||
closeSpan.classList.add('close-download-bar');
|
||||
closeSpan.classList.add('tempo-icon');
|
||||
closeSpan.classList.add('tempo-icon--close');
|
||||
mainDownloadDiv.appendChild(closeSpan);
|
||||
}
|
||||
|
||||
let closeDownloadManager = document.getElementById('close-download-bar');
|
||||
if (closeDownloadManager) {
|
||||
closeDownloadManager.addEventListener('click', () => {
|
||||
local.downloadItems = [];
|
||||
document.getElementById('footer').classList.add('hidden');
|
||||
document.getElementById('download-main').innerHTML = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a file display name for the download item
|
||||
*/
|
||||
function getFileDisplayName(fileName) {
|
||||
let fileList = local.downloadItems;
|
||||
let fileNameCount = 0;
|
||||
let fileDisplayName = fileName;
|
||||
|
||||
/* Check if a file with the same name exists
|
||||
* (akin to the user downloading a file with the same name again)
|
||||
* in the download bar
|
||||
*/
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
if (fileName === fileList[i].fileName) {
|
||||
fileNameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/* If it exists, add a count to the name like how Chrome does */
|
||||
if (fileNameCount) {
|
||||
let extLastIndex = fileDisplayName.lastIndexOf('.');
|
||||
let fileCount = ' (' + fileNameCount + ')';
|
||||
|
||||
fileDisplayName = fileDisplayName.slice(0, extLastIndex) + fileCount + fileDisplayName.slice(extLastIndex);
|
||||
}
|
||||
|
||||
return fileDisplayName;
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let keyMirror = require('keymirror');
|
||||
|
||||
/**
|
||||
* Set of APIs exposed to the remote object
|
||||
* @type {Object}
|
||||
*/
|
||||
const cmds = keyMirror({
|
||||
isOnline: null,
|
||||
registerLogger: null,
|
||||
setBadgeCount: null,
|
||||
badgeDataUrl: null,
|
||||
activate: null,
|
||||
registerBoundsChange: null,
|
||||
registerProtocolHandler: null,
|
||||
registerActivityDetection: null,
|
||||
showNotificationSettings: null,
|
||||
sanitize: null,
|
||||
bringToFront: null,
|
||||
openScreenPickerWindow: null,
|
||||
popupMenu: null,
|
||||
optimizeMemoryConsumption: null,
|
||||
optimizeMemoryRegister: null,
|
||||
setIsInMeeting: null,
|
||||
setLocale: null,
|
||||
keyPress: null,
|
||||
openScreenSharingIndicator: null,
|
||||
cancelNetworkStatusCheck: null,
|
||||
quitWindow: null,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
cmds: cmds,
|
||||
apiName: 'symphony-api'
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let keyMirror = require('keymirror');
|
||||
|
||||
/**
|
||||
* The different log levels
|
||||
* @type {Object}
|
||||
*/
|
||||
module.exports = keyMirror({
|
||||
ERROR: null,
|
||||
CONFLICT: null,
|
||||
WARN: null,
|
||||
ACTION: null,
|
||||
INFO: null,
|
||||
DEBUG: null
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let keyMirror = require('keymirror');
|
||||
|
||||
/**
|
||||
* The different title bar styles
|
||||
* @type {Object}
|
||||
*/
|
||||
module.exports = keyMirror({
|
||||
CUSTOM: null,
|
||||
NATIVE: null,
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
// These method should only be used in main process
|
||||
module.exports = {
|
||||
emit: eventEmitter.emit,
|
||||
on: eventEmitter.on
|
||||
};
|
168
js/log.js
168
js/log.js
@ -1,168 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const getCmdLineArg = require('./utils/getCmdLineArg.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
const MAX_LOG_QUEUE_LENGTH = 100;
|
||||
|
||||
let electronLog;
|
||||
|
||||
class Logger {
|
||||
|
||||
constructor() {
|
||||
// browser window that has registered a logger
|
||||
this.logWindow = null;
|
||||
|
||||
// holds log messages received before logger has been registered.
|
||||
this.logQueue = [];
|
||||
|
||||
// Initializes the local logger
|
||||
if (!process.env.ELECTRON_QA) {
|
||||
initializeLocalLogger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send log messages from main process to logger hosted by
|
||||
* renderer process. Allows main process to use logger
|
||||
* provided by JS.
|
||||
* @param {enum} level enum from ./enums/LogLevel.js
|
||||
* @param {string} details msg to be logged
|
||||
*/
|
||||
send(level, details) {
|
||||
if (!level || !details) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.ELECTRON_QA) {
|
||||
logLocally(level, details);
|
||||
}
|
||||
|
||||
let logMsg = {
|
||||
level: level,
|
||||
details: details,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
if (this.logWindow) {
|
||||
this.logWindow.send('log', {
|
||||
msgs: [ logMsg ]
|
||||
});
|
||||
} else {
|
||||
// store log msgs for later when (if) we get logger registered
|
||||
this.logQueue.push(logMsg);
|
||||
// don't store more than 100 msgs. keep most recent log msgs.
|
||||
if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) {
|
||||
this.logQueue.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a window instance for the remote object
|
||||
* @param win
|
||||
*/
|
||||
setLogWindow(win) {
|
||||
this.logWindow = win;
|
||||
|
||||
if (this.logWindow) {
|
||||
let logMsg = {};
|
||||
|
||||
if (Array.isArray(this.logQueue)) {
|
||||
logMsg.msgs = this.logQueue;
|
||||
}
|
||||
|
||||
// configure desired log level and send pending log msgs
|
||||
let logLevel = getCmdLineArg(process.argv, '--logLevel=', false);
|
||||
if (logLevel) {
|
||||
let level = logLevel.split('=')[1];
|
||||
if (level) {
|
||||
logMsg.logLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (getCmdLineArg(process.argv, '--enableConsoleLogging', false)) {
|
||||
logMsg.showInConsole = true;
|
||||
}
|
||||
|
||||
if (Object.keys(logMsg).length) {
|
||||
this.logWindow.send('log', logMsg);
|
||||
}
|
||||
|
||||
this.logQueue = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let loggerInstance = new Logger();
|
||||
|
||||
/**
|
||||
* Initializes the electron logger for local logging
|
||||
*/
|
||||
function initializeLocalLogger() {
|
||||
|
||||
// If the user has specified a custom log path use it.
|
||||
let customLogPathArg = getCmdLineArg(process.argv, '--logPath=', false);
|
||||
let customLogsFolder = customLogPathArg && customLogPathArg.substring(customLogPathArg.indexOf('=') + 1);
|
||||
|
||||
if (customLogsFolder && fs.existsSync(customLogsFolder)) {
|
||||
app.setPath('logs', customLogsFolder);
|
||||
}
|
||||
// eslint-disable-next-line global-require
|
||||
electronLog = require('electron-log');
|
||||
const logPath = app.getPath('logs');
|
||||
cleanupOldLogs(logPath);
|
||||
electronLog.transports.file.file = path.join(logPath, `app_${Date.now()}.log`);
|
||||
electronLog.transports.file.level = 'debug';
|
||||
electronLog.transports.file.format = '{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}';
|
||||
electronLog.transports.file.appName = 'Symphony';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up old log files in the given path
|
||||
* @param {String} logPath Path of the log files
|
||||
*/
|
||||
function cleanupOldLogs(logPath) {
|
||||
let files = fs.readdirSync(logPath);
|
||||
const deleteTimeStamp = new Date().getTime() + (10 * 24 * 60 * 60 * 1000);
|
||||
files.forEach((file) => {
|
||||
if (file === '.DS_Store' || file === 'app.log') {
|
||||
return;
|
||||
}
|
||||
const filePath = path.join(logPath, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
const fileTimestamp = new Date(util.inspect(stat.mtime)).getTime();
|
||||
if (fileTimestamp > deleteTimeStamp) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs locally using the electron-logger
|
||||
* @param level
|
||||
* @param message
|
||||
*/
|
||||
function logLocally(level, message) {
|
||||
switch (level) {
|
||||
case logLevels.ERROR: electronLog.error(message); break;
|
||||
case logLevels.CONFLICT: electronLog.error(message); break;
|
||||
case logLevels.WARN: electronLog.warn(message); break;
|
||||
case logLevels.ACTION: electronLog.warn(message); break;
|
||||
case logLevels.INFO: electronLog.info(message); break;
|
||||
case logLevels.DEBUG: electronLog.debug(message); break;
|
||||
default: electronLog.debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Logger class is only exposed for testing purposes.
|
||||
module.exports = {
|
||||
Logger: Logger,
|
||||
send: loggerInstance.send.bind(loggerInstance),
|
||||
setLogWindow: loggerInstance.setLogWindow.bind(loggerInstance)
|
||||
};
|
479
js/main.js
479
js/main.js
@ -1,479 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// Third Party Dependencies
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const crashReporter = electron.crashReporter;
|
||||
const nodeURL = require('url');
|
||||
const shellPath = require('shell-path');
|
||||
const urlParser = require('url');
|
||||
const nodePath = require('path');
|
||||
require('electron-dl')();
|
||||
|
||||
const { version, clientVersion, buildNumber } = require('../package.json');
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
log.send(logLevels.INFO, `-----------------Starting the app with version ${version}-----------------`);
|
||||
|
||||
// Local Dependencies
|
||||
require('./stats');
|
||||
const { getConfigField, readConfigFileSync, updateUserConfigOnLaunch, getUserConfigField } = require('./config.js');
|
||||
const protocolHandler = require('./protocolHandler');
|
||||
const { setCheckboxValues } = require('./menus/menuTemplate.js');
|
||||
const autoLaunch = require('./autoLaunch');
|
||||
const { handleCacheFailureCheckOnStartup, handleCacheFailureCheckOnExit} = require('./cacheHandler');
|
||||
|
||||
const { isMac, isDevEnv } = require('./utils/misc.js');
|
||||
const getCmdLineArg = require('./utils/getCmdLineArg.js');
|
||||
|
||||
//setting the env path child_process issue https://github.com/electron/electron/issues/7688
|
||||
shellPath()
|
||||
.then((path) => {
|
||||
process.env.PATH = path
|
||||
})
|
||||
.catch(() => {
|
||||
process.env.PATH = [
|
||||
'./node_modules/.bin',
|
||||
'/usr/local/bin',
|
||||
process.env.PATH
|
||||
].join(':');
|
||||
});
|
||||
|
||||
// used to check if a url was opened when the app was already open
|
||||
let isAppAlreadyOpen = false;
|
||||
|
||||
require('./mainApiMgr.js');
|
||||
|
||||
// monitor memory of main process
|
||||
require('./memoryMonitor.js');
|
||||
|
||||
// adds 'symphony' as a protocol in the system.
|
||||
// plist file in macOS and registry entries on windows
|
||||
app.setAsDefaultProtocolClient('symphony');
|
||||
|
||||
const windowMgr = require('./windowMgr.js');
|
||||
const i18n = require('./translation/i18n');
|
||||
let ContextMenuBuilder;
|
||||
|
||||
getConfigField('url')
|
||||
.then(initializeCrashReporter)
|
||||
.catch(app.quit);
|
||||
|
||||
function initializeCrashReporter(podUrl) {
|
||||
|
||||
getConfigField('crashReporter')
|
||||
.then((crashReporterConfig) => {
|
||||
crashReporter.start({ companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, extra: { 'process': 'main', podUrl: podUrl } });
|
||||
log.send(logLevels.INFO, 'initialized crash reporter on the main process!');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.send(logLevels.ERROR, 'Unable to initialize crash reporter in the main process. Error is -> ' + err);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const allowMultiInstance = getCmdLineArg(process.argv, '--multiInstance', true) || isDevEnv;
|
||||
|
||||
if (!allowMultiInstance) {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
// quit if another instance is already running, ignore for dev env or if app was started with multiInstance flag
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
app.on('second-instance', (event, argv) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
log.send(logLevels.INFO, `Second instance created with args ${argv}`);
|
||||
let mainWin = windowMgr.getMainWindow();
|
||||
if (mainWin) {
|
||||
isAppAlreadyOpen = true;
|
||||
if (mainWin.isMinimized()) {
|
||||
mainWin.restore();
|
||||
}
|
||||
mainWin.focus();
|
||||
}
|
||||
processProtocolAction(argv);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
app.releaseSingleInstanceLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets chrome authentication flags in electron
|
||||
*/
|
||||
function setChromeFlags() {
|
||||
|
||||
// Read the config parameters synchronously
|
||||
let config = readConfigFileSync();
|
||||
|
||||
// If we cannot find any config, just skip setting any flags
|
||||
if (config && config.customFlags) {
|
||||
|
||||
if (config.customFlags.authServerWhitelist && config.customFlags.authServerWhitelist !== "") {
|
||||
app.commandLine.appendSwitch('auth-server-whitelist', config.customFlags.authServerWhitelist);
|
||||
}
|
||||
|
||||
if (config.customFlags.authNegotiateDelegateWhitelist && config.customFlags.authNegotiateDelegateWhitelist !== "") {
|
||||
app.commandLine.appendSwitch('auth-negotiate-delegate-whitelist', config.customFlags.authNegotiateDelegateWhitelist);
|
||||
}
|
||||
|
||||
if (config.customFlags.disableGpu) {
|
||||
app.commandLine.appendSwitch("disable-gpu", true);
|
||||
app.commandLine.appendSwitch("disable-gpu-compositing", true);
|
||||
app.commandLine.appendSwitch("disable-d3d11", true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch("disable-background-timer-throttling", true);
|
||||
|
||||
setChromeFlagsFromCommandLine();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments from command line
|
||||
* and set as chrome flags if applicable
|
||||
*/
|
||||
function setChromeFlagsFromCommandLine() {
|
||||
|
||||
// Special args that need to be excluded as part of the chrome command line switch
|
||||
let specialArgs = ['--url', '--multiInstance', '--userDataPath=', 'symphony://', '--inspect-brk', '--inspect'];
|
||||
|
||||
const cmdArgs = process.argv;
|
||||
cmdArgs.forEach((arg) => {
|
||||
// We need to check if the argument key matches the one
|
||||
// in the special args array and return if it does match
|
||||
const argSplit = arg.split('=');
|
||||
const argKey = argSplit[0];
|
||||
const argValue = argSplit[1] && arg.substring(arg.indexOf('=')+1);
|
||||
if (arg.startsWith('--') && specialArgs.includes(argKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All the chrome flags starts with --
|
||||
// So, any other arg (like 'electron' or '.')
|
||||
// need to be skipped
|
||||
if (arg.startsWith('--')) {
|
||||
// Since chrome takes values after an equals
|
||||
// We split the arg and set it either as
|
||||
// just a key, or as a key-value pair
|
||||
if (argKey && argValue) {
|
||||
app.commandLine.appendSwitch(argKey.substr(2), argValue);
|
||||
} else {
|
||||
app.commandLine.appendSwitch(argKey);
|
||||
}
|
||||
log.send(logLevels.INFO, `Appended chrome command line switch ${argKey} with value ${argValue}`);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Set the chrome flags
|
||||
setChromeFlags();
|
||||
|
||||
/**
|
||||
* This method will be called when Electron has finished
|
||||
* initialization and is ready to create browser windows.
|
||||
* Some APIs can only be used after this event occurs.
|
||||
*/
|
||||
app.on('ready', () => {
|
||||
log.send(logLevels.INFO, `App is ready, proceeding to load the POD`);
|
||||
handlePowerEvents();
|
||||
handleCacheFailureCheckOnStartup()
|
||||
.then(() => {
|
||||
initiateApp();
|
||||
})
|
||||
.catch((err) => {
|
||||
log.send(logLevels.INFO, `Couldn't clear cache and refresh -> ${err}`);
|
||||
initiateApp();
|
||||
});
|
||||
|
||||
function initiateApp() {
|
||||
checkFirstTimeLaunch()
|
||||
.then(readConfigThenOpenMainWindow)
|
||||
.catch(readConfigThenOpenMainWindow);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Is triggered when all the windows are closed
|
||||
* In which case we quit the app
|
||||
*/
|
||||
app.on('window-all-closed', function () {
|
||||
log.send(logLevels.INFO, `All windows closed, quitting the app`);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('quit', function () {
|
||||
log.send(logLevels.INFO, `-----------------Quitting the app-----------------`);
|
||||
handleCacheFailureCheckOnExit();
|
||||
});
|
||||
|
||||
/**
|
||||
* Is triggered when the app is up & running
|
||||
*/
|
||||
app.on('activate', function () {
|
||||
log.send(logLevels.INFO, `SDA is activated again`);
|
||||
if (windowMgr.isMainWindow(null)) {
|
||||
log.send(logLevels.INFO, `Main window instance is null, creating new instance`);
|
||||
setupThenOpenMainWindow();
|
||||
} else {
|
||||
log.send(logLevels.INFO, `Main window instance is available, showing it`);
|
||||
windowMgr.showMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
if (isMac) {
|
||||
// Sets application version info that will be displayed in about app panel
|
||||
app.setAboutPanelOptions({ applicationVersion: `${clientVersion}-${version}`, version: buildNumber });
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is emitted only on macOS
|
||||
* at this moment, support for windows
|
||||
* is in pipeline (https://github.com/electron/electron/pull/8052)
|
||||
*/
|
||||
app.on('open-url', function (event, url) {
|
||||
log.send(logLevels.INFO, `Open URL event triggered with url ${JSON.stringify(url)}`);
|
||||
handleProtocolAction(url);
|
||||
});
|
||||
|
||||
app.on('web-contents-created', function (event, webContents) {
|
||||
onWebContent(webContents);
|
||||
});
|
||||
|
||||
function onWebContent(webContents) {
|
||||
|
||||
if (!ContextMenuBuilder) {
|
||||
// eslint-disable-next-line global-require
|
||||
ContextMenuBuilder = require('electron-spellchecker').ContextMenuBuilder;
|
||||
}
|
||||
|
||||
const spellchecker = windowMgr.getSpellchecker();
|
||||
spellchecker.initializeSpellChecker();
|
||||
spellchecker.updateContextMenuLocale(i18n.getMessageFor('ContextMenu'));
|
||||
const contextMenuBuilder = new ContextMenuBuilder(spellchecker.spellCheckHandler, webContents, false, spellchecker.processMenu.bind(spellchecker));
|
||||
contextMenuBuilder.setAlternateStringFormatter(spellchecker.getStringTable(i18n.getMessageFor('ContextMenu')));
|
||||
let currentLocale = i18n.getLanguage();
|
||||
|
||||
const contextMenuListener = (event, info) => {
|
||||
log.send(logLevels.INFO, `Context menu event triggered for web contents with info ${JSON.stringify(info)}`);
|
||||
if (currentLocale !== i18n.getLanguage()) {
|
||||
contextMenuBuilder.setAlternateStringFormatter(spellchecker.getStringTable(i18n.getMessageFor('ContextMenu')));
|
||||
spellchecker.updateContextMenuLocale(i18n.getMessageFor('ContextMenu'));
|
||||
currentLocale = i18n.getLanguage();
|
||||
}
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
};
|
||||
|
||||
webContents.on('context-menu', contextMenuListener);
|
||||
|
||||
webContents.once('destroyed', () => {
|
||||
webContents.removeListener('context-menu', contextMenuListener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the config fields that are required for the menu items
|
||||
* then opens the main window
|
||||
*
|
||||
* This is a workaround for the issue where the menu template was returned
|
||||
* even before the config data was populated
|
||||
* https://perzoinc.atlassian.net/browse/ELECTRON-154
|
||||
*/
|
||||
function readConfigThenOpenMainWindow() {
|
||||
setCheckboxValues()
|
||||
.then(setupThenOpenMainWindow)
|
||||
.catch(setupThenOpenMainWindow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the app (to handle various things like config changes, protocol handling etc.)
|
||||
* and opens the main window
|
||||
*/
|
||||
function setupThenOpenMainWindow() {
|
||||
|
||||
processProtocolAction(process.argv);
|
||||
|
||||
isAppAlreadyOpen = true;
|
||||
getUrlAndCreateMainWindow();
|
||||
|
||||
// Allows a developer to set custom user data path from command line when
|
||||
// launching the app. Mostly used for running automation tests with
|
||||
// multiple instances
|
||||
let customDataArg = getCmdLineArg(process.argv, '--userDataPath=', false);
|
||||
let customDataFolder = customDataArg && customDataArg.substring(customDataArg.indexOf('=') + 1);
|
||||
|
||||
if (customDataArg && customDataFolder) {
|
||||
app.setPath('userData', customDataFolder);
|
||||
}
|
||||
|
||||
// Event that fixes the remote desktop issue in Windows
|
||||
// by repositioning the browser window
|
||||
electron.screen.on('display-removed', windowMgr.verifyDisplays);
|
||||
|
||||
}
|
||||
|
||||
function checkFirstTimeLaunch() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getUserConfigField('configBuildNumber')
|
||||
.then((configBuildNumber) => {
|
||||
const execPath = nodePath.dirname(app.getPath('exe'));
|
||||
const shouldUpdateUserConfig = execPath.indexOf('AppData\\Local\\Programs') !== -1 || isMac;
|
||||
|
||||
if (configBuildNumber && typeof configBuildNumber === 'string' && configBuildNumber !== buildNumber) {
|
||||
return setupFirstTimeLaunch(resolve, reject, shouldUpdateUserConfig);
|
||||
}
|
||||
log.send(logLevels.INFO, `not a first-time launch as
|
||||
configBuildNumber: ${configBuildNumber} installerBuildNumber: ${buildNumber} shouldUpdateUserConfig: ${shouldUpdateUserConfig}`);
|
||||
return resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
log.send(logLevels.ERROR, `Error reading configVersion error: ${e}`);
|
||||
return setupFirstTimeLaunch(resolve, reject, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup and update user config
|
||||
* on first time launch or if the latest app version
|
||||
*
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
function setupFirstTimeLaunch(resolve, reject, shouldUpdateUserConfig) {
|
||||
log.send(logLevels.INFO, 'setting first time launch config');
|
||||
getConfigField('launchOnStartup')
|
||||
.then(setStartup)
|
||||
.then(() => {
|
||||
if (shouldUpdateUserConfig) {
|
||||
log.send(logLevels.INFO, `Resetting user config data? ${shouldUpdateUserConfig}`);
|
||||
return updateUserConfigOnLaunch(resolve, reject);
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Symphony on startup
|
||||
* @param lStartup
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function setStartup(lStartup) {
|
||||
log.send(logLevels.INFO, `launch on startup parameter value is ${lStartup}`);
|
||||
return new Promise((resolve) => {
|
||||
let launchOnStartup = (String(lStartup) === 'true');
|
||||
log.send(logLevels.INFO, `launchOnStartup value is ${launchOnStartup}`);
|
||||
if (launchOnStartup) {
|
||||
log.send(logLevels.INFO, `enabling launch on startup`);
|
||||
autoLaunch.enable();
|
||||
return resolve();
|
||||
}
|
||||
autoLaunch.disable();
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the url argument, processes it
|
||||
* and creates the main window
|
||||
*/
|
||||
function getUrlAndCreateMainWindow() {
|
||||
// allow passing url argument
|
||||
let url = getCmdLineArg(process.argv, '--url=', false);
|
||||
if (url) {
|
||||
windowMgr.createMainWindow(url.substr(6));
|
||||
return;
|
||||
}
|
||||
|
||||
getConfigField('url')
|
||||
.then(createWin).catch(function (err) {
|
||||
log.send(logLevels.ERROR, `unable to create main window -> ${err}`);
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window
|
||||
* @param urlFromConfig
|
||||
*/
|
||||
function createWin(urlFromConfig) {
|
||||
// add https protocol if none found.
|
||||
let parsedUrl = nodeURL.parse(urlFromConfig);
|
||||
|
||||
if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') {
|
||||
parsedUrl.protocol = 'https:';
|
||||
parsedUrl.slashes = true
|
||||
}
|
||||
let url = nodeURL.format(parsedUrl);
|
||||
|
||||
windowMgr.createMainWindow(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* processes protocol action for windows clients
|
||||
* @param argv {Array} an array of command line arguments
|
||||
*/
|
||||
function processProtocolAction(argv) {
|
||||
|
||||
// In case of windows, we need to handle protocol handler
|
||||
// manually because electron doesn't emit
|
||||
// 'open-url' event on windows
|
||||
if (!(process.platform === 'win32')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let protocolUri = getCmdLineArg(argv, 'symphony://', false);
|
||||
log.send(logLevels.INFO, `Trying to process a protocol action for uri ${protocolUri}`);
|
||||
|
||||
if (protocolUri) {
|
||||
const parsedURL = urlParser.parse(protocolUri);
|
||||
if (!parsedURL.protocol || !parsedURL.slashes) {
|
||||
return;
|
||||
}
|
||||
log.send(logLevels.INFO, `Parsing protocol url successful for ${parsedURL}`);
|
||||
handleProtocolAction(protocolUri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a protocol action based on the current state of the app
|
||||
* @param uri
|
||||
*/
|
||||
function handleProtocolAction(uri) {
|
||||
if (!isAppAlreadyOpen) {
|
||||
log.send(logLevels.INFO, `App started by protocol url ${uri}. We are caching this to be processed later!`);
|
||||
// app is opened by the protocol url, cache the protocol url to be used later
|
||||
protocolHandler.setProtocolUrl(uri);
|
||||
} else {
|
||||
// This is needed for mac OS as it brings pop-outs to foreground
|
||||
// (if it has been previously focused) instead of main window
|
||||
if (isMac) {
|
||||
const mainWindow = windowMgr.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
windowMgr.activate(mainWindow.winName);
|
||||
}
|
||||
}
|
||||
// app is already open, so, just trigger the protocol action method
|
||||
log.send(logLevels.INFO, `App opened by protocol url ${uri}`);
|
||||
protocolHandler.processProtocolAction(uri);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePowerEvents = () => {
|
||||
|
||||
const events = [
|
||||
'suspend', 'resume', 'on-ac', 'on-battery', 'shutdown', 'lock-screen', 'unlock-screen'
|
||||
];
|
||||
|
||||
events.forEach((appEvent) => {
|
||||
electron.powerMonitor.on(appEvent, () => {
|
||||
log.send(logLevels.INFO, `Power Monitor Event Occurred: ${appEvent}`)
|
||||
});
|
||||
});
|
||||
};
|
210
js/mainApiMgr.js
210
js/mainApiMgr.js
@ -1,210 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This module runs in the main process and handles api calls
|
||||
* from the renderer process.
|
||||
*/
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
|
||||
const windowMgr = require('./windowMgr.js');
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels');
|
||||
const activityDetection = require('./activityDetection');
|
||||
const badgeCount = require('./badgeCount.js');
|
||||
const protocolHandler = require('./protocolHandler');
|
||||
const configureNotification = require('./notify/settings/configure-notification-position');
|
||||
const { bringToFront } = require('./bringToFront.js');
|
||||
const eventEmitter = require('./eventEmitter');
|
||||
const { isMac } = require('./utils/misc');
|
||||
const { openScreenPickerWindow } = require('./desktopCapturer');
|
||||
const { openScreenSharingIndicator } = require('./screenSharingIndicator');
|
||||
const { setPreloadMemoryInfo, setIsInMeeting, setPreloadWindow } = require('./memoryMonitor');
|
||||
|
||||
const apiEnums = require('./enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
|
||||
// can be overridden for testing
|
||||
let checkValidWindow = true;
|
||||
|
||||
/**
|
||||
* Ensure events comes from a window that we have created.
|
||||
* @param {EventEmitter} event node emitter event to be tested
|
||||
* @return {Boolean} returns true if exists otherwise false
|
||||
*/
|
||||
function isValidWindow(event) {
|
||||
if (!checkValidWindow) {
|
||||
return true;
|
||||
}
|
||||
let result = false;
|
||||
if (event && event.sender) {
|
||||
// validate that event sender is from window we created
|
||||
const browserWin = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
const winKey = event.sender.browserWindowOptions &&
|
||||
event.sender.browserWindowOptions.winKey;
|
||||
|
||||
result = windowMgr.hasWindow(browserWin, winKey);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
log.send(logLevels.WARN, 'invalid window try to perform action, ignoring action');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that is invoked when the application is reloaded/navigated
|
||||
* window.addEventListener('beforeunload')
|
||||
* @param windowName
|
||||
*/
|
||||
function sanitize(windowName) {
|
||||
// To make sure the reload event is from the main window
|
||||
if (windowMgr.getMainWindow() && windowName === windowMgr.getMainWindow().winName) {
|
||||
// reset the badge count whenever an user refreshes the electron client
|
||||
badgeCount.show(0);
|
||||
|
||||
// Terminates the screen snippet process on reload
|
||||
if (!isMac) {
|
||||
eventEmitter.emit('killScreenSnippet');
|
||||
}
|
||||
// Closes all the child windows
|
||||
windowMgr.cleanUpChildWindows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API related ipc messages from renderers. Only messages from windows
|
||||
* we have created are allowed.
|
||||
*/
|
||||
electron.ipcMain.on(apiName, (event, arg) => {
|
||||
|
||||
if (!isValidWindow(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(arg.cmd) {
|
||||
case apiCmds.isOnline:
|
||||
if (typeof arg.isOnline === 'boolean') {
|
||||
windowMgr.setIsOnline(arg.isOnline);
|
||||
}
|
||||
break;
|
||||
case apiCmds.setBadgeCount:
|
||||
if (typeof arg.count === 'number') {
|
||||
badgeCount.show(arg.count);
|
||||
}
|
||||
break;
|
||||
case apiCmds.registerProtocolHandler:
|
||||
protocolHandler.setProtocolWindow(event.sender);
|
||||
protocolHandler.checkProtocolAction();
|
||||
break;
|
||||
case apiCmds.badgeDataUrl:
|
||||
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
|
||||
badgeCount.setDataUrl(arg.dataUrl, arg.count);
|
||||
}
|
||||
break;
|
||||
case apiCmds.activate:
|
||||
if (typeof arg.windowName === 'string') {
|
||||
windowMgr.activate(arg.windowName);
|
||||
}
|
||||
break;
|
||||
case apiCmds.registerBoundsChange:
|
||||
windowMgr.setBoundsChangeWindow(event.sender);
|
||||
break;
|
||||
case apiCmds.registerLogger:
|
||||
// renderer window that has a registered logger from JS.
|
||||
log.setLogWindow(event.sender);
|
||||
break;
|
||||
case apiCmds.registerActivityDetection:
|
||||
if (typeof arg.period === 'number') {
|
||||
// renderer window that has a registered activity detection from JS.
|
||||
activityDetection.setActivityWindow(arg.period, event.sender);
|
||||
}
|
||||
break;
|
||||
case apiCmds.showNotificationSettings:
|
||||
if (typeof arg.windowName === 'string') {
|
||||
configureNotification.openConfigurationWindow(arg.windowName);
|
||||
}
|
||||
break;
|
||||
case apiCmds.sanitize:
|
||||
if (typeof arg.windowName === 'string') {
|
||||
sanitize(arg.windowName);
|
||||
}
|
||||
break;
|
||||
case apiCmds.bringToFront:
|
||||
// validates the user bring to front config and activates the wrapper
|
||||
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
|
||||
bringToFront(arg.windowName, arg.reason);
|
||||
}
|
||||
break;
|
||||
case apiCmds.openScreenPickerWindow:
|
||||
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
|
||||
openScreenPickerWindow(event.sender, arg.sources, arg.id);
|
||||
}
|
||||
break;
|
||||
case apiCmds.popupMenu: {
|
||||
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
if (browserWin && !browserWin.isDestroyed()) {
|
||||
windowMgr.getMenu().popup(browserWin, {x: 20, y: 15, async: true});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case apiCmds.optimizeMemoryConsumption:
|
||||
if (typeof arg.activeRequests === 'number') {
|
||||
log.send(logLevels.INFO, 'Received active network request from renderer processes');
|
||||
setPreloadMemoryInfo(arg.activeRequests);
|
||||
}
|
||||
break;
|
||||
case apiCmds.optimizeMemoryRegister:
|
||||
setPreloadWindow(event.sender);
|
||||
break;
|
||||
case apiCmds.setIsInMeeting:
|
||||
if (typeof arg.isInMeeting === 'boolean') {
|
||||
setIsInMeeting(arg.isInMeeting);
|
||||
}
|
||||
break;
|
||||
case apiCmds.setLocale:
|
||||
if (typeof arg.locale === 'string') {
|
||||
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
windowMgr.setLocale(browserWin, { language: arg.locale });
|
||||
}
|
||||
break;
|
||||
case apiCmds.keyPress:
|
||||
if (typeof arg.keyCode === 'number') {
|
||||
windowMgr.handleKeyPress(arg.keyCode);
|
||||
}
|
||||
break;
|
||||
case apiCmds.isMisspelled:
|
||||
if (typeof arg.text === 'string') {
|
||||
/* eslint-disable no-param-reassign */
|
||||
event.returnValue = windowMgr.isMisspelled(arg.text);
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
break;
|
||||
case apiCmds.openScreenSharingIndicator:
|
||||
if (typeof arg.displayId === 'string' && typeof arg.id === 'number') {
|
||||
openScreenSharingIndicator(event.sender, arg.displayId, arg.id);
|
||||
}
|
||||
break;
|
||||
case apiCmds.cancelNetworkStatusCheck:
|
||||
windowMgr.cancelNetworkStatusCheck();
|
||||
break;
|
||||
case apiCmds.quitWindow:
|
||||
app.quit();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// expose these methods primarily for testing...
|
||||
module.exports = {
|
||||
shouldCheckValidWindow: function(shouldCheck) {
|
||||
checkValidWindow = shouldCheck;
|
||||
}
|
||||
};
|
@ -1,143 +0,0 @@
|
||||
'use strict';
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
const { getMainWindow, setIsAutoReload, getIsOnline } = require('./windowMgr');
|
||||
const { getConfigField } = require('./config');
|
||||
|
||||
const memoryRefreshThreshold = 60 * 60 * 1000;
|
||||
const maxIdleTime = 4 * 60 * 60 * 1000;
|
||||
const memoryRefreshInterval = 60 * 60 * 1000;
|
||||
const cpuUsageThreshold = 5;
|
||||
|
||||
let isInMeeting = false;
|
||||
let canReload = true;
|
||||
let preloadMemory;
|
||||
let preloadWindow;
|
||||
|
||||
// once a minute
|
||||
setInterval(gatherMemory, 1000 * 60);
|
||||
|
||||
/**
|
||||
* Gathers system memory and logs it to the remote system
|
||||
*/
|
||||
function gatherMemory() {
|
||||
let appMetrics = app.getAppMetrics();
|
||||
log.send(logLevels.INFO, `Current App Metrics -> ${JSON.stringify(appMetrics)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that checks memory usage every minute
|
||||
* and verify if the user in inactive if so it reloads the
|
||||
* application to free up some memory consumption
|
||||
*/
|
||||
function optimizeMemory() {
|
||||
|
||||
if (!preloadMemory.cpuUsage) {
|
||||
log.send(logLevels.INFO, `cpu usage not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cpuUsagePercentage = preloadMemory.cpuUsage.percentCPUUsage;
|
||||
const activeNetworkRequest = preloadMemory.activeRequests === 0;
|
||||
|
||||
electron.powerMonitor.querySystemIdleTime((time) => {
|
||||
const idleTime = time * 1000;
|
||||
if (cpuUsagePercentage <= cpuUsageThreshold
|
||||
&& !isInMeeting
|
||||
&& getIsOnline()
|
||||
&& canReload
|
||||
&& idleTime > maxIdleTime
|
||||
&& activeNetworkRequest
|
||||
) {
|
||||
getConfigField('memoryRefresh')
|
||||
.then((enabled) => {
|
||||
if (enabled) {
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
setIsAutoReload(true);
|
||||
log.send(logLevels.INFO, `Reloading the app to optimize memory usage as
|
||||
memory consumption is no longer detectable
|
||||
CPU usage percentage was ${preloadMemory.cpuUsage.percentCPUUsage}
|
||||
user was in a meeting? ${isInMeeting}
|
||||
pending network request on the client was ${preloadMemory.activeRequests}
|
||||
is network online? ${getIsOnline()}`);
|
||||
mainWindow.reload();
|
||||
|
||||
// do not refresh for another 1hrs
|
||||
canReload = false;
|
||||
setTimeout(() => {
|
||||
canReload = true;
|
||||
}, memoryRefreshThreshold);
|
||||
}
|
||||
} else {
|
||||
log.send(logLevels.INFO, `Memory refresh not enabled by the user so Not Reloading the app`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.send(logLevels.INFO, `Not Reloading the app as
|
||||
application was refreshed less than a hour ago? ${canReload ? 'no' : 'yes'}
|
||||
memory consumption is no longer detectable
|
||||
CPU usage percentage was ${preloadMemory.cpuUsage.percentCPUUsage}
|
||||
user was in a meeting? ${isInMeeting}
|
||||
pending network request on the client was ${preloadMemory.activeRequests}
|
||||
is network online? ${getIsOnline()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current user meeting status
|
||||
* @param meetingStatus - Whether user is in an active meeting
|
||||
*/
|
||||
function setIsInMeeting(meetingStatus) {
|
||||
isInMeeting = meetingStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets preload memory info and calls optimize memory func
|
||||
*
|
||||
* @param activeRequests - pending active network requests on the client
|
||||
*/
|
||||
function setPreloadMemoryInfo(activeRequests) {
|
||||
log.send(logLevels.INFO, 'Memory info received from preload process now running optimize memory logic');
|
||||
const cpuUsage = process.getCPUUsage();
|
||||
preloadMemory = { cpuUsage, activeRequests };
|
||||
optimizeMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preload window
|
||||
*
|
||||
* @param win - preload window
|
||||
*/
|
||||
function setPreloadWindow(win) {
|
||||
log.send(logLevels.INFO, 'Preload window registered');
|
||||
preloadWindow = win;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request memory info from the registered preload window
|
||||
* which invokes the optimize memory func
|
||||
*/
|
||||
function requestMemoryInfo() {
|
||||
if (preloadWindow) {
|
||||
log.send(logLevels.INFO, 'Requesting memory information from the preload script');
|
||||
preloadWindow.send('memory-info-request');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests memory info from the renderer every 4 hrs
|
||||
*/
|
||||
setInterval(() => {
|
||||
requestMemoryInfo();
|
||||
}, memoryRefreshInterval);
|
||||
|
||||
module.exports = {
|
||||
setIsInMeeting,
|
||||
setPreloadMemoryInfo,
|
||||
setPreloadWindow,
|
||||
};
|
@ -1,582 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const electron = require('electron');
|
||||
|
||||
const { updateConfigField, getMultipleConfigField, readConfigFromFile } = require('../config.js');
|
||||
const { isMac, isWindowsOS } = require('../utils/misc.js');
|
||||
const archiveHandler = require('../utils/archiveHandler');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const eventEmitter = require('../eventEmitter');
|
||||
const aboutApp = require('../aboutApp');
|
||||
const moreInfo = require('../moreInfo');
|
||||
const titleBarStyles = require('../enums/titleBarStyles');
|
||||
const i18n = require('../translation/i18n');
|
||||
const autoLaunch = require('../autoLaunch');
|
||||
|
||||
const configFields = [
|
||||
'minimizeOnClose',
|
||||
'launchOnStartup',
|
||||
'alwaysOnTop',
|
||||
'notificationSettings',
|
||||
'bringToFront',
|
||||
'memoryRefresh',
|
||||
'isCustomTitleBar'
|
||||
];
|
||||
|
||||
let minimizeOnClose = false;
|
||||
let launchOnStartup = false;
|
||||
let isAlwaysOnTop = false;
|
||||
let bringToFront = false;
|
||||
let memoryRefresh = false;
|
||||
let titleBarStyle = titleBarStyles.CUSTOM;
|
||||
|
||||
const windowsAccelerator = Object.assign({
|
||||
undo: 'Ctrl+Z',
|
||||
redo: 'Ctrl+Y',
|
||||
cut: 'Ctrl+X',
|
||||
copy: 'Ctrl+C',
|
||||
paste: 'Ctrl+V',
|
||||
pasteandmatchstyle: 'Ctrl+Shift+V',
|
||||
selectall: 'Ctrl+A',
|
||||
resetzoom: 'Ctrl+0',
|
||||
zoomin: 'Ctrl+Shift+Plus',
|
||||
zoomout: 'Ctrl+-',
|
||||
togglefullscreen: 'F11',
|
||||
minimize: 'Ctrl+M',
|
||||
close: 'Ctrl+W',
|
||||
});
|
||||
|
||||
function getTemplate(app) {
|
||||
|
||||
const template = [{
|
||||
label: i18n.getMessageFor('Edit'),
|
||||
submenu: [
|
||||
buildMenuItem('undo', i18n.getMessageFor('Undo')),
|
||||
buildMenuItem('redo', i18n.getMessageFor('Redo')),
|
||||
{ type: 'separator' },
|
||||
buildMenuItem('cut', i18n.getMessageFor('Cut')),
|
||||
buildMenuItem('copy', i18n.getMessageFor('Copy')),
|
||||
buildMenuItem('paste', i18n.getMessageFor('Paste')),
|
||||
buildMenuItem('pasteandmatchstyle', i18n.getMessageFor('Paste and Match Style')),
|
||||
buildMenuItem('delete', i18n.getMessageFor('Delete')),
|
||||
buildMenuItem('selectall', i18n.getMessageFor('Select All'))
|
||||
]
|
||||
},
|
||||
{
|
||||
label: i18n.getMessageFor('View'),
|
||||
submenu: [{
|
||||
label: i18n.getMessageFor('Reload'),
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
buildMenuItem('resetzoom', i18n.getMessageFor('Actual Size')),
|
||||
buildMenuItem('zoomin', i18n.getMessageFor('Zoom In')),
|
||||
buildMenuItem('zoomout', i18n.getMessageFor('Zoom Out')),
|
||||
{ type: 'separator' },
|
||||
buildMenuItem('togglefullscreen', i18n.getMessageFor('Toggle Full Screen')),
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
label: i18n.getMessageFor('Window'),
|
||||
submenu: [
|
||||
buildMenuItem('minimize', i18n.getMessageFor('Minimize')),
|
||||
buildMenuItem('close', i18n.getMessageFor('Close')),
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: i18n.getMessageFor('Help'),
|
||||
submenu:
|
||||
[
|
||||
{
|
||||
label: i18n.getMessageFor('Symphony Help'),
|
||||
click() {
|
||||
let helpUrl = i18n.getMessageFor('Help Url');
|
||||
electron.shell.openExternal(helpUrl);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: i18n.getMessageFor('Learn More'),
|
||||
click() {
|
||||
let symUrl = i18n.getMessageFor('Symphony Url');
|
||||
electron.shell.openExternal(symUrl);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: i18n.getMessageFor('Troubleshooting'),
|
||||
submenu: [
|
||||
{
|
||||
label: isMac ? i18n.getMessageFor('Show Logs in Finder') : i18n.getMessageFor('Show Logs in Explorer'),
|
||||
click(item, focusedWindow) {
|
||||
|
||||
const FILE_EXTENSIONS = ['.log'];
|
||||
const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
|
||||
const WINDOWS_LOGS_PATH = '\\AppData\\Roaming\\Symphony\\logs';
|
||||
|
||||
let logsPath = isMac ? MAC_LOGS_PATH : WINDOWS_LOGS_PATH;
|
||||
let source = electron.app.getPath('home') + logsPath;
|
||||
|
||||
if (!fs.existsSync(source) && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Failed!'),
|
||||
message: i18n.getMessageFor('No logs are available to share')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let destPath = isMac ? '/logs_symphony_' : '\\logs_symphony_';
|
||||
let timestamp = new Date().getTime();
|
||||
|
||||
let destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
|
||||
|
||||
archiveHandler.generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
|
||||
.then(() => {
|
||||
electron.shell.showItemInFolder(destination);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Failed!'),
|
||||
message: i18n.getMessageFor('Unable to generate logs due to ') + err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
label: isMac ? i18n.getMessageFor('Show crash dump in Finder') : i18n.getMessageFor('Show crash dump in Explorer'),
|
||||
click(item, focusedWindow) {
|
||||
const FILE_EXTENSIONS = isMac ? ['.dmp'] : ['.dmp', '.txt'];
|
||||
const crashesDirectory = electron.crashReporter.getCrashesDirectory();
|
||||
let source = isMac ? crashesDirectory + '/completed' : crashesDirectory;
|
||||
|
||||
// TODO: Add support to get diagnostic reports from ~/Library/Logs/DiagnosticReports
|
||||
if (!fs.existsSync(source) || fs.readdirSync(source).length === 0 && focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Failed!'),
|
||||
message: i18n.getMessageFor('No crashes available to share')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let destPath = isMac ? '/crashes_symphony_' : '\\crashes_symphony_';
|
||||
let timestamp = new Date().getTime();
|
||||
|
||||
let destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
|
||||
|
||||
archiveHandler.generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
|
||||
.then(() => {
|
||||
electron.shell.showItemInFolder(destination);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Failed!'),
|
||||
message: i18n.getMessageFor('Unable to generate crash reports due to ') + err
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: i18n.getMessageFor('Toggle Developer Tools'),
|
||||
accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
let devToolsEnabled = readConfigFromFile('devToolsEnabled');
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
if (devToolsEnabled) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
} else {
|
||||
log.send(logLevels.INFO, `dev tools disabled for ${focusedWindow.winName} window`);
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Ok'],
|
||||
title: i18n.getMessageFor('Dev Tools disabled'),
|
||||
message: i18n.getMessageFor('Dev Tools has been disabled! Please contact your system administrator to enable it!'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: i18n.getMessageFor('More Information'),
|
||||
click(item, focusedWindow) {
|
||||
let windowName = focusedWindow ? focusedWindow.name : '';
|
||||
moreInfo.openMoreInfoWindow(windowName);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
if (isMac && template[0].label !== app.getName()) {
|
||||
template.unshift({
|
||||
label: app.getName(),
|
||||
submenu: [{
|
||||
role: 'about',
|
||||
label: i18n.getMessageFor('About Symphony')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'services',
|
||||
label: i18n.getMessageFor('Services'),
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'hide',
|
||||
label: i18n.getMessageFor('Hide Symphony')
|
||||
},
|
||||
{
|
||||
role: 'hideothers',
|
||||
label: i18n.getMessageFor('Hide Others')
|
||||
},
|
||||
{
|
||||
role: 'unhide',
|
||||
label: i18n.getMessageFor('Show All')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'quit',
|
||||
label: i18n.getMessageFor('Quit Symphony')
|
||||
}
|
||||
]
|
||||
});
|
||||
// Edit menu.
|
||||
template[1].submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: i18n.getMessageFor('Speech'),
|
||||
submenu: [{
|
||||
role: 'startspeaking',
|
||||
label: i18n.getMessageFor('Start Speaking')
|
||||
},
|
||||
{
|
||||
role: 'stopspeaking',
|
||||
label: i18n.getMessageFor('Stop Speaking')
|
||||
}
|
||||
]
|
||||
});
|
||||
// Window menu.
|
||||
template[3].submenu = [{
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
role: 'close',
|
||||
label: i18n.getMessageFor('Close')
|
||||
},
|
||||
{
|
||||
accelerator: 'CmdOrCtrl+M',
|
||||
role: 'minimize',
|
||||
label: i18n.getMessageFor('Minimize')
|
||||
},
|
||||
{
|
||||
role: 'zoom',
|
||||
label: i18n.getMessageFor('Zoom')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'front',
|
||||
label: i18n.getMessageFor('Bring All to Front')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
let index = 2;
|
||||
if (isMac && template[0].label !== app.getName()) {
|
||||
index = 3;
|
||||
}
|
||||
|
||||
template[index].submenu.push({
|
||||
type: 'separator'
|
||||
});
|
||||
|
||||
// Window menu -> launchOnStartup.
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Auto Launch On Startup'),
|
||||
type: 'checkbox',
|
||||
checked: launchOnStartup,
|
||||
click: function (item, focusedWindow) {
|
||||
if (item.checked) {
|
||||
autoLaunch.enable()
|
||||
.catch(function (err) {
|
||||
let title = 'Error setting AutoLaunch configuration';
|
||||
log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': auto launch error ' + err);
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor(title),
|
||||
message: i18n.getMessageFor(title) + ': ' + err
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
autoLaunch.disable()
|
||||
.catch(function (err) {
|
||||
let title = 'Error setting AutoLaunch configuration';
|
||||
log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': auto launch error ' + err);
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.dialog.showMessageBox(focusedWindow, {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor(title),
|
||||
message: i18n.getMessageFor(title) + ': ' + err
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
launchOnStartup = item.checked;
|
||||
updateConfigField('launchOnStartup', launchOnStartup);
|
||||
}
|
||||
});
|
||||
|
||||
// Window menu -> alwaysOnTop.
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Always on Top'),
|
||||
type: 'checkbox',
|
||||
checked: isAlwaysOnTop,
|
||||
click: (item) => {
|
||||
isAlwaysOnTop = item.checked;
|
||||
eventEmitter.emit('isAlwaysOnTop', {
|
||||
isAlwaysOnTop,
|
||||
shouldActivateMainWindow: true
|
||||
});
|
||||
updateConfigField('alwaysOnTop', isAlwaysOnTop);
|
||||
}
|
||||
});
|
||||
|
||||
// Window menu -> minimizeOnClose.
|
||||
// ToDo: Add behavior on Close.
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Minimize on Close'),
|
||||
type: 'checkbox',
|
||||
checked: minimizeOnClose,
|
||||
click: function (item) {
|
||||
minimizeOnClose = item.checked;
|
||||
updateConfigField('minimizeOnClose', minimizeOnClose);
|
||||
}
|
||||
});
|
||||
|
||||
// Window menu -> bringToFront
|
||||
template[index].submenu.push({
|
||||
label: isWindowsOS ? i18n.getMessageFor('Flash Notification in Taskbar') : i18n.getMessageFor('Bring to Front on Notifications'),
|
||||
type: 'checkbox',
|
||||
checked: bringToFront,
|
||||
click: function (item) {
|
||||
bringToFront = item.checked;
|
||||
updateConfigField('bringToFront', bringToFront);
|
||||
}
|
||||
});
|
||||
|
||||
// Window/View menu -> separator
|
||||
template[index].submenu.push({
|
||||
type: 'separator',
|
||||
});
|
||||
|
||||
// Window - View menu -> memoryRefresh
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Refresh app when idle'),
|
||||
type: 'checkbox',
|
||||
checked: memoryRefresh,
|
||||
click: function (item) {
|
||||
memoryRefresh = item.checked;
|
||||
updateConfigField('memoryRefresh', memoryRefresh);
|
||||
}
|
||||
});
|
||||
|
||||
// Window - View menu -> Clear Cache
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Clear cache and Reload'),
|
||||
click: function (item, focusedWindow) {
|
||||
if (focusedWindow && !focusedWindow.isDestroyed()) {
|
||||
electron.session.defaultSession.clearCache(() => {
|
||||
focusedWindow.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isMac) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
template[index].submenu.push({
|
||||
label: titleBarStyle === titleBarStyles.NATIVE ?
|
||||
i18n.getMessageFor('Enable Hamburger menu') :
|
||||
i18n.getMessageFor('Disable Hamburger menu'),
|
||||
click: function () {
|
||||
const isNativeStyle = titleBarStyle === titleBarStyles.NATIVE;
|
||||
|
||||
titleBarStyle = isNativeStyle ? titleBarStyles.NATIVE : titleBarStyles.CUSTOM;
|
||||
titleBarActions(app, isNativeStyle);
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
});
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
template[index].submenu.push({
|
||||
label: i18n.getMessageFor('Quit Symphony'),
|
||||
click: function () {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// This adds About Symphony under help menu for windows
|
||||
template[3].submenu.push({
|
||||
label: i18n.getMessageFor('About Symphony'),
|
||||
click(menuItem, focusedWindow) {
|
||||
let windowName = focusedWindow ? focusedWindow.winName : '';
|
||||
aboutApp.openAboutWindow(windowName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the checkbox values for different menu items
|
||||
* based on configuration
|
||||
*/
|
||||
function setCheckboxValues() {
|
||||
return new Promise((resolve) => {
|
||||
/**
|
||||
* Method that reads multiple config fields
|
||||
*/
|
||||
getMultipleConfigField(configFields)
|
||||
.then(function (configData) {
|
||||
for (let key in configData) {
|
||||
if (configData.hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins
|
||||
switch (key) {
|
||||
case 'minimizeOnClose':
|
||||
minimizeOnClose = configData[key];
|
||||
break;
|
||||
case 'launchOnStartup':
|
||||
launchOnStartup = configData[key];
|
||||
break;
|
||||
case 'alwaysOnTop':
|
||||
isAlwaysOnTop = configData[key];
|
||||
eventEmitter.emit('isAlwaysOnTop', {
|
||||
isAlwaysOnTop: configData[key],
|
||||
shouldActivateMainWindow: true
|
||||
});
|
||||
break;
|
||||
case 'notificationSettings':
|
||||
eventEmitter.emit('notificationSettings', configData[key]);
|
||||
break;
|
||||
case 'bringToFront':
|
||||
bringToFront = configData[key];
|
||||
break;
|
||||
case 'isCustomTitleBar':
|
||||
titleBarStyle = configData[key] ? titleBarStyles.CUSTOM : titleBarStyles.NATIVE;
|
||||
break;
|
||||
case 'memoryRefresh':
|
||||
memoryRefresh = configData[key];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
let title = 'Error loading configuration';
|
||||
log.send(logLevels.ERROR, 'MenuTemplate: error reading configuration fields, error: ' + err);
|
||||
if (electron.BrowserWindow.getFocusedWindow() && !electron.BrowserWindow.getFocusedWindow().isDestroyed()) {
|
||||
electron.dialog.showMessageBox(electron.BrowserWindow.getFocusedWindow(), {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor(title),
|
||||
message: i18n.getMessageFor(title) + ': ' + err
|
||||
});
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets respective accelerators w.r.t roles for the menu template
|
||||
*
|
||||
* @param role {String} The action of the menu item
|
||||
* @param label {String} Menu item name
|
||||
* @return {Object}
|
||||
* @return {Object}.role The action of the menu item
|
||||
* @return {Object}.accelerator keyboard shortcuts and modifiers
|
||||
*/
|
||||
function buildMenuItem(role, label) {
|
||||
|
||||
if (isMac) {
|
||||
return label ? { role: role, label: label } : { role: role }
|
||||
}
|
||||
|
||||
if (isWindowsOS) {
|
||||
return label ? { role: role, label: label, accelerator: windowsAccelerator[role] || '', registerAccelerator: true }
|
||||
: { role: role, accelerator: windowsAccelerator[role] || '', registerAccelerator: true }
|
||||
}
|
||||
|
||||
return label ? { role: role, label: label } : { role: role }
|
||||
}
|
||||
|
||||
function getMinimizeOnClose() {
|
||||
return minimizeOnClose;
|
||||
}
|
||||
|
||||
function getTitleBarStyle() {
|
||||
return titleBarStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an option to the user whether
|
||||
* to relaunch application
|
||||
*
|
||||
* @param app
|
||||
* @param isNativeStyle
|
||||
*/
|
||||
function titleBarActions(app, isNativeStyle) {
|
||||
const options = {
|
||||
type: 'question',
|
||||
title: i18n.getMessageFor('Relaunch Application'),
|
||||
message: i18n.getMessageFor('Updating Title bar style requires Symphony to relaunch.'),
|
||||
detail: i18n.getMessageFor('Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.'),
|
||||
buttons: [i18n.getMessageFor('Relaunch'), i18n.getMessageFor('Cancel')],
|
||||
cancelId: 1
|
||||
};
|
||||
electron.dialog.showMessageBox(electron.BrowserWindow.getFocusedWindow(), options, function (index) {
|
||||
if (index === 0) {
|
||||
updateConfigField('isCustomTitleBar', !!isNativeStyle)
|
||||
.then(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}).catch((e) => {
|
||||
log.send(logLevels.ERROR, `Unable to disable / enable hamburger menu due to error: ${e}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTemplate: getTemplate,
|
||||
getMinimizeOnClose: getMinimizeOnClose,
|
||||
setCheckboxValues: setCheckboxValues,
|
||||
getTitleBarStyle: getTitleBarStyle
|
||||
};
|
@ -1,141 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { version, clientVersion, buildNumber } = require('../../package.json');
|
||||
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
const { isMac } = require('../utils/misc');
|
||||
|
||||
let moreInfoWindow;
|
||||
|
||||
let windowConfig = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
modal: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: true,
|
||||
resizable: false,
|
||||
fullscreenable: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'renderer.js'),
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* method to get the HTML template path
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'more-info.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'more-info: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
return 'file://' + templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the about application window for a specific window
|
||||
* @param {String} windowName - name of the window upon
|
||||
* which this window should show
|
||||
*/
|
||||
function openMoreInfoWindow(windowName) {
|
||||
|
||||
// This prevents creating multiple instances of the
|
||||
// about window
|
||||
if (moreInfoWindow) {
|
||||
if (moreInfoWindow.isMinimized()) {
|
||||
moreInfoWindow.restore();
|
||||
}
|
||||
moreInfoWindow.focus();
|
||||
return;
|
||||
}
|
||||
let allWindows = BrowserWindow.getAllWindows();
|
||||
allWindows = allWindows.find((window) => { return window.winName === windowName });
|
||||
|
||||
// if we couldn't find any window matching the window name
|
||||
// it will render as a new window
|
||||
if (allWindows) {
|
||||
windowConfig.parent = allWindows;
|
||||
}
|
||||
|
||||
moreInfoWindow = new BrowserWindow(windowConfig);
|
||||
moreInfoWindow.setVisibleOnAllWorkspaces(true);
|
||||
moreInfoWindow.loadURL(getTemplatePath());
|
||||
|
||||
// sets the AlwaysOnTop property for the about window
|
||||
// if the main window's AlwaysOnTop is true
|
||||
let focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.isAlwaysOnTop()) {
|
||||
moreInfoWindow.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
moreInfoWindow.once('ready-to-show', () => {
|
||||
moreInfoWindow.show();
|
||||
});
|
||||
|
||||
moreInfoWindow.webContents.on('did-finish-load', () => {
|
||||
const moreInfoContext = i18n.getMessageFor('MoreInfo');
|
||||
moreInfoWindow.webContents.send('i18n-more-info', moreInfoContext);
|
||||
// initialize crash reporter
|
||||
initCrashReporterMain({ process: 'more info window' });
|
||||
initCrashReporterRenderer(moreInfoWindow, { process: 'render | more info window' });
|
||||
moreInfoWindow.webContents.send('versionInfo', { version, clientVersion, buildNumber });
|
||||
if (!isMac) {
|
||||
// prevents from displaying menu items when "alt" key is pressed
|
||||
moreInfoWindow.setMenu(null);
|
||||
}
|
||||
});
|
||||
|
||||
moreInfoWindow.webContents.on('crashed', function (event, killed) {
|
||||
|
||||
log.send(logLevels.INFO, `More Info Window crashed! Killed? ${killed}`);
|
||||
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Renderer Process Crashed'),
|
||||
message: i18n.getMessageFor('Oops! Looks like we have had a crash.'),
|
||||
buttons: ['Close']
|
||||
};
|
||||
|
||||
electron.dialog.showMessageBox(options, function () {
|
||||
if (moreInfoWindow && !moreInfoWindow.isDestroyed()) {
|
||||
moreInfoWindow.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
moreInfoWindow.on('close', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
moreInfoWindow.on('closed', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a window
|
||||
*/
|
||||
function destroyWindow() {
|
||||
moreInfoWindow = null;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
openMoreInfoWindow: openMoreInfoWindow
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title data-i18n-text="More Information"></title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
flex: 1;
|
||||
font-size: 1em;
|
||||
color: #2f2f2f;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 20px
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<span><b data-i18n-text="Version Information"></b></span>
|
||||
<span id="electron" class="content"></span>
|
||||
<span id="chromium" class="content"></span>
|
||||
<span id="v8" class="content"></span>
|
||||
<span id="node" class="content"></span>
|
||||
<span id="openssl" class="content"></span>
|
||||
<span id="zlib" class="content"></span>
|
||||
<span id="uv" class="content"></span>
|
||||
<span id="ares" class="content"></span>
|
||||
<span id="httpparser" class="content"></span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
const { ipcRenderer, crashReporter } = require('electron');
|
||||
|
||||
renderDom();
|
||||
|
||||
/**
|
||||
* Method that renders application data
|
||||
*/
|
||||
function renderDom() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const electronV = document.getElementById('electron');
|
||||
const chromiumV = document.getElementById('chromium');
|
||||
const v8V = document.getElementById('v8');
|
||||
const nodeV = document.getElementById('node');
|
||||
const opensslV = document.getElementById('openssl');
|
||||
const zlibV = document.getElementById('zlib');
|
||||
const uvV = document.getElementById('uv');
|
||||
const aresV = document.getElementById('ares');
|
||||
const httpparserV = document.getElementById('httpparser');
|
||||
|
||||
electronV.innerHTML = `<u>Electron</u> ${process.versions.electron}`;
|
||||
chromiumV.innerHTML = `<u>Chromium</u> ${process.versions.chrome}`;
|
||||
v8V.innerHTML = `<u>V8</u> ${process.versions.v8}`;
|
||||
nodeV.innerHTML = `<u>Node</u> ${process.versions.node}`;
|
||||
opensslV.innerHTML = `<u>OpenSSL</u> ${process.versions.openssl}`;
|
||||
zlibV.innerHTML = `<u>ZLib</u> ${process.versions.zlib}`;
|
||||
uvV.innerHTML = `<u>UV</u> ${process.versions.uv}`;
|
||||
aresV.innerHTML = `<u>Ares</u> ${process.versions.ares}`;
|
||||
httpparserV.innerHTML = `<u>HTTP Parser</u> ${process.versions.http_parser}`;
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('i18n-more-info', (event, content) => {
|
||||
if (content && typeof content === 'object') {
|
||||
const i18nNodes = document.querySelectorAll('[data-i18n-text]');
|
||||
|
||||
for (let node of i18nNodes) {
|
||||
if (node.attributes['data-i18n-text'] && node.attributes['data-i18n-text'].value) {
|
||||
node.innerText = content[node.attributes['data-i18n-text'].value] || node.attributes['data-i18n-text'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// note: this is a workaround until
|
||||
// https://github.com/electron/electron/issues/8841
|
||||
// is fixed on the electron. where 'will-navigate'
|
||||
// is never fired in sandbox mode
|
||||
//
|
||||
// This is required in order to prevent from loading
|
||||
// dropped content
|
||||
window.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
@ -1,126 +0,0 @@
|
||||
const errorContent = (data) => {
|
||||
return (`<div id="main-frame" class="content-wrapper">
|
||||
<div id="main-content">
|
||||
<div class="NetworkError-icon">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="50px" viewBox="0 0 19.7 32" style="enable-background:new 0 0 19.7 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: url(#Shape_8_);
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: url(#Shape_9_);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: url(#Shape_10_);
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: url(#Shape_11_);
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: url(#Shape_12_);
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: url(#Shape_13_);
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: url(#Shape_14_);
|
||||
}
|
||||
|
||||
.st7 {
|
||||
fill: url(#Shape_15_);
|
||||
}
|
||||
</style>
|
||||
<title>Symphony_logo</title>
|
||||
<g id="Page-1">
|
||||
<g id="Symphony_logo">
|
||||
<g id="logo2">
|
||||
<linearGradient id="Shape_8_" gradientUnits="userSpaceOnUse" x1="39.8192" y1="23.9806"
|
||||
x2="39.2461" y2="23.7258"
|
||||
gradientTransform="matrix(7.6157 0 0 -3.458 -295.3247 101.0398)">
|
||||
<stop offset="0" style="stop-color:#197A68"/>
|
||||
<stop offset="1" style="stop-color:#329D87"/>
|
||||
</linearGradient>
|
||||
<path id="Shape" class="st0" d="M2.4,17.4c0,1.2,0.3,2.4,0.8,3.5l6.8-3.5H2.4z"/>
|
||||
|
||||
<linearGradient id="Shape_9_" gradientUnits="userSpaceOnUse" x1="28.9162" y1="22.8111" x2="29.9162"
|
||||
y2="22.8111" gradientTransform="matrix(2.7978 0 0 -4.7596 -73.7035 128.374)">
|
||||
<stop offset="0" style="stop-color:#1D7E7B"/>
|
||||
<stop offset="1" style="stop-color:#35B0B7"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_1_" class="st1" d="M7.2,21.3C8,21.9,9,22.2,10,22.2v-4.8L7.2,21.3z"/>
|
||||
|
||||
<linearGradient id="Shape_10_" gradientUnits="userSpaceOnUse" x1="37.9575" y1="21.1358" x2="38.1785"
|
||||
y2="21.8684" gradientTransform="matrix(6.1591 0 0 -11.4226 -223.952 256.8769)">
|
||||
<stop offset="0" style="stop-color:#175952"/>
|
||||
<stop offset="1" style="stop-color:#3A8F88"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_2_" class="st2"
|
||||
d="M14.4,6.9C13,6.3,11.5,6,10,6C9.4,6,8.8,6,8.2,6.1L10,17.4L14.4,6.9z"/>
|
||||
|
||||
<linearGradient id="Shape_11_" gradientUnits="userSpaceOnUse" x1="40.5688" y1="22.0975" x2="41.0294"
|
||||
y2="22.3767" gradientTransform="matrix(9.5186 0 0 -5.5951 -373.339 140.3243)">
|
||||
<stop offset="0" style="stop-color:#39A8BA"/>
|
||||
<stop offset="1" style="stop-color:#3992B4"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_3_" class="st3" d="M10,17.4h9.5c0-2-0.6-4-1.8-5.6L10,17.4z"/>
|
||||
|
||||
<linearGradient id="Shape_12_" gradientUnits="userSpaceOnUse" x1="41.2142" y1="22.3254" x2="40.7055"
|
||||
y2="22.5477" gradientTransform="matrix(9.9955 0 0 -5.2227 -404.7955 132.8762)">
|
||||
<stop offset="0" style="stop-color:#021C3C"/>
|
||||
<stop offset="1" style="stop-color:#215180"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_4_" class="st4" d="M1.5,12.2c-1,1.6-1.5,3.4-1.5,5.2h10L1.5,12.2z"/>
|
||||
|
||||
<linearGradient id="Shape_13_" gradientUnits="userSpaceOnUse" x1="33.5112" y1="22.1509" x2="34.5112"
|
||||
y2="22.1509" gradientTransform="matrix(3.9169 0 0 -6.6631 -125.1783 161.684)">
|
||||
<stop offset="0" style="stop-color:#23796C"/>
|
||||
<stop offset="1" style="stop-color:#41BEAF"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_5_" class="st5" d="M10,10.8c-1.4,0-2.8,0.4-3.9,1.3l3.9,5.4V10.8z"/>
|
||||
|
||||
<linearGradient id="Shape_14_" gradientUnits="userSpaceOnUse" x1="36.1289" y1="21.9578" x2="36.4868"
|
||||
y2="21.4811" gradientTransform="matrix(5.0353 0 0 -8.5671 -171.5901 208.3326)">
|
||||
<stop offset="0" style="stop-color:#14466A"/>
|
||||
<stop offset="1" style="stop-color:#286395"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_6_" class="st6" d="M10,26c1.8,0,3.6-0.6,5-1.6l-5-6.9V26z"/>
|
||||
|
||||
<linearGradient id="Shape_15_" gradientUnits="userSpaceOnUse" x1="38.5336" y1="23.6558" x2="39.0866"
|
||||
y2="23.5777" gradientTransform="matrix(6.663 0 0 -3.5931 -244.8399 102.8351)">
|
||||
<stop offset="0" style="stop-color:#261D49"/>
|
||||
<stop offset="1" style="stop-color:#483A6D"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_7_" class="st7"
|
||||
d="M16.6,16.4l-6.6,1l6.2,2.6c0.3-0.8,0.5-1.7,0.5-2.6C16.7,17.1,16.6,16.7,16.6,16.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="main-message">
|
||||
<p class="NetworkError-header">${data["Problem connecting to Symphony"] || "Problem connecting to Symphony"}</p>
|
||||
<p id="NetworkError-reason">
|
||||
${data["Looks like you are not connected to the Internet. We'll try to reconnect automatically."]
|
||||
|| "Looks like you are not connected to the Internet. We'll try to reconnect automatically."}
|
||||
</p>
|
||||
<div id="error-code" class="NetworkError-error-code">ERR_INTERNET_DISCONNECTED</div>
|
||||
<button id="cancel-retry-button" class="NetworkError-button">${data["Cancel Retry"] || "Cancel Retry"}</button>
|
||||
<button id="quit-button" class="NetworkError-button">${data["Quit Symphony"] || "Quit Symphony"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
errorContent,
|
||||
};
|
@ -1,47 +0,0 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
const htmlContents = require('./contents');
|
||||
|
||||
class NetworkError {
|
||||
|
||||
constructor() {
|
||||
this.domParser = new DOMParser();
|
||||
}
|
||||
|
||||
showError(data) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { message, error } = data;
|
||||
const errorContent = this.domParser.parseFromString(htmlContents.errorContent(message), 'text/html');
|
||||
errorContent.getElementById('error-code').innerText = error || "UNKNOWN_ERROR";
|
||||
|
||||
// Add event listeners for buttons
|
||||
const cancelRetryButton = errorContent.getElementById('cancel-retry-button');
|
||||
const cancelRetry = () => {
|
||||
ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.cancelNetworkStatusCheck
|
||||
});
|
||||
cancelRetryButton.classList.add('disabled');
|
||||
cancelRetryButton.removeEventListener('click', cancelRetry);
|
||||
};
|
||||
cancelRetryButton.addEventListener('click', cancelRetry);
|
||||
|
||||
const quitButton = errorContent.getElementById('quit-button');
|
||||
quitButton.addEventListener('click', () => {
|
||||
ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.quitWindow
|
||||
});
|
||||
});
|
||||
|
||||
const mainFrame = errorContent.getElementById('main-frame');
|
||||
document.body.appendChild(mainFrame);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NetworkError,
|
||||
};
|
@ -1,77 +0,0 @@
|
||||
body {
|
||||
background-color: rgb(247, 247, 247);
|
||||
color: rgb(100, 100, 100);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(17, 85, 204);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.NetworkError-header {
|
||||
color: #333;
|
||||
font-size: 1.6em;
|
||||
font-weight: normal;
|
||||
line-height: 1.25em;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.NetworkError-icon {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
height: 72px;
|
||||
margin: 0 0 40px;
|
||||
width: 72px;
|
||||
-webkit-user-select: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.NetworkError-error-code {
|
||||
color: #646464;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
box-sizing: border-box;
|
||||
line-height: 1.6em;
|
||||
margin: 14vh auto 0;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-size: 100%;
|
||||
font-family: sans-serif, "Segoe UI", "Helvetica Neue", Arial;
|
||||
}
|
||||
|
||||
.NetworkError-reason {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.NetworkError-button {
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
text-transform: uppercase;
|
||||
transform: translatez(0);
|
||||
background: rgb(66, 133, 244);
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: .875em;
|
||||
padding: 10px 24px;
|
||||
transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
background: #cccccc;
|
||||
color: #666666;
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
/**
|
||||
* Manages one animation at a time
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
const AnimationQueue = function(options) {
|
||||
this.options = options;
|
||||
this.queue = [];
|
||||
this.running = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes each animation to a queue
|
||||
* @param object
|
||||
*/
|
||||
AnimationQueue.prototype.push = function(object) {
|
||||
if (this.running) {
|
||||
this.queue.push(object);
|
||||
} else {
|
||||
this.running = true;
|
||||
setTimeout(this.animate.bind(this, object), 0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Animates an animation that is part of the queue
|
||||
* @param object
|
||||
*/
|
||||
AnimationQueue.prototype.animate = function(object) {
|
||||
object.func.apply(null, object.args)
|
||||
.then(function() {
|
||||
if (this.queue.length > 0) {
|
||||
// Run next animation
|
||||
this.animate.call(this, this.queue.shift());
|
||||
} else {
|
||||
this.running = false;
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function(err) {
|
||||
log.send(logLevels.ERROR, 'animationQueue: encountered an error: ' + err +
|
||||
' with stack trace:' + err.stack);
|
||||
/* eslint-disable no-console */
|
||||
console.error('animation queue encountered an error: ' + err +
|
||||
' with stack trace:' + err.stack);
|
||||
/* eslint-enable no-console */
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the queue
|
||||
*/
|
||||
AnimationQueue.prototype.clear = function() {
|
||||
this.queue = [];
|
||||
};
|
||||
|
||||
module.exports = AnimationQueue;
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
@ -1,257 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//
|
||||
// BrowserWindow preload script use to create notifications window for
|
||||
// elctron-notify project.
|
||||
//
|
||||
// code here adapted from: https://www.npmjs.com/package/electron-notify
|
||||
//
|
||||
const electron = require('electron');
|
||||
const ipc = electron.ipcRenderer;
|
||||
|
||||
const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i);
|
||||
// event functions ref
|
||||
let onMouseLeaveFunc;
|
||||
let onMouseOverFunc;
|
||||
|
||||
/**
|
||||
* Sets style for a notification
|
||||
* @param config
|
||||
*/
|
||||
function setStyle(config) {
|
||||
// Style it
|
||||
const notiDoc = window.document;
|
||||
const container = notiDoc.getElementById('container');
|
||||
const header = notiDoc.getElementById('header');
|
||||
const image = notiDoc.getElementById('image');
|
||||
const logo = notiDoc.getElementById('symphony-logo');
|
||||
const title = notiDoc.getElementById('title');
|
||||
const company = notiDoc.getElementById('company');
|
||||
const message = notiDoc.getElementById('message');
|
||||
const close = notiDoc.getElementById('close');
|
||||
const picture = notiDoc.getElementById('picture');
|
||||
const logoContainer = notiDoc.getElementById('logo-container');
|
||||
|
||||
// Default style
|
||||
setStyleOnDomElement(config.defaultStyleContainer, container);
|
||||
|
||||
let style = {
|
||||
height: config.height,
|
||||
width: config.width,
|
||||
borderRadius: config.borderRadius + 'px'
|
||||
};
|
||||
setStyleOnDomElement(style, container);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleHeader, header);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleImage, image);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleLogo, logo);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleTitle, title);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleCompany, company);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleText, message);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleClose, close);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleImageContainer, picture);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleLogoContainer, logoContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets contents for a notification
|
||||
* @param event
|
||||
* @param notificationObj
|
||||
*/
|
||||
function setContents(event, notificationObj) {
|
||||
// sound
|
||||
if (notificationObj.sound) {
|
||||
// Check if file is accessible
|
||||
try {
|
||||
// If it's a local file, check it's existence
|
||||
// Won't check remote files e.g. http://
|
||||
if (notificationObj.sound.match(/^file:/) !== null
|
||||
|| notificationObj.sound.match(/^\//) !== null) {
|
||||
let audio = new window.Audio(notificationObj.sound);
|
||||
audio.play()
|
||||
}
|
||||
} catch (e) {
|
||||
/* eslint-disable no-console */
|
||||
console.error('electron-notify: ERROR could not find sound file: '
|
||||
+ notificationObj.sound.replace('file://', ''), e, e.stack);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
const notiDoc = window.document;
|
||||
|
||||
// All the required DOM elements to update the content
|
||||
const container = notiDoc.getElementById('container');
|
||||
const titleDoc = notiDoc.getElementById('title');
|
||||
const companyDoc = notiDoc.getElementById('company');
|
||||
const messageDoc = notiDoc.getElementById('message');
|
||||
const imageDoc = notiDoc.getElementById('image');
|
||||
const closeButton = notiDoc.getElementById('close');
|
||||
closeButton.title = notificationObj.i18n.close;
|
||||
|
||||
if (notificationObj.color) {
|
||||
container.style.backgroundColor = notificationObj.color;
|
||||
let logo = notiDoc.getElementById('symphony-logo');
|
||||
|
||||
if (notificationObj.color.match(whiteColorRegExp)) {
|
||||
logo.src = './assets/symphony-logo-black.png';
|
||||
} else {
|
||||
messageDoc.style.color = '#000000';
|
||||
titleDoc.style.color = '#000000';
|
||||
companyDoc.style.color = notificationObj.color;
|
||||
logo.src = './assets/symphony-logo-white.png';
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationObj.flash) {
|
||||
let origColor = container.style.backgroundColor;
|
||||
setInterval(function() {
|
||||
if (container.style.backgroundColor === 'red') {
|
||||
container.style.backgroundColor = origColor;
|
||||
} else {
|
||||
container.style.backgroundColor = 'red';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Title
|
||||
titleDoc.innerText = notificationObj.title || '';
|
||||
|
||||
// message
|
||||
messageDoc.innerText = notificationObj.text || '';
|
||||
|
||||
// Image
|
||||
if (notificationObj.image) {
|
||||
imageDoc.src = notificationObj.image;
|
||||
} else {
|
||||
setStyleOnDomElement({ display: 'none'}, imageDoc);
|
||||
}
|
||||
|
||||
// Company
|
||||
if (notificationObj.company) {
|
||||
companyDoc.innerText = notificationObj.company
|
||||
} else {
|
||||
messageDoc.style.marginTop = '15px';
|
||||
}
|
||||
|
||||
const winId = notificationObj.windowId;
|
||||
|
||||
if (!notificationObj.sticky) {
|
||||
onMouseLeaveFunc = onMouseLeave.bind(this);
|
||||
onMouseOverFunc = onMouseOver.bind(this);
|
||||
container.addEventListener('mouseleave', onMouseLeaveFunc);
|
||||
container.addEventListener('mouseover', onMouseOverFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new timer to close the notification
|
||||
*/
|
||||
function onMouseLeave() {
|
||||
ipc.send('electron-notify-mouseleave', winId, notificationObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all timeouts to prevent notification
|
||||
* from closing
|
||||
*/
|
||||
function onMouseOver() {
|
||||
ipc.send('electron-notify-mouseover', winId);
|
||||
}
|
||||
|
||||
// note: use onclick because we only want one handler, for case
|
||||
// when content gets overwritten by notf with same tag
|
||||
closeButton.onclick = function(clickEvent) {
|
||||
clickEvent.stopPropagation();
|
||||
ipc.send('electron-notify-close', winId, notificationObj)
|
||||
};
|
||||
|
||||
container.onclick = function() {
|
||||
ipc.send('electron-notify-click', winId, notificationObj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets style on a notification for a DOM element
|
||||
* @param styleObj
|
||||
* @param domElement
|
||||
*/
|
||||
function setStyleOnDomElement(styleObj, domElement) {
|
||||
try {
|
||||
let styleAttr = Object.keys(styleObj);
|
||||
styleAttr.forEach(function(attr) {
|
||||
/* eslint-disable */
|
||||
domElement.style[attr] = styleObj[attr];
|
||||
/* eslint-enable */
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('electron-notify: Could not set style on domElement', styleObj, domElement)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
window.addEventListener('keyup', handleKeyPress, true);
|
||||
|
||||
/**
|
||||
* Method the prevent key stroke on notification window
|
||||
*
|
||||
* @param e keydown event
|
||||
*/
|
||||
function handleKeyPress(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the config
|
||||
* @param event
|
||||
* @param conf
|
||||
*/
|
||||
function loadConfig(event, conf) {
|
||||
setStyle(conf || {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the notification window
|
||||
*/
|
||||
function reset() {
|
||||
let notiDoc = window.document;
|
||||
let container = notiDoc.getElementById('container');
|
||||
let closeButton = notiDoc.getElementById('close');
|
||||
|
||||
// Remove event listener
|
||||
let newContainer = container.cloneNode(true);
|
||||
container.parentNode.replaceChild(newContainer, container);
|
||||
let newCloseButton = closeButton.cloneNode(true);
|
||||
closeButton.parentNode.replaceChild(newCloseButton, closeButton);
|
||||
|
||||
container.removeEventListener('mouseleave', onMouseLeaveFunc);
|
||||
container.removeEventListener('mouseover', onMouseOverFunc);
|
||||
}
|
||||
|
||||
ipc.on('electron-notify-set-contents', setContents);
|
||||
ipc.on('electron-notify-load-config', loadConfig);
|
||||
ipc.on('electron-notify-reset', reset);
|
||||
|
||||
// note: this is a workaround until
|
||||
// https://github.com/electron/electron/issues/8841
|
||||
// is fixed on the electron. where 'will-navigate'
|
||||
// is never fired in sandbox mode
|
||||
//
|
||||
// This is required in order to prevent from loading
|
||||
// dropped content
|
||||
window.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body style='margin:0; overflow: hidden; -webkit-user-select: none;'>
|
||||
<div id="container">
|
||||
<div id="logo-container">
|
||||
<img src="./assets/symphony-logo-white.png" id="symphony-logo">
|
||||
</div>
|
||||
<div id="header">
|
||||
<span id="title"></span>
|
||||
<span id="company"></span>
|
||||
<span id="message"></span>
|
||||
</div>
|
||||
<div id="picture">
|
||||
<img src="" id="image" style="border-radius: 4px" />
|
||||
</div>
|
||||
<div id="close">
|
||||
<svg fill="#000000" height="16" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,856 +0,0 @@
|
||||
'use strict';
|
||||
//
|
||||
// code here adapted from https://www.npmjs.com/package/electron-notify
|
||||
// made following changes:
|
||||
// - place notification in corner of screen
|
||||
// - notification color
|
||||
// - notification flash/blink
|
||||
// - custom design for symphony notification style
|
||||
// - if screen added/removed or size change then close all notifications
|
||||
//
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const electron = require('electron');
|
||||
const asyncMap = require('async.map');
|
||||
const asyncMapSeries = require('async.mapseries');
|
||||
const app = electron.app;
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const ipc = electron.ipcMain;
|
||||
const { isMac, isNodeEnv } = require('../utils/misc');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
// maximum number of notifications that can be queued, after limit is
|
||||
// reached then error func callback will be invoked.
|
||||
const MAX_QUEUE_SIZE = 30;
|
||||
|
||||
let AnimationQueue = require('./AnimationQueue.js');
|
||||
|
||||
// Array of windows with currently showing notifications
|
||||
let activeNotifications = [];
|
||||
|
||||
// Recycle windows
|
||||
let inactiveWindows = [];
|
||||
|
||||
// If we cannot show all notifications, queue them
|
||||
let notificationQueue = [];
|
||||
|
||||
// To prevent executing mutliple animations at once
|
||||
let animationQueue = new AnimationQueue();
|
||||
|
||||
// To prevent double-close notification window
|
||||
let closedNotifications = {};
|
||||
|
||||
// Give each notification a unique id
|
||||
let latestID = 0;
|
||||
|
||||
let nextInsertPos = {};
|
||||
let externalDisplay;
|
||||
// user selected display id for notification
|
||||
let displayId;
|
||||
|
||||
let sandboxed = false;
|
||||
|
||||
let config = {
|
||||
// corner to put notifications
|
||||
// upper-right, upper-left, lower-right, lower-left
|
||||
startCorner: 'upper-right',
|
||||
width: 380,
|
||||
height: 100,
|
||||
borderRadius: 5,
|
||||
displayTime: 5000,
|
||||
animationSteps: 5,
|
||||
animationStepMs: 5,
|
||||
animateInParallel: true,
|
||||
pathToModule: '',
|
||||
logging: true,
|
||||
defaultStyleContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f0f0f0',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
lineHeight: '15px',
|
||||
boxSizing: 'border-box'
|
||||
},
|
||||
defaultStyleHeader: {
|
||||
width: 245,
|
||||
minWidth: 230,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
defaultStyleImageContainer: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
},
|
||||
defaultStyleImage: {
|
||||
height: 43,
|
||||
borderRadius: 4,
|
||||
width: 43
|
||||
},
|
||||
defaultStyleClose: {
|
||||
width: 16,
|
||||
height: 80,
|
||||
display: 'flex',
|
||||
margin: 'auto',
|
||||
opacity: 0.54,
|
||||
fontSize: 12,
|
||||
color: '#CCC'
|
||||
},
|
||||
defaultStyleTitle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: '#4a4a4a',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
webkitLineClamp: 1,
|
||||
webkitBoxOrient: 'vertical',
|
||||
},
|
||||
defaultStyleCompany: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 11,
|
||||
color: '#adadad',
|
||||
overflow: 'hidden',
|
||||
filter: 'brightness(70%)',
|
||||
display: '-webkit-box',
|
||||
webkitLineClamp: 1,
|
||||
webkitBoxOrient: 'vertical',
|
||||
},
|
||||
defaultStyleText: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 12,
|
||||
color: '#4a4a4a',
|
||||
marginTop: 5,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
webkitLineClamp: 3,
|
||||
webkitBoxOrient: 'vertical',
|
||||
cursor: 'default',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%',
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
defaultStyleLogoContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
defaultStyleLogo: {
|
||||
marginLeft: '5px',
|
||||
opacity: 0.6,
|
||||
width: '43px',
|
||||
},
|
||||
defaultWindow: {
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
show: false,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
acceptFirstMouse: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-notify-preload.js'),
|
||||
sandbox: sandboxed,
|
||||
nodeIntegration: isNodeEnv,
|
||||
devTools: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (app.isReady()) {
|
||||
setup();
|
||||
} else {
|
||||
app.on('ready', setup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update notification config
|
||||
* @param customConfig
|
||||
*/
|
||||
function updateConfig(customConfig) {
|
||||
// Fetching user preferred notification position from config
|
||||
if (customConfig.position) {
|
||||
config = Object.assign(config, {startCorner: customConfig.position});
|
||||
|
||||
calcDimensions();
|
||||
}
|
||||
|
||||
// Fetching user preferred notification screen from config
|
||||
if (customConfig.display) {
|
||||
displayId = customConfig.display;
|
||||
}
|
||||
// Reposition active notification on config changes
|
||||
setupConfig();
|
||||
moveOneDown(0)
|
||||
.then(() => {
|
||||
log.send(logLevels.INFO, 'updateConfig: repositioned '+ activeNotifications.length +' active notification');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to setup the notification configuration
|
||||
*/
|
||||
function setup() {
|
||||
setupConfig();
|
||||
|
||||
// if display added/removed/changed then re-run setup and remove all existing
|
||||
// notifications.
|
||||
electron.screen.on('display-added', setupConfig);
|
||||
electron.screen.on('display-removed', setupConfig);
|
||||
electron.screen.on('display-metrics-changed', setupConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the notification template path
|
||||
* @returns {string|*}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'electron-notify.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'electron-notify: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
config.templatePath = 'file://' + templatePath;
|
||||
return config.templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimensions of the screen
|
||||
*/
|
||||
function calcDimensions() {
|
||||
const vertSpaceBetweenNotf = 8;
|
||||
|
||||
// Calc totalHeight & totalWidth
|
||||
config.totalHeight = config.height + vertSpaceBetweenNotf;
|
||||
config.totalWidth = config.width;
|
||||
|
||||
let firstPosX, firstPosY;
|
||||
switch (config.startCorner) {
|
||||
case 'upper-right':
|
||||
firstPosX = config.corner.x - config.totalWidth;
|
||||
firstPosY = config.corner.y;
|
||||
break;
|
||||
case 'lower-right':
|
||||
firstPosX = config.corner.x - config.totalWidth;
|
||||
firstPosY = config.corner.y - config.totalHeight;
|
||||
break;
|
||||
case 'lower-left':
|
||||
firstPosX = config.corner.x;
|
||||
firstPosY = config.corner.y - config.totalHeight;
|
||||
break;
|
||||
case 'upper-left':
|
||||
default:
|
||||
firstPosX = config.corner.x;
|
||||
firstPosY = config.corner.y;
|
||||
break;
|
||||
}
|
||||
|
||||
// Calc pos of first notification:
|
||||
config.firstPos = {
|
||||
x: firstPosX,
|
||||
y: firstPosY
|
||||
};
|
||||
|
||||
// Set nextInsertPos
|
||||
nextInsertPos.x = config.firstPos.x;
|
||||
nextInsertPos.y = config.firstPos.y
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification config
|
||||
*/
|
||||
function setupConfig() {
|
||||
|
||||
log.send(logLevels.INFO, `Either a display was added / removed / the metrics were changed`);
|
||||
|
||||
// This feature only applies to windows
|
||||
if (!isMac) {
|
||||
let screens = electron.screen.getAllDisplays();
|
||||
if (screens && screens.length >= 0) {
|
||||
externalDisplay = screens.find((screen) => {
|
||||
let screenId = screen.id.toString();
|
||||
return screenId === displayId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let display = externalDisplay ? externalDisplay : electron.screen.getPrimaryDisplay();
|
||||
config.corner = {};
|
||||
config.corner.x = display.workArea.x;
|
||||
config.corner.y = display.workArea.y;
|
||||
|
||||
// update corner x/y based on corner of screen where notf should appear
|
||||
const workAreaWidth = display.workAreaSize.width;
|
||||
const workAreaHeight = display.workAreaSize.height;
|
||||
switch (config.startCorner) {
|
||||
case 'upper-right':
|
||||
config.corner.x += workAreaWidth;
|
||||
break;
|
||||
case 'lower-right':
|
||||
config.corner.x += workAreaWidth;
|
||||
config.corner.y += workAreaHeight;
|
||||
break;
|
||||
case 'lower-left':
|
||||
config.corner.y += workAreaHeight;
|
||||
break;
|
||||
case 'upper-left':
|
||||
default:
|
||||
// no change needed
|
||||
break;
|
||||
}
|
||||
|
||||
calcDimensions();
|
||||
|
||||
// Maximum amount of Notifications we can show:
|
||||
config.maxVisibleNotifications = Math.floor(display.workAreaSize.height / config.totalHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the user
|
||||
* @param notification
|
||||
* @returns {*}
|
||||
*/
|
||||
function notify(notification) {
|
||||
// Is it an object and only one argument?
|
||||
if (arguments.length === 1 && typeof notification === 'object') {
|
||||
let notf = Object.assign({}, notification);
|
||||
// Use object instead of supplied parameters
|
||||
notf.id = latestID;
|
||||
incrementId();
|
||||
animationQueue.push({
|
||||
func: showNotification,
|
||||
args: [ notf ]
|
||||
});
|
||||
return notf.id
|
||||
}
|
||||
log.send(logLevels.ERROR, 'electron-notify: ERROR notify() only accepts a single object with notification parameters.');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the notification
|
||||
*/
|
||||
function incrementId() {
|
||||
latestID++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification to the user
|
||||
* @param notificationObj
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function showNotification(notificationObj) {
|
||||
return new Promise(function(resolve) {
|
||||
|
||||
if (notificationQueue.length >= MAX_QUEUE_SIZE) {
|
||||
if (typeof notificationObj.onErrorFunc === 'function') {
|
||||
setTimeout(function() {
|
||||
notificationObj.onErrorFunc({
|
||||
id: notificationObj.id,
|
||||
error: 'max notification queue size reached: ' + MAX_QUEUE_SIZE
|
||||
});
|
||||
log.send(logLevels.INFO, 'showNotification: max notification queue size reached: ' + MAX_QUEUE_SIZE);
|
||||
}, 0);
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// check if tag id provided. should replace existing notification
|
||||
// if has same grouping id.
|
||||
let tag = notificationObj.tag;
|
||||
if (tag) {
|
||||
// first check queued notifications
|
||||
for(let i = 0; i < notificationQueue.length; i++) {
|
||||
if (tag === notificationQueue[ i ].tag) {
|
||||
let existingNotfObj = notificationQueue[ i ];
|
||||
// be sure to call close event for existing, so it gets
|
||||
// cleaned up.
|
||||
if (typeof existingNotfObj.onCloseFunc === 'function') {
|
||||
existingNotfObj.onCloseFunc({
|
||||
event: 'close',
|
||||
id: notificationObj.id
|
||||
});
|
||||
}
|
||||
// update with new notf
|
||||
notificationQueue[ i ] = notificationObj;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// next check notfs being shown
|
||||
for(let i = 0; i < activeNotifications.length; i++) {
|
||||
if (activeNotifications[ i ] && windowExists(activeNotifications[ i ])) {
|
||||
let existingNotfyObj = activeNotifications[ i ].notfyObj;
|
||||
if (existingNotfyObj && tag === existingNotfyObj.tag) {
|
||||
let notificationWindow = activeNotifications[ i ];
|
||||
|
||||
// be sure to call close event for existing, so it gets
|
||||
// cleaned up.
|
||||
if (notificationWindow.electronNotifyOnCloseFunc) {
|
||||
notificationWindow.electronNotifyOnCloseFunc({
|
||||
event: 'close',
|
||||
id: existingNotfyObj.id
|
||||
});
|
||||
delete notificationWindow.electronNotifyOnCloseFunc;
|
||||
}
|
||||
setNotificationContents(notificationWindow, notificationObj);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can we show it?
|
||||
if (activeNotifications.length < config.maxVisibleNotifications) {
|
||||
// Get inactiveWindow or create new:
|
||||
getWindow().then(function(notificationWindow) {
|
||||
// Move window to position
|
||||
calcInsertPos();
|
||||
setWindowPosition(notificationWindow, nextInsertPos.x, nextInsertPos.y);
|
||||
|
||||
let updatedNotfWindow = setNotificationContents(notificationWindow, notificationObj);
|
||||
|
||||
activeNotifications.push(updatedNotfWindow);
|
||||
|
||||
resolve(updatedNotfWindow);
|
||||
})
|
||||
} else {
|
||||
// Add to notificationQueue
|
||||
notificationQueue.push(notificationObj);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTML notification contents along with other options
|
||||
* @param notfWindow
|
||||
* @param notfObj
|
||||
* @returns {*}
|
||||
*/
|
||||
function setNotificationContents(notfWindow, notfObj) {
|
||||
|
||||
// Display time per notification basis.
|
||||
let displayTime = notfObj.displayTime ? notfObj.displayTime : config.displayTime;
|
||||
|
||||
let browserWindows = BrowserWindow.getAllWindows();
|
||||
const mainWindow = browserWindows.find((window) => { return window.winName === 'main' });
|
||||
if (mainWindow && windowExists(mainWindow)) {
|
||||
if (mainWindow.isAlwaysOnTop()) {
|
||||
notfWindow.setAlwaysOnTop(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (notfWindow.displayTimer) {
|
||||
clearTimeout(notfWindow.displayTimer);
|
||||
}
|
||||
|
||||
const updatedNotificationWindow = notfWindow;
|
||||
|
||||
updatedNotificationWindow.notfyObj = notfObj;
|
||||
|
||||
let timeoutId;
|
||||
let closeFunc = buildCloseNotification(notfWindow, notfObj, function() {
|
||||
return timeoutId
|
||||
});
|
||||
let closeNotificationSafely = buildCloseNotificationSafely(closeFunc);
|
||||
|
||||
if (!notfObj.sticky) {
|
||||
timeoutId = setTimeout(function() {
|
||||
closeNotificationSafely('timeout');
|
||||
}, displayTime);
|
||||
updatedNotificationWindow.displayTimer = timeoutId;
|
||||
}
|
||||
|
||||
// Trigger onShowFunc if existent
|
||||
if (notfObj.onShowFunc) {
|
||||
notfObj.onShowFunc({
|
||||
event: 'show',
|
||||
id: notfObj.id,
|
||||
closeNotification: closeNotificationSafely
|
||||
})
|
||||
}
|
||||
|
||||
// Save onClickFunc in notification window
|
||||
if (notfObj.onClickFunc) {
|
||||
updatedNotificationWindow.electronNotifyOnClickFunc = notfObj.onClickFunc
|
||||
} else {
|
||||
delete updatedNotificationWindow.electronNotifyOnClickFunc;
|
||||
}
|
||||
|
||||
if (notfObj.onCloseFunc) {
|
||||
updatedNotificationWindow.electronNotifyOnCloseFunc = notfObj.onCloseFunc
|
||||
} else {
|
||||
delete updatedNotificationWindow.electronNotifyOnCloseFunc;
|
||||
}
|
||||
// Set strings about i18n
|
||||
const translation = {};
|
||||
translation.close = i18n.getMessageFor('Close');
|
||||
|
||||
const windowId = notfWindow.id;
|
||||
|
||||
// Set contents, ...
|
||||
updatedNotificationWindow.webContents.send('electron-notify-set-contents',
|
||||
Object.assign({ windowId: windowId, i18n: translation}, notfObj));
|
||||
// Show window
|
||||
updatedNotificationWindow.showInactive();
|
||||
|
||||
return updatedNotificationWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the notification
|
||||
* @param notificationWindow
|
||||
* @param notificationObj
|
||||
* @param getTimeoutId
|
||||
* @returns {Function}
|
||||
*/
|
||||
function buildCloseNotification(notificationWindow, notificationObj, getTimeoutId) {
|
||||
return function(event) {
|
||||
|
||||
// safety check to prevent from using an
|
||||
// already destroyed notification window
|
||||
if (!notificationWindow || typeof notificationWindow.isDestroyed !== 'function' || notificationWindow.isDestroyed()) {
|
||||
return new Promise(function(exitEarly) { exitEarly() })
|
||||
}
|
||||
|
||||
if (closedNotifications[notificationObj.id]) {
|
||||
delete closedNotifications[notificationObj.id];
|
||||
return new Promise(function(exitEarly) { exitEarly() });
|
||||
}
|
||||
closedNotifications[notificationObj.id] = true;
|
||||
|
||||
|
||||
if (notificationWindow.electronNotifyOnCloseFunc) {
|
||||
notificationWindow.electronNotifyOnCloseFunc({
|
||||
event: event,
|
||||
id: notificationObj.id
|
||||
});
|
||||
// ToDo: fix this: shouldn't delete method on arg
|
||||
/* eslint-disable */
|
||||
delete notificationWindow.electronNotifyOnCloseFunc;
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
// reset content
|
||||
notificationWindow.webContents.send('electron-notify-reset');
|
||||
if (getTimeoutId && typeof getTimeoutId === 'function') {
|
||||
let timeoutId = getTimeoutId();
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// Recycle window
|
||||
let pos = activeNotifications.indexOf(notificationWindow);
|
||||
activeNotifications.splice(pos, 1);
|
||||
inactiveWindows.push(notificationWindow);
|
||||
|
||||
// Hide notification
|
||||
notificationWindow.hide();
|
||||
|
||||
checkForQueuedNotifications();
|
||||
|
||||
// Move notifications down
|
||||
return moveOneDown(pos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an active notification the close notification queue
|
||||
* Always add to animationQueue to prevent erros (e.g. notification
|
||||
* got closed while it was moving will produce an error)
|
||||
* @param closeFunc
|
||||
* @returns {Function}
|
||||
*/
|
||||
function buildCloseNotificationSafely(closeFunc) {
|
||||
return function(reason) {
|
||||
animationQueue.push({
|
||||
func: closeFunc,
|
||||
args: [ reason || 'closedByAPI' ]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ipc.on('electron-notify-close', function (event, winId, notificationObj) {
|
||||
log.send(logLevels.INFO, `Closing notification for ${winId}}`);
|
||||
let closeFunc = buildCloseNotification(BrowserWindow.fromId(winId), notificationObj);
|
||||
buildCloseNotificationSafely(closeFunc)('close');
|
||||
});
|
||||
|
||||
ipc.on('electron-notify-click', function (event, winId, notificationObj) {
|
||||
log.send(logLevels.INFO, `Notification click event triggered for ${winId}}`);
|
||||
let notificationWindow = BrowserWindow.fromId(winId);
|
||||
if (notificationWindow && notificationWindow.electronNotifyOnClickFunc) {
|
||||
let closeFunc = buildCloseNotification(notificationWindow, notificationObj);
|
||||
notificationWindow.electronNotifyOnClickFunc({
|
||||
event: 'click',
|
||||
id: notificationObj.id,
|
||||
closeNotification: buildCloseNotificationSafely(closeFunc)
|
||||
});
|
||||
delete notificationWindow.electronNotifyOnClickFunc;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks for queued notifications and add them
|
||||
* to AnimationQueue if possible
|
||||
*/
|
||||
function checkForQueuedNotifications() {
|
||||
if (notificationQueue.length > 0 &&
|
||||
activeNotifications.length < config.maxVisibleNotifications) {
|
||||
// Add new notification to animationQueue
|
||||
animationQueue.push({
|
||||
func: showNotification,
|
||||
args: [ notificationQueue.shift() ]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the notifications one position down,
|
||||
* starting with notification at startPos
|
||||
* @param startPos
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function moveOneDown(startPos) {
|
||||
return new Promise(function(resolve) {
|
||||
if (startPos >= activeNotifications || startPos === -1) {
|
||||
resolve();
|
||||
return
|
||||
}
|
||||
// Build array with index of affected notifications
|
||||
let notificationPosArray = [];
|
||||
for (let i = startPos; i < activeNotifications.length; i++) {
|
||||
notificationPosArray.push(i)
|
||||
}
|
||||
// Start to animate all notifications at once or in parallel
|
||||
let asyncFunc = asyncMap; // Best performance
|
||||
if (config.animateInParallel === false) {
|
||||
asyncFunc = asyncMapSeries // Sluggish
|
||||
}
|
||||
asyncFunc(notificationPosArray, moveNotificationAnimation, function() {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the notification animation
|
||||
* @param i
|
||||
* @param done
|
||||
*/
|
||||
function moveNotificationAnimation(i, done) {
|
||||
// Get notification to move
|
||||
let notificationWindow = activeNotifications[i];
|
||||
|
||||
// Calc new y position
|
||||
let newY;
|
||||
switch(config.startCorner) {
|
||||
case 'upper-right':
|
||||
case 'upper-left':
|
||||
newY = config.corner.y + (config.totalHeight * i);
|
||||
break;
|
||||
default:
|
||||
case 'lower-right':
|
||||
case 'lower-left':
|
||||
newY = config.corner.y - (config.totalHeight * (i + 1));
|
||||
break;
|
||||
}
|
||||
|
||||
// Get startPos, calc step size and start animationInterval
|
||||
let startY = notificationWindow.getPosition()[1];
|
||||
let step = (newY - startY) / config.animationSteps;
|
||||
let curStep = 1;
|
||||
let animationInterval = setInterval(function() {
|
||||
// Abort condition
|
||||
if (curStep === config.animationSteps) {
|
||||
setWindowPosition(notificationWindow, config.firstPos.x, newY);
|
||||
clearInterval(animationInterval);
|
||||
done(null, 'done');
|
||||
return;
|
||||
}
|
||||
// Move one step down
|
||||
setWindowPosition(notificationWindow, config.firstPos.x, startY + curStep * step);
|
||||
curStep++
|
||||
}, config.animationStepMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the window's position
|
||||
* @param browserWin
|
||||
* @param posX
|
||||
* @param posY
|
||||
*/
|
||||
function setWindowPosition(browserWin, posX, posY) {
|
||||
if (browserWin && windowExists(browserWin)) {
|
||||
browserWin.setPosition(parseInt(posX, 10), parseInt(posY, 10))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next possible insert position (on top)
|
||||
*/
|
||||
function calcInsertPos() {
|
||||
if (activeNotifications.length < config.maxVisibleNotifications) {
|
||||
switch(config.startCorner) {
|
||||
case 'upper-right':
|
||||
case 'upper-left':
|
||||
nextInsertPos.y = config.corner.y + (config.totalHeight * activeNotifications.length);
|
||||
break;
|
||||
|
||||
default:
|
||||
case 'lower-right':
|
||||
case 'lower-left':
|
||||
nextInsertPos.y = config.corner.y - (config.totalHeight * (activeNotifications.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a window to display a notification. Use inactiveWindows or
|
||||
* create a new window
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function getWindow() {
|
||||
return new Promise(function(resolve) {
|
||||
let notificationWindow;
|
||||
// Are there still inactiveWindows?
|
||||
if (inactiveWindows.length > 0) {
|
||||
notificationWindow = inactiveWindows.pop();
|
||||
resolve(notificationWindow)
|
||||
} else {
|
||||
// Or create a new window
|
||||
let windowProperties = config.defaultWindow;
|
||||
windowProperties.width = config.width;
|
||||
windowProperties.height = config.height;
|
||||
notificationWindow = new BrowserWindow(windowProperties);
|
||||
notificationWindow.winName = 'notification-window';
|
||||
notificationWindow.setVisibleOnAllWorkspaces(true);
|
||||
notificationWindow.loadURL(getTemplatePath());
|
||||
notificationWindow.webContents.on('did-finish-load', function() {
|
||||
// Done
|
||||
notificationWindow.webContents.send('electron-notify-load-config', config);
|
||||
resolve(notificationWindow)
|
||||
});
|
||||
|
||||
notificationWindow.once('closed', cleanUpActiveNotification);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Once a minute, remove inactive windows to free up memory used.
|
||||
*/
|
||||
setInterval(cleanUpInactiveWindow, 60000);
|
||||
|
||||
function cleanUpActiveNotification(event) {
|
||||
|
||||
if (!event || !event.sender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pos = activeNotifications.indexOf(event.sender);
|
||||
if (pos !== -1) {
|
||||
activeNotifications.splice(pos, 1);
|
||||
return moveOneDown(pos);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up inactive windows
|
||||
*/
|
||||
function cleanUpInactiveWindow() {
|
||||
inactiveWindows.forEach(function(window) {
|
||||
if (window && windowExists(window)) {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
inactiveWindows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all the notification windows
|
||||
* and cleans up the reference variables
|
||||
*/
|
||||
function closeAll() {
|
||||
resetAnimationQueue();
|
||||
const notificationWin = Object.assign([], activeNotifications);
|
||||
for (let activeNotification of notificationWin) {
|
||||
if (activeNotification && windowExists(activeNotification)) {
|
||||
activeNotification.close();
|
||||
}
|
||||
}
|
||||
|
||||
nextInsertPos = {};
|
||||
activeNotifications = [];
|
||||
notificationQueue = [];
|
||||
cleanUpInactiveWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the animation queue instance
|
||||
*/
|
||||
function resetAnimationQueue() {
|
||||
animationQueue = new AnimationQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new timer to close the notification
|
||||
* @param event
|
||||
* @param winId
|
||||
* @param notificationObj
|
||||
*/
|
||||
function onMouseLeave(event, winId, notificationObj) {
|
||||
if (winId) {
|
||||
log.send(logLevels.INFO, `Mouse was removed from the notification ${winId}`);
|
||||
const notificationWindow = BrowserWindow.fromId(winId);
|
||||
if (notificationWindow && windowExists(notificationWindow)) {
|
||||
notificationWindow.displayTimer = setTimeout(function () {
|
||||
let closeFunc = buildCloseNotification(BrowserWindow.fromId(winId), notificationObj);
|
||||
buildCloseNotificationSafely(closeFunc)('close');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer for a specific notification window
|
||||
* @param event
|
||||
* @param winId
|
||||
*/
|
||||
function onMouseOver(event, winId) {
|
||||
if (winId) {
|
||||
log.send(logLevels.INFO, `Mouse hover on notification ${winId}`);
|
||||
const notificationWindow = BrowserWindow.fromId(winId);
|
||||
if (notificationWindow) {
|
||||
clearTimeout(notificationWindow.displayTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the passed window is valid and exists
|
||||
* @param browserWindow {Electron.BrowserWindow}
|
||||
*/
|
||||
function windowExists(browserWindow) {
|
||||
return !!browserWindow && typeof browserWindow.isDestroyed === 'function' && !browserWindow.isDestroyed();
|
||||
}
|
||||
|
||||
// capturing mouse events
|
||||
ipc.on('electron-notify-mouseleave', onMouseLeave);
|
||||
ipc.on('electron-notify-mouseover', onMouseOver);
|
||||
|
||||
|
||||
module.exports.notify = notify;
|
||||
module.exports.updateConfig = updateConfig;
|
||||
module.exports.reset = setupConfig;
|
||||
module.exports.closeAll = closeAll;
|
||||
module.exports.resetAnimationQueue = resetAnimationQueue;
|
@ -1,246 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { notify } = require('./electron-notify.js');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
/**
|
||||
* implementation for notifications interface,
|
||||
* wrapper around electron-notify.
|
||||
*/
|
||||
class Notify {
|
||||
/**
|
||||
* Dislays a notifications
|
||||
*
|
||||
* @param {String} title Title of notification
|
||||
* @param {Object} options {
|
||||
* body {string} main text to display in notifications
|
||||
* image {string} url of image to show in notification
|
||||
* icon {string} url of image to show in notification
|
||||
* flash {bool} true if notification should flash (default false)
|
||||
* color {string} background color for notification
|
||||
* tag {string} non-empty string to unique identify notf, if another
|
||||
* notification arrives with same tag then it's content will
|
||||
* replace existing notification.
|
||||
* sticky {bool} if true notification will stay until user closes. default
|
||||
* is false.
|
||||
* data {object} arbitrary object to be stored with notification
|
||||
* }
|
||||
*/
|
||||
constructor(title, options) {
|
||||
log.send(logLevels.INFO, 'creating notification text');
|
||||
|
||||
let emitter = new EventEmitter();
|
||||
this.emitter = Queue(emitter);
|
||||
|
||||
let message = options.body;
|
||||
|
||||
this._id = notify({
|
||||
title: title,
|
||||
text: message || '',
|
||||
image: options.image || options.icon,
|
||||
flash: options.flash,
|
||||
color: options.color,
|
||||
tag: options.tag,
|
||||
sticky: options.sticky || false,
|
||||
company: options.company,
|
||||
onShowFunc: onShow.bind(this),
|
||||
onClickFunc: onClick.bind(this),
|
||||
onCloseFunc: onClose.bind(this),
|
||||
onErrorFunc: onError.bind(this)
|
||||
});
|
||||
|
||||
log.send(logLevels.INFO, `created notification with id: ${this._id}`);
|
||||
|
||||
this._data = options.data || null;
|
||||
|
||||
/**
|
||||
* Handles on show event
|
||||
* @param arg
|
||||
*/
|
||||
function onShow(arg) {
|
||||
if (arg.id === this._id) {
|
||||
log.send(logLevels.INFO, 'showing notification, id=' + this._id);
|
||||
this.emitter.queue('show', {
|
||||
target: this
|
||||
});
|
||||
this._closeNotification = arg.closeNotification;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles on click event
|
||||
* @param arg
|
||||
*/
|
||||
function onClick(arg) {
|
||||
if (arg.id === this._id) {
|
||||
log.send(logLevels.INFO, 'clicking notification, id=' + this._id);
|
||||
this.emitter.queue('click', {
|
||||
target: this
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles on close event
|
||||
* @param arg
|
||||
*/
|
||||
function onClose(arg) {
|
||||
if (arg.id === this._id || arg.event === 'close-all') {
|
||||
log.send(logLevels.INFO, 'closing notification, id=' + this._id);
|
||||
this.emitter.queue('close', {
|
||||
target: this
|
||||
});
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles on error event
|
||||
* @param arg
|
||||
*/
|
||||
function onError(arg) {
|
||||
if (arg.id === this._id) {
|
||||
// don't raise error event if handler doesn't exist, node
|
||||
// will throw an exception
|
||||
log.send(logLevels.ERROR, 'error for notification, id=' + this._id +
|
||||
' error=' + (arg && arg.error));
|
||||
if (this.emitter.eventNames().includes('error')) {
|
||||
this.emitter.queue('error', arg.error || 'notification error');
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes notification
|
||||
*/
|
||||
close() {
|
||||
if (typeof this._closeNotification === 'function') {
|
||||
this._closeNotification('close');
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Always allow showing notifications.
|
||||
* @return {string} 'granted'
|
||||
*/
|
||||
static get permission() {
|
||||
return 'granted';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data object passed in via constructor options
|
||||
*/
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event listeners for 'click', 'close', 'show', 'error' events
|
||||
*
|
||||
* @param {String} event event to listen for
|
||||
* @param {func} cb callback invoked when event occurs
|
||||
*/
|
||||
addEventListener(event, cb) {
|
||||
if (event && typeof cb === 'function') {
|
||||
this.emitter.on(event, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes event listeners for 'click', 'close', 'show', 'error' events
|
||||
*
|
||||
* @param {String} event event to stop listening for.
|
||||
* @param {func} cb callback associated with original addEventListener
|
||||
*/
|
||||
removeEventListener(event, cb) {
|
||||
if (event && typeof cb === 'function') {
|
||||
this.emitter.removeListener(event, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all event listeners
|
||||
*/
|
||||
removeAllEvents() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
//
|
||||
// private stuff below here
|
||||
//
|
||||
destroy() {
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow emitter events to be queued before addEventListener called.
|
||||
* Code adapted from: https://github.com/bredele/emitter-queue
|
||||
*
|
||||
* @param {Object} emitter Instance of node emitter that will get augmented.
|
||||
* @return {Object} Modified emitter
|
||||
*/
|
||||
function Queue(emitter) {
|
||||
/**
|
||||
* Cache emitter on.
|
||||
* @api private
|
||||
*/
|
||||
const cache = emitter.on;
|
||||
let modifiedEmitter = emitter;
|
||||
/**
|
||||
* Emit event and store it if no
|
||||
* defined callbacks.
|
||||
* example:
|
||||
*
|
||||
* .queue('message', 'hi');
|
||||
*
|
||||
* @param {String} topic
|
||||
*/
|
||||
modifiedEmitter.queue = function(topic) {
|
||||
this._queue = this._queue || {};
|
||||
this._callbacks = this._callbacks || {};
|
||||
if (this._callbacks[topic]) {
|
||||
this.emit.apply(this, arguments);
|
||||
} else {
|
||||
(this._queue[topic] = this._queue[topic] || [])
|
||||
.push([].slice.call(arguments, 1));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen on the given `event` with `fn`.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} fn
|
||||
* @return {Event}
|
||||
*/
|
||||
modifiedEmitter.on = modifiedEmitter.addEventListener = function(topic, fn) {
|
||||
this._queue = this._queue || {};
|
||||
const topics = this._queue[topic];
|
||||
cache.apply(this, arguments);
|
||||
|
||||
if (!this._callbacks) {
|
||||
this._callbacks = {};
|
||||
}
|
||||
this._callbacks[topic] = true;
|
||||
|
||||
if (topics) {
|
||||
let i = 0;
|
||||
const l = topics.length;
|
||||
for(; i < l; i++) {
|
||||
fn.apply(this, topics[i]);
|
||||
}
|
||||
delete this._queue[topic];
|
||||
}
|
||||
};
|
||||
|
||||
return modifiedEmitter;
|
||||
}
|
||||
|
||||
module.exports = Notify;
|
@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const ipc = electron.ipcRenderer;
|
||||
|
||||
let availableScreens;
|
||||
let selectedPosition;
|
||||
let selectedDisplay;
|
||||
|
||||
renderSettings();
|
||||
|
||||
/**
|
||||
* Method that renders the data from user config
|
||||
*/
|
||||
function renderSettings() {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let okButton = document.getElementById('ok-button');
|
||||
let cancel = document.getElementById('cancel');
|
||||
|
||||
okButton.addEventListener('click', function () {
|
||||
selectedPosition = document.querySelector('input[name="position"]:checked').value;
|
||||
let selector = document.getElementById('screen-selector');
|
||||
selectedDisplay = selector.options[selector.selectedIndex].value;
|
||||
|
||||
// update the user selected data and close the window
|
||||
updateAndClose();
|
||||
});
|
||||
|
||||
cancel.addEventListener('click', function () {
|
||||
ipc.send('close-alert');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the configuration and closes the alert
|
||||
*/
|
||||
function updateAndClose() {
|
||||
ipc.send('update-config', {position: selectedPosition, display: selectedDisplay});
|
||||
ipc.send('close-alert');
|
||||
}
|
||||
|
||||
ipc.on('notificationSettings', (event, args) => {
|
||||
// update position from user config
|
||||
if (args && args.position) {
|
||||
document.getElementById(args.position).checked = true;
|
||||
}
|
||||
|
||||
// update selected display from user config
|
||||
if (args && args.display) {
|
||||
if (availableScreens) {
|
||||
let index = availableScreens.findIndex((item) => {
|
||||
let id = item.id.toString();
|
||||
return id === args.display;
|
||||
});
|
||||
if (index !== -1){
|
||||
let option = document.getElementById(availableScreens[index].id);
|
||||
|
||||
if (option){
|
||||
option.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('screens', (event, screens) => {
|
||||
availableScreens = screens;
|
||||
let screenSelector = document.getElementById('screen-selector');
|
||||
|
||||
if (screenSelector && screens){
|
||||
|
||||
// clearing the previously added content to
|
||||
// make sure the content is not repeated
|
||||
screenSelector.innerHTML = '';
|
||||
|
||||
screens.forEach((scr, index) => {
|
||||
let option = document.createElement('option');
|
||||
option.value = scr.id;
|
||||
option.id = scr.id;
|
||||
option.innerHTML = index + 1;
|
||||
screenSelector.appendChild(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('i18n-notification-settings', (event, content) => {
|
||||
if (content && typeof content === 'object') {
|
||||
const i18nNodes = document.querySelectorAll('[data-i18n-text]');
|
||||
|
||||
for (let node of i18nNodes) {
|
||||
if (node.attributes['data-i18n-text'] && node.attributes['data-i18n-text'].value) {
|
||||
node.innerText = content[node.attributes['data-i18n-text'].value] || node.attributes['data-i18n-text'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// note: this is a workaround until
|
||||
// https://github.com/electron/electron/issues/8841
|
||||
// is fixed on the electron. where 'will-navigate'
|
||||
// is never fired in sandbox mode
|
||||
//
|
||||
// This is required in order to prevent from loading
|
||||
// dropped content
|
||||
window.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
@ -1,91 +0,0 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.content {
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.3;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
margin: 0 0 0 4px;
|
||||
font-size: 1.4rem;
|
||||
min-height: 13px;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.selector {
|
||||
padding: 0 9px 0 16px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
border: 1px solid #ccc !important;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.main .first-set {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.main .second-set {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.radio {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.radio__label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio__label input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px 10px;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttonLayout {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttonDismiss {
|
||||
margin-right: 10px;
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title data-i18n-text="Symphony - Configure Notification Position"></title>
|
||||
<link rel="stylesheet" type="text/css" href="./configure-notification-position.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<header class="header">
|
||||
<span class="header__title" data-i18n-text="Notification Settings"></span>
|
||||
</header>
|
||||
<div class="form">
|
||||
<form>
|
||||
<label class="label" data-i18n-text="Monitor"></label>
|
||||
<div id="screens" class="main">
|
||||
<label data-i18n-text="Notification shown on Monitor: "></label>
|
||||
<select class="selector" id="screen-selector" title="position">
|
||||
</select>
|
||||
</div>
|
||||
<label class="label" data-i18n-text="Position"></label>
|
||||
<div class="main">
|
||||
<div class="first-set">
|
||||
<div class="radio">
|
||||
<label class="radio__label"><input id="upper-left" type="radio" name="position" value="upper-left">
|
||||
<span data-i18n-text="Top Left"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label class="radio__label">
|
||||
<input id="lower-left" type="radio" name="position" value="lower-left">
|
||||
<span data-i18n-text="Bottom Left"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="second-set">
|
||||
<div class="radio">
|
||||
<label class="radio__label"><span data-i18n-text="Top Right"></span>
|
||||
<input id="upper-right" type="radio" name="position" value="upper-right">
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label class="radio__label"><span data-i18n-text="Bottom Right"></span>
|
||||
<input id="lower-right" type="radio" name="position" value="lower-right">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="buttonLayout">
|
||||
<button id="cancel" class="buttonDismiss" data-i18n-text="CANCEL"></button>
|
||||
<button id="ok-button" class="button" data-i18n-text="OK"></button>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -1,211 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const electron = require('electron');
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const app = electron.app;
|
||||
const ipc = electron.ipcMain;
|
||||
const log = require('../../log.js');
|
||||
const logLevels = require('../../enums/logLevels.js');
|
||||
const notify = require('./../electron-notify');
|
||||
const eventEmitter = require('./../../eventEmitter');
|
||||
const { updateConfigField } = require('../../config');
|
||||
const i18n = require('../../translation/i18n');
|
||||
const { isMac } = require('../../utils/misc');
|
||||
|
||||
let configurationWindow;
|
||||
let screens;
|
||||
let position;
|
||||
let display;
|
||||
let sandboxed = false;
|
||||
|
||||
let windowConfig = {
|
||||
width: 460,
|
||||
height: 360,
|
||||
show: false,
|
||||
modal: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'configure-notification-position-preload.js'),
|
||||
sandbox: sandboxed,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
app.on('ready', () => {
|
||||
screens = electron.screen.getAllDisplays();
|
||||
|
||||
// if display added/removed update the list of screens
|
||||
electron.screen.on('display-added', updateScreens);
|
||||
electron.screen.on('display-removed', updateScreens);
|
||||
});
|
||||
|
||||
/**
|
||||
* Update all the screens
|
||||
*/
|
||||
function updateScreens() {
|
||||
log.send(logLevels.INFO, `Updating screens as there is change in display connections`);
|
||||
screens = electron.screen.getAllDisplays();
|
||||
|
||||
// Notifying renderer when a display is added/removed
|
||||
if (configurationWindow) {
|
||||
// Event that updates the DOM elements
|
||||
// notification position checkbox and monitor selection drop-down
|
||||
configurationWindow.webContents.send('notificationSettings', {position: position, display: display});
|
||||
|
||||
if (screens && screens.length >= 0) {
|
||||
configurationWindow.webContents.send('screens', screens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template path
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'configure-notification-position.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, 'configure-notification-position: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
return 'file://' + templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the configuration window for a specific window
|
||||
* @param windowName
|
||||
*/
|
||||
function openConfigurationWindow(windowName) {
|
||||
// This prevents creating multiple instances of the
|
||||
// notification configuration window
|
||||
if (configurationWindow && !configurationWindow.isDestroyed()) {
|
||||
if (configurationWindow.isMinimized()) {
|
||||
configurationWindow.restore();
|
||||
}
|
||||
configurationWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
const selectedParentWindow = allWindows.find((window) => { return window.winName === windowName });
|
||||
|
||||
// if we couldn't find any window matching the window name
|
||||
// it will render as a new window
|
||||
if (selectedParentWindow) {
|
||||
windowConfig.parent = selectedParentWindow;
|
||||
|
||||
/**
|
||||
* This is a temporary work around until there
|
||||
* is a fix for the modal window in windows from the electron
|
||||
* issue - https://github.com/electron/electron/issues/10721
|
||||
*/
|
||||
const { x, y, width, height } = selectedParentWindow.getBounds();
|
||||
|
||||
const windowWidth = Math.round(width * 0.5);
|
||||
const windowHeight = Math.round(height * 0.5);
|
||||
|
||||
// Calculating the center of the parent window
|
||||
// to place the configuration window
|
||||
const centerX = x + width / 2.0;
|
||||
const centerY = y + height / 2.0;
|
||||
windowConfig.x = Math.round(centerX - (windowWidth / 2.0));
|
||||
windowConfig.y = Math.round(centerY - (windowHeight / 2.0));
|
||||
}
|
||||
|
||||
configurationWindow = new BrowserWindow(windowConfig);
|
||||
configurationWindow.setVisibleOnAllWorkspaces(true);
|
||||
configurationWindow.loadURL(getTemplatePath());
|
||||
|
||||
configurationWindow.once('ready-to-show', () => {
|
||||
configurationWindow.show();
|
||||
});
|
||||
|
||||
configurationWindow.webContents.on('did-finish-load', () => {
|
||||
const notificationSettingsContent = i18n.getMessageFor('NotificationSettings');
|
||||
configurationWindow.webContents.send('i18n-notification-settings', notificationSettingsContent);
|
||||
if (screens && screens.length >= 0) {
|
||||
configurationWindow.webContents.send('screens', screens);
|
||||
}
|
||||
configurationWindow.webContents.send('notificationSettings', {position: position, display: display});
|
||||
if (!isMac) {
|
||||
// prevents from displaying menu items when "alt" key is pressed
|
||||
configurationWindow.setMenu(null);
|
||||
}
|
||||
});
|
||||
|
||||
configurationWindow.on('close', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
|
||||
configurationWindow.on('closed', () => {
|
||||
destroyWindow();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a window
|
||||
*/
|
||||
function destroyWindow() {
|
||||
log.send(logLevels.INFO, `Closing notification configure window`);
|
||||
configurationWindow = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save 'position' & 'display' to the config file
|
||||
*/
|
||||
function updateConfig() {
|
||||
let settings = {
|
||||
position: position,
|
||||
display: display
|
||||
};
|
||||
updateConfigField('notificationSettings', settings);
|
||||
updateNotification(position, display);
|
||||
log.send(logLevels.INFO, `Updating notification position on screen`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the Notification class with the new 'position' & 'screen'
|
||||
* @param mPosition - position to display the notifications
|
||||
* ('upper-right, upper-left, lower-right, lower-left')
|
||||
* @param mDisplay - id of the selected display
|
||||
*/
|
||||
function updateNotification(mPosition, mDisplay) {
|
||||
notify.updateConfig({position: mPosition, display: mDisplay});
|
||||
eventEmitter.emit('notificationSettings', {position: mPosition, display: mDisplay});
|
||||
notify.reset();
|
||||
}
|
||||
|
||||
ipc.on('close-alert', function () {
|
||||
configurationWindow.close();
|
||||
});
|
||||
|
||||
ipc.on('update-config', (event, config) => {
|
||||
|
||||
if (config) {
|
||||
if (config.position) {
|
||||
position = config.position;
|
||||
}
|
||||
if (config.display) {
|
||||
display = config.display;
|
||||
}
|
||||
}
|
||||
|
||||
updateConfig();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event to read 'Position' & 'Display' from config and
|
||||
* updated the configuration view
|
||||
*/
|
||||
eventEmitter.on('notificationSettings', (notificationSettings) => {
|
||||
position = notificationSettings.position;
|
||||
display = notificationSettings.display;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
openConfigurationWindow: openConfigurationWindow
|
||||
};
|
@ -1,601 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// script run before others and still has access to node integration, even
|
||||
// when turned off - allows us to leak only what want into window object.
|
||||
// see: http://electron.atom.io/docs/api/browser-window/
|
||||
//
|
||||
// to leak some node module into:
|
||||
// https://medium.com/@leonli/securing-embedded-external-content-in-electron-node-js-8b6ef665cd8e#.fex4e68p7
|
||||
// https://slack.engineering/building-hybrid-applications-with-electron-dc67686de5fb#.tp6zz1nrk
|
||||
//
|
||||
// also to bring pieces of node.js:
|
||||
// https://github.com/electron/electron/issues/2984
|
||||
//
|
||||
const { ipcRenderer, remote, crashReporter, webFrame } = require('electron');
|
||||
|
||||
const throttle = require('../utils/throttle.js');
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
const getMediaSources = require('../desktopCapturer/getSources');
|
||||
const getMediaSource = require('../desktopCapturer/getSource');
|
||||
const showScreenSharingIndicator = require('../screenSharingIndicator/showScreenSharingIndicator');
|
||||
const { TitleBar } = require('../windowsTitlebar');
|
||||
const { NetworkError } = require('../networkError');
|
||||
const titleBar = new TitleBar();
|
||||
const networkError = new NetworkError();
|
||||
const { buildNumber } = require('../../package.json');
|
||||
const SnackBar = require('../snackBar').SnackBar;
|
||||
const KeyCodes = {
|
||||
Esc: 27,
|
||||
Alt: 18,
|
||||
};
|
||||
|
||||
let Search;
|
||||
let SearchUtils;
|
||||
let CryptoLib;
|
||||
let isAltKey = false;
|
||||
let isMenuOpen = false;
|
||||
|
||||
try {
|
||||
Search = remote.require('swift-search').Search;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to initialize swift search. You'll need to include the search dependency. Contact the developers for more details");
|
||||
}
|
||||
|
||||
try {
|
||||
SearchUtils = remote.require('swift-search').SearchUtils;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to initialize swift search (Utils). You'll need to include the search dependency. Contact the developers for more details");
|
||||
}
|
||||
|
||||
try {
|
||||
CryptoLib = remote.require('./cryptoLib.js');
|
||||
} catch (e) {
|
||||
CryptoLib = null;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to initialize Crypto Lib. You'll need to include the Crypto library. Contact the developers for more details");
|
||||
}
|
||||
|
||||
require('../downloadManager');
|
||||
let snackBar;
|
||||
|
||||
// hold ref so doesn't get GC'ed
|
||||
const local = {
|
||||
ipcRenderer: ipcRenderer
|
||||
};
|
||||
|
||||
// throttle calls to this func to at most once per sec, called on leading edge.
|
||||
const throttledSetBadgeCount = throttle(1000, function (count) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.setBadgeCount,
|
||||
count: count
|
||||
});
|
||||
});
|
||||
|
||||
const throttledSetIsInMeetingStatus = throttle(1000, function (isInMeeting) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.setIsInMeeting,
|
||||
isInMeeting
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process onload event
|
||||
*/
|
||||
local.ipcRenderer.on('on-page-load', () => {
|
||||
snackBar = new SnackBar();
|
||||
|
||||
webFrame.setSpellCheckProvider('en-US', true, {
|
||||
spellCheck(text) {
|
||||
return !local.ipcRenderer.sendSync(apiName, {
|
||||
cmd: apiCmds.isMisspelled,
|
||||
text
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// only registers main window's preload
|
||||
if (window.name === 'main') {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.optimizeMemoryRegister,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const throttledActivate = throttle(1000, function (windowName) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.activate,
|
||||
windowName: windowName
|
||||
});
|
||||
});
|
||||
|
||||
const throttledBringToFront = throttle(1000, function (windowName, reason) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.bringToFront,
|
||||
windowName: windowName,
|
||||
reason: reason
|
||||
});
|
||||
});
|
||||
|
||||
const throttledSetLocale = throttle(1000, function (locale) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.setLocale,
|
||||
locale,
|
||||
});
|
||||
});
|
||||
|
||||
createAPI();
|
||||
|
||||
// creates API exposed from electron.
|
||||
// wrapped in a function so we can abort early in function coming from an iframe
|
||||
function createAPI() {
|
||||
// iframes (and any other non-top level frames) get no api access
|
||||
// http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t/326076
|
||||
if (window.self !== window.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
// note: window.open from main window (if in the same domain) will get
|
||||
// api access. window.open in another domain will be opened in the default
|
||||
// browser (see: handler for event 'new-window' in windowMgr.js)
|
||||
|
||||
//
|
||||
// API exposed to renderer process.
|
||||
//
|
||||
window.ssf = {
|
||||
getVersionInfo: function () {
|
||||
return new Promise(function (resolve) {
|
||||
let appName = remote.app.getName();
|
||||
let appVer = remote.app.getVersion();
|
||||
|
||||
const verInfo = {
|
||||
containerIdentifier: appName,
|
||||
containerVer: appVer,
|
||||
buildNumber: buildNumber,
|
||||
apiVer: '2.0.0',
|
||||
searchApiVer: '3.0.0'
|
||||
};
|
||||
resolve(verInfo);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* sets the count on the tray icon to the given number.
|
||||
* @param {number} count count to be displayed
|
||||
* note: count of 0 will remove the displayed count.
|
||||
* note: for mac the number displayed will be 1 to infinity
|
||||
* note: for windws the number displayed will be 1 to 99 and 99+
|
||||
*/
|
||||
setBadgeCount: function (count) {
|
||||
throttledSetBadgeCount(count);
|
||||
},
|
||||
|
||||
/**
|
||||
* provides api similar to html5 Notification, see details
|
||||
* in notify/notifyImpl.js
|
||||
*/
|
||||
Notification: remote.require('./notify/notifyImpl.js'),
|
||||
|
||||
/**
|
||||
* provides api to allow user to capture portion of screen, see api
|
||||
* details in screenSnipper/index.js
|
||||
*/
|
||||
ScreenSnippet: remote.require('./screenSnippet/index.js').ScreenSnippet,
|
||||
|
||||
/**
|
||||
* Provides api for client side searching
|
||||
* using the SymphonySearchEngine library
|
||||
* details in ./search/search.js & ./search/searchLibrary.js
|
||||
*/
|
||||
Search: Search || null,
|
||||
|
||||
/**
|
||||
* Provides api for search module utils
|
||||
* like checking free space / search user config data to the client app
|
||||
* details in ./search/searchUtils.js & ./search/searchConfig.js
|
||||
*/
|
||||
SearchUtils: SearchUtils || null,
|
||||
|
||||
/**
|
||||
* Native encryption and decryption.
|
||||
*/
|
||||
CryptoLib: CryptoLib,
|
||||
|
||||
/**
|
||||
* Brings window forward and gives focus.
|
||||
* @param {String} windowName Name of window. Note: main window name is 'main'
|
||||
*/
|
||||
activate: function (windowName) {
|
||||
if (typeof windowName === 'string') {
|
||||
throttledActivate(windowName);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Brings window forward and gives focus.
|
||||
* @param {String} windowName Name of window. Note: main window name is 'main'
|
||||
* @param {String} reason, The reason for which the window is to be activated
|
||||
*/
|
||||
bringToFront: function (windowName, reason) {
|
||||
if (typeof windowName === 'string') {
|
||||
throttledBringToFront(windowName, reason);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Allows JS to register a callback to be invoked when size/positions
|
||||
* changes for any pop-out window (i.e., window.open). The main
|
||||
* process will emit IPC event 'boundsChange' (see below). Currently
|
||||
* only one window can register for bounds change.
|
||||
* @param {Function} callback Function invoked when bounds changes.
|
||||
*/
|
||||
registerBoundsChange: function (callback) {
|
||||
if (typeof callback === 'function') {
|
||||
local.boundsChangeCallback = callback;
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.registerBoundsChange
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* allows JS to register a logger that can be used by electron main process.
|
||||
* @param {Object} logger function that can be called accepting
|
||||
* object: {
|
||||
* logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG',
|
||||
* logDetails: String
|
||||
* }
|
||||
*/
|
||||
registerLogger: function (logger) {
|
||||
if (typeof logger === 'function') {
|
||||
local.logger = logger;
|
||||
|
||||
// only main window can register
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.registerLogger
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* allows JS to register a protocol handler that can be used by the
|
||||
* electron main process.
|
||||
*
|
||||
* @param protocolHandler {Function} callback will be called when app is
|
||||
* invoked with registered protocol (e.g., symphony). The callback
|
||||
* receives a single string argument: full uri that the app was
|
||||
* invoked with e.g., symphony://?streamId=xyz123&streamType=chatroom
|
||||
*
|
||||
* Note: this function should only be called after client app is fully
|
||||
* able for protocolHandler callback to be invoked. It is possible
|
||||
* the app was started using protocol handler, in this case as soon as
|
||||
* this registration func is invoked then the protocolHandler callback
|
||||
* will be immediately called.
|
||||
*/
|
||||
registerProtocolHandler: function (protocolHandler) {
|
||||
if (typeof protocolHandler === 'function') {
|
||||
|
||||
local.processProtocolAction = protocolHandler;
|
||||
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.registerProtocolHandler
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* allows JS to register a activity detector that can be used by electron main process.
|
||||
* @param {Object} activityDetection - function that can be called accepting
|
||||
* @param {Object} period - minimum user idle time in millisecond
|
||||
* object: {
|
||||
* period: Number
|
||||
* systemIdleTime: Number
|
||||
* }
|
||||
*/
|
||||
registerActivityDetection: function (period, activityDetection) {
|
||||
if (typeof activityDetection === 'function') {
|
||||
local.activityDetection = activityDetection;
|
||||
|
||||
// only main window can register
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.registerActivityDetection,
|
||||
period: period
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements equivalent of desktopCapturer.getSources - that works in
|
||||
* a sandboxed renderer process.
|
||||
* see: https://electron.atom.io/docs/api/desktop-capturer/
|
||||
* for interface: see documentation in desktopCapturer/getSources.js
|
||||
*
|
||||
* @deprecated instead use getMediaSource
|
||||
*/
|
||||
getMediaSources: getMediaSources,
|
||||
|
||||
/**
|
||||
* Implements equivalent of desktopCapturer.getSources - that works in
|
||||
* a sandboxed renderer process.
|
||||
* see: https://electron.atom.io/docs/api/desktop-capturer/
|
||||
* for interface: see documentation in desktopCapturer/getSource.js
|
||||
*
|
||||
* This opens a window and displays all the desktop sources
|
||||
* and returns selected source
|
||||
*/
|
||||
getMediaSource: getMediaSource,
|
||||
|
||||
/**
|
||||
* Shows a banner that informs user that the screen is being shared.
|
||||
*
|
||||
* @param params 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.
|
||||
*/
|
||||
showScreenSharingIndicator: showScreenSharingIndicator,
|
||||
|
||||
/**
|
||||
* Opens a modal window to configure notification preference.
|
||||
*/
|
||||
showNotificationSettings: function () {
|
||||
let windowName = remote.getCurrentWindow().winName;
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.showNotificationSettings,
|
||||
windowName: windowName
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets if the user is in an active meeting
|
||||
* will be used to handle memory refresh functionality
|
||||
*/
|
||||
setIsInMeeting: function (isInMeeting) {
|
||||
throttledSetIsInMeetingStatus(isInMeeting);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the language which updates the application locale
|
||||
* @param {string} locale - language identifier and a region identifier
|
||||
* Ex: en-US, ja-JP
|
||||
*/
|
||||
setLocale: function (locale) {
|
||||
if (typeof locale === 'string') {
|
||||
throttledSetLocale(locale);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Allows JS to register activeRequests that can be used by electron to
|
||||
* get the active network request from the client
|
||||
*
|
||||
* @param activeRequests
|
||||
*/
|
||||
registerActiveRequests: function (activeRequests) {
|
||||
if (typeof activeRequests === 'function') {
|
||||
local.activeRequests = activeRequests;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// add support for both ssf and SYM_API name-space.
|
||||
window.SYM_API = window.ssf;
|
||||
Object.freeze(window.ssf);
|
||||
Object.freeze(window.SYM_API);
|
||||
|
||||
// listen for log message from main process
|
||||
local.ipcRenderer.on('log', (event, arg) => {
|
||||
if (arg && local.logger) {
|
||||
local.logger(arg.msgs || [], arg.logLevel, arg.showInConsole);
|
||||
}
|
||||
});
|
||||
|
||||
// listen for notifications that some window size/position has changed
|
||||
local.ipcRenderer.on('boundsChange', (event, arg) => {
|
||||
if (local.boundsChangeCallback && arg.windowName &&
|
||||
arg.x && arg.y && arg.width && arg.height) {
|
||||
local.boundsChangeCallback({
|
||||
x: arg.x,
|
||||
y: arg.y,
|
||||
width: arg.width,
|
||||
height: arg.height,
|
||||
windowName: arg.windowName
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// listen for user activity from main process
|
||||
local.ipcRenderer.on('activity', (event, arg) => {
|
||||
if (local.activityDetection && arg && typeof arg.systemIdleTime === 'number') {
|
||||
local.activityDetection(arg.systemIdleTime);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Use render process to create badge count img and send back to main process.
|
||||
* If number is greater than 99 then 99+ img is returned.
|
||||
* note: with sandboxing turned on only get arg and no event passed in, so
|
||||
* need to use ipcRenderer to callback to main process.
|
||||
* @type {object} arg.count - number: count to be displayed
|
||||
*/
|
||||
local.ipcRenderer.on('createBadgeDataUrl', (event, arg) => {
|
||||
const count = arg && arg.count || 0;
|
||||
|
||||
// create 32 x 32 img
|
||||
let radius = 16;
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.height = radius * 2;
|
||||
canvas.width = radius * 2;
|
||||
|
||||
let ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.beginPath();
|
||||
ctx.arc(radius, radius, radius, 0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = 'white';
|
||||
|
||||
let text = count > 99 ? '99+' : count.toString();
|
||||
|
||||
if (text.length > 2) {
|
||||
ctx.font = 'bold 18px sans-serif';
|
||||
ctx.fillText(text, radius, 22);
|
||||
} else if (text.length > 1) {
|
||||
ctx.font = 'bold 24px sans-serif';
|
||||
ctx.fillText(text, radius, 24);
|
||||
} else {
|
||||
ctx.font = 'bold 26px sans-serif';
|
||||
ctx.fillText(text, radius, 26);
|
||||
}
|
||||
|
||||
let dataUrl = canvas.toDataURL('image/png', 1.0);
|
||||
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.badgeDataUrl,
|
||||
dataUrl: dataUrl,
|
||||
count: count
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process for processing protocol urls
|
||||
* @type {String} arg - the protocol url
|
||||
*/
|
||||
local.ipcRenderer.on('protocol-action', (event, arg) => {
|
||||
if (local.processProtocolAction && arg) {
|
||||
local.processProtocolAction(arg);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process to start crash reporter
|
||||
* in the render thread
|
||||
*/
|
||||
local.ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
// Adds custom title bar style for Windows 10 OS
|
||||
local.ipcRenderer.on('initiate-windows-title-bar', (event, arg) => {
|
||||
if (arg && typeof arg === 'string') {
|
||||
titleBar.initiateWindowsTitleBar(arg);
|
||||
}
|
||||
});
|
||||
|
||||
// display network error info
|
||||
local.ipcRenderer.on('network-error', (event, data) => {
|
||||
if (data && typeof data === 'object') {
|
||||
networkError.showError(data);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process when
|
||||
* the locale is changed
|
||||
* @param dataObj {Object} - locale content
|
||||
*/
|
||||
local.ipcRenderer.on('locale-changed', (event, dataObj) => {
|
||||
if (dataObj && typeof dataObj === 'object') {
|
||||
if (dataObj.titleBar) {
|
||||
titleBar.updateLocale(dataObj.titleBar);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process when
|
||||
* the window enters full screen
|
||||
*/
|
||||
local.ipcRenderer.on('window-enter-full-screen', (event, arg) => {
|
||||
if (snackBar && typeof arg === 'object' && arg.snackBar) {
|
||||
setTimeout(() => snackBar.showSnackBar(arg.snackBar), 500);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process when
|
||||
* the window leave full screen
|
||||
*/
|
||||
local.ipcRenderer.on('window-leave-full-screen', () => {
|
||||
if (snackBar) {
|
||||
snackBar.removeSnackBar();
|
||||
}
|
||||
});
|
||||
|
||||
local.ipcRenderer.on('memory-info-request', () => {
|
||||
if (window.name === 'main') {
|
||||
const activeRequests = typeof local.activeRequests === 'function' ? local.activeRequests() : 0;
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.optimizeMemoryConsumption,
|
||||
activeRequests,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateOnlineStatus() {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.isOnline,
|
||||
isOnline: window.navigator.onLine
|
||||
});
|
||||
}
|
||||
|
||||
// Invoked whenever the app is reloaded/navigated
|
||||
function sanitize() {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.sanitize,
|
||||
windowName: window.name || 'main'
|
||||
});
|
||||
}
|
||||
|
||||
// Handle key down events
|
||||
const throttledKeyDown = throttle(500, (event) => {
|
||||
isAltKey = event.keyCode === KeyCodes.Alt;
|
||||
if (event.keyCode === KeyCodes.Esc) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.keyPress,
|
||||
keyCode: event.keyCode
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle key up events
|
||||
const throttledKeyUp = throttle(500, (event) => {
|
||||
if (isAltKey && (event.keyCode === KeyCodes.Alt || KeyCodes.Esc)) {
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
if (isAltKey && isMenuOpen && event.keyCode === KeyCodes.Alt) {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.keyPress,
|
||||
keyCode: event.keyCode
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const throttleMouseDown = throttle(500, () => {
|
||||
if (isAltKey && isMenuOpen) {
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('offline', updateOnlineStatus, false);
|
||||
window.addEventListener('online', updateOnlineStatus, false);
|
||||
window.addEventListener('beforeunload', sanitize, false);
|
||||
window.addEventListener('keyup', throttledKeyUp, true);
|
||||
window.addEventListener('keydown', throttledKeyDown, true);
|
||||
window.addEventListener('mousedown', throttleMouseDown, { capture: true });
|
||||
|
||||
updateOnlineStatus();
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
let protocolWindow;
|
||||
let protocolUrl;
|
||||
|
||||
/**
|
||||
* processes a protocol uri
|
||||
* @param {String} uri - the uri opened in the format 'symphony://...'
|
||||
*/
|
||||
function processProtocolAction(uri) {
|
||||
log.send(logLevels.INFO, `protocol action, uri ${uri}`);
|
||||
if (!protocolWindow) {
|
||||
log.send(logLevels.INFO, `protocol window not yet initialized, caching the uri ${uri}`);
|
||||
setProtocolUrl(uri);
|
||||
return;
|
||||
}
|
||||
if (uri && uri.startsWith('symphony://')) {
|
||||
protocolWindow.send('protocol-action', uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the protocol window
|
||||
* @param {Object} win - the renderer window
|
||||
*/
|
||||
function setProtocolWindow(win) {
|
||||
protocolWindow = win;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks to see if the app was opened by a uri
|
||||
*/
|
||||
function checkProtocolAction() {
|
||||
log.send(logLevels.INFO, `checking if we have a cached protocol url`);
|
||||
if (protocolUrl) {
|
||||
log.send(logLevels.INFO, `found a cached protocol url, processing it!`);
|
||||
processProtocolAction(protocolUrl);
|
||||
protocolUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* caches the protocol uri
|
||||
* @param {String} uri - the uri opened in the format 'symphony://...'
|
||||
*/
|
||||
function setProtocolUrl(uri) {
|
||||
protocolUrl = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the protocol url set against an instance
|
||||
* @returns {*}
|
||||
*/
|
||||
function getProtocolUrl() {
|
||||
return protocolUrl;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processProtocolAction: processProtocolAction,
|
||||
setProtocolWindow: setProtocolWindow,
|
||||
checkProtocolAction: checkProtocolAction,
|
||||
setProtocolUrl: setProtocolUrl,
|
||||
getProtocolUrl: getProtocolUrl
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const ipcMain = electron.ipcMain;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
|
||||
const i18n = require('../translation/i18n');
|
||||
const { isMac } = require('./../utils/misc.js');
|
||||
|
||||
const baseWindowConfig = {
|
||||
width: 592,
|
||||
height: 48,
|
||||
show: false,
|
||||
modal: true,
|
||||
frame: false,
|
||||
focusable: false,
|
||||
transparent: true,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'renderer.js'),
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'screen-sharing-indicator.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log.send(logLevels.ERROR, `screen-sharing-indicator: Could not find template ("${templatePath}").`);
|
||||
}
|
||||
return `file://${templatePath}`;
|
||||
}
|
||||
|
||||
function openScreenSharingIndicator(eventSender, displayId, id) {
|
||||
const indicatorScreen = (displayId && electron.screen.getAllDisplays().filter(d => displayId.includes(d.id))[0]) || electron.screen.getPrimaryDisplay();
|
||||
const screenRect = indicatorScreen.workArea;
|
||||
const windowConfig = Object.assign({}, baseWindowConfig, {
|
||||
x: screenRect.x + Math.round((screenRect.width - baseWindowConfig.width) / 2),
|
||||
y: screenRect.y + screenRect.height - baseWindowConfig.height
|
||||
});
|
||||
|
||||
const indicatorWindow = new electron.BrowserWindow(windowConfig);
|
||||
indicatorWindow.setVisibleOnAllWorkspaces(true);
|
||||
indicatorWindow.setMenu(null);
|
||||
indicatorWindow.loadURL(getTemplatePath());
|
||||
|
||||
indicatorWindow.once('ready-to-show', () => {
|
||||
indicatorWindow.show();
|
||||
});
|
||||
|
||||
indicatorWindow.webContents.on('did-finish-load', () => {
|
||||
initCrashReporterMain({ process: 'screen sharing indicator window' });
|
||||
initCrashReporterRenderer(indicatorWindow, { process: 'render | screen sharing indicator window' });
|
||||
indicatorWindow.webContents.send('window-data', {
|
||||
id,
|
||||
i18n: i18n.getMessageFor('ScreenSharingIndicator'),
|
||||
isMac
|
||||
});
|
||||
});
|
||||
|
||||
indicatorWindow.webContents.on('crashed', (event, killed) => {
|
||||
|
||||
log.send(logLevels.INFO, `Screen Sharing Indicator Window crashed! Killed? ${killed}`);
|
||||
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorDialogOptions = {
|
||||
type: 'error',
|
||||
title: i18n.getMessageFor('Renderer Process Crashed'),
|
||||
message: i18n.getMessageFor('Oops! Looks like we have had a crash.'),
|
||||
buttons: ['Close']
|
||||
};
|
||||
electron.dialog.showMessageBox(errorDialogOptions, () => indicatorWindow.close());
|
||||
});
|
||||
|
||||
const handleStopSharingClicked = (event, indicatorId) => {
|
||||
if (indicatorId === id) {
|
||||
eventSender.send('stop-sharing-requested', id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDestroyScreensharingIndicator = (event, indicatorId) => {
|
||||
if (indicatorId === id) {
|
||||
if (!indicatorWindow.isDestroyed()) {
|
||||
indicatorWindow.close();
|
||||
}
|
||||
ipcMain.removeListener('stop-sharing-clicked', handleStopSharingClicked);
|
||||
ipcMain.removeListener('destroy-screensharing-indicator', handleDestroyScreensharingIndicator);
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.on('stop-sharing-clicked', handleStopSharingClicked);
|
||||
ipcMain.on('destroy-screensharing-indicator', handleDestroyScreensharingIndicator);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openScreenSharingIndicator
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, crashReporter } = require('electron');
|
||||
|
||||
let indicatorId;
|
||||
|
||||
function renderDom() {
|
||||
const stopSharingButton = document.getElementById('stop-sharing-button');
|
||||
stopSharingButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('stop-sharing-clicked', indicatorId);
|
||||
}, false);
|
||||
|
||||
const hideButton = document.getElementById('hide-button');
|
||||
hideButton.addEventListener('click', () => {
|
||||
window.close();
|
||||
}, false);
|
||||
}
|
||||
|
||||
ipcRenderer.on('window-data', (event, content) => {
|
||||
indicatorId = content.id;
|
||||
const setText = (el, text) => {
|
||||
document.getElementById(el).innerHTML = (content.i18n[text] || text).replace('Symphony', '<b>Symphony</b>');
|
||||
};
|
||||
setText('stop-sharing-button', 'Stop sharing');
|
||||
setText('hide-button', 'Hide');
|
||||
setText('text-label', 'You are sharing your screen on Symphony');
|
||||
document.body.className = content.isMac ? 'mac' : '';
|
||||
});
|
||||
|
||||
ipcRenderer.on('register-crash-reporter', (event, arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
crashReporter.start(arg);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderDom();
|
||||
});
|
@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title data-i18n-text="Screen Sharing"></title>
|
||||
<style>
|
||||
@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: "system";
|
||||
font-size: 13px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body:not(.mac) .container {
|
||||
border: 1px solid rgb(212, 212, 212);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#stop-sharing-button {
|
||||
text-transform: uppercase;
|
||||
background: #107ccc;
|
||||
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: #6b717c;
|
||||
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;
|
||||
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.22) 1px,
|
||||
rgba(0, 0, 0, 0.22) 2px
|
||||
);
|
||||
}
|
||||
|
||||
.mac #hide-button {
|
||||
color: #303237;
|
||||
}
|
||||
|
||||
.mac #stop-sharing-button {
|
||||
padding: 6px 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<span id='drag-area'></span>
|
||||
<span id='text-label'>You are sharing your screen on <b>Symphony</b></span>
|
||||
<span class='buttons'>
|
||||
<a id='hide-button' href='#'>Hide</a>
|
||||
<button id='stop-sharing-button'>Stop sharing</button>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,46 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
|
||||
let nextIndicatorId = 0;
|
||||
|
||||
function showScreenSharingIndicator(options, callback) {
|
||||
const { stream, displayId } = options;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const id = ++nextIndicatorId;
|
||||
ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.openScreenSharingIndicator,
|
||||
displayId: options.displayId,
|
||||
id
|
||||
});
|
||||
|
||||
const handleStopRequest = (e, indicatorId) => {
|
||||
if (indicatorId === id) {
|
||||
callback({type: 'stopRequested'});
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
ipcRenderer.send('destroy-screensharing-indicator', id);
|
||||
options.stream.removeEventListener('inactive', destroy);
|
||||
ipcRenderer.removeListener('stop-sharing-requested', handleStopRequest);
|
||||
};
|
||||
|
||||
ipcRenderer.on('stop-sharing-requested', handleStopRequest);
|
||||
options.stream.addEventListener('inactive', destroy);
|
||||
}
|
||||
|
||||
|
||||
module.exports = showScreenSharingIndicator;
|
@ -1,215 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const childProcess = require('child_process');
|
||||
const fs = require('fs');
|
||||
const Jimp = require('jimp');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { isMac, isDevEnv, isWindowsOS } = require('../utils/misc.js');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
const eventEmitter = require('.././eventEmitter');
|
||||
const { getLanguage } = require('../translation/i18n');
|
||||
|
||||
// static ref to child process, only allow one screen snippet at time, so
|
||||
// hold ref to prev, so can kill before starting next snippet.
|
||||
let child;
|
||||
let isAlwaysOnTop;
|
||||
|
||||
/**
|
||||
* Captures a user selected portion of the monitor and returns jpeg image
|
||||
* encoded in base64 format.
|
||||
*/
|
||||
class ScreenSnippet {
|
||||
/**
|
||||
* Returns promise.
|
||||
*
|
||||
* If successful will resolves with jpeg image encoded in base64 format:
|
||||
* {
|
||||
* type: 'image/jpg;base64',
|
||||
* data: base64-data
|
||||
* }
|
||||
*
|
||||
* Otherwise if not successful will reject with object
|
||||
* containing: { type: ['WARN','ERROR'], message: String }
|
||||
*/
|
||||
capture() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let captureUtil, captureUtilArgs;
|
||||
|
||||
log.send(logLevels.INFO, 'ScreenSnippet: starting screen capture');
|
||||
|
||||
let tmpFilename = (isWindowsOS) ? 'symphonyImage-' + Date.now() + '.png' : 'symphonyImage-' + Date.now() + '.jpg';
|
||||
let tmpDir = os.tmpdir();
|
||||
|
||||
let outputFileName = path.join(tmpDir, tmpFilename);
|
||||
|
||||
if (isMac) {
|
||||
// utilize Mac OSX built-in screencapture tool which has been
|
||||
// available since OSX ver 10.2.
|
||||
captureUtil = '/usr/sbin/screencapture';
|
||||
captureUtilArgs = ['-i', '-s', '-t', 'jpg', outputFileName];
|
||||
} else {
|
||||
// use custom built windows screen capture tool
|
||||
if (isDevEnv) {
|
||||
// for dev env pick up tool from node nodules
|
||||
captureUtil =
|
||||
path.join(__dirname,
|
||||
'../../node_modules/screen-snippet/bin/Release/ScreenSnippet.exe');
|
||||
} else {
|
||||
// for production gets installed next to exec.
|
||||
let execPath = path.dirname(app.getPath('exe'));
|
||||
captureUtil = path.join(execPath, 'ScreenSnippet.exe');
|
||||
}
|
||||
|
||||
// Method to verify and disable always on top property
|
||||
// as an issue with the ScreenSnippet.exe not being on top
|
||||
// of the electron wrapper
|
||||
const windows = electron.BrowserWindow.getAllWindows();
|
||||
if (windows && windows.length > 0) {
|
||||
isAlwaysOnTop = windows[ 0 ].isAlwaysOnTop();
|
||||
if (isAlwaysOnTop) {
|
||||
eventEmitter.emit('isAlwaysOnTop', {
|
||||
isAlwaysOnTop: false,
|
||||
shouldActivateMainWindow: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
captureUtilArgs = [outputFileName, getLanguage()];
|
||||
}
|
||||
|
||||
log.send(logLevels.INFO, 'ScreenSnippet: starting screen capture util: ' + captureUtil + ' with args=' + captureUtilArgs);
|
||||
|
||||
// only allow one screen capture at a time.
|
||||
if (child) {
|
||||
child.kill();
|
||||
}
|
||||
|
||||
child = childProcess.execFile(captureUtil, captureUtilArgs, (error) => {
|
||||
// Method to reset always on top feature
|
||||
if (isAlwaysOnTop) {
|
||||
eventEmitter.emit('isAlwaysOnTop', {
|
||||
isAlwaysOnTop: true,
|
||||
shouldActivateMainWindow: false
|
||||
});
|
||||
}
|
||||
// will be called when child process exits.
|
||||
if (error && error.killed) {
|
||||
// processs was killed, just resolve with no data.
|
||||
resolve();
|
||||
} else {
|
||||
readResult.call(this, outputFileName, resolve, reject, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this function was moved outside of class since class is exposed to web
|
||||
* client via preload API, we do NOT want web client to be able to call this
|
||||
* method - then they could read any file on the disk!
|
||||
* @param outputFileName
|
||||
* @param resolve
|
||||
* @param reject
|
||||
* @param childProcessErr
|
||||
*/
|
||||
function readResult(outputFileName, resolve, reject, childProcessErr) {
|
||||
fs.readFile(outputFileName, (readErr, data) => {
|
||||
if (readErr) {
|
||||
let returnErr;
|
||||
if (readErr.code === 'ENOENT') {
|
||||
// no such file exists, user likely aborted
|
||||
// creating snippet. also include any error when
|
||||
// creating child process.
|
||||
returnErr = createWarn('file does not exist ' +
|
||||
childProcessErr);
|
||||
} else {
|
||||
returnErr = createError(readErr + ',' +
|
||||
childProcessErr);
|
||||
}
|
||||
|
||||
reject(returnErr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
reject(createWarn('no file data provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// convert binary data to base64 encoded string
|
||||
Jimp.read(outputFileName, (error, image) => {
|
||||
if (error) throw error;
|
||||
image.quality(100)
|
||||
.getBufferAsync(Jimp.MIME_JPEG).then(src => {
|
||||
const value = src.toString('base64');
|
||||
const resultOutput = {
|
||||
type: 'image/jpg;base64',
|
||||
data: value,
|
||||
};
|
||||
resolve(resultOutput);
|
||||
}).catch((e) => {
|
||||
log.send(logLevels.ERROR, e);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(createError(error));
|
||||
} finally {
|
||||
// remove tmp file (async)
|
||||
fs.unlink(outputFileName, function(removeErr) {
|
||||
// note: node complains if calling async
|
||||
// func without callback.
|
||||
if (removeErr) {
|
||||
log.send(logLevels.ERROR, 'ScreenSnippet: error removing temp snippet file: ' +
|
||||
outputFileName + ', err:' + removeErr);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/**
|
||||
* Create an error object with the ERROR level
|
||||
* @param message
|
||||
* @returns {{message: string, type: string}}
|
||||
*/
|
||||
function createError(message) {
|
||||
return {
|
||||
message,
|
||||
type: 'ERROR',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error object with the WARN level
|
||||
* @param message
|
||||
* @returns {{message: string, type: string}}
|
||||
*/
|
||||
function createWarn(message) {
|
||||
return {
|
||||
message,
|
||||
type: 'WARN',
|
||||
};
|
||||
}
|
||||
/* eslint-enable class-methods-use-this */
|
||||
|
||||
// terminates the screen snippet process wherever the
|
||||
// main window is reloaded/navigated
|
||||
eventEmitter.on('killScreenSnippet', function () {
|
||||
if (child) {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ScreenSnippet: ScreenSnippet,
|
||||
// note: readResult only exposed for testing purposes
|
||||
readResult: readResult
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
const snackBarContent = (
|
||||
`<div id="snackbar">
|
||||
<span data-i18n-text="Press "></span>
|
||||
<span id="snackbar-esc" data-i18n-text="esc"></span>
|
||||
<span data-i18n-text=" to exit full screen"></span>
|
||||
</div>`
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
snackBarContent
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
const snackBarContent = require('./content').snackBarContent;
|
||||
|
||||
class SnackBar {
|
||||
|
||||
constructor() {
|
||||
this.body = document.getElementsByTagName('body');
|
||||
this.domParser = new DOMParser();
|
||||
|
||||
const snackBar = this.domParser.parseFromString(snackBarContent, 'text/html');
|
||||
this.snackBar = snackBar.getElementById('snackbar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that displays a snack bar for 3 sec
|
||||
*/
|
||||
showSnackBar(content) {
|
||||
if (content && typeof content === 'object') {
|
||||
const i18nNodes = this.snackBar.querySelectorAll('[data-i18n-text]');
|
||||
|
||||
for (let node of i18nNodes) {
|
||||
if (node.attributes['data-i18n-text'] && node.attributes['data-i18n-text'].value) {
|
||||
node.innerText = content[node.attributes['data-i18n-text'].value] || node.attributes['data-i18n-text'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.body && this.body.length > 0 && this.snackBar) {
|
||||
this.body[0].appendChild(this.snackBar);
|
||||
this.snackBar.className = "show";
|
||||
this.snackBarTimmer = setTimeout(() => {
|
||||
this.body[0].removeChild(this.snackBar);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that removes snack bar from the DOM
|
||||
*/
|
||||
removeSnackBar() {
|
||||
if (this.body && this.body.length > 0 && this.snackBar) {
|
||||
if (document.getElementById('snackbar')) {
|
||||
this.body[0].removeChild(this.snackBar);
|
||||
if (this.snackBarTimmer) {
|
||||
clearTimeout(this.snackBarTimmer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SnackBar,
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
#snackbar {
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
margin-left: -135px;
|
||||
background-color: rgba(51,51,51, 1);
|
||||
color: rgba(255,255,255, 1);
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
left: 50%;
|
||||
top: 30px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#snackbar-esc {
|
||||
padding: 5px;
|
||||
border: 0.5px solid rgba(51,51,51, 1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#snackbar.show {
|
||||
visibility: visible;
|
||||
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
}
|
||||
|
||||
#snackbar-esc {
|
||||
border: 2px solid white;
|
||||
background-color: rgba(51,51,51, 1);
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadein {
|
||||
from {
|
||||
top: 0; opacity: 0;
|
||||
}
|
||||
to {
|
||||
top: 30px; opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeout {
|
||||
from {
|
||||
top: 30px; opacity: 1;
|
||||
}
|
||||
to {
|
||||
top: 0; opacity: 0;
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
const { app, MenuItem } = require('electron');
|
||||
const path = require('path');
|
||||
const { isMac, isDevEnv } = require('./../utils/misc');
|
||||
const { SpellCheckHandler, DictionarySync } = require('electron-spellchecker');
|
||||
const stringFormat = require('./../utils/stringFormat');
|
||||
|
||||
class SpellCheckHelper {
|
||||
|
||||
/**
|
||||
* A constructor to create an instance of the spell checker
|
||||
*/
|
||||
constructor() {
|
||||
const dictionariesDirName = 'dictionaries';
|
||||
let dictionaryPath;
|
||||
if (isDevEnv) {
|
||||
dictionaryPath = path.join(app.getAppPath(), dictionariesDirName);
|
||||
} else {
|
||||
let execPath = path.dirname(app.getPath('exe'));
|
||||
dictionaryPath = path.join(execPath, isMac ? '..' : '', dictionariesDirName);
|
||||
}
|
||||
this.dictionarySync = new DictionarySync(dictionaryPath);
|
||||
this.spellCheckHandler = new SpellCheckHandler(this.dictionarySync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to initialize spell checker
|
||||
*/
|
||||
initializeSpellChecker() {
|
||||
this.spellCheckHandler.automaticallyIdentifyLanguages = false;
|
||||
|
||||
// This is only for window as in mac the
|
||||
// language is switched w.r.t to the current system language.
|
||||
//
|
||||
// In windows we need to implement RxJS observable
|
||||
// in order to switch language dynamically
|
||||
if (!isMac) {
|
||||
const sysLocale = app.getLocale() || 'en-US';
|
||||
this.spellCheckHandler.switchLanguage(sysLocale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the locale for context menu labels
|
||||
* @param content {Object} - locale content for context menu
|
||||
*/
|
||||
updateContextMenuLocale(content) {
|
||||
this.localeContent = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the string table for context menu
|
||||
*
|
||||
* @param content {Object} - locale content for context menu
|
||||
* @return {Object} - String table for context menu
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getStringTable(content) {
|
||||
return {
|
||||
copyMail: () => content['Copy Email Address'] || `Copy Email Address`,
|
||||
copyLinkUrl: () => content['Copy Link'] || 'Copy Link',
|
||||
openLinkUrl: () => content['Open Link'] || 'Open Link',
|
||||
copyImageUrl: () => content['Copy Image URL'] || 'Copy Image URL',
|
||||
copyImage: () => content['Copy Image'] || 'Copy Image',
|
||||
addToDictionary: () => content['Add to Dictionary'] || 'Add to Dictionary',
|
||||
lookUpDefinition: (lookup) => {
|
||||
const formattedString = stringFormat(content['Look Up {searchText}'], { searchText: lookup.word });
|
||||
return formattedString || `Look Up ${lookup.word}`;
|
||||
},
|
||||
searchGoogle: () => content['Search with Google'] || 'Search with Google',
|
||||
cut: () => content.Cut || 'Cut',
|
||||
copy: () => content.Copy || 'Copy',
|
||||
paste: () => content.Paste || 'Paste',
|
||||
inspectElement: () => content['Inspect Element'] || 'Inspect Element',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add default menu items to the
|
||||
* menu that was generated by ContextMenuBuilder
|
||||
*
|
||||
* This method will be invoked by electron-spellchecker
|
||||
* before showing the context menu
|
||||
*
|
||||
* @param menu
|
||||
* @returns menu
|
||||
*/
|
||||
processMenu(menu) {
|
||||
|
||||
let isLink = false;
|
||||
menu.items.map((item) => {
|
||||
if (item.label === 'Copy Link'){
|
||||
isLink = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
if (!isLink){
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
menu.append(new MenuItem({
|
||||
role: 'reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
label: this.localeContent && this.localeContent.Reload || 'Reload',
|
||||
}));
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that checks if a text is misspelled
|
||||
*
|
||||
* @param text {string}
|
||||
* @returns {*}
|
||||
*/
|
||||
isMisspelled(text) {
|
||||
return this.spellCheckHandler.isMisspelled(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SpellCheckHelper: SpellCheckHelper
|
||||
};
|
94
js/stats.js
94
js/stats.js
@ -1,94 +0,0 @@
|
||||
const os = require('os');
|
||||
const { app } = require('electron');
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
const log = require('./log.js');
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
const MB_IN_BYTES = 1048576;
|
||||
|
||||
const logSystemStats = () => {
|
||||
log.send(logLevels.INFO, `-----------------Gathering system information-----------------`);
|
||||
log.send(logLevels.INFO, `Network Info -> ${JSON.stringify(os.networkInterfaces())}`);
|
||||
log.send(logLevels.INFO, `CPU Info -> ${JSON.stringify(os.cpus())}`);
|
||||
log.send(logLevels.INFO, `Operating System -> ${JSON.stringify(os.type())}`);
|
||||
log.send(logLevels.INFO, `Platform -> ${JSON.stringify(os.platform())}`);
|
||||
log.send(logLevels.INFO, `Architecture -> ${JSON.stringify(os.arch())}`);
|
||||
log.send(logLevels.INFO, `Hostname -> ${JSON.stringify(os.hostname())}`);
|
||||
log.send(logLevels.INFO, `Temp Directory -> ${JSON.stringify(os.tmpdir())}`);
|
||||
log.send(logLevels.INFO, `Home Directory -> ${JSON.stringify(os.homedir())}`);
|
||||
log.send(logLevels.INFO, `Total Memory (MB) -> ${JSON.stringify(os.totalmem() / MB_IN_BYTES)}`);
|
||||
log.send(logLevels.INFO, `Free Memory (MB) -> ${JSON.stringify(os.freemem() / MB_IN_BYTES)}`);
|
||||
log.send(logLevels.INFO, `Load Average -> ${JSON.stringify(os.loadavg())}`);
|
||||
log.send(logLevels.INFO, `Uptime -> ${JSON.stringify(os.uptime())}`);
|
||||
log.send(logLevels.INFO, `User Info (OS Returned) -> ${JSON.stringify(os.userInfo())}`);
|
||||
};
|
||||
|
||||
const logGPUStats = () => {
|
||||
log.send(logLevels.INFO, `-----------------Gathering GPU information-----------------`);
|
||||
log.send(logLevels.INFO, `GPU Feature Status -> ${JSON.stringify(app.getGPUFeatureStatus())}`);
|
||||
};
|
||||
|
||||
const logPodStats = () => {
|
||||
const fields = ['url', 'minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'bringToFront', 'whitelistUrl', 'isCustomTitleBar', 'memoryRefresh', 'devToolsEnabled', 'ctWhitelist', 'notificationSettings', 'crashReporter', 'customFlags', 'permissions', 'autoLaunchPath'];
|
||||
config.getMultipleConfigField(fields)
|
||||
.then((data) => {
|
||||
log.send(logLevels.INFO, `-----------------Gathering POD & App information-----------------`);
|
||||
log.send(logLevels.INFO, `Is app packaged? ${app.isPackaged}`);
|
||||
for (let field in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, field)) {
|
||||
log.send(logLevels.INFO, `${field} -> ${JSON.stringify(data[field])}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const logAppMetrics = () => {
|
||||
log.send(logLevels.INFO, `-----------------Gathering App Metrics-----------------`);
|
||||
const metrics = app.getAppMetrics();
|
||||
metrics.forEach((metric) => {
|
||||
log.send(logLevels.INFO, `PID -> ${metric.pid}, Type -> ${metric.type}, CPU Usage -> ${JSON.stringify(metric.cpu)}`);
|
||||
});
|
||||
};
|
||||
|
||||
const logAppEvents = () => {
|
||||
|
||||
const events = [
|
||||
'will-finish-launching', 'ready', 'window-all-closed', 'before-quit', 'will-quit', 'quit',
|
||||
'open-file', 'open-url', 'activate',
|
||||
'browser-window-created', 'web-contents-created', 'certificate-error', 'login', 'gpu-process-crashed',
|
||||
'accessibility-support-changed', 'session-created', 'second-instance'
|
||||
];
|
||||
|
||||
events.forEach((appEvent) => {
|
||||
app.on(appEvent, () => {
|
||||
log.send(logLevels.INFO, `App Event Occurred: ${appEvent}`)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logProcessInfo = () => {
|
||||
log.send(logLevels.INFO, `Is default app? ${process.defaultApp}`);
|
||||
log.send(logLevels.INFO, `Is Mac Store app? ${process.mas}`);
|
||||
log.send(logLevels.INFO, `Is Windows Store app? ${process.windowsStore}`);
|
||||
log.send(logLevels.INFO, `Resources Path? ${process.resourcesPath}`);
|
||||
log.send(logLevels.INFO, `Sandboxed? ${process.sandboxed}`);
|
||||
log.send(logLevels.INFO, `Chrome Version? ${process.versions.chrome}`);
|
||||
log.send(logLevels.INFO, `Electron Version? ${process.versions.electron}`);
|
||||
log.send(logLevels.INFO, `Creation Time? ${process.getCreationTime()}`);
|
||||
}
|
||||
|
||||
logSystemStats();
|
||||
logGPUStats();
|
||||
logPodStats();
|
||||
logAppMetrics();
|
||||
logAppEvents();
|
||||
logProcessInfo();
|
||||
|
||||
module.exports = {
|
||||
getSystemStats: logSystemStats,
|
||||
getGPUStats: logGPUStats,
|
||||
getPodStats: logPodStats,
|
||||
getAppMetrics: logAppMetrics
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const path = require("path");
|
||||
const fs = require('fs');
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
let language;
|
||||
let loadedTranslations = {};
|
||||
|
||||
const getMessageFor = function(phrase) {
|
||||
let translation = loadedTranslations[phrase];
|
||||
if(translation === undefined) {
|
||||
translation = phrase;
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
const setLanguage = function(lng) {
|
||||
language = lng ? lng : 'en-US';
|
||||
let file = path.join(__dirname, '..', '..', 'locale', language + '.json');
|
||||
if (!fs.existsSync(file)) {
|
||||
file = path.join(__dirname, '..', '..', 'locale', 'en-US.json');
|
||||
}
|
||||
let data = fs.readFileSync(file, 'utf8');
|
||||
try {
|
||||
loadedTranslations = JSON.parse(data);
|
||||
} catch (e) {
|
||||
loadedTranslations = {}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current locale
|
||||
* @return {*|string}
|
||||
*/
|
||||
const getLanguage = function() {
|
||||
let sysLocale;
|
||||
try {
|
||||
sysLocale = app.getLocale();
|
||||
} catch (err) {
|
||||
log.send(logLevels.WARN, `i18n: Failed to fetch app.getLocale with an ${err}`);
|
||||
}
|
||||
return language || sysLocale || 'en-US';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setLanguage: setLanguage,
|
||||
getMessageFor: getMessageFor,
|
||||
getLanguage: getLanguage,
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
/**
|
||||
* Archives files in the source directory
|
||||
* that matches the given file extension
|
||||
*
|
||||
* @param source {String} source path
|
||||
* @param destination {String} destination path
|
||||
* @param fileExtensions {Array} array of file ext
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
function generateArchiveForDirectory(source, destination, fileExtensions) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let output = fs.createWriteStream(destination);
|
||||
let archive = archiver('zip', {zlib: {level: 9}});
|
||||
|
||||
output.on('close', function () {
|
||||
log.send(logLevels.INFO, `Successfully archived the files`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
archive.on('error', function(err){
|
||||
log.send(logLevels.INFO, `Error archiving ${JSON.stringify(err)}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
let files = fs.readdirSync(source);
|
||||
files
|
||||
.filter((file) => fileExtensions.indexOf(path.extname(file)) !== -1)
|
||||
.forEach((file) => {
|
||||
switch (path.extname(file)) {
|
||||
case '.log':
|
||||
archive.file(source + '/' + file, { name: 'logs/' + file });
|
||||
break;
|
||||
case '.dmp':
|
||||
case '.txt': // on Windows .txt files will be created as part of crash dump
|
||||
archive.file(source + '/' + file, { name: 'crashes/' + file });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateArchiveForDirectory: generateArchiveForDirectory
|
||||
};
|
@ -1,86 +0,0 @@
|
||||
// regex match the semver (semantic version) this checks for the pattern X.Y.Z
|
||||
// ex-valid v1.2.0, 1.2.0, 2.3.4-r51
|
||||
const semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)?)?$/i;
|
||||
const patch = /-([0-9A-Za-z-.]+)/;
|
||||
|
||||
/**
|
||||
* This function splits the versions
|
||||
* into major, minor and patch
|
||||
* @param v
|
||||
* @returns {T[]}
|
||||
*/
|
||||
function split(v) {
|
||||
const temp = v.replace(/^v/, '').split('.');
|
||||
const arr = temp.splice(0, 2);
|
||||
arr.push(temp.join('.'));
|
||||
return arr;
|
||||
}
|
||||
|
||||
function tryParse(v) {
|
||||
return Number.isNaN(Number(v)) ? v : Number(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* This validates the version
|
||||
* with the semver regex and returns
|
||||
* -1 if not valid else 1
|
||||
* @param version
|
||||
* @returns {number}
|
||||
*/
|
||||
function validate(version) {
|
||||
if (typeof version !== 'string') {
|
||||
return -1;
|
||||
}
|
||||
if (!semver.test(version)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function compares the v1 version
|
||||
* with the v2 version for all major, minor, patch
|
||||
* if v1 > v2 returns 1
|
||||
* if v1 < v2 returns -1
|
||||
* if v1 = v2 returns 0
|
||||
* @param v1
|
||||
* @param v2
|
||||
* @returns {number}
|
||||
*/
|
||||
function check(v1, v2) {
|
||||
if (validate(v1) === -1 || validate(v2) === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const s1 = split(v1);
|
||||
const s2 = split(v2);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const n1 = parseInt(s1[i] || '0', 10);
|
||||
const n2 = parseInt(s2[i] || '0', 10);
|
||||
|
||||
if (n1 > n2) return 1;
|
||||
if (n2 > n1) return -1;
|
||||
}
|
||||
|
||||
if ([ s1[2], s2[2] ].every(patch.test.bind(patch))) {
|
||||
const p1 = patch.exec(s1[2])[1].split('.').map(tryParse);
|
||||
const p2 = patch.exec(s2[2])[1].split('.').map(tryParse);
|
||||
|
||||
for (let k = 0; k < Math.max(p1.length, p2.length); k++) {
|
||||
if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') return -1;
|
||||
if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') return 1;
|
||||
|
||||
if (p1[k] > p2[k]) return 1;
|
||||
if (p2[k] > p1[k]) return -1;
|
||||
}
|
||||
} else if ([ s1[2], s2[2] ].some(patch.test.bind(patch))) {
|
||||
return patch.test(s1[2]) ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
check
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
/**
|
||||
* Search given argv for argName using exact match or starts with. Comparison is case insensitive
|
||||
* @param {Array} argv Array of strings
|
||||
* @param {String} argName Arg name to search for.
|
||||
* @param {Boolean} exactMatch If true then look for exact match otherwise
|
||||
* try finding arg that starts with argName.
|
||||
* @return {Array} If found, returns the arg, otherwise null.
|
||||
*/
|
||||
function getCmdLineArg(argv, argName, exactMatch) {
|
||||
if (!Array.isArray(argv)) {
|
||||
log.send(logLevels.WARN, 'getCmdLineArg: TypeError invalid func arg, must be an array: '+ argv);
|
||||
return null;
|
||||
}
|
||||
let argNameToFind = argName.toLocaleLowerCase();
|
||||
for (let i = 0, len = argv.length; i < len; i++) {
|
||||
let arg = argv[i].toLocaleLowerCase();
|
||||
if ((exactMatch && arg === argNameToFind) ||
|
||||
(!exactMatch && arg.startsWith(argNameToFind))) {
|
||||
return argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = getCmdLineArg;
|
@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Generates a guid,
|
||||
* http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
|
||||
*
|
||||
* @return {String} guid value in string
|
||||
*/
|
||||
function getGuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
|
||||
function (c) {
|
||||
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = getGuid;
|
@ -1,64 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const symphonyRegistry = '\\Software\\Symphony\\Symphony\\';
|
||||
const { isMac } = require('./misc.js');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
let Registry = require('winreg');
|
||||
let symphonyRegistryHKCU = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: symphonyRegistry
|
||||
});
|
||||
|
||||
let symphonyRegistryHKLM = new Registry({
|
||||
key: symphonyRegistry
|
||||
});
|
||||
|
||||
let symphonyRegistryHKLM6432 = new Registry({
|
||||
key: symphonyRegistry.replace('\\Software','\\Software\\WOW6432Node')
|
||||
});
|
||||
|
||||
/**
|
||||
* Reads Windows Registry key. This Registry holds the Symphony registry keys
|
||||
* that are intended to be used as global (or default) value for all users
|
||||
* running this app.
|
||||
*/
|
||||
let getRegistry = function (name) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (isMac) {
|
||||
reject(new Error('registry is not supported for mac osx.'));
|
||||
return;
|
||||
}
|
||||
|
||||
//Try to get registry on HKEY_CURRENT_USER
|
||||
symphonyRegistryHKCU.get(name, function (err1, reg1) {
|
||||
if (!err1 && reg1 !== null && reg1.value) {
|
||||
log.send(logLevels.WARN, 'getRegistry: Cannot find ' + name + ' Registry. Using HKCU');
|
||||
resolve(reg1.value);
|
||||
return;
|
||||
}
|
||||
|
||||
//Try to get registry on HKEY_LOCAL_MACHINE
|
||||
symphonyRegistryHKLM.get(name, function (err2, reg2) {
|
||||
if (!err2 && reg2 !== null && reg2.value) {
|
||||
log.send(logLevels.WARN, 'getRegistry: Cannot find ' + name + ' Registry. Using HKLM');
|
||||
resolve(reg2.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get registry on HKEY_LOCAL_MACHINE in case 32bit app installed on 64bit system.
|
||||
// winreg does not merge keys as normally windows does.
|
||||
symphonyRegistryHKLM6432.get(name, function (err3, reg3) {
|
||||
if (!err3 && reg3 !== null && reg3.value) {
|
||||
resolve(reg3.value);
|
||||
} else {
|
||||
reject(new Error('Cannot find PodUrl Registry. Using default url.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = getRegistry;
|
@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if given rectangle is contained within the workArea of at
|
||||
* least one of the screens.
|
||||
* @param {Object} rect - ex:- {x: Number, y: Number, width: Number, height: Number}
|
||||
* @return {Boolean} true if condition in desc is met.
|
||||
*/
|
||||
function isInDisplayBounds(rect) {
|
||||
if (!rect) {
|
||||
return false;
|
||||
}
|
||||
let displays = electron.screen.getAllDisplays();
|
||||
|
||||
for(let i = 0, len = displays.length; i < len; i++) {
|
||||
let workArea = displays[i].workArea;
|
||||
if (rect.x >= workArea.x && rect.y >= workArea.y &&
|
||||
((rect.x + rect.width) <= (workArea.x + workArea.width)) &&
|
||||
((rect.y + rect.height) <= (workArea.y + workArea.height))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = isInDisplayBounds;
|
@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const isDevEnv = process.env.ELECTRON_DEV ?
|
||||
process.env.ELECTRON_DEV.trim().toLowerCase() === 'true' : false;
|
||||
|
||||
const isMac = (process.platform === 'darwin');
|
||||
const isWindowsOS = (process.platform === 'win32');
|
||||
|
||||
const isNodeEnv = !!process.env.NODE_ENV;
|
||||
|
||||
module.exports = {
|
||||
isDevEnv: isDevEnv,
|
||||
isMac: isMac,
|
||||
isWindowsOS: isWindowsOS,
|
||||
isNodeEnv: isNodeEnv
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Injects content into string
|
||||
* @param string {String}
|
||||
* @param data {Object} - content to replace
|
||||
* @return {*}
|
||||
*/
|
||||
function stringFormat(string, data) {
|
||||
for (let key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
return string.replace(/({([^}]+)})/g, function (i) {
|
||||
let replacedKey = i.replace(/{/, '').replace(/}/, '');
|
||||
if (!data[replacedKey]) {
|
||||
return i;
|
||||
}
|
||||
return data[replacedKey];
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = stringFormat;
|
@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* throttles calls to given function at most once a second.
|
||||
* @param {number} throttleTime minimum time between calls
|
||||
* @param {function} func function to invoke
|
||||
*/
|
||||
function throttle(throttleTime, func) {
|
||||
if (typeof throttleTime !== 'number' || throttleTime <= 0) {
|
||||
throw Error('throttle: invalid throttleTime arg, must be a number: ' + throttleTime);
|
||||
}
|
||||
if (typeof func !== 'function') {
|
||||
throw Error('throttle: invalid func arg, must be a function: ' + func);
|
||||
}
|
||||
let timer, lastInvoke = 0;
|
||||
return function() {
|
||||
let args = arguments;
|
||||
|
||||
function invoke(argsToInvoke) {
|
||||
timer = null;
|
||||
lastInvoke = Date.now();
|
||||
func.apply(null, argsToInvoke);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
let now = Date.now();
|
||||
if (now - lastInvoke < throttleTime) {
|
||||
cancel();
|
||||
timer = setTimeout(function() {
|
||||
invoke(args);
|
||||
}, lastInvoke + throttleTime - now);
|
||||
} else {
|
||||
cancel();
|
||||
invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = throttle;
|
@ -1,166 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { getGlobalConfigField } = require('./../config.js');
|
||||
const isEqual = require('lodash.isequal');
|
||||
const log = require('../log.js');
|
||||
const logLevels = require('../enums/logLevels.js');
|
||||
|
||||
let customTlds = [];
|
||||
|
||||
getGlobalConfigField('customTlds')
|
||||
.then((domains) => {
|
||||
|
||||
if (domains && Array.isArray(domains) && domains.length > 0) {
|
||||
log.send(logLevels.INFO, `setting custom tlds that are -> ${domains}`);
|
||||
customTlds = domains;
|
||||
}
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
log.send(logLevels.INFO, `error setting custom tlds -> ${err}`);
|
||||
});
|
||||
|
||||
const urlParts = /^(https?:\/\/)?([^/]*@)?(.+?)(:\d{2,5})?([/?].*)?$/;
|
||||
const dot = /\./g;
|
||||
|
||||
/**
|
||||
* Loops through the list of whitelist urls
|
||||
* @param url {String} - url the electron is navigated to
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function isWhitelisted(url) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getGlobalConfigField('whitelistUrl').then((whitelist) => {
|
||||
|
||||
if (checkWhitelist(url, whitelist)) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
return reject(new Error('URL does not match with the whitelist'));
|
||||
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that compares url against a list of whitelist
|
||||
* returns true if hostName or domain present in the whitelist
|
||||
* @param url {String} - url the electron is navigated to
|
||||
* @param whitelist {String} - coma separated whitelists
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkWhitelist(url, whitelist) {
|
||||
let whitelistArray = whitelist.split(',');
|
||||
const parsedUrl = parseDomain(url, {customTlds});
|
||||
|
||||
if (!parsedUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!whitelist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!whitelistArray.length || whitelistArray.indexOf('*') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return whitelistArray.some((whitelistHost) => {
|
||||
let parsedWhitelist = parseDomain(whitelistHost);
|
||||
|
||||
if (!parsedWhitelist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return matchDomains(parsedUrl, parsedWhitelist);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the respective hostName
|
||||
* @param parsedUrl {Object} - parsed url
|
||||
* @param parsedWhitelist {Object} - parsed whitelist
|
||||
*
|
||||
* example:
|
||||
* matchDomain({ subdomain: www, domain: example, tld: com }, { subdomain: app, domain: example, tld: com })
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
function matchDomains(parsedUrl, parsedWhitelist) {
|
||||
|
||||
if (isEqual(parsedUrl, parsedWhitelist)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hostNameFromUrl = parsedUrl.domain + parsedUrl.tld;
|
||||
const hostNameFromWhitelist = parsedWhitelist.domain + parsedWhitelist.tld;
|
||||
|
||||
if (!parsedWhitelist.subdomain) {
|
||||
return hostNameFromUrl === hostNameFromWhitelist
|
||||
}
|
||||
|
||||
return hostNameFromUrl === hostNameFromWhitelist && matchSubDomains(parsedUrl.subdomain, parsedWhitelist.subdomain);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the last occurrence in the sub-domain
|
||||
* @param subDomainUrl {String} - sub-domain from url
|
||||
* @param subDomainWhitelist {String} - sub-domain from whitelist
|
||||
*
|
||||
* example: matchSubDomains('www', 'app')
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matchSubDomains(subDomainUrl, subDomainWhitelist) {
|
||||
|
||||
if (subDomainUrl === subDomainWhitelist) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subDomainUrlArray = subDomainUrl.split('.');
|
||||
const lastCharSubDomainUrl = subDomainUrlArray[subDomainUrlArray.length - 1];
|
||||
|
||||
const subDomainWhitelistArray = subDomainWhitelist.split('.');
|
||||
const lastCharWhitelist = subDomainWhitelistArray[subDomainWhitelistArray.length - 1];
|
||||
|
||||
return lastCharSubDomainUrl === lastCharWhitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the url into tld, domain, subdomain
|
||||
* @param url
|
||||
* @return {{tld: string | *, domain: string | *, subdomain: string}}
|
||||
*/
|
||||
function parseDomain(url) {
|
||||
let urlSplit = url.match(urlParts);
|
||||
let domain = urlSplit[3];
|
||||
|
||||
// capture top level domain
|
||||
const tld = domain.slice(domain.lastIndexOf('.'));
|
||||
urlSplit = domain.slice(0, -tld.length).split(dot);
|
||||
|
||||
// capture domain
|
||||
domain = urlSplit.pop();
|
||||
|
||||
// capture subdomain
|
||||
const subdomain = urlSplit.join(".");
|
||||
|
||||
return {
|
||||
tld: tld.trim(),
|
||||
domain: domain.trim(),
|
||||
subdomain: subdomain.trim()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isWhitelisted,
|
||||
parseDomain,
|
||||
|
||||
// items below here are only exported for testing, do NOT use!
|
||||
checkWhitelist
|
||||
|
||||
};
|
1374
js/windowMgr.js
1374
js/windowMgr.js
File diff suppressed because it is too large
Load Diff
@ -1,144 +0,0 @@
|
||||
const titleBar = (`
|
||||
<div id="title-bar">
|
||||
<div class="title-bar-button-container">
|
||||
<button title="Menu" id='hamburger-menu-button'>
|
||||
<svg x='0px' y='0px' viewBox='0 0 15 10'>
|
||||
<rect fill="rgba(255, 255, 255, 0.9)" width='15' height='1'/>
|
||||
<rect fill="rgba(255, 255, 255, 0.9)" y='4' width='15' height='1'/>
|
||||
<rect fill="rgba(255, 255, 255, 0.9)" y='8' width='152' height='1'/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="title-container">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="20px" viewBox="0 0 19.7 32" style="enable-background:new 0 0 19.7 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#Shape_8_);}
|
||||
.st1{fill:url(#Shape_9_);}
|
||||
.st2{fill:url(#Shape_10_);}
|
||||
.st3{fill:url(#Shape_11_);}
|
||||
.st4{fill:url(#Shape_12_);}
|
||||
.st5{fill:url(#Shape_13_);}
|
||||
.st6{fill:url(#Shape_14_);}
|
||||
.st7{fill:url(#Shape_15_);}
|
||||
</style>
|
||||
<title>Symphony_logo</title>
|
||||
<g id="Page-1">
|
||||
<g id="Symphony_logo">
|
||||
<g id="logo2">
|
||||
<linearGradient id="Shape_8_" gradientUnits="userSpaceOnUse" x1="39.8192" y1="23.9806" x2="39.2461" y2="23.7258" gradientTransform="matrix(7.6157 0 0 -3.458 -295.3247 101.0398)">
|
||||
<stop offset="0" style="stop-color:#197A68"/>
|
||||
<stop offset="1" style="stop-color:#329D87"/>
|
||||
</linearGradient>
|
||||
<path id="Shape" class="st0" d="M2.4,17.4c0,1.2,0.3,2.4,0.8,3.5l6.8-3.5H2.4z"/>
|
||||
|
||||
<linearGradient id="Shape_9_" gradientUnits="userSpaceOnUse" x1="28.9162" y1="22.8111" x2="29.9162" y2="22.8111" gradientTransform="matrix(2.7978 0 0 -4.7596 -73.7035 128.374)">
|
||||
<stop offset="0" style="stop-color:#1D7E7B"/>
|
||||
<stop offset="1" style="stop-color:#35B0B7"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_1_" class="st1" d="M7.2,21.3C8,21.9,9,22.2,10,22.2v-4.8L7.2,21.3z"/>
|
||||
|
||||
<linearGradient id="Shape_10_" gradientUnits="userSpaceOnUse" x1="37.9575" y1="21.1358" x2="38.1785" y2="21.8684" gradientTransform="matrix(6.1591 0 0 -11.4226 -223.952 256.8769)">
|
||||
<stop offset="0" style="stop-color:#175952"/>
|
||||
<stop offset="1" style="stop-color:#3A8F88"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_2_" class="st2" d="M14.4,6.9C13,6.3,11.5,6,10,6C9.4,6,8.8,6,8.2,6.1L10,17.4L14.4,6.9z"/>
|
||||
|
||||
<linearGradient id="Shape_11_" gradientUnits="userSpaceOnUse" x1="40.5688" y1="22.0975" x2="41.0294" y2="22.3767" gradientTransform="matrix(9.5186 0 0 -5.5951 -373.339 140.3243)">
|
||||
<stop offset="0" style="stop-color:#39A8BA"/>
|
||||
<stop offset="1" style="stop-color:#3992B4"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_3_" class="st3" d="M10,17.4h9.5c0-2-0.6-4-1.8-5.6L10,17.4z"/>
|
||||
|
||||
<linearGradient id="Shape_12_" gradientUnits="userSpaceOnUse" x1="41.2142" y1="22.3254" x2="40.7055" y2="22.5477" gradientTransform="matrix(9.9955 0 0 -5.2227 -404.7955 132.8762)">
|
||||
<stop offset="0" style="stop-color:#021C3C"/>
|
||||
<stop offset="1" style="stop-color:#215180"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_4_" class="st4" d="M1.5,12.2c-1,1.6-1.5,3.4-1.5,5.2h10L1.5,12.2z"/>
|
||||
|
||||
<linearGradient id="Shape_13_" gradientUnits="userSpaceOnUse" x1="33.5112" y1="22.1509" x2="34.5112" y2="22.1509" gradientTransform="matrix(3.9169 0 0 -6.6631 -125.1783 161.684)">
|
||||
<stop offset="0" style="stop-color:#23796C"/>
|
||||
<stop offset="1" style="stop-color:#41BEAF"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_5_" class="st5" d="M10,10.8c-1.4,0-2.8,0.4-3.9,1.3l3.9,5.4V10.8z"/>
|
||||
|
||||
<linearGradient id="Shape_14_" gradientUnits="userSpaceOnUse" x1="36.1289" y1="21.9578" x2="36.4868" y2="21.4811" gradientTransform="matrix(5.0353 0 0 -8.5671 -171.5901 208.3326)">
|
||||
<stop offset="0" style="stop-color:#14466A"/>
|
||||
<stop offset="1" style="stop-color:#286395"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_6_" class="st6" d="M10,26c1.8,0,3.6-0.6,5-1.6l-5-6.9V26z"/>
|
||||
|
||||
<linearGradient id="Shape_15_" gradientUnits="userSpaceOnUse" x1="38.5336" y1="23.6558" x2="39.0866" y2="23.5777" gradientTransform="matrix(6.663 0 0 -3.5931 -244.8399 102.8351)">
|
||||
<stop offset="0" style="stop-color:#261D49"/>
|
||||
<stop offset="1" style="stop-color:#483A6D"/>
|
||||
</linearGradient>
|
||||
<path id="Shape_7_" class="st7" d="M16.6,16.4l-6.6,1l6.2,2.6c0.3-0.8,0.5-1.7,0.5-2.6C16.7,17.1,16.6,16.7,16.6,16.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<p id="title-bar-title">Symphony</p>
|
||||
</div>
|
||||
<div class="branding-logo" align="right" />
|
||||
</div>
|
||||
`);
|
||||
|
||||
const button = (`
|
||||
<div class="action-items">
|
||||
<div class="title-bar-button-container">
|
||||
<button class="title-bar-button" id="title-bar-minimize-button" title='Minimize'>
|
||||
<svg x='0px' y='0px' viewBox='0 0 14 1'>
|
||||
<rect fill="rgba(255, 255, 255, 0.9)" width='14' height='0.6' />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title-bar-button-container">
|
||||
<button class="title-bar-button" id="title-bar-maximize-button" title='Maximize'>
|
||||
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
|
||||
<path
|
||||
fill='rgba(255, 255, 255, 0.9)'
|
||||
d='M0,0v10.1h10.2V0H0z M9.2,9.2H1.1V1h8.1V9.2z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title-bar-button-container">
|
||||
<button class="title-bar-button" id="title-bar-close-button" title='Close'>
|
||||
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
|
||||
<polygon
|
||||
fill="rgba(255, 255, 255, 0.9)"
|
||||
points='10.2,0.7 9.5,0 5.1,4.4 0.7,0 0,0.7 4.4,5.1 0,9.5 0.7,10.2 5.1,5.8 9.5,10.2 10.2,9.5 5.8,5.1 '
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const unMaximizeButton = (`
|
||||
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
|
||||
<path
|
||||
fill='rgba(255, 255, 255, 0.9)'
|
||||
d='M2.1,0v2H0v8.1h8.2v-2h2V0H2.1z M7.2,9.2H1.1V3h6.1V9.2z M9.2,7.1h-1V2H3.1V1h6.1V7.1z'
|
||||
/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const maximizeButton = (`
|
||||
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
|
||||
<path
|
||||
fill='rgba(255, 255, 255, 0.9)'
|
||||
d='M0,0v10.1h10.2V0H0z M9.2,9.2H1.1V1h8.1V9.2z'
|
||||
/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
|
||||
module.exports = {
|
||||
titleBar: titleBar,
|
||||
button: button,
|
||||
unMaximizeButton: unMaximizeButton,
|
||||
maximizeButton: maximizeButton
|
||||
};
|
@ -1,286 +0,0 @@
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
const htmlContents = require('./contents');
|
||||
const titleBarStyles = require('../enums/titleBarStyles');
|
||||
|
||||
// Default title bar height
|
||||
const titleBarHeight = '32px';
|
||||
|
||||
class TitleBar {
|
||||
|
||||
constructor() {
|
||||
this.window = remote.getCurrentWindow();
|
||||
this.domParser = new DOMParser();
|
||||
|
||||
const titleBarParsed = this.domParser.parseFromString(htmlContents.titleBar, 'text/html');
|
||||
this.titleBar = titleBarParsed.getElementById('title-bar');
|
||||
}
|
||||
|
||||
initiateWindowsTitleBar(titleBarStyle) {
|
||||
|
||||
const actionItemsParsed = this.domParser.parseFromString(htmlContents.button, 'text/html');
|
||||
|
||||
if (titleBarStyle === titleBarStyles.CUSTOM) {
|
||||
const buttons = actionItemsParsed.getElementsByClassName('action-items');
|
||||
|
||||
let items = Array.from(buttons[0].children);
|
||||
for (let i of items) {
|
||||
this.titleBar.appendChild(i);
|
||||
}
|
||||
}
|
||||
|
||||
const updateIcon = TitleBar.updateIcons;
|
||||
|
||||
// Event to capture and update icons
|
||||
this.window.on('maximize', updateIcon.bind(this, true));
|
||||
this.window.on('unmaximize', updateIcon.bind(this, false));
|
||||
this.window.on('enter-full-screen', this.updateTitleBar.bind(this, true));
|
||||
this.window.on('leave-full-screen', this.updateTitleBar.bind(this, false));
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.window.removeListener('maximize', updateIcon);
|
||||
this.window.removeListener('unmaximize', updateIcon);
|
||||
this.window.removeListener('enter-full-screen', this.updateTitleBar);
|
||||
this.window.removeListener('leave-full-screen', this.updateTitleBar);
|
||||
});
|
||||
|
||||
document.body.appendChild(this.titleBar);
|
||||
|
||||
switch (titleBarStyle) {
|
||||
case titleBarStyles.CUSTOM:
|
||||
TitleBar.setTitleBarTitle();
|
||||
TitleBar.addWindowBorders();
|
||||
break;
|
||||
case titleBarStyles.NATIVE:
|
||||
TitleBar.hideTitleContainer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.hamburgerMenuButton = document.getElementById('hamburger-menu-button');
|
||||
this.minimizeButton = document.getElementById('title-bar-minimize-button');
|
||||
this.maximizeButton = document.getElementById('title-bar-maximize-button');
|
||||
this.closeButton = document.getElementById('title-bar-close-button');
|
||||
|
||||
this.initiateEventListeners();
|
||||
|
||||
this.updateTitleBar(this.window.isFullScreen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that attaches Event Listeners for elements
|
||||
*/
|
||||
initiateEventListeners() {
|
||||
attachEventListeners(this.titleBar, 'dblclick', this.maximizeOrUnmaximize.bind(this));
|
||||
attachEventListeners(this.hamburgerMenuButton, 'click', this.popupMenu.bind(this));
|
||||
attachEventListeners(this.closeButton, 'click', this.closeWindow.bind(this));
|
||||
attachEventListeners(this.maximizeButton, 'click', this.maximizeOrUnmaximize.bind(this));
|
||||
attachEventListeners(this.minimizeButton, 'click', this.minimize.bind(this));
|
||||
attachEventListeners(this.minimizeButton, 'contextmenu', this.removeContextMenu.bind(this));
|
||||
attachEventListeners(this.maximizeButton, 'contextmenu', this.removeContextMenu.bind(this));
|
||||
attachEventListeners(this.closeButton, 'contextmenu', this.removeContextMenu.bind(this));
|
||||
|
||||
attachEventListeners(this.hamburgerMenuButton, 'mousedown', TitleBar.handleMouseDown.bind(this));
|
||||
attachEventListeners(this.closeButton, 'mousedown', TitleBar.handleMouseDown.bind(this));
|
||||
attachEventListeners(this.maximizeButton, 'mousedown', TitleBar.handleMouseDown.bind(this));
|
||||
attachEventListeners(this.minimizeButton, 'mousedown', TitleBar.handleMouseDown.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button's title w.r.t current locale
|
||||
* @param content {Object}
|
||||
*/
|
||||
updateLocale(content) {
|
||||
this.hamburgerMenuButton.title = content.Menu || 'Menu';
|
||||
this.minimizeButton.title = content.Minimize || 'Minimize';
|
||||
this.maximizeButton.title = content.Maximize || 'Maximize';
|
||||
this.closeButton.title = content.Close || 'Close';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that remove context menu
|
||||
* ELECTRON-769: Hamburger Menu - Copy/ Reload in menu context displays when right
|
||||
* clicking on minimize, maximize, close button of the hamburger menu
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
removeContextMenu(event) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that adds borders
|
||||
*/
|
||||
static addWindowBorders() {
|
||||
const borderBottom = document.createElement('div');
|
||||
borderBottom.className = 'bottom-window-border';
|
||||
|
||||
document.body.appendChild(borderBottom);
|
||||
document.body.classList.add('window-border');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that sets the title bar title
|
||||
* from document.title
|
||||
*/
|
||||
static setTitleBarTitle() {
|
||||
const titleBarTitle = document.getElementById('title-bar-title');
|
||||
|
||||
if (titleBarTitle) {
|
||||
titleBarTitle.innerText = document.title || 'Symphony';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that hides the title container
|
||||
* if the title bar style is NATIVE
|
||||
*/
|
||||
static hideTitleContainer() {
|
||||
const titleContainer = document.getElementById('title-container');
|
||||
|
||||
if (titleContainer) {
|
||||
titleContainer.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the state of the maximize or
|
||||
* unmaximize icons
|
||||
* @param isMaximized
|
||||
*/
|
||||
static updateIcons(isMaximized) {
|
||||
const button = document.getElementById('title-bar-maximize-button');
|
||||
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMaximized) {
|
||||
button.innerHTML = htmlContents.unMaximizeButton;
|
||||
} else {
|
||||
button.innerHTML = htmlContents.maximizeButton;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the title bar display property
|
||||
* based on the full screen event
|
||||
* @param isFullScreen {Boolean}
|
||||
*/
|
||||
updateTitleBar(isFullScreen) {
|
||||
if (isFullScreen) {
|
||||
this.titleBar.style.display = 'none';
|
||||
updateContentHeight('0px');
|
||||
} else {
|
||||
this.titleBar.style.display = 'flex';
|
||||
updateContentHeight();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that popup the application menu
|
||||
*/
|
||||
popupMenu() {
|
||||
if (this.isValidWindow()) {
|
||||
ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.popupMenu
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that minimizes browser window
|
||||
*/
|
||||
minimize() {
|
||||
if (this.isValidWindow()) {
|
||||
this.window.minimize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that maximize or unmaximize browser window
|
||||
*/
|
||||
maximizeOrUnmaximize() {
|
||||
|
||||
if (!this.isValidWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.window.isMaximized()) {
|
||||
this.window.unmaximize();
|
||||
} else {
|
||||
this.window.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that closes the browser window
|
||||
*/
|
||||
closeWindow() {
|
||||
if (this.isValidWindow()) {
|
||||
this.window.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the window exists and is not destroyed
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidWindow() {
|
||||
return !!(this.window && !this.window.isDestroyed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent default to make sure buttons don't take focus
|
||||
* @param e
|
||||
*/
|
||||
static handleMouseDown(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attach event listeners for a given element
|
||||
* @param element
|
||||
* @param eventName
|
||||
* @param func
|
||||
*/
|
||||
function attachEventListeners(element, eventName, func) {
|
||||
|
||||
if (!element || !eventName) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventName.split(" ").forEach((name) => {
|
||||
element.addEventListener(name, func, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that adds margin property to the push
|
||||
* the client content below the title bar
|
||||
* @param height
|
||||
*/
|
||||
function updateContentHeight(height = titleBarHeight) {
|
||||
const contentWrapper = document.getElementById('content-wrapper');
|
||||
const titleBar = document.getElementById('title-bar');
|
||||
|
||||
if (!titleBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentWrapper) {
|
||||
contentWrapper.style.marginTop = titleBar ? height : '0px';
|
||||
document.body.style.removeProperty('margin-top');
|
||||
} else {
|
||||
document.body.style.marginTop = titleBar ? height : '0px'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TitleBar
|
||||
};
|
@ -1,106 +0,0 @@
|
||||
#title-bar {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
background: rgba(74,74,74,1);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding-left: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
box-sizing: content-box;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#hamburger-menu-button {
|
||||
color: rgba(255,255,255,1);
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
background: center;
|
||||
border: none;
|
||||
border-image: initial;
|
||||
display: inline-grid;
|
||||
border-radius: 0;
|
||||
padding: 11px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#hamburger-menu-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#title-container {
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#title-bar-title {
|
||||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title-bar-button-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
right: 0;
|
||||
color: rgba(255,255,255,1);
|
||||
-webkit-app-region: no-drag;
|
||||
text-align: center;
|
||||
width: 45px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
box-sizing: border-box !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title-bar-button {
|
||||
color: rgba(255,255,255,1);
|
||||
text-align: center;
|
||||
width: 45px;
|
||||
height: 32px;
|
||||
background: center;
|
||||
border: none;
|
||||
border-image: initial;
|
||||
display: inline-grid;
|
||||
border-radius: 0;
|
||||
padding: 10px 15px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title-bar-button:hover {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.title-bar-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.title-bar-button-container:hover {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.window-border {
|
||||
border-left: 1px solid rgba(74,74,74,1);
|
||||
border-right: 1px solid rgba(74,74,74,1);
|
||||
}
|
||||
|
||||
.bottom-window-border {
|
||||
position: fixed;
|
||||
border-bottom: 1px solid rgba(74,74,74,1);
|
||||
width: 100%;
|
||||
z-index: 3000;
|
||||
bottom: 0;
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "About Symphony",
|
||||
"Actual Size": "Actual Size",
|
||||
"Always on Top": "Always on Top",
|
||||
"Auto Launch On Startup": "Auto Launch On Startup",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "Authentication Request",
|
||||
"Cancel": "Cancel",
|
||||
"hostname": "hostname",
|
||||
"Invalid user name/password": "Invalid user name/password",
|
||||
"Log In": "Log In",
|
||||
"Password:": "Password:",
|
||||
"Please provide your login credentials for:": "Please provide your login credentials for:",
|
||||
"User name:": "User name:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "More Information",
|
||||
"Version Information": "Version Information"
|
||||
},
|
||||
"Bring All to Front": "Bring All to Front",
|
||||
"Bring to Front on Notifications": "Bring to Front on Notifications",
|
||||
"Certificate Error": "Certificate Error",
|
||||
"Close": "Close",
|
||||
"Cancel": "Cancel",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "Add to Dictionary",
|
||||
"Copy": "Copy",
|
||||
"Copy Email Address": "Copy Email Address",
|
||||
"Copy Image": "Copy Image",
|
||||
"Copy Image URL": "Copy Image URL",
|
||||
"Copy Link": "Copy Link",
|
||||
"Cut": "Cut",
|
||||
"Inspect Element": "Inspect Element",
|
||||
"Look Up {searchText}": "Look Up \"{searchText}\"",
|
||||
"Open Link": "Open Link",
|
||||
"Paste": "Paste",
|
||||
"Reload": "Reload",
|
||||
"Search with Google": "Search with Google"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "Show in Folder",
|
||||
"Reveal in Finder": "Reveal in Finder",
|
||||
"Open": "Open",
|
||||
"Downloaded": "Downloaded",
|
||||
"File not Found": "File not Found",
|
||||
"The file you are trying to open cannot be found in the specified path.": "The file you are trying to open cannot be found in the specified path."
|
||||
},
|
||||
"Copy": "Copy",
|
||||
"Custom": "Custom",
|
||||
"Cut": "Cut",
|
||||
"Delete": "Delete",
|
||||
"Disable Hamburger menu": "Disable Hamburger menu",
|
||||
"Dev Tools disabled": "Dev Tools disabled",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "Dev Tools has been disabled! Please contact your system administrator to enable it!",
|
||||
"Edit": "Edit",
|
||||
"Enable Hamburger menu": "Enable Hamburger menu",
|
||||
"Error loading configuration": "Error loading configuration",
|
||||
"Error loading URL": "Error loading URL",
|
||||
"Error loading window": "Error loading window",
|
||||
"Error setting AutoLaunch configuration": "Error setting AutoLaunch configuration",
|
||||
"Failed!": "Failed!",
|
||||
"Flash Notification in Taskbar": "Flash Notification in Taskbar",
|
||||
"Help": "Help",
|
||||
"Help Url": "https://support.symphony.com",
|
||||
"Symphony Url": "https://symphony.com/en-US",
|
||||
"Hide Others": "Hide Others",
|
||||
"Hide Symphony": "Hide Symphony",
|
||||
"Ignore": "Ignore",
|
||||
"Learn More": "Learn More",
|
||||
"Loading Error": "Loading Error",
|
||||
"Minimize": "Minimize",
|
||||
"Minimize on Close": "Minimize on Close",
|
||||
"Native": "Native",
|
||||
"Network connectivity has been lost. Check your internet connection.": "Network connectivity has been lost. Check your internet connection.",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Problem connecting to Symphony",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "Looks like you are not connected to the Internet. We'll try to reconnect automatically.",
|
||||
"Cancel Retry": "Cancel Retry",
|
||||
"Quit Symphony": "Quit Symphony"
|
||||
},
|
||||
"No crashes available to share": "No crashes available to share",
|
||||
"No logs are available to share": "No logs are available to share",
|
||||
"Not Allowed": "Not Allowed",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the \"Alt\" key.",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "Bottom Left",
|
||||
"Bottom Right": "Bottom Right",
|
||||
"CANCEL": "CANCEL",
|
||||
"Monitor": "Monitor",
|
||||
"Notification Settings": "Notification Settings",
|
||||
"Notification shown on Monitor: ": "Notification shown on Monitor: ",
|
||||
"OK": "OK",
|
||||
"Position": "Position",
|
||||
"Symphony - Configure Notification Position": "Symphony - Configure Notification Position",
|
||||
"Top Left": "Top Left",
|
||||
"Top Right": "Top Right"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "Oops! Looks like we have had a crash.",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! Looks like we have had a crash. Please reload or close this window.",
|
||||
"Paste": "Paste",
|
||||
"Paste and Match Style": "Paste and Match Style",
|
||||
"Permission Denied": "Permission Denied",
|
||||
"Please contact your admin for help": "Please contact your admin for help",
|
||||
"please contact your administrator for more details": "please contact your administrator for more details",
|
||||
"Quit Symphony": "Quit Symphony",
|
||||
"Redo": "Redo",
|
||||
"Refresh app when idle": "Refresh app when idle",
|
||||
"Clear cache and Reload": "Clear cache and Reload",
|
||||
"Relaunch Application": "Relaunch Application",
|
||||
"Relaunch": "Relaunch",
|
||||
"Reload": "Reload",
|
||||
"Renderer Process Crashed": "Renderer Process Crashed",
|
||||
"ScreenPicker": {
|
||||
"Applications": "Applications",
|
||||
"Cancel": "Cancel",
|
||||
"Choose what you'd like to share": "Choose what you'd like to share",
|
||||
"No screens or applications are currently available.": "No screens or applications are currently available.",
|
||||
"Screen Picker": "Screen Picker",
|
||||
"Screens": "Screens",
|
||||
"Select Application": "Select Application",
|
||||
"Select Screen": "Select Screen",
|
||||
"Share": "Share",
|
||||
"Entire screen": "Entire screen",
|
||||
"Screen {number}": "Screen {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "You are sharing your screen on Symphony",
|
||||
"Stop sharing": "Stop sharing",
|
||||
"Hide": "Hide"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Done",
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
"Snipping Tool": "Snipping Tool"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "About Symphony",
|
||||
"Version": "Version"
|
||||
},
|
||||
"Select All": "Select All",
|
||||
"Services": "Services",
|
||||
"Show All": "Show All",
|
||||
"Show crash dump in Explorer": "Show crash dump in Explorer",
|
||||
"Show crash dump in Finder": "Show crash dump in Finder",
|
||||
"Toggle Developer Tools": "Toggle Developer Tools",
|
||||
"More Information": "More Information",
|
||||
"Show Logs in Explorer": "Show Logs in Explorer",
|
||||
"Show Logs in Finder": "Show Logs in Finder",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " to exit full screen",
|
||||
"esc": "esc",
|
||||
"Press ": "Press "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "Sorry, you are not allowed to access this website",
|
||||
"Speech": "Speech",
|
||||
"Start Speaking": "Start Speaking",
|
||||
"Stop Speaking": "Stop Speaking",
|
||||
"Symphony Help": "Symphony Help",
|
||||
"TitleBar": {
|
||||
"Close": "Close",
|
||||
"Maximize": "Maximize",
|
||||
"Menu": "Menu",
|
||||
"Minimize": "Minimize"
|
||||
},
|
||||
"Title Bar Style": "Title Bar Style",
|
||||
"Toggle Full Screen": "Toggle Full Screen",
|
||||
"Troubleshooting": "Troubleshooting",
|
||||
"Unable to generate crash reports due to ": "Unable to generate crash reports due to ",
|
||||
"Unable to generate logs due to ": "Unable to generate logs due to ",
|
||||
"Undo": "Undo",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "Updating Title bar style requires Symphony to relaunch.",
|
||||
"View": "View",
|
||||
"Window": "Window",
|
||||
"Your administrator has disabled": "Your administrator has disabled",
|
||||
"Zoom": "Zoom",
|
||||
"Zoom In": "Zoom In",
|
||||
"Zoom Out": "Zoom Out"
|
||||
}
|
180
locale/en.json
180
locale/en.json
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "About Symphony",
|
||||
"Actual Size": "Actual Size",
|
||||
"Always on Top": "Always on Top",
|
||||
"Auto Launch On Startup": "Auto Launch On Startup",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "Authentication Request",
|
||||
"Cancel": "Cancel",
|
||||
"hostname": "hostname",
|
||||
"Invalid user name/password": "Invalid user name/password",
|
||||
"Log In": "Log In",
|
||||
"Password:": "Password:",
|
||||
"Please provide your login credentials for:": "Please provide your login credentials for:",
|
||||
"User name:": "User name:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "More Information",
|
||||
"Version Information": "Version Information"
|
||||
},
|
||||
"Bring All to Front": "Bring All to Front",
|
||||
"Bring to Front on Notifications": "Bring to Front on Notifications",
|
||||
"Certificate Error": "Certificate Error",
|
||||
"Close": "Close",
|
||||
"Cancel": "Cancel",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "Add to Dictionary",
|
||||
"Copy": "Copy",
|
||||
"Copy Email Address": "Copy Email Address",
|
||||
"Copy Image": "Copy Image",
|
||||
"Copy Image URL": "Copy Image URL",
|
||||
"Copy Link": "Copy Link",
|
||||
"Cut": "Cut",
|
||||
"Inspect Element": "Inspect Element",
|
||||
"Look Up {searchText}": "Look Up \"{searchText}\"",
|
||||
"Open Link": "Open Link",
|
||||
"Paste": "Paste",
|
||||
"Reload": "Reload",
|
||||
"Search with Google": "Search with Google"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "Show in Folder",
|
||||
"Reveal in Finder": "Reveal in Finder",
|
||||
"Open": "Open",
|
||||
"Downloaded": "Downloaded",
|
||||
"File not Found": "File not Found",
|
||||
"The file you are trying to open cannot be found in the specified path.": "The file you are trying to open cannot be found in the specified path."
|
||||
},
|
||||
"Copy": "Copy",
|
||||
"Custom": "Custom",
|
||||
"Cut": "Cut",
|
||||
"Delete": "Delete",
|
||||
"Disable Hamburger menu": "Disable Hamburger menu",
|
||||
"Dev Tools disabled": "Dev Tools disabled",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "Dev Tools has been disabled! Please contact your system administrator to enable it!",
|
||||
"Edit": "Edit",
|
||||
"Enable Hamburger menu": "Enable Hamburger menu",
|
||||
"Error loading configuration": "Error loading configuration",
|
||||
"Error loading URL": "Error loading URL",
|
||||
"Error loading window": "Error loading window",
|
||||
"Error setting AutoLaunch configuration": "Error setting AutoLaunch configuration",
|
||||
"Failed!": "Failed!",
|
||||
"Flash Notification in Taskbar": "Flash Notification in Taskbar",
|
||||
"Help": "Help",
|
||||
"Help Url": "https://support.symphony.com",
|
||||
"Symphony Url": "https://symphony.com/en-US",
|
||||
"Hide Others": "Hide Others",
|
||||
"Hide Symphony": "Hide Symphony",
|
||||
"Ignore": "Ignore",
|
||||
"Learn More": "Learn More",
|
||||
"Loading Error": "Loading Error",
|
||||
"Minimize": "Minimize",
|
||||
"Minimize on Close": "Minimize on Close",
|
||||
"Native": "Native",
|
||||
"Network connectivity has been lost. Check your internet connection.": "Network connectivity has been lost. Check your internet connection.",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Problem connecting to Symphony",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "Looks like you are not connected to the Internet. We'll try to reconnect automatically.",
|
||||
"Cancel Retry": "Cancel Retry",
|
||||
"Quit Symphony": "Quit Symphony"
|
||||
},
|
||||
"No crashes available to share": "No crashes available to share",
|
||||
"No logs are available to share": "No logs are available to share",
|
||||
"Not Allowed": "Not Allowed",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the \"Alt\" key.",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "Bottom Left",
|
||||
"Bottom Right": "Bottom Right",
|
||||
"CANCEL": "CANCEL",
|
||||
"Monitor": "Monitor",
|
||||
"Notification Settings": "Notification Settings",
|
||||
"Notification shown on Monitor: ": "Notification shown on Monitor: ",
|
||||
"OK": "OK",
|
||||
"Position": "Position",
|
||||
"Symphony - Configure Notification Position": "Symphony - Configure Notification Position",
|
||||
"Top Left": "Top Left",
|
||||
"Top Right": "Top Right"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "Oops! Looks like we have had a crash.",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! Looks like we have had a crash. Please reload or close this window.",
|
||||
"Paste": "Paste",
|
||||
"Paste and Match Style": "Paste and Match Style",
|
||||
"Permission Denied": "Permission Denied",
|
||||
"Please contact your admin for help": "Please contact your admin for help",
|
||||
"please contact your administrator for more details": "please contact your administrator for more details",
|
||||
"Quit Symphony": "Quit Symphony",
|
||||
"Redo": "Redo",
|
||||
"Refresh app when idle": "Refresh app when idle",
|
||||
"Clear cache and Reload": "Clear cache and Reload",
|
||||
"Relaunch Application": "Relaunch Application",
|
||||
"Relaunch": "Relaunch",
|
||||
"Reload": "Reload",
|
||||
"Renderer Process Crashed": "Renderer Process Crashed",
|
||||
"ScreenPicker": {
|
||||
"Applications": "Applications",
|
||||
"Cancel": "Cancel",
|
||||
"Choose what you'd like to share": "Choose what you'd like to share",
|
||||
"No screens or applications are currently available.": "No screens or applications are currently available.",
|
||||
"Screen Picker": "Screen Picker",
|
||||
"Screens": "Screens",
|
||||
"Select Application": "Select Application",
|
||||
"Select Screen": "Select Screen",
|
||||
"Share": "Share",
|
||||
"Entire screen": "Entire screen",
|
||||
"Screen {number}": "Screen {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "You are sharing your screen on Symphony",
|
||||
"Stop sharing": "Stop sharing",
|
||||
"Hide": "Hide"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Done",
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
"Snipping Tool": "Snipping Tool"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "About Symphony",
|
||||
"Version": "Version"
|
||||
},
|
||||
"Select All": "Select All",
|
||||
"Services": "Services",
|
||||
"Show All": "Show All",
|
||||
"Show crash dump in Explorer": "Show crash dump in Explorer",
|
||||
"Show crash dump in Finder": "Show crash dump in Finder",
|
||||
"Toggle Developer Tools": "Toggle Developer Tools",
|
||||
"More Information": "More Information",
|
||||
"Show Logs in Explorer": "Show Logs in Explorer",
|
||||
"Show Logs in Finder": "Show Logs in Finder",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " to exit full screen",
|
||||
"esc": "esc",
|
||||
"Press ": "Press "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "Sorry, you are not allowed to access this website",
|
||||
"Speech": "Speech",
|
||||
"Start Speaking": "Start Speaking",
|
||||
"Stop Speaking": "Stop Speaking",
|
||||
"Symphony Help": "Symphony Help",
|
||||
"TitleBar": {
|
||||
"Close": "Close",
|
||||
"Maximize": "Maximize",
|
||||
"Menu": "Menu",
|
||||
"Minimize": "Minimize"
|
||||
},
|
||||
"Title Bar Style": "Title Bar Style",
|
||||
"Toggle Full Screen": "Toggle Full Screen",
|
||||
"Troubleshooting": "Troubleshooting",
|
||||
"Unable to generate crash reports due to ": "Unable to generate crash reports due to ",
|
||||
"Unable to generate logs due to ": "Unable to generate logs due to ",
|
||||
"Undo": "Undo",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "Updating Title bar style requires Symphony to relaunch.",
|
||||
"View": "View",
|
||||
"Window": "Window",
|
||||
"Your administrator has disabled": "Your administrator has disabled",
|
||||
"Zoom": "Zoom",
|
||||
"Zoom In": "Zoom In",
|
||||
"Zoom Out": "Zoom Out"
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "À propos de Symphony",
|
||||
"Actual Size": "Taille actuelle",
|
||||
"Always on Top": "Toujours au top",
|
||||
"Auto Launch On Startup": "Lancement Automatique au démarrage",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "Demande d'authentification",
|
||||
"Cancel": "Annuler",
|
||||
"hostname": "hostname",
|
||||
"Invalid user name/password": "Nom d'utilisateur/mot de passe invalide",
|
||||
"Log In": "S'identifier",
|
||||
"Password:": "Mot de passe:",
|
||||
"Please provide your login credentials for:": "Fournissez vos identifiants de connexion pour:",
|
||||
"User name:": "Nom d'utilisateur:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "Plus d’informations",
|
||||
"Version Information": "Information sur la version"
|
||||
},
|
||||
"Bring All to Front": "Amener tous au front",
|
||||
"Bring to Front on Notifications": "Mettre les notifications au premier plan",
|
||||
"Certificate Error": "Erreur de certificat",
|
||||
"Close": "Fermer",
|
||||
"Cancel": "Annuler",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "Ajouter au dictionnaire",
|
||||
"Copy": "Copier",
|
||||
"Copy Email Address": "Copier l'adresse e-mail",
|
||||
"Copy Image": "Copier l'image",
|
||||
"Copy Image URL": "Copier l'URL de l'image",
|
||||
"Copy Link": "Copier le lien",
|
||||
"Cut": "Couper",
|
||||
"Inspect Element": "Inspecter l'élément",
|
||||
"Look Up {searchText}": "Rechercher \"{searchText}\"",
|
||||
"Open Link": "Ouvrir le lien",
|
||||
"Paste": "Coller",
|
||||
"Reload": "Recharger",
|
||||
"Search with Google": "Rechercher avec Google"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "Afficher dans le dossier",
|
||||
"Reveal in Finder": "Révéler dans le Finder",
|
||||
"Open": "Ouvrir",
|
||||
"Downloaded": "Téléchargé",
|
||||
"File not Found": "Fichier non trouvé",
|
||||
"The file you are trying to open cannot be found in the specified path.": "Le fichier que vous essayez d'ouvrir est introuvable dans le chemin spécifié."
|
||||
},
|
||||
"Copy": "Copier",
|
||||
"Custom": "Personnalisé",
|
||||
"Cut": "Couper",
|
||||
"Delete": "Effacer",
|
||||
"Disable Hamburger menu": "Désactiver le menu Hamburger",
|
||||
"Dev Tools disabled": "Outils de développement désactivés",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "Dev Tools a été désactivé ! Veuillez contacter votre administrateur système pour l’activer !",
|
||||
"Edit": "Modifier",
|
||||
"Enable Hamburger menu": "Activer le menu Hamburger",
|
||||
"Error loading configuration": "Erreur de chargement de la configuration",
|
||||
"Error loading URL": "Erreur de chargement de l'URL",
|
||||
"Error loading window": "Erreur de chargement de la fenêtre",
|
||||
"Error setting AutoLaunch configuration": "Erreur de configuration dans le Lancement Automatique",
|
||||
"Failed!": "Échoué!",
|
||||
"Flash Notification in Taskbar": "Notification Flash dans la barre des tâches",
|
||||
"Help": "Aide",
|
||||
"Help Url": "https://support.symphony.com",
|
||||
"Symphony Url": "https://symphony.com/fr-FR",
|
||||
"Hide Others": "Cacher les autres",
|
||||
"Hide Symphony": "Cacher Symphony",
|
||||
"Ignore": "Ignorer",
|
||||
"Learn More": "Apprendre encore plus",
|
||||
"Loading Error": "Erreur lors du chargement",
|
||||
"Minimize": "Minimiser",
|
||||
"Minimiser on Close": "Minimiser à la fermeture",
|
||||
"Native": "Originaire",
|
||||
"Network connectivity has been lost. Check your internet connection.": "La connectivité a été perdue. Vérifiez votre connection à l'internet.",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Problème de connexion à Symphony",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "On dirait que vous n'êtes pas connecté à Internet. Nous allons essayer de vous reconnecter automatiquement.",
|
||||
"Cancel Retry": "Annuler nouvelle tentative",
|
||||
"Quit Symphony": "Quitter Symphony"
|
||||
},
|
||||
"No crashes available to share": "Pas de crash à partager",
|
||||
"No logs are available to share": "Pas de journal à partager",
|
||||
"Not Allowed": "Interdit",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "Remarque: lorsque le menu Hamburger est désactivé, vous pouvez activer le menu principal en appuyant sur la touche \"Alt\" key.",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "Gauche inférieure",
|
||||
"Bottom Right": "Droite inférieure",
|
||||
"CANCEL": "ANNULER",
|
||||
"Monitor": "Moniteur",
|
||||
"Notification Settings": "Paramètres des notifications",
|
||||
"Notification shown on Monitor: ": "Notification affichée sur le Moniteur: ",
|
||||
"OK": "OK",
|
||||
"Position": "Position",
|
||||
"Symphony - Configure Notification Position": "Symphony - Configurer la position des notifications",
|
||||
"Top Left": "Gauche supérieure",
|
||||
"Top Right": "Droite supérieure"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "Oops! On dirait que nous avons eu un crash.",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! On dirait que nous avons eu un crash. Veuillez recharger ou fermer cette fenêtre.",
|
||||
"Paste": "Coller",
|
||||
"Paste and Match Style": "Coller et appliquer le style",
|
||||
"Permission Denied": "Permission refusée",
|
||||
"Please contact your admin for help": "Veuillez contacter votre administrateur pour obtenir de l'aide.",
|
||||
"please contact your administrator for more details": "veuillez contacter votre administrateur pour plus de détails",
|
||||
"Quit Symphony": "Quitter Symphony",
|
||||
"Redo": "Refaire",
|
||||
"Refresh app when idle": "Actualiser l'application en mode inactive",
|
||||
"Clear cache and Reload": "Vider le cache et recharger",
|
||||
"Relaunch Application": "Redémarrer l'application",
|
||||
"Relaunch": "Redémarrer",
|
||||
"Reload": "Recharger",
|
||||
"Renderer Process Crashed": "Processus de rendu a eu un crash",
|
||||
"ScreenPicker": {
|
||||
"Applications": "Applications",
|
||||
"Cancel": "Annuler",
|
||||
"Choose what you'd like to share": "Choisissez ce que vous souhaitez partager",
|
||||
"No screens or applications are currently available.": "Aucun écran ou application n'est actuellement disponible.",
|
||||
"Screen Picker": "Sélecteur d'écran",
|
||||
"Screens": "Écrans",
|
||||
"Select Application": "Sélectionnez une application",
|
||||
"Select Screen": "Sélectionnez l'écran",
|
||||
"Share": "Partager",
|
||||
"Entire screen": "Écran entier",
|
||||
"Screen {number}": "Écran {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "Vous partagez votre écran sur Symphony",
|
||||
"Stop sharing": "Arrêter le partage",
|
||||
"Hide": "Masquer"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Terminé",
|
||||
"Erase": "Effacer",
|
||||
"Highlight": "Surligner",
|
||||
"Pen": "Stylo",
|
||||
"Snipping Tool": "Outil Capture"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "À propos de Symphony",
|
||||
"Version": "Version"
|
||||
},
|
||||
"Select All": "Tout sélectionner",
|
||||
"Services": "Services",
|
||||
"Show All": "Tout afficher",
|
||||
"Show crash dump in Explorer": "Afficher rapport de crash dans Explorateur",
|
||||
"Show crash dump in Finder": "Afficher rapport de crash dans Finder",
|
||||
"Toggle Developer Tools": "Basculer, les outils de développement",
|
||||
"More Information": "Plus d'information",
|
||||
"Show Logs in Explorer": "Afficher journal d'évenements dans Explorateur",
|
||||
"Show Logs in Finder": "Afficher journal d'évenements dans Finder",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " pour quitter le plein écran",
|
||||
"esc": "esc",
|
||||
"Press ": "Appuyer sur "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "Désolé, vous n'êtes pas autorisé à accéder à ce site.",
|
||||
"Speech": "Dictée vocale",
|
||||
"Start Speaking": "Commencer à dicter",
|
||||
"Stop Speaking": "Arreter de dicter",
|
||||
"Symphony Help": "Aide Symphony",
|
||||
"TitleBar": {
|
||||
"Close": "Fermer",
|
||||
"Maximize": "Maximiser",
|
||||
"Menu": "Menu",
|
||||
"Minimize": "Minimiser"
|
||||
},
|
||||
"Title Bar Style": "Style de la barre de titre",
|
||||
"Toggle Full Screen": "Basculer plein écran",
|
||||
"Troubleshooting": "Dépannage",
|
||||
"Unable to generate crash reports due to ": "Impossible de générer le rapport de crash en raison de ",
|
||||
"Unable to generate logs due to ": "Impossible de générer le journal d'évenements en raison de",
|
||||
"Undo": "Défaire",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "La mise à jour du style de la barre de titre nécessite le redémarrage de Symphony.",
|
||||
"View": "Visualiser",
|
||||
"Window": "Fenêtre",
|
||||
"Your administrator has disabled": "Votre administrateur a désactivé",
|
||||
"Zoom": "Zoom",
|
||||
"Zoom In": "Zoom Avant",
|
||||
"Zoom Out": "Zoom Arrière"
|
||||
}
|
180
locale/fr.json
180
locale/fr.json
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "À propos de Symphony",
|
||||
"Actual Size": "Taille actuelle",
|
||||
"Always on Top": "Toujours au top",
|
||||
"Auto Launch On Startup": "Lancement Automatique au démarrage",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "Demande d'authentification",
|
||||
"Cancel": "Annuler",
|
||||
"hostname": "hostname",
|
||||
"Invalid user name/password": "Nom d'utilisateur/mot de passe invalide",
|
||||
"Log In": "S'identifier",
|
||||
"Password:": "Mot de passe:",
|
||||
"Please provide your login credentials for:": "Fournissez vos identifiants de connexion pour:",
|
||||
"User name:": "Nom d'utilisateur:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "Plus d’informations",
|
||||
"Version Information": "Information sur la version"
|
||||
},
|
||||
"Bring All to Front": "Amener tous au front",
|
||||
"Bring to Front on Notifications": "Mettre les notifications au premier plan",
|
||||
"Certificate Error": "Erreur de certificat",
|
||||
"Close": "Fermer",
|
||||
"Cancel": "Annuler",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "Ajouter au dictionnaire",
|
||||
"Copy": "Copier",
|
||||
"Copy Email Address": "Copier l'adresse e-mail",
|
||||
"Copy Image": "Copier l'image",
|
||||
"Copy Image URL": "Copier l'URL de l'image",
|
||||
"Copy Link": "Copier le lien",
|
||||
"Cut": "Couper",
|
||||
"Inspect Element": "Inspecter l'élément",
|
||||
"Look Up {searchText}": "Rechercher \"{searchText}\"",
|
||||
"Open Link": "Ouvrir le lien",
|
||||
"Paste": "Coller",
|
||||
"Reload": "Recharger",
|
||||
"Search with Google": "Rechercher avec Google"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "Afficher dans le dossier",
|
||||
"Reveal in Finder": "Révéler dans le Finder",
|
||||
"Open": "Ouvrir",
|
||||
"Downloaded": "Téléchargé",
|
||||
"File not Found": "Fichier non trouvé",
|
||||
"The file you are trying to open cannot be found in the specified path.": "Le fichier que vous essayez d'ouvrir est introuvable dans le chemin spécifié."
|
||||
},
|
||||
"Copy": "Copier",
|
||||
"Custom": "Personnalisé",
|
||||
"Cut": "Couper",
|
||||
"Delete": "Effacer",
|
||||
"Disable Hamburger menu": "Désactiver le menu Hamburger",
|
||||
"Dev Tools disabled": "Outils de développement désactivés",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "Dev Tools a été désactivé ! Veuillez contacter votre administrateur système pour l’activer !",
|
||||
"Edit": "Modifier",
|
||||
"Enable Hamburger menu": "Activer le menu Hamburger",
|
||||
"Error loading configuration": "Erreur de chargement de la configuration",
|
||||
"Error loading URL": "Erreur de chargement de l'URL",
|
||||
"Error loading window": "Erreur de chargement de la fenêtre",
|
||||
"Error setting AutoLaunch configuration": "Erreur de configuration dans le Lancement Automatique",
|
||||
"Failed!": "Échoué!",
|
||||
"Flash Notification in Taskbar": "Notification Flash dans la barre des tâches",
|
||||
"Help": "Aide",
|
||||
"Help Url": "https://support.symphony.com",
|
||||
"Symphony Url": "https://symphony.com/fr-FR",
|
||||
"Hide Others": "Cacher les autres",
|
||||
"Hide Symphony": "Cacher Symphony",
|
||||
"Ignore": "Ignorer",
|
||||
"Learn More": "Apprendre encore plus",
|
||||
"Loading Error": "Erreur lors du chargement",
|
||||
"Minimize": "Minimiser",
|
||||
"Minimiser on Close": "Minimiser à la fermeture",
|
||||
"Native": "Originaire",
|
||||
"Network connectivity has been lost. Check your internet connection.": "La connectivité a été perdue. Vérifiez votre connection à l'internet.",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Problème de connexion à Symphony",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "On dirait que vous n'êtes pas connecté à Internet. Nous allons essayer de vous reconnecter automatiquement.",
|
||||
"Cancel Retry": "Annuler nouvelle tentative",
|
||||
"Quit Symphony": "Quitter Symphony"
|
||||
},
|
||||
"No crashes available to share": "Pas de crash à partager",
|
||||
"No logs are available to share": "Pas de journal à partager",
|
||||
"Not Allowed": "Interdit",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "Remarque: lorsque le menu Hamburger est désactivé, vous pouvez activer le menu principal en appuyant sur la touche \"Alt\" key.",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "Gauche inférieure",
|
||||
"Bottom Right": "Droite inférieure",
|
||||
"CANCEL": "ANNULER",
|
||||
"Monitor": "Moniteur",
|
||||
"Notification Settings": "Paramètres des notifications",
|
||||
"Notification shown on Monitor: ": "Notification affichée sur le Moniteur: ",
|
||||
"OK": "OK",
|
||||
"Position": "Position",
|
||||
"Symphony - Configure Notification Position": "Symphony - Configurer la position des notifications",
|
||||
"Top Left": "Gauche supérieure",
|
||||
"Top Right": "Droite supérieure"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "Oops! On dirait que nous avons eu un crash.",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! On dirait que nous avons eu un crash. Veuillez recharger ou fermer cette fenêtre.",
|
||||
"Paste": "Coller",
|
||||
"Paste and Match Style": "Coller et appliquer le style",
|
||||
"Permission Denied": "Permission refusée",
|
||||
"Please contact your admin for help": "Veuillez contacter votre administrateur pour obtenir de l'aide.",
|
||||
"please contact your administrator for more details": "veuillez contacter votre administrateur pour plus de détails",
|
||||
"Quit Symphony": "Quitter Symphony",
|
||||
"Redo": "Refaire",
|
||||
"Refresh app when idle": "Actualiser l'application en mode inactive",
|
||||
"Clear cache and Reload": "Vider le cache et recharger",
|
||||
"Relaunch Application": "Redémarrer l'application",
|
||||
"Relaunch": "Redémarrer",
|
||||
"Reload": "Recharger",
|
||||
"Renderer Process Crashed": "Processus de rendu a eu un crash",
|
||||
"ScreenPicker": {
|
||||
"Applications": "Applications",
|
||||
"Cancel": "Annuler",
|
||||
"Choose what you'd like to share": "Choisissez ce que vous souhaitez partager",
|
||||
"No screens or applications are currently available.": "Aucun écran ou application n'est actuellement disponible.",
|
||||
"Screen Picker": "Sélecteur d'écran",
|
||||
"Screens": "Écrans",
|
||||
"Select Application": "Sélectionnez une application",
|
||||
"Select Screen": "Sélectionnez l'écran",
|
||||
"Share": "Partager",
|
||||
"Entire screen": "Écran entier",
|
||||
"Screen {number}": "Écran {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "Vous partagez votre écran sur Symphony",
|
||||
"Stop sharing": "Arrêter le partage",
|
||||
"Hide": "Masquer"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Terminé",
|
||||
"Erase": "Effacer",
|
||||
"Highlight": "Surligner",
|
||||
"Pen": "Stylo",
|
||||
"Snipping Tool": "Outil Capture"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "À propos de Symphony",
|
||||
"Version": "Version"
|
||||
},
|
||||
"Select All": "Tout sélectionner",
|
||||
"Services": "Services",
|
||||
"Show All": "Tout afficher",
|
||||
"Show crash dump in Explorer": "Afficher rapport de crash dans Explorateur",
|
||||
"Show crash dump in Finder": "Afficher rapport de crash dans Finder",
|
||||
"Toggle Developer Tools": "Basculer, les outils de développement",
|
||||
"More Information": "Plus d'information",
|
||||
"Show Logs in Explorer": "Afficher journal d'évenements dans Explorateur",
|
||||
"Show Logs in Finder": "Afficher journal d'évenements dans Finder",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " pour quitter le plein écran",
|
||||
"esc": "esc",
|
||||
"Press ": "Appuyer sur "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "Désolé, vous n'êtes pas autorisé à accéder à ce site.",
|
||||
"Speech": "Dictée vocale",
|
||||
"Start Speaking": "Commencer à dicter",
|
||||
"Stop Speaking": "Arreter de dicter",
|
||||
"Symphony Help": "Aide Symphony",
|
||||
"TitleBar": {
|
||||
"Close": "Fermer",
|
||||
"Maximize": "Maximiser",
|
||||
"Menu": "Menu",
|
||||
"Minimize": "Minimiser"
|
||||
},
|
||||
"Title Bar Style": "Style de la barre de titre",
|
||||
"Toggle Full Screen": "Basculer plein écran",
|
||||
"Troubleshooting": "Dépannage",
|
||||
"Unable to generate crash reports due to ": "Impossible de générer le rapport de crash en raison de ",
|
||||
"Unable to generate logs due to ": "Impossible de générer le journal d'évenements en raison de",
|
||||
"Undo": "Défaire",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "La mise à jour du style de la barre de titre nécessite le redémarrage de Symphony.",
|
||||
"View": "Visualiser",
|
||||
"Window": "Fenêtre",
|
||||
"Your administrator has disabled": "Votre administrateur a désactivé",
|
||||
"Zoom": "Zoom",
|
||||
"Zoom In": "Zoom Avant",
|
||||
"Zoom Out": "Zoom Arrière"
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "Symphonyについて",
|
||||
"Actual Size": "実際のサイズ",
|
||||
"Always on Top": "つねに前面に表示",
|
||||
"Auto Launch On Startup": "スタートアップ時に自動起動",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "認証要求",
|
||||
"Cancel": "キャンセル",
|
||||
"hostname": "ホスト名",
|
||||
"Invalid user name/password": "ユーザー名かパスワード、または両方が無効",
|
||||
"Log In": "ログイン",
|
||||
"Password:": "パスワード:",
|
||||
"Please provide your login credentials for:": "あなたのログイン認証情報を入力してください。",
|
||||
"User name:": "ユーザー名:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "詳しくは",
|
||||
"Version Information": "バージョン情報"
|
||||
},
|
||||
"Bring All to Front": "すべて前面に表示",
|
||||
"Bring to Front on Notifications": "通知時に前面に表示",
|
||||
"Certificate Error": "証明書のエラー",
|
||||
"Close": "閉じる",
|
||||
"Cancel": "キャンセル",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "辞書に追加",
|
||||
"Copy": "コピー",
|
||||
"Copy Email Address": "電子メールアドレスをコピー",
|
||||
"Copy Image": "画像をコピー",
|
||||
"Copy Image URL": "画像のURLをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"Cut": "切り取り",
|
||||
"Inspect Element": "要素を調査",
|
||||
"Look Up {searchText}": "「{searchText}」を検索",
|
||||
"Open Link": "リンクを開く",
|
||||
"Paste": "貼り付け",
|
||||
"Reload": "再読み込み",
|
||||
"Search with Google": "Googleで検索"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "フォルダーを開く",
|
||||
"Reveal in Finder": "Finderで確認する",
|
||||
"Open": "開く",
|
||||
"Downloaded": "ダウンロード済み",
|
||||
"File not Found": "ファイルが見つかりません",
|
||||
"The file you are trying to open cannot be found in the specified path.": "開こうとしているファイルが指定されたパスに見つかりません."
|
||||
},
|
||||
"Copy": "コピー",
|
||||
"Custom": "カスタム",
|
||||
"Cut": "切り取り",
|
||||
"Delete": "削除",
|
||||
"Disable Hamburger menu": "ハンバーガーメニューを無効にする",
|
||||
"Dev Tools disabled": "開発ツールが無効",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "開発ツールが無効になっています。システム管理者に連絡して、有効にしてください。",
|
||||
"Edit": "編集",
|
||||
"Enable Hamburger menu": "ハンバーガーメニューを有効にする",
|
||||
"Error loading configuration": "構成の読み込みエラー",
|
||||
"Error loading URL": "URLの読み込みエラー",
|
||||
"Error loading window": "ウィンドウを読み込みエラー",
|
||||
"Error setting AutoLaunch configuration": "自動起動の構成の設定エラー",
|
||||
"Failed!": "問題が起きました!",
|
||||
"Flash Notification in Taskbar": "タスクバーの通知を点滅",
|
||||
"Help": "ヘルプ",
|
||||
"Help Url": "https://support.symphony.com/hc/ja",
|
||||
"Symphony Url": "https://symphony.com/ja",
|
||||
"Hide Others": "他を隠す",
|
||||
"Hide Symphony": "Symphonyを隠す",
|
||||
"Ignore": "無視",
|
||||
"Learn More": "詳細",
|
||||
"Loading Error": "読み込みエラー",
|
||||
"Minimize": "最小化",
|
||||
"Minimize on Close": "閉じるで最小化",
|
||||
"Native": "Native",
|
||||
"Network connectivity has been lost. Check your internet connection.": "ネットワーク接続が失われました。インターネット接続を確認してください。",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Symphonyへの接続に関する問題",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "インターネットに接続していないようです。 自動的に再接続します。",
|
||||
"Cancel Retry": "再試行をキャンセル",
|
||||
"Quit Symphony": "Symphonyを終了"
|
||||
},
|
||||
"No crashes available to share": "共有できるクラッシュはありません",
|
||||
"No logs are available to share": "共有できるログはありません",
|
||||
"Not Allowed": "許可されていませ。",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "注:ハンバーガーメニューが無効になっている場合、「Alt」キーを押してメインメニューをトリガーすることができます。",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "左下",
|
||||
"Bottom Right": "右下",
|
||||
"CANCEL": "キャンセル",
|
||||
"Monitor": "モニター",
|
||||
"Notification Settings": "通知設定",
|
||||
"Notification shown on Monitor: ": "モニターに表示する通知:",
|
||||
"OK": "OK",
|
||||
"Position": "位置",
|
||||
"Symphony - Configure Notification Position": "Symphony - 通知位置の設定",
|
||||
"Top Left": "左上",
|
||||
"Top Right": "右上"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "おっと!クラッシュしたようです。",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "おっと!クラッシュしたようです。このウィンドウを再度読み込むか閉じてください。",
|
||||
"Paste": "貼り付け",
|
||||
"Paste and Match Style": "貼り付けでスタイルを合わせる",
|
||||
"Permission Denied": "アクセス許可が拒否されています",
|
||||
"Please contact your admin for help": "不明な点がある場合は、管理者にお問い合わせください",
|
||||
"please contact your administrator for more details": "詳細は、管理者にお問い合わせください",
|
||||
"Quit Symphony": "Symphonyを終了",
|
||||
"Redo": "やり直し",
|
||||
"Refresh app when idle": "アイドル時にアプリを再表示",
|
||||
"Clear cache and Reload": "キャッシュをクリアしてリロードする",
|
||||
"Relaunch Application": "アプリケーションの再起動",
|
||||
"Relaunch": "「リスタート」",
|
||||
"Reload": "再読み込み",
|
||||
"Renderer Process Crashed": "レンダラープロセスがクラッシュしました",
|
||||
"ScreenPicker": {
|
||||
"Applications": "アプリケーション",
|
||||
"Cancel": "キャンセル",
|
||||
"Choose what you'd like to share": "共有するものを選択する",
|
||||
"No screens or applications are currently available.": "現在利用できる画面およびアプリケーションはありません。",
|
||||
"Screen Picker": "画面の選択",
|
||||
"Screens": "画面",
|
||||
"Select Application": "アプリケーションを選択",
|
||||
"Select Screen": "画面を選択",
|
||||
"Share": "共有",
|
||||
"Entire screen": "画面全体",
|
||||
"Screen {number}": "画面 {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "あなたはSymphony上であなたの画面を共有しています",
|
||||
"Stop sharing": "画面共有を停止",
|
||||
"Hide": "非表示にする"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "完了",
|
||||
"Erase": "消去",
|
||||
"Highlight": "強調",
|
||||
"Pen": "ペン",
|
||||
"Snipping Tool": "切り取りツール"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "Symphonyについて",
|
||||
"Version": "バージョン"
|
||||
},
|
||||
"Select All": "すべてを選択",
|
||||
"Services": "サービス",
|
||||
"Show All": "すべてを表示",
|
||||
"Show crash dump in Explorer": "Explorerにクラッシュダンプを表示",
|
||||
"Show crash dump in Finder": "ファインダーにクラッシュダンプを表示",
|
||||
"Toggle Developer Tools": "開発者ツールの切り替え",
|
||||
"More Information": "詳しくは",
|
||||
"Show Logs in Explorer": "Explorerにログを表示",
|
||||
"Show Logs in Finder": "ファインダーにログを表示",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " を押します",
|
||||
"esc": "esc",
|
||||
"Press ": "全画面表示を終了するには "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "申し訳ありませんが、このウェブサイトへのアクセスは許可されていません",
|
||||
"Speech": "スピーチ",
|
||||
"Start Speaking": "スピーチを開始",
|
||||
"Stop Speaking": "スピーチを終了",
|
||||
"Symphony Help": "Symphonyのヘルプ",
|
||||
"TitleBar": {
|
||||
"Close": "閉じる",
|
||||
"Maximize": "最大化する",
|
||||
"Menu": "メニュー",
|
||||
"Minimize": "最小化する"
|
||||
},
|
||||
"Title Bar Style": "タイトルバーのスタイル",
|
||||
"Toggle Full Screen": "画面表示を切り替え",
|
||||
"Troubleshooting": "トラブルシューティング",
|
||||
"Unable to generate crash reports due to ": "クラッシュレポートを生成できません。理由: ",
|
||||
"Unable to generate logs due to ": "ログを生成できません。理由:",
|
||||
"Undo": "元に戻す",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "タイトルバーのスタイルを更新するには、Symphonyが再起動する必要があります。",
|
||||
"View": "ビュー",
|
||||
"Window": "ウインドウ",
|
||||
"Your administrator has disabled": "管理者によて無効にされています",
|
||||
"Zoom": "ズーム",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
}
|
180
locale/ja.json
180
locale/ja.json
@ -1,180 +0,0 @@
|
||||
{
|
||||
"About Symphony": "Symphonyについて",
|
||||
"Actual Size": "実際のサイズ",
|
||||
"Always on Top": "つねに前面に表示",
|
||||
"Auto Launch On Startup": "スタートアップ時に自動起動",
|
||||
"BasicAuth": {
|
||||
"Authentication Request": "認証要求",
|
||||
"Cancel": "キャンセル",
|
||||
"hostname": "ホスト名",
|
||||
"Invalid user name/password": "ユーザー名かパスワード、または両方が無効",
|
||||
"Log In": "ログイン",
|
||||
"Password:": "パスワード:",
|
||||
"Please provide your login credentials for:": "あなたのログイン認証情報を入力してください。",
|
||||
"User name:": "ユーザー名:"
|
||||
},
|
||||
"MoreInfo": {
|
||||
"More Information": "詳しくは",
|
||||
"Version Information": "バージョン情報"
|
||||
},
|
||||
"Bring All to Front": "すべて前面に表示",
|
||||
"Bring to Front on Notifications": "通知時に前面に表示",
|
||||
"Certificate Error": "証明書のエラー",
|
||||
"Close": "閉じる",
|
||||
"Cancel": "キャンセル",
|
||||
"ContextMenu": {
|
||||
"Add to Dictionary": "辞書に追加",
|
||||
"Copy": "コピー",
|
||||
"Copy Email Address": "電子メールアドレスをコピー",
|
||||
"Copy Image": "画像をコピー",
|
||||
"Copy Image URL": "画像のURLをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"Cut": "切り取り",
|
||||
"Inspect Element": "要素を調査",
|
||||
"Look Up {searchText}": "「{searchText}」を検索",
|
||||
"Open Link": "リンクを開く",
|
||||
"Paste": "貼り付け",
|
||||
"Reload": "再読み込み",
|
||||
"Search with Google": "Googleで検索"
|
||||
},
|
||||
"DownloadManager": {
|
||||
"Show in Folder": "フォルダーを開く",
|
||||
"Reveal in Finder": "Finderで確認する",
|
||||
"Open": "開く",
|
||||
"Downloaded": "ダウンロード済み",
|
||||
"File not Found": "ファイルが見つかりません",
|
||||
"The file you are trying to open cannot be found in the specified path.": "開こうとしているファイルが指定されたパスに見つかりません."
|
||||
},
|
||||
"Copy": "コピー",
|
||||
"Custom": "カスタム",
|
||||
"Cut": "切り取り",
|
||||
"Delete": "削除",
|
||||
"Disable Hamburger menu": "ハンバーガーメニューを無効にする",
|
||||
"Dev Tools disabled": "開発ツールが無効",
|
||||
"Dev Tools has been disabled! Please contact your system administrator to enable it!": "開発ツールが無効になっています。システム管理者に連絡して、有効にしてください。",
|
||||
"Edit": "編集",
|
||||
"Enable Hamburger menu": "ハンバーガーメニューを有効にする",
|
||||
"Error loading configuration": "構成の読み込みエラー",
|
||||
"Error loading URL": "URLの読み込みエラー",
|
||||
"Error loading window": "ウィンドウを読み込みエラー",
|
||||
"Error setting AutoLaunch configuration": "自動起動の構成の設定エラー",
|
||||
"Failed!": "問題が起きました!",
|
||||
"Flash Notification in Taskbar": "タスクバーの通知を点滅",
|
||||
"Help": "ヘルプ",
|
||||
"Help Url": "https://support.symphony.com/hc/ja",
|
||||
"Symphony Url": "https://symphony.com/ja",
|
||||
"Hide Others": "他を隠す",
|
||||
"Hide Symphony": "Symphonyを隠す",
|
||||
"Ignore": "無視",
|
||||
"Learn More": "詳細",
|
||||
"Loading Error": "読み込みエラー",
|
||||
"Minimize": "最小化",
|
||||
"Minimize on Close": "閉じるで最小化",
|
||||
"Native": "Native",
|
||||
"Network connectivity has been lost. Check your internet connection.": "ネットワーク接続が失われました。インターネット接続を確認してください。",
|
||||
"NetworkError": {
|
||||
"Problem connecting to Symphony": "Symphonyへの接続に関する問題",
|
||||
"Looks like you are not connected to the Internet. We'll try to reconnect automatically.": "インターネットに接続していないようです。 自動的に再接続します。",
|
||||
"Cancel Retry": "再試行をキャンセル",
|
||||
"Quit Symphony": "Symphonyを終了"
|
||||
},
|
||||
"No crashes available to share": "共有できるクラッシュはありません",
|
||||
"No logs are available to share": "共有できるログはありません",
|
||||
"Not Allowed": "許可されていませ。",
|
||||
"Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "注:ハンバーガーメニューが無効になっている場合、「Alt」キーを押してメインメニューをトリガーすることができます。",
|
||||
"NotificationSettings": {
|
||||
"Bottom Left": "左下",
|
||||
"Bottom Right": "右下",
|
||||
"CANCEL": "キャンセル",
|
||||
"Monitor": "モニター",
|
||||
"Notification Settings": "通知設定",
|
||||
"Notification shown on Monitor: ": "モニターに表示する通知:",
|
||||
"OK": "OK",
|
||||
"Position": "位置",
|
||||
"Symphony - Configure Notification Position": "Symphony - 通知位置の設定",
|
||||
"Top Left": "左上",
|
||||
"Top Right": "右上"
|
||||
},
|
||||
"Oops! Looks like we have had a crash.": "おっと!クラッシュしたようです。",
|
||||
"Oops! Looks like we have had a crash. Please reload or close this window.": "おっと!クラッシュしたようです。このウィンドウを再度読み込むか閉じてください。",
|
||||
"Paste": "貼り付け",
|
||||
"Paste and Match Style": "貼り付けでスタイルを合わせる",
|
||||
"Permission Denied": "アクセス許可が拒否されています",
|
||||
"Please contact your admin for help": "不明な点がある場合は、管理者にお問い合わせください",
|
||||
"please contact your administrator for more details": "詳細は、管理者にお問い合わせください",
|
||||
"Quit Symphony": "Symphonyを終了",
|
||||
"Redo": "やり直し",
|
||||
"Refresh app when idle": "アイドル時にアプリを再表示",
|
||||
"Clear cache and Reload": "キャッシュをクリアしてリロードする",
|
||||
"Relaunch Application": "アプリケーションの再起動",
|
||||
"Relaunch": "「リスタート」",
|
||||
"Reload": "再読み込み",
|
||||
"Renderer Process Crashed": "レンダラープロセスがクラッシュしました",
|
||||
"ScreenPicker": {
|
||||
"Applications": "アプリケーション",
|
||||
"Cancel": "キャンセル",
|
||||
"Choose what you'd like to share": "共有するものを選択する",
|
||||
"No screens or applications are currently available.": "現在利用できる画面およびアプリケーションはありません。",
|
||||
"Screen Picker": "画面の選択",
|
||||
"Screens": "画面",
|
||||
"Select Application": "アプリケーションを選択",
|
||||
"Select Screen": "画面を選択",
|
||||
"Share": "共有",
|
||||
"Entire screen": "画面全体",
|
||||
"Screen {number}": "画面 {number}"
|
||||
},
|
||||
"ScreenSharingIndicator": {
|
||||
"You are sharing your screen on Symphony": "あなたはSymphony上であなたの画面を共有しています",
|
||||
"Stop sharing": "画面共有を停止",
|
||||
"Hide": "非表示にする"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "完了",
|
||||
"Erase": "消去",
|
||||
"Highlight": "強調",
|
||||
"Pen": "ペン",
|
||||
"Snipping Tool": "切り取りツール"
|
||||
},
|
||||
"AboutSymphony": {
|
||||
"About Symphony": "Symphonyについて",
|
||||
"Version": "バージョン"
|
||||
},
|
||||
"Select All": "すべてを選択",
|
||||
"Services": "サービス",
|
||||
"Show All": "すべてを表示",
|
||||
"Show crash dump in Explorer": "Explorerにクラッシュダンプを表示",
|
||||
"Show crash dump in Finder": "ファインダーにクラッシュダンプを表示",
|
||||
"Toggle Developer Tools": "開発者ツールの切り替え",
|
||||
"More Information": "詳しくは",
|
||||
"Show Logs in Explorer": "Explorerにログを表示",
|
||||
"Show Logs in Finder": "ファインダーにログを表示",
|
||||
"SnackBar": {
|
||||
" to exit full screen": " を押します",
|
||||
"esc": "esc",
|
||||
"Press ": "全画面表示を終了するには "
|
||||
},
|
||||
"Sorry, you are not allowed to access this website": "申し訳ありませんが、このウェブサイトへのアクセスは許可されていません",
|
||||
"Speech": "スピーチ",
|
||||
"Start Speaking": "スピーチを開始",
|
||||
"Stop Speaking": "スピーチを終了",
|
||||
"Symphony Help": "Symphonyのヘルプ",
|
||||
"TitleBar": {
|
||||
"Close": "閉じる",
|
||||
"Maximize": "最大化する",
|
||||
"Menu": "メニュー",
|
||||
"Minimize": "最小化する"
|
||||
},
|
||||
"Title Bar Style": "タイトルバーのスタイル",
|
||||
"Toggle Full Screen": "画面表示を切り替え",
|
||||
"Troubleshooting": "トラブルシューティング",
|
||||
"Unable to generate crash reports due to ": "クラッシュレポートを生成できません。理由: ",
|
||||
"Unable to generate logs due to ": "ログを生成できません。理由:",
|
||||
"Undo": "元に戻す",
|
||||
"Updating Title bar style requires Symphony to relaunch.": "タイトルバーのスタイルを更新するには、Symphonyが再起動する必要があります。",
|
||||
"View": "ビュー",
|
||||
"Window": "ウインドウ",
|
||||
"Your administrator has disabled": "管理者によて無効にされています",
|
||||
"Zoom": "ズーム",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
}
|
@ -82,12 +82,12 @@
|
||||
"devDependencies": {
|
||||
"@types/enzyme": "3.9.0",
|
||||
"@types/ffi-napi": "2.4.1",
|
||||
"@types/ref-napi": "1.4.0",
|
||||
"@types/jest": "23.3.12",
|
||||
"@types/lodash.omit": "4.5.4",
|
||||
"@types/node": "10.11.4",
|
||||
"@types/react": "16.8.3",
|
||||
"@types/react-dom": "16.0.9",
|
||||
"@types/ref-napi": "1.4.0",
|
||||
"bluebird": "3.5.3",
|
||||
"browserify": "16.2.3",
|
||||
"chromedriver": "2.45.0",
|
||||
@ -101,6 +101,7 @@
|
||||
"electron-rebuild": "1.8.2",
|
||||
"enzyme": "3.9.0",
|
||||
"enzyme-adapter-react-16": "1.10.0",
|
||||
"enzyme-to-json": "3.3.5",
|
||||
"eslint": "5.6.1",
|
||||
"eslint-config-airbnb": "17.1.0",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
|
@ -8,6 +8,7 @@ const appName: string = 'Symphony';
|
||||
const executableName: string = '/Symphony.exe';
|
||||
const isReady: boolean = true;
|
||||
const version: string = '4.0.0';
|
||||
const openAtLogin: boolean = true;
|
||||
interface IApp {
|
||||
commandLine: any;
|
||||
getAppPath(): string;
|
||||
@ -18,6 +19,11 @@ interface IApp {
|
||||
on(eventName: any, cb: any): void;
|
||||
once(eventName: any, cb: any): void;
|
||||
setPath(value: string, path: string): void;
|
||||
setLoginItemSettings(settings: { openAtLogin: boolean, path: string }): void;
|
||||
getLoginItemSettings(options?: { path: string, args: string[] }): LoginItemSettings;
|
||||
}
|
||||
interface LoginItemSettings {
|
||||
openAtLogin: boolean;
|
||||
}
|
||||
interface IIpcMain {
|
||||
on(event: any, cb: any): void;
|
||||
@ -67,6 +73,8 @@ export const app: IApp = {
|
||||
once: (eventName, cb) => {
|
||||
ipcEmitter.on(eventName, cb);
|
||||
},
|
||||
setLoginItemSettings: () => jest.fn(),
|
||||
getLoginItemSettings: () => { openAtLogin },
|
||||
};
|
||||
|
||||
// simple ipc mocks for render and main process ipc using
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { autoLaunchInstance } from '../src/app/auto-launch-controller';
|
||||
import { config } from '../src/app/config-handler';
|
||||
import * as electron from './__mocks__/electron';
|
||||
import { app } from './__mocks__/electron';
|
||||
|
||||
jest.mock('electron-log');
|
||||
|
||||
@ -30,42 +30,26 @@ describe('auto launch controller', async () => {
|
||||
});
|
||||
|
||||
it('should call `enableAutoLaunch` correctly', async () => {
|
||||
const spyFn = 'enable';
|
||||
const spy = jest.spyOn(autoLaunchInstance, spyFn);
|
||||
const spyFn = 'setLoginItemSettings';
|
||||
const spy = jest.spyOn(app, spyFn);
|
||||
await autoLaunchInstance.enableAutoLaunch();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should call `disableAutoLaunch` correctly', async () => {
|
||||
const spyFn = 'disable';
|
||||
const spy = jest.spyOn(autoLaunchInstance, spyFn);
|
||||
const spyFn = 'setLoginItemSettings';
|
||||
const spy = jest.spyOn(app, spyFn);
|
||||
await autoLaunchInstance.disableAutoLaunch();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should call `isAutoLaunchEnabled` correctly', async () => {
|
||||
const spyFn = 'isEnabled';
|
||||
const spy = jest.spyOn(autoLaunchInstance, spyFn);
|
||||
const spyFn = 'getLoginItemSettings';
|
||||
const spy = jest.spyOn(app, spyFn);
|
||||
await autoLaunchInstance.isAutoLaunchEnabled();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should fail `enableAutoLaunch` when catch is trigged', async () => {
|
||||
const spyFn = 'showMessageBox';
|
||||
const spy = jest.spyOn(electron.dialog, spyFn);
|
||||
autoLaunchInstance.enable = jest.fn(() => Promise.reject());
|
||||
await autoLaunchInstance.enableAutoLaunch();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should fail `disableAutoLaunch` when catch is trigged', async () => {
|
||||
const spyFn = 'showMessageBox';
|
||||
const spy = jest.spyOn(electron.dialog, spyFn);
|
||||
autoLaunchInstance.disable = jest.fn(() => Promise.reject());
|
||||
await autoLaunchInstance.disableAutoLaunch();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should enable AutoLaunch when `handleAutoLaunch` is called', async () => {
|
||||
const spyFn = 'enableAutoLaunch';
|
||||
const spy = jest.spyOn(autoLaunchInstance, spyFn);
|
||||
@ -84,6 +68,7 @@ describe('auto launch controller', async () => {
|
||||
const spy = jest.spyOn(autoLaunchInstance, spyFn);
|
||||
jest.spyOn(autoLaunchInstance,'isAutoLaunchEnabled').mockImplementation(() => true);
|
||||
await autoLaunchInstance.handleAutoLaunch();
|
||||
console.log(autoLaunchInstance.isAutoLaunchEnabled());
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
@ -57,12 +57,15 @@ class AutoLaunchController {
|
||||
const { launchOnStartup }: IConfig = config.getConfigFields([ 'launchOnStartup' ]);
|
||||
const { openAtLogin: isAutoLaunchEnabled }: LoginItemSettings = this.isAutoLaunchEnabled();
|
||||
|
||||
console.log(this.isAutoLaunchEnabled());
|
||||
|
||||
if (typeof launchOnStartup === 'boolean' && launchOnStartup) {
|
||||
if (!isAutoLaunchEnabled) {
|
||||
this.enableAutoLaunch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(`Auto launch enabled!! ${isAutoLaunchEnabled}`)
|
||||
if (isAutoLaunchEnabled) {
|
||||
this.disableAutoLaunch();
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export class WindowHandler {
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false,
|
||||
contextIsolation: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
@ -83,7 +83,7 @@ export class WindowHandler {
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
@ -106,7 +106,7 @@ export class WindowHandler {
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false,
|
||||
contextIsolation: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
@ -131,7 +131,7 @@ export class WindowHandler {
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false,
|
||||
contextIsolation: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
@ -152,7 +152,7 @@ export class WindowHandler {
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
devTools: false,
|
||||
contextIsolation: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
winKey: getGuid(),
|
||||
};
|
||||
|
@ -1,90 +0,0 @@
|
||||
const downloadManager = require('../js/downloadManager');
|
||||
const electron = require('./__mocks__/electron');
|
||||
|
||||
describe('download manager', function() {
|
||||
describe('Download Manager to create DOM once download is initiated', function() {
|
||||
beforeEach(function() {
|
||||
global.document.body.innerHTML =
|
||||
'<div id="download-main">' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
it('should inject download bar element into DOM once download is initiated', function() {
|
||||
electron.ipcRenderer.send('downloadCompleted', { _id: '12345', fileName: 'test.png', total: 100 });
|
||||
expect(document.getElementsByClassName('text-cutoff')[0].innerHTML).toBe('test.png');
|
||||
expect(document.getElementById('per').innerHTML).toBe('100 Downloaded');
|
||||
});
|
||||
|
||||
it('should inject multiple download items during multiple downloads', function() {
|
||||
electron.ipcRenderer.send('downloadCompleted', { _id: '12345', fileName: 'test (1).png', total: 100 });
|
||||
electron.ipcRenderer.send('downloadCompleted', { _id: '67890', fileName: 'test (2).png', total: 200 });
|
||||
|
||||
let fileNames = document.getElementsByClassName('text-cutoff');
|
||||
let fNames = [];
|
||||
|
||||
for (var i = 0; i < fileNames.length; i++) {
|
||||
fNames.push(fileNames[i].innerHTML);
|
||||
}
|
||||
|
||||
expect(fNames).toEqual(expect.arrayContaining(['test (1).png', 'test (2).png']));
|
||||
expect(document.getElementById('per').innerHTML).toBe('100 Downloaded');
|
||||
|
||||
let downloadElements = document.getElementsByClassName('download-element');
|
||||
expect(downloadElements[0].id).toBe('67890');
|
||||
expect(downloadElements[1].id).toBe('12345');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Download Manager to initiate footer', function() {
|
||||
beforeEach(function() {
|
||||
global.document.body.innerHTML =
|
||||
'<div id="footer" class="hidden">' +
|
||||
'<div id="download-manager-footer">' +
|
||||
'<div id="download-main">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
it('should inject dom element once download is completed', function() {
|
||||
electron.ipcRenderer.send('downloadProgress');
|
||||
expect(document.getElementById('footer').classList).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('should remove the download bar and clear up the download items', function() {
|
||||
|
||||
electron.ipcRenderer.send('downloadProgress');
|
||||
expect(document.getElementById('footer').classList).not.toContain('hidden');
|
||||
|
||||
document.getElementById('close-download-bar').click();
|
||||
expect(document.getElementById('footer').classList).toContain('hidden');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Download Manager to initiate footer', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
global.document.body.innerHTML =
|
||||
'<div id="footer" class="hidden">' +
|
||||
'<div id="download-manager-footer">' +
|
||||
'<div id="download-main">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
it('should inject ul element if not found', function() {
|
||||
|
||||
electron.ipcRenderer.send('downloadProgress');
|
||||
|
||||
expect(document.getElementById('download-main')).not.toBeNull();
|
||||
expect(document.getElementById('footer').classList).not.toContain('hidden');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,118 +0,0 @@
|
||||
/**
|
||||
* Created by vishwas on 10/05/17.
|
||||
*/
|
||||
const protocolHandler = require('../js/protocolHandler');
|
||||
const electron = require('./__mocks__/electron');
|
||||
|
||||
describe('protocol handler', function () {
|
||||
|
||||
const url = 'symphony://?userId=100001';
|
||||
const nonProtocolUrl = 'sy://abc=123';
|
||||
|
||||
const mainProcess = electron.ipcMain;
|
||||
const protocolWindow = electron.ipcRenderer;
|
||||
|
||||
beforeAll(function () {
|
||||
protocolHandler.setProtocolWindow(protocolWindow);
|
||||
});
|
||||
|
||||
it('process a protocol action', function (done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'processProtocolAction');
|
||||
protocolHandler.processProtocolAction(url);
|
||||
expect(spy).toHaveBeenCalledWith(url);
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('protocol url should be undefined by default', function (done) {
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('protocol handler open url should be called', function (done) {
|
||||
|
||||
const spy = jest.spyOn(mainProcess, 'send');
|
||||
mainProcess.send('open-url', url);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('protocol handler open url should be called', function(done) {
|
||||
|
||||
const spy = jest.spyOn(mainProcess, 'send');
|
||||
mainProcess.send('open-url', nonProtocolUrl);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('check protocol action should be called', function (done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'checkProtocolAction');
|
||||
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
|
||||
|
||||
protocolHandler.setProtocolUrl(url);
|
||||
expect(setSpy).toHaveBeenCalledWith(url);
|
||||
|
||||
protocolHandler.checkProtocolAction();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('check protocol action should be called when we have an incorrect protocol url', function (done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'checkProtocolAction');
|
||||
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
|
||||
|
||||
protocolHandler.setProtocolUrl(nonProtocolUrl);
|
||||
expect(setSpy).toHaveBeenCalledWith(nonProtocolUrl);
|
||||
|
||||
protocolHandler.checkProtocolAction();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('check protocol action should be called when the protocol url is undefined', function(done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'checkProtocolAction');
|
||||
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
|
||||
|
||||
protocolHandler.setProtocolUrl(undefined);
|
||||
expect(setSpy).toHaveBeenCalledWith(undefined);
|
||||
|
||||
protocolHandler.checkProtocolAction();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('should cache the protocol url if the protocol window is not defined yet', (done) => {
|
||||
protocolHandler.setProtocolWindow(null);
|
||||
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
|
||||
protocolHandler.setProtocolUrl(url);
|
||||
|
||||
protocolHandler.checkProtocolAction();
|
||||
expect(setSpy).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
@ -1,2 +0,0 @@
|
||||
# to override dependencies
|
||||
- use https://github.com/thlorenz/proxyquire
|
@ -1,134 +0,0 @@
|
||||
const { ScreenSnippet, readResult } = require('../js/screenSnippet');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
const { isMac } = require('../js/utils/misc.js');
|
||||
|
||||
const snippetBase64 = require('./fixtures/snippet/snippet-base64.js');
|
||||
|
||||
// mock child_process used in ScreenSnippet
|
||||
jest.mock('child_process', function() {
|
||||
return {
|
||||
execFile: mockedExecFile
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* mock version of execFile just creates a copy of a test jpeg file.
|
||||
*/
|
||||
function mockedExecFile(util, args, doneCallback) {
|
||||
let outputFileName = args[args.length - 1];
|
||||
|
||||
copyTestFile(outputFileName, function(copyTestFile) {
|
||||
doneCallback();
|
||||
});
|
||||
}
|
||||
|
||||
function copyTestFile(destFile, done) {
|
||||
const testfile = path.join(__dirname,
|
||||
'fixtures/snippet/ScreenSnippet.jpeg');
|
||||
|
||||
let reader = fs.createReadStream(testfile);
|
||||
let writer = fs.createWriteStream(destFile);
|
||||
|
||||
writer.on('close', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
reader.pipe(writer);
|
||||
}
|
||||
|
||||
function createTestFile(done) {
|
||||
let tmpDir = os.tmpdir();
|
||||
const testFileName = path.join(tmpDir,
|
||||
'ScreenSnippet-' + Date.now() + '.jpeg');
|
||||
|
||||
copyTestFile(testFileName, function() {
|
||||
done(testFileName)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
describe('Tests for ScreenSnippet', function() {
|
||||
describe('when reading a valid jpeg file', function() {
|
||||
|
||||
// skip test for windows - until feature is supported
|
||||
if (isMac) {
|
||||
it('should match base64 output', function(done) {
|
||||
let s = new ScreenSnippet();
|
||||
s.capture().then(gotImage);
|
||||
|
||||
function gotImage(rsp) {
|
||||
expect(rsp.type).toEqual('image/jpg;base64');
|
||||
expect(rsp.data).toEqual(snippetBase64);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('should remove output file after completed', function(done) {
|
||||
createTestFile(function(testfileName) {
|
||||
readResult(testfileName, resolve);
|
||||
|
||||
function resolve() {
|
||||
// should be long enough before file
|
||||
// gets removed
|
||||
setTimeout(function() {
|
||||
let exists = fs.existsSync(testfileName);
|
||||
expect(exists).toBe(false);
|
||||
done();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if output file does not exist', function(done) {
|
||||
let nonExistentFile = 'bogus.jpeg';
|
||||
readResult(nonExistentFile, resolve, reject);
|
||||
|
||||
function resolve() {
|
||||
// shouldn't get here
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
|
||||
function reject(err) {
|
||||
expect(err).toBeTruthy();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// skip test for windows - until feature is supported
|
||||
if (isMac) {
|
||||
it('should fail if read file fails', function(done) {
|
||||
const origFsReadFile = fs.readFile;
|
||||
|
||||
fs.readFile = jest.fn(mockedReadFile);
|
||||
|
||||
function mockedReadFile(filename, callback) {
|
||||
callback(new Error('failed'));
|
||||
}
|
||||
|
||||
let s = new ScreenSnippet();
|
||||
s.capture().then(resolved).catch(rejected);
|
||||
|
||||
function resolved(err) {
|
||||
cleanup();
|
||||
// shouldn't get here
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
|
||||
function rejected(err) {
|
||||
expect(err).toBeTruthy();
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
fs.readFile = origFsReadFile;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -1,80 +0,0 @@
|
||||
const path = require('path');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
let ipcEmitter = new EventEmitter();
|
||||
|
||||
// use config provided by test framework
|
||||
function pathToConfigDir() {
|
||||
return path.join(__dirname, '/../fixtures');
|
||||
}
|
||||
|
||||
// electron app mock...
|
||||
const app = {
|
||||
getAppPath: pathToConfigDir,
|
||||
getPath: function(type) {
|
||||
if (type === 'exe') {
|
||||
return path.join(pathToConfigDir(), '/Symphony.exe');
|
||||
}
|
||||
return pathToConfigDir();
|
||||
},
|
||||
on: function() {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
// simple ipc mocks for render and main process ipc using
|
||||
// nodes' EventEmitter
|
||||
const ipcMain = {
|
||||
on: function(event, cb) {
|
||||
ipcEmitter.on(event, cb);
|
||||
},
|
||||
send: function (event, args) {
|
||||
const senderEvent = {
|
||||
sender: {
|
||||
send: function (event, arg) {
|
||||
ipcEmitter.emit(event, arg);
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcEmitter.emit(event, senderEvent, args);
|
||||
},
|
||||
};
|
||||
|
||||
const ipcRenderer = {
|
||||
sendSync: function(event, args) {
|
||||
let listeners = ipcEmitter.listeners(event);
|
||||
if (listeners.length > 0) {
|
||||
let listener = listeners[0];
|
||||
const eventArg = {};
|
||||
listener(eventArg, args);
|
||||
return eventArg.returnValue;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
send: function(event, args) {
|
||||
const senderEvent = {
|
||||
sender: {
|
||||
send: function (event, arg) {
|
||||
ipcEmitter.emit(event, arg);
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcEmitter.emit(event, senderEvent, args);
|
||||
},
|
||||
on: function(eventName, cb) {
|
||||
ipcEmitter.on(eventName, cb);
|
||||
},
|
||||
removeListener: function(eventName, cb) {
|
||||
ipcEmitter.removeListener(eventName, cb);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
require: jest.fn(),
|
||||
match: jest.fn(),
|
||||
app: app,
|
||||
ipcMain: ipcMain,
|
||||
ipcRenderer: ipcRenderer,
|
||||
remote: jest.fn(),
|
||||
dialog: jest.fn()
|
||||
};
|
@ -1,84 +0,0 @@
|
||||
const electron = require('./__mocks__/electron');
|
||||
const activityDetection = require('../js/activityDetection');
|
||||
describe('Tests for Activity Detection', function() {
|
||||
|
||||
const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
|
||||
|
||||
beforeAll(function(done) {
|
||||
electron.app.isReady = jest.fn().mockReturnValue(true);
|
||||
electron.powerMonitor = { querySystemIdleTime: jest.fn() }
|
||||
done();
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
jest.clearAllMocks()
|
||||
});
|
||||
|
||||
afterAll(function(done) {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
|
||||
done();
|
||||
});
|
||||
|
||||
it('should send activity event', function() {
|
||||
const spy = jest.spyOn(activityDetection, 'send');
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
activityDetection.send({ systemIdleTime: 120000 });
|
||||
expect(spy).toHaveBeenCalledWith({ systemIdleTime: 120000 });
|
||||
|
||||
});
|
||||
|
||||
it('should monitor user activity', function() {
|
||||
activityDetection.setActivityWindow(500000, electron.ipcRenderer);
|
||||
const spy = jest.spyOn(activityDetection, 'monitorUserActivity');
|
||||
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
activityDetection.monitorUserActivity();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
it('should start `activityDetection()`', () => {
|
||||
const spy = jest.spyOn(activityDetection, 'activityDetection');
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
activityDetection.activityDetection();
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not send activity event as data is undefined', function() {
|
||||
const spy = jest.spyOn(activityDetection, 'send');
|
||||
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
activityDetection.send(undefined);
|
||||
expect(spy).toHaveBeenCalledWith(undefined);
|
||||
|
||||
});
|
||||
|
||||
it('should call `send()` when period was greater than idleTime', () => {
|
||||
|
||||
const spy = jest.spyOn(activityDetection, 'send');
|
||||
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
electron.powerMonitor = { querySystemIdleTime: jest.fn().mockImplementationOnce(cb => cb(1)) };
|
||||
activityDetection.setActivityWindow(900000, electron.ipcRenderer);
|
||||
|
||||
expect(spy).toBeCalled();
|
||||
|
||||
});
|
||||
|
||||
it('should start `activityDetection()` when `setActivityWindow()` was called', () => {
|
||||
const spy = jest.spyOn(activityDetection, 'activityDetection');
|
||||
|
||||
expect(spy).not.toBeCalled();
|
||||
|
||||
activityDetection.setActivityWindow(900000, electron.ipcRenderer);
|
||||
|
||||
expect(spy).toBeCalled();
|
||||
|
||||
});
|
||||
});
|
@ -1,322 +0,0 @@
|
||||
const { clearCachedConfigs, getConfigField, updateConfigField, configFileName, saveUserConfig } = require('../js/config');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// mock required so getConfig reads config from correct path
|
||||
jest.mock('../js/utils/misc.js', function() {
|
||||
return {
|
||||
isDevEnv: false,
|
||||
isMac: false
|
||||
}
|
||||
});
|
||||
|
||||
let globalConfigDir;
|
||||
let userConfigDir;
|
||||
|
||||
jest.mock('electron', function() {
|
||||
return {
|
||||
app: {
|
||||
getPath: mockedGetPath,
|
||||
getVersion: mockedGetVersion
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function mockedGetVersion() {
|
||||
return '3.1.0';
|
||||
}
|
||||
|
||||
function mockedGetPath(type) {
|
||||
if (type === 'exe') {
|
||||
return globalConfigDir;
|
||||
}
|
||||
|
||||
if (type === 'userData') {
|
||||
return userConfigDir
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
describe('read/write config tests', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
/// reset module vars between running tests.
|
||||
globalConfigDir = null;
|
||||
userConfigDir = null;
|
||||
|
||||
// reset module values so each test starts clean.
|
||||
clearCachedConfigs();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// clean up temp files creating during tests
|
||||
if (globalConfigDir) {
|
||||
fs.unlinkSync(path.join(globalConfigDir, configFileName));
|
||||
fs.rmdirSync(globalConfigDir);
|
||||
fs.rmdirSync(path.join(globalConfigDir, '..'));
|
||||
}
|
||||
if (userConfigDir) {
|
||||
fs.unlinkSync(path.join(userConfigDir, configFileName));
|
||||
fs.rmdirSync(userConfigDir);
|
||||
}
|
||||
});
|
||||
|
||||
function createTempConfigFile(filePath, config) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(config));
|
||||
}
|
||||
|
||||
function createTempUserConfig(config) {
|
||||
const tmpDir = os.tmpdir();
|
||||
userConfigDir = fs.mkdtempSync(path.join(tmpDir, 'config-'));
|
||||
return createTempConfigFile(path.join(userConfigDir, configFileName), config);
|
||||
}
|
||||
|
||||
function createTempGlobalConfig(config) {
|
||||
const tmpDir = os.tmpdir();
|
||||
globalConfigDir = path.join(fs.mkdtempSync(path.join(tmpDir, 'config-')), 'config');
|
||||
fs.mkdirSync(globalConfigDir);
|
||||
return createTempConfigFile(path.join(globalConfigDir, configFileName), config);
|
||||
}
|
||||
|
||||
function removeTempConfigFile(filePath) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
describe('getConfigField tests', function() {
|
||||
it('should fail when field not present in either user or global config', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
const globalConfig = {
|
||||
url: 'something-else'
|
||||
};
|
||||
|
||||
createTempGlobalConfig(globalConfig);
|
||||
|
||||
return getConfigField('noturl').catch(function(err) {
|
||||
expect(err).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when field only present in user config', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return getConfigField('url').then(function(url) {
|
||||
expect(url).toBe('something');
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when field only present in global config', function() {
|
||||
const globalConfig = {
|
||||
url: 'something-else'
|
||||
};
|
||||
|
||||
createTempGlobalConfig(globalConfig);
|
||||
|
||||
return getConfigField('url').then(function(url) {
|
||||
expect(url).toBe('something-else');
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed and return user config field when value is in both', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
const globalConfig = {
|
||||
url: 'something-else'
|
||||
};
|
||||
|
||||
createTempGlobalConfig(globalConfig);
|
||||
|
||||
return getConfigField('url').then(function(url) {
|
||||
expect(url).toBe('something');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when global config path is invalid', function() {
|
||||
const globalConfig = {
|
||||
url: 'something-else'
|
||||
};
|
||||
createTempGlobalConfig(globalConfig);
|
||||
|
||||
let correctConfigDir = globalConfigDir;
|
||||
globalConfigDir = '//';
|
||||
|
||||
return getConfigField('url').catch(function(err) {
|
||||
globalConfigDir = correctConfigDir;
|
||||
expect(err).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when user config path is invalid', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
let correctConfigDir = userConfigDir;
|
||||
userConfigDir = '//';
|
||||
|
||||
return getConfigField('url').catch(function(err) {
|
||||
userConfigDir = correctConfigDir;
|
||||
expect(err).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should read cached user config value rather than reading file from disk again', function(done) {
|
||||
const userConfig = {
|
||||
url: 'qa4.symphony.com'
|
||||
};
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
const userConfig2 = {
|
||||
url: 'qa5.symphony.com'
|
||||
};
|
||||
|
||||
return getConfigField('url')
|
||||
.then(function() {
|
||||
createTempUserConfig(userConfig2);
|
||||
})
|
||||
.then(function() {
|
||||
return getConfigField('url')
|
||||
})
|
||||
.then(function(url) {
|
||||
expect(url).toBe('qa4.symphony.com');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should read cache global config value rather than reading file from disk again', function(done) {
|
||||
const globalConfig = {
|
||||
url: 'qa8.symphony.com'
|
||||
};
|
||||
createTempGlobalConfig(globalConfig);
|
||||
|
||||
const globalConfig2 = {
|
||||
url: 'qa9.symphony.com'
|
||||
};
|
||||
|
||||
return getConfigField('url')
|
||||
.then(function() {
|
||||
createTempGlobalConfig(globalConfig2);
|
||||
})
|
||||
.then(function() {
|
||||
return getConfigField('url')
|
||||
})
|
||||
.then(function(url) {
|
||||
expect(url).toBe('qa8.symphony.com');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('updateConfigField tests', function() {
|
||||
|
||||
it('should succeed and overwrite existing field', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return updateConfigField('url', 'hello world')
|
||||
.then(function(newConfig) {
|
||||
expect(newConfig).toEqual({
|
||||
url: 'hello world'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed and add new field', function() {
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return updateConfigField('url2', 'hello world')
|
||||
.then(function(newConfig) {
|
||||
expect(newConfig).toEqual({
|
||||
url: 'something',
|
||||
url2: 'hello world'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to update if invalid field name', function() {
|
||||
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return updateConfigField('', 'hello word').catch(function (err) {
|
||||
expect(err.message).toBe('can not save config, invalid input');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw error if path is not defined', function() {
|
||||
|
||||
userConfigDir = null;
|
||||
|
||||
return updateConfigField('url2', 'hello world')
|
||||
.catch(function (err) {
|
||||
expect(err.message).toBe('The "path" argument must be of type string. Received type object');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw error if fieldName is not defined', function() {
|
||||
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return saveUserConfig(undefined, 'something', 'oldConfig')
|
||||
.catch(function (reject) {
|
||||
expect(reject.message).toBe('can not save config, invalid input');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if config is not defined', function() {
|
||||
|
||||
const userConfig = {
|
||||
url: 'something'
|
||||
};
|
||||
|
||||
createTempUserConfig(userConfig);
|
||||
|
||||
return saveUserConfig('url2', 'something', undefined)
|
||||
.catch(function (reject) {
|
||||
expect(reject.message).toBe('can not save config, invalid input');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if path is not defined for saveUserConfig()', function() {
|
||||
|
||||
userConfigDir = null;
|
||||
|
||||
return saveUserConfig('url2', 'hello world')
|
||||
.catch(function (err) {
|
||||
expect(err.message).toBe('The "path" argument must be of type string. Received type object');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
BIN
tests/fixtures/snippet/ScreenSnippet.jpeg
vendored
BIN
tests/fixtures/snippet/ScreenSnippet.jpeg
vendored
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user