mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
notifications implementation (#34)
* wip: notifications * wip: more notifications * wip: add getter to proxy * wip: add setter * wip: add static getter and method * wip: add event handlers * wip: add doc * wip: add api demo
This commit is contained in:
@@ -12,6 +12,10 @@ Our goal is to improve the performance and development agility of Symphony's des
|
||||
|
||||
In order to achieve those goals Symphony is participating and working in close collaboration with the [Foundation Desktop Wrapper Working Group](https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/Working+Group+-+Desktop+Wrapper+API)
|
||||
|
||||
## Run demo:
|
||||
- npm install
|
||||
- on mac: npm run demo:mac
|
||||
- on windows: npm run demo:win
|
||||
|
||||
## Build Instructions:
|
||||
- to pick up dependencies: npm install
|
||||
|
4
demo/README.md
Normal file
4
demo/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
To start the demo from command line do:
|
||||
- npm install
|
||||
- npm run demo:mac (on mac)
|
||||
- npm run demo:win (on windows)
|
75
demo/index.html
Normal file
75
demo/index.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Symphony Electron API Demo</h1>
|
||||
<hr>
|
||||
<p>Notifications:<p>
|
||||
<button id='notf'>show notification</button>
|
||||
<p>
|
||||
<label for='title'>title:</label>
|
||||
<input type='text' id='title' value='Notification Demo'/>
|
||||
</p>
|
||||
<p>
|
||||
<label for='body'>body:</label>
|
||||
<input type='text' id='body' value='Some message'/>
|
||||
</p>
|
||||
<p>
|
||||
<label for='image'>image url:</label>
|
||||
<input type='text' id='image' value='https://lh3.googleusercontent.com/-s2PXL6wWMCc/AAAAAAAAAAI/AAAAAAAAAAA/AAomvV2gUNMMeFsOijwVVpihfg_anpKWQA/s32-c-mo/photo.jpg'/>
|
||||
</p>
|
||||
<p>
|
||||
<label for='flash'>flash:</label>
|
||||
<input type='checkbox' id='flash'/>
|
||||
</p>
|
||||
<p>
|
||||
<label for='color'>color:</label>
|
||||
<input type='text' id='color' value='white'/>
|
||||
<br>
|
||||
<hr>
|
||||
<p>Badge Count:<p>
|
||||
<button id='inc-badge'>increment badge count</button>
|
||||
<br>
|
||||
<button id='clear-badge'>clear badge count</button>
|
||||
</body>
|
||||
<script>
|
||||
var notfEl = document.getElementById('notf');
|
||||
notfEl.addEventListener('click', function() {
|
||||
var title = document.getElementById('title').value;
|
||||
var body = document.getElementById('body').value;
|
||||
var imageUrl = document.getElementById('image').value;
|
||||
var shouldFlash = document.getElementById('flash').checked;
|
||||
var color = document.getElementById('color').value;
|
||||
|
||||
var notf = new SYM_API.Notification(title, {
|
||||
body: body,
|
||||
image: imageUrl,
|
||||
flash: shouldFlash,
|
||||
color: color || 'white'
|
||||
});
|
||||
|
||||
notf.addEventListener('click', function() {
|
||||
alert('notification clicked');
|
||||
});
|
||||
|
||||
notf.addEventListener('close', function() {
|
||||
alert('notification closed');
|
||||
});
|
||||
});
|
||||
|
||||
var badgeCount = 0;
|
||||
|
||||
var incBadgeEl = document.getElementById('inc-badge');
|
||||
incBadgeEl.addEventListener('click', function() {
|
||||
badgeCount++;
|
||||
SYM_API.setBadgeCount(badgeCount);
|
||||
});
|
||||
|
||||
var incBadgeEl = document.getElementById('clear-badge');
|
||||
incBadgeEl.addEventListener('click', function() {
|
||||
badgeCount = 0;
|
||||
SYM_API.setBadgeCount(0);
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
@@ -7,10 +7,23 @@ const cmds = keyMirror({
|
||||
open: null,
|
||||
registerLogger: null,
|
||||
setBadgeCount: null,
|
||||
badgeDataUrl: null,
|
||||
badgeDataUrl: null
|
||||
});
|
||||
|
||||
const proxyCmds = keyMirror({
|
||||
createObject: null,
|
||||
addEvent: null,
|
||||
removeEvent: null,
|
||||
eventCallback: null,
|
||||
invokeMethod: null,
|
||||
invokeResult: null,
|
||||
get: null,
|
||||
getResult: null,
|
||||
set: null
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
cmds: cmds,
|
||||
proxyCmds: proxyCmds,
|
||||
apiName: 'symphony-api'
|
||||
}
|
||||
|
16
js/main.js
16
js/main.js
@@ -6,7 +6,7 @@ const nodeURL = require('url');
|
||||
const squirrelStartup = require('electron-squirrel-startup');
|
||||
|
||||
const getConfig = require('./getConfig.js');
|
||||
const { isMac } = require('./utils/misc.js');
|
||||
const { isMac, isDevEnv } = require('./utils/misc.js');
|
||||
|
||||
// exit early for squirrel installer
|
||||
if (squirrelStartup) {
|
||||
@@ -28,6 +28,20 @@ const windowMgr = require('./windowMgr.js');
|
||||
app.on('ready', getUrlAndOpenMainWindow);
|
||||
|
||||
function getUrlAndOpenMainWindow() {
|
||||
// for dev env allow passing url argument
|
||||
if (isDevEnv) {
|
||||
let url;
|
||||
process.argv.forEach((val) => {
|
||||
if (val.startsWith('--url=')) {
|
||||
url = val.substr(6);
|
||||
}
|
||||
});
|
||||
if (url) {
|
||||
windowMgr.createMainWindow(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getConfig().then(function(config) {
|
||||
let protocol = '';
|
||||
// add https protocol if none found.
|
||||
|
355
js/mainApiMgr.js
355
js/mainApiMgr.js
@@ -13,6 +13,7 @@ const badgeCount = require('./badgeCount.js');
|
||||
const apiEnums = require('./enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
const apiProxyCmds = apiEnums.proxyCmds
|
||||
|
||||
/**
|
||||
* Ensure events comes from a window that we have created.
|
||||
@@ -20,6 +21,7 @@ const apiName = apiEnums.apiName;
|
||||
* @return {Boolean} returns true if exists otherwise false
|
||||
*/
|
||||
function isValidWindow(event) {
|
||||
var result = false;
|
||||
if (event && event.sender) {
|
||||
// validate that event sender is from window we created
|
||||
const browserWin = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
@@ -27,10 +29,16 @@ function isValidWindow(event) {
|
||||
event.sender.browserWindowOptions.webPreferences &&
|
||||
event.sender.browserWindowOptions.webPreferences.winKey;
|
||||
|
||||
return windowMgr.hasWindow(browserWin, winKey);
|
||||
result = windowMgr.hasWindow(browserWin, winKey);
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!result) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('invalid window try to perform action, ignoring action.');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,9 +47,6 @@ function isValidWindow(event) {
|
||||
*/
|
||||
electron.ipcMain.on(apiName, (event, arg) => {
|
||||
if (!isValidWindow(event)) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('invalid window try to perform action, ignoring action.');
|
||||
/* eslint-enable no-console */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,3 +83,343 @@ electron.ipcMain.on(apiName, (event, arg) => {
|
||||
windowMgr.createChildWindow(arg.url, title, width, height);
|
||||
}
|
||||
});
|
||||
|
||||
const Notify = require('./notify/notifyImpl.js');
|
||||
|
||||
// holds all project classes that can be created.
|
||||
const api = {
|
||||
Notify: Notify
|
||||
}
|
||||
|
||||
// holds all proxy object instances
|
||||
let liveObjs = {};
|
||||
|
||||
let id = 1;
|
||||
function uniqueId() {
|
||||
return id++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instance of given class for proxy in renderer process.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* className {String} name of class to create.
|
||||
* }
|
||||
*
|
||||
* @return {Number} unique id for class intance created.
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.createObject, function(event, args) {
|
||||
if (!isValidWindow(event)) {
|
||||
setResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
function setResult(value) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
event.returnValue = value;
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
if (args.className && api[args.className]) {
|
||||
var obj = new api[args.className](...args.constructorArgsArray);
|
||||
obj._callbacks = {};
|
||||
|
||||
let objId = uniqueId();
|
||||
liveObjs[objId] = obj;
|
||||
|
||||
obj.addEventListener('destroy', function() {
|
||||
var liveObj = liveObjs[objId];
|
||||
if (liveObj) {
|
||||
delete liveObjs[objId];
|
||||
}
|
||||
});
|
||||
|
||||
setResult(objId);
|
||||
} else {
|
||||
setResult(null);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Invokes a method for the proxy.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* objId {Number} id of object previously created, not needed for static call.
|
||||
* invokeId {Number} id used by proxy to uniquely identify this method call
|
||||
* methodName {String} name of method to call
|
||||
* arguments: {Array} arguments to invoke method with.
|
||||
* isStatic: {bool} true is this is a static method.
|
||||
* className {String} name of class on which func is invoked.
|
||||
* }
|
||||
*
|
||||
* @return {Object} {
|
||||
* returnValue {Object} result of calling method
|
||||
* invokeId {Number} id so proxy can identify method call
|
||||
* }
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.invokeMethod, function(event, args) {
|
||||
if (!isValidWindow(event) || !args.invokeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.isStatic && (!args.objId || !liveObjs[args.objId])) {
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
error: 'calling obj is not present',
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// static method call must have className and class must exist in api
|
||||
if (args.isStatic) {
|
||||
if (!args.className) {
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
error: 'for static method must provide class name',
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!api[args.className]) {
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
error: 'no class exists: ' + args.className,
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let result;
|
||||
let funcArgs = args.arguments || [];
|
||||
|
||||
if (args.isStatic) {
|
||||
let classType = api[args.className];
|
||||
|
||||
if (!args.methodName || !classType[args.methodName]) {
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
error: 'no such static method',
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
result = classType[args.methodName](...funcArgs);
|
||||
} else {
|
||||
let obj = liveObjs[args.objId];
|
||||
|
||||
if (!args.methodName || !obj[args.methodName]) {
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
error: 'no such method',
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// special method to lose ref to obj
|
||||
if (args.methodName === 'destroy') {
|
||||
delete liveObjs[args.objId];
|
||||
}
|
||||
|
||||
result = obj[args.methodName](...funcArgs);
|
||||
}
|
||||
|
||||
event.sender.send(apiProxyCmds.invokeResult, {
|
||||
returnValue: result,
|
||||
invokeId: args.invokeId
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Getter implementation. Allows proxy to retrieve value from implementation
|
||||
* object.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* objId {Number} id of object previously created (not needed for static get)
|
||||
* getterId {Number} id used by proxy to uniquely identify this getter call.
|
||||
* getterProperty {String} name of getter property to retrieve.
|
||||
* isStatic {boo} true if this if getter is for static property.
|
||||
* className {String} name of class we are operating on here.
|
||||
* }
|
||||
*
|
||||
* @return {Object} {
|
||||
* returnValue {Object} result of calling method
|
||||
* getterId {Number} id so proxy can identify getter call
|
||||
* }
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.get, function(event, args) {
|
||||
if (!isValidWindow(event) || !args.getterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// non-static calls must have an live instance available
|
||||
if (!args.isStatic && (!args.objId || !liveObjs[args.objId])) {
|
||||
event.sender.send(apiProxyCmds.getResult, {
|
||||
error: 'calling obj is not present',
|
||||
getterId: args.getterId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// static get must have className and class must exist in api
|
||||
if (args.isStatic) {
|
||||
if (!args.className) {
|
||||
event.sender.send(apiProxyCmds.getResult, {
|
||||
error: 'for static getter must provide class name',
|
||||
getterId: args.getterId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!api[args.className]) {
|
||||
event.sender.send(apiProxyCmds.getResult, {
|
||||
error: 'no class exists: ' + args.className,
|
||||
getterId: args.getterId
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.getterProperty) {
|
||||
event.sender.send(apiProxyCmds.getResult, {
|
||||
error: 'property name not provided',
|
||||
getterId: args.getterId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
if (args.isStatic) {
|
||||
let classType = api[args.className]
|
||||
result = classType[args.getterProperty];
|
||||
} else {
|
||||
let obj = liveObjs[args.objId];
|
||||
result = obj[args.getterProperty];
|
||||
}
|
||||
|
||||
event.sender.send(apiProxyCmds.getResult, {
|
||||
returnValue: result,
|
||||
getterId: args.getterId
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Setter implementation. Allows proxy to set value on implementation object.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* objId {Number} id of object previously created.
|
||||
* setterProperty {String} name of setter property.
|
||||
* setterValue {object} new value to set.
|
||||
* }
|
||||
*
|
||||
* @return {Object} input setter value
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.set, function(event, args) {
|
||||
if (!isValidWindow(event)) {
|
||||
setResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.objId || !liveObjs[args.objId]) {
|
||||
setResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.setterProperty) {
|
||||
setResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
function setResult(value) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
event.returnValue = value;
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
let obj = liveObjs[args.objId];
|
||||
obj[args.setterProperty] = args.setterValue;
|
||||
setResult(args.setterValue);
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens to an event and calls back to renderer proxy when given event occurs.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* objId {Number} id of object previously created.
|
||||
* callbackId {Number} id used by proxy to uniquely identify this event.
|
||||
* eventName {String} name of event to listen for.
|
||||
* }
|
||||
*
|
||||
* @return {Object} {
|
||||
* result {Object} result from invoking callback.
|
||||
* callbackId {Number} id so proxy can identify event that occurred.
|
||||
* }
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.addEvent, function(event, args) {
|
||||
if (!isValidWindow(event)) {
|
||||
return;
|
||||
}
|
||||
/* eslint-disable no-console */
|
||||
if (!args.objId || !liveObjs[args.objId]) {
|
||||
console.log('calling obj is not present');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.callbackId) {
|
||||
console.log('no callback id provided');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.eventName) {
|
||||
console.log('no eventName provided');
|
||||
return;
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
let obj = liveObjs[args.objId];
|
||||
let callbackFunc = function(result) {
|
||||
event.sender.send(apiProxyCmds.eventCallback, {
|
||||
callbackId: args.callbackId,
|
||||
result: result
|
||||
});
|
||||
}
|
||||
obj._callbacks[args.callbackId] = callbackFunc;
|
||||
obj.addEventListener(args.eventName, callbackFunc);
|
||||
});
|
||||
|
||||
/**
|
||||
* Stops listening to given event.
|
||||
*
|
||||
* @param {Object} args {
|
||||
* objId {Number} id of object previously created.
|
||||
* callbackId {Number} id used by proxy to uniquely identify this event.
|
||||
* eventName {String} name of event to listen for.
|
||||
* }
|
||||
*/
|
||||
electron.ipcMain.on(apiProxyCmds.removeEvent, function(event, args) {
|
||||
if (!isValidWindow(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
if (!args.objId || !liveObjs[args.objId]) {
|
||||
console.log('calling obj is not present');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.callbackId) {
|
||||
console.log('no callback id provided');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.eventName) {
|
||||
console.log('no eventName provided');
|
||||
return;
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
let obj = liveObjs[args.objId];
|
||||
let callbackFunc = obj._callbacks[args.callbackId];
|
||||
if (typeof callbackFunc === 'function') {
|
||||
obj.removeEventListener(args.eventName, callbackFunc);
|
||||
}
|
||||
});
|
||||
|
41
js/notify/AnimationQueue.js
Normal file
41
js/notify/AnimationQueue.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
// One animation at a time
|
||||
const AnimationQueue = function(options) {
|
||||
this.options = options;
|
||||
this.queue = [];
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
AnimationQueue.prototype.push = function(object) {
|
||||
if (this.running) {
|
||||
this.queue.push(object);
|
||||
} else {
|
||||
this.running = true;
|
||||
this.animate(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) {
|
||||
/* eslint-disable no-console */
|
||||
console.error('animation queue encountered an error: ' + err +
|
||||
' with stack trace:' + err.stack);
|
||||
/* eslint-enable no-console */
|
||||
})
|
||||
}
|
||||
|
||||
AnimationQueue.prototype.clear = function() {
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
module.exports = AnimationQueue;
|
148
js/notify/electron-notify-preload.js
Normal file
148
js/notify/electron-notify-preload.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'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;
|
||||
|
||||
function setStyle(config) {
|
||||
// Style it
|
||||
let notiDoc = window.document;
|
||||
let container = notiDoc.getElementById('container');
|
||||
let header = notiDoc.getElementById('header');
|
||||
let image = notiDoc.getElementById('image');
|
||||
let title = notiDoc.getElementById('title');
|
||||
let message = notiDoc.getElementById('message');
|
||||
let close = notiDoc.getElementById('close');
|
||||
|
||||
// Default style
|
||||
setStyleOnDomElement(config.defaultStyleContainer, container)
|
||||
|
||||
// Size and radius
|
||||
let style = {
|
||||
height: config.height - 2 * config.borderRadius - 2 * config.defaultStyleContainer.padding,
|
||||
width: config.width - 2 * config.borderRadius - 2 * config.defaultStyleContainer.padding,
|
||||
borderRadius: config.borderRadius + 'px'
|
||||
}
|
||||
setStyleOnDomElement(style, container)
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleHeader, header);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleImage, image);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleTitle, title);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleText, message);
|
||||
|
||||
setStyleOnDomElement(config.defaultStyleClose, close);
|
||||
}
|
||||
|
||||
function setContents(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) {
|
||||
log('electron-notify: ERROR could not find sound file: ' + notificationObj.sound.replace('file://', ''), e, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
let notiDoc = window.document
|
||||
|
||||
let container = notiDoc.getElementById('container');
|
||||
|
||||
if (notificationObj.color) {
|
||||
container.style.backgroundColor = notificationObj.color;
|
||||
}
|
||||
|
||||
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
|
||||
let titleDoc = notiDoc.getElementById('title')
|
||||
titleDoc.innerHTML = notificationObj.title || ''
|
||||
|
||||
// message
|
||||
let messageDoc = notiDoc.getElementById('message')
|
||||
messageDoc.innerHTML = notificationObj.text || ''
|
||||
|
||||
// Image
|
||||
let imageDoc = notiDoc.getElementById('image')
|
||||
if (notificationObj.image) {
|
||||
imageDoc.src = notificationObj.image
|
||||
} else {
|
||||
setStyleOnDomElement({ display: 'none'}, imageDoc)
|
||||
}
|
||||
|
||||
const winId = notificationObj.windowId;
|
||||
|
||||
// Close button
|
||||
let closeButton = notiDoc.getElementById('close')
|
||||
closeButton.addEventListener('click', function(clickEvent) {
|
||||
clickEvent.stopPropagation()
|
||||
ipc.send('electron-notify-close', winId, notificationObj)
|
||||
})
|
||||
|
||||
// URL
|
||||
container.addEventListener('click', function() {
|
||||
ipc.send('electron-notify-click', winId, notificationObj)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig(conf) {
|
||||
setStyle(conf || {})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
ipc.on('electron-notify-set-contents', setContents)
|
||||
ipc.on('electron-notify-load-config', loadConfig)
|
||||
ipc.on('electron-notify-reset', reset)
|
||||
|
||||
function log() {
|
||||
/* eslint-disable no-console */
|
||||
console.log.apply(console, arguments)
|
||||
/* eslint-enable no-console */
|
||||
}
|
13
js/notify/electron-notify.html
Normal file
13
js/notify/electron-notify.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body style='overflow: hidden; -webkit-user-select: none;'>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<img src="" id="image" />
|
||||
<span id="title"></span>
|
||||
</div>
|
||||
<p id="message"></p>
|
||||
<div id="close">X</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
560
js/notify/electron-notify.js
Normal file
560
js/notify/electron-notify.js
Normal file
@@ -0,0 +1,560 @@
|
||||
'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 async = require('async');
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const ipc = electron.ipcMain;
|
||||
|
||||
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 config = {
|
||||
// corner to put notifications
|
||||
// upper-right, upper-left, lower-right, lower-left
|
||||
startCorner: 'upper-right',
|
||||
width: 300,
|
||||
height: 80,
|
||||
padding: 4,
|
||||
borderRadius: 5,
|
||||
displayTime: 5000,
|
||||
animationSteps: 5,
|
||||
animationStepMs: 5,
|
||||
animateInParallel: true,
|
||||
pathToModule: '',
|
||||
logging: true,
|
||||
defaultStyleContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#f0f0f0',
|
||||
overflow: 'hidden',
|
||||
padding: 4,
|
||||
border: '1px solid #CCC',
|
||||
position: 'relative',
|
||||
lineHeight: '15px'
|
||||
},
|
||||
defaultStyleHeader: {
|
||||
flex: '0 0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
defaultStyleImage: {
|
||||
flex: '0 0 auto',
|
||||
overflow: 'hidden',
|
||||
height: 30,
|
||||
width: 30,
|
||||
marginLeft: 0,
|
||||
marginRight: 8
|
||||
},
|
||||
defaultStyleClose: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
fontSize: 12,
|
||||
color: '#CCC'
|
||||
},
|
||||
defaultStyleTitle: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 13,
|
||||
marginRight: 10,
|
||||
alignSelf: 'center',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
webkitLineClamp: 2,
|
||||
webkitBoxOrient: 'vertical',
|
||||
},
|
||||
defaultStyleText: {
|
||||
flex: '0 0 auto',
|
||||
fontFamily: 'Calibri',
|
||||
fontSize: 12,
|
||||
margin: 0,
|
||||
marginTop: 3,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
webkitLineClamp: 2,
|
||||
webkitBoxOrient: 'vertical',
|
||||
cursor: 'default'
|
||||
},
|
||||
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: true,
|
||||
nodeIntegration: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// function setConfig(customConfig) {
|
||||
// Object.assign(customConfig, config);
|
||||
//
|
||||
// calcDimensions();
|
||||
// }
|
||||
|
||||
app.on('ready', function() {
|
||||
setupConfig();
|
||||
|
||||
// if display added/removed/changed then re-run setup and remove all existing
|
||||
// notifications. ToDo: should reposition notifications rather than closing.
|
||||
electron.screen.on('display-added', setupConfig);
|
||||
electron.screen.on('display-removed', setupConfig);
|
||||
electron.screen.on('display-metrics-changed', setupConfig);
|
||||
});
|
||||
|
||||
|
||||
function getTemplatePath() {
|
||||
let templatePath = path.join(__dirname, 'electron-notify.html');
|
||||
try {
|
||||
fs.statSync(templatePath).isFile();
|
||||
} catch (err) {
|
||||
log('electron-notify: Could not find template ("' + templatePath + '").');
|
||||
}
|
||||
config.templatePath = 'file://' + templatePath;
|
||||
return config.templatePath;
|
||||
}
|
||||
|
||||
function calcDimensions() {
|
||||
// Calc totalHeight & totalWidth
|
||||
config.totalHeight = config.height + config.padding
|
||||
config.totalWidth = config.width + config.padding
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function setupConfig() {
|
||||
closeAll();
|
||||
|
||||
// Use primary display only
|
||||
let display = electron.screen.getPrimaryDisplay();
|
||||
|
||||
config.corner = {};
|
||||
config.corner.x = display.bounds.x + display.workArea.x;
|
||||
config.corner.y = display.bounds.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);
|
||||
config.maxVisibleNotifications = config.maxVisibleNotifications > 5 ? 5 : config.maxVisibleNotifications;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
latestID++
|
||||
animationQueue.push({
|
||||
func: showNotification,
|
||||
args: [ notf ]
|
||||
})
|
||||
return notf.id
|
||||
}
|
||||
log('electron-notify: ERROR notify() only accepts a single object with notification parameters.')
|
||||
return null;
|
||||
}
|
||||
|
||||
function showNotification(notificationObj) {
|
||||
return new Promise(function(resolve) {
|
||||
// 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)
|
||||
|
||||
// Add to activeNotifications
|
||||
activeNotifications.push(notificationWindow)
|
||||
|
||||
// Display time per notification basis.
|
||||
let displayTime = notificationObj.displayTime ? notificationObj.displayTime : config.displayTime
|
||||
|
||||
// Set timeout to hide notification
|
||||
let timeoutId
|
||||
let closeFunc = buildCloseNotification(notificationWindow, notificationObj, function() {
|
||||
return timeoutId
|
||||
})
|
||||
let closeNotificationSafely = buildCloseNotificationSafely(closeFunc)
|
||||
timeoutId = setTimeout(function() {
|
||||
closeNotificationSafely('timeout')
|
||||
}, displayTime)
|
||||
|
||||
// Trigger onShowFunc if existent
|
||||
if (notificationObj.onShowFunc) {
|
||||
notificationObj.onShowFunc({
|
||||
event: 'show',
|
||||
id: notificationObj.id,
|
||||
closeNotification: closeNotificationSafely
|
||||
})
|
||||
}
|
||||
|
||||
var updatedNotificationWindow = notificationWindow;
|
||||
|
||||
// Save onClickFunc in notification window
|
||||
if (notificationObj.onClickFunc) {
|
||||
updatedNotificationWindow.electronNotifyOnClickFunc = notificationObj.onClickFunc
|
||||
} else {
|
||||
delete updatedNotificationWindow.electronNotifyOnClickFunc
|
||||
}
|
||||
|
||||
if (notificationObj.onCloseFunc) {
|
||||
updatedNotificationWindow.electronNotifyOnCloseFunc = notificationObj.onCloseFunc
|
||||
} else {
|
||||
delete updatedNotificationWindow.electronNotifyOnCloseFunc
|
||||
}
|
||||
|
||||
const windowId = notificationWindow.id;
|
||||
// Set contents, ...
|
||||
updatedNotificationWindow.webContents.send('electron-notify-set-contents',
|
||||
Object.assign({ windowId: windowId}, notificationObj));
|
||||
// Show window
|
||||
updatedNotificationWindow.showInactive();
|
||||
resolve(updatedNotificationWindow)
|
||||
})
|
||||
} else {
|
||||
// Add to notificationQueue
|
||||
notificationQueue.push(notificationObj)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Close notification function
|
||||
function buildCloseNotification(notificationWindow, notificationObj, getTimeoutId) {
|
||||
return function(event) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Always add to animationQueue to prevent erros (e.g. notification
|
||||
// got closed while it was moving will produce an error)
|
||||
function buildCloseNotificationSafely(closeFunc) {
|
||||
return function(reason) {
|
||||
animationQueue.push({
|
||||
func: closeFunc,
|
||||
args: [ reason || 'closedByAPI' ]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ipc.on('electron-notify-close', function (event, winId, notificationObj) {
|
||||
let closeFunc = buildCloseNotification(BrowserWindow.fromId(winId), notificationObj);
|
||||
buildCloseNotificationSafely(closeFunc)('close');
|
||||
});
|
||||
|
||||
ipc.on('electron-notify-click', function (event, winId, notificationObj) {
|
||||
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 {int} startPos
|
||||
*/
|
||||
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 = async.map // Best performance
|
||||
if (config.animateInParallel === false) {
|
||||
asyncFunc = async.mapSeries // Sluggish
|
||||
}
|
||||
asyncFunc(notificationPosArray, moveNotificationAnimation, function() {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function setWindowPosition(browserWin, posX, posY) {
|
||||
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
|
||||
* @return {Window}
|
||||
*/
|
||||
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.setVisibleOnAllWorkspaces(true)
|
||||
notificationWindow.loadURL(getTemplatePath())
|
||||
notificationWindow.webContents.on('did-finish-load', function() {
|
||||
// Done
|
||||
notificationWindow.webContents.send('electron-notify-load-config', config)
|
||||
resolve(notificationWindow)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
// Clear out animation Queue and close windows
|
||||
animationQueue.clear();
|
||||
|
||||
activeNotifications.forEach(function(window) {
|
||||
if (window.electronNotifyOnCloseFunc) {
|
||||
window.electronNotifyOnCloseFunc({
|
||||
event: 'close-all'
|
||||
});
|
||||
// ToDo: fix this: shouldn't delete method on arg
|
||||
/* eslint-disable */
|
||||
delete window.electronNotifyOnCloseFunc;
|
||||
/* eslint-enable */
|
||||
}
|
||||
window.close();
|
||||
});
|
||||
|
||||
cleanUpInactiveWindow();
|
||||
|
||||
// Reset certain vars
|
||||
nextInsertPos = {};
|
||||
activeNotifications = [];
|
||||
}
|
||||
|
||||
/**
|
||||
/* once a minute, remove inactive windows to free up memory used.
|
||||
*/
|
||||
setInterval(cleanUpInactiveWindow, 60000);
|
||||
|
||||
function cleanUpInactiveWindow() {
|
||||
inactiveWindows.forEach(function(window) {
|
||||
window.close();
|
||||
});
|
||||
inactiveWindows = [];
|
||||
}
|
||||
|
||||
function log() {
|
||||
if (config.logging === true) {
|
||||
/* eslint-disable no-console */
|
||||
console.log.apply(console, arguments);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.notify = notify
|
||||
// module.exports.setConfig = setConfig
|
||||
module.exports.closeAll = closeAll
|
78
js/notify/notifyImpl.js
Normal file
78
js/notify/notifyImpl.js
Normal file
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { notify } = require('./electron-notify.js');
|
||||
|
||||
/**
|
||||
* implementation for notifications interface,
|
||||
* wrapper around electron-notify.
|
||||
*/
|
||||
class Notify {
|
||||
constructor(title, options) {
|
||||
this.emitter = new EventEmitter();
|
||||
|
||||
this._id = notify({
|
||||
title: title,
|
||||
text: options.body,
|
||||
image: options.image,
|
||||
flash: options.flash,
|
||||
color: options.color,
|
||||
onShowFunc: onShow.bind(this),
|
||||
onClickFunc: onClick.bind(this),
|
||||
onCloseFunc: onClose.bind(this)
|
||||
});
|
||||
|
||||
function onShow(arg) {
|
||||
if (arg.id === this._id) {
|
||||
this.emitter.emit('show');
|
||||
this._closeNotification = arg.closeNotification;
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(arg) {
|
||||
if (arg.id === this._id) {
|
||||
this.emitter.emit('click');
|
||||
}
|
||||
}
|
||||
|
||||
function onClose(arg) {
|
||||
if (arg.id === this._id || arg.event === 'close-all') {
|
||||
this.emitter.emit('close');
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (typeof this._closeNotification === 'function') {
|
||||
this._closeNotification('close');
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
static get permission() {
|
||||
return 'granted';
|
||||
}
|
||||
|
||||
addEventListener(event, cb) {
|
||||
if (event && typeof cb === 'function') {
|
||||
this.emitter.on(event, cb);
|
||||
}
|
||||
}
|
||||
|
||||
removeEventListener(event, cb) {
|
||||
if (event && typeof cb === 'function') {
|
||||
this.emitter.removeEventListener(event, cb);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// private stuff below here
|
||||
//
|
||||
}
|
||||
|
||||
module.exports = Notify;
|
65
js/notify/notifyInterface.js
Normal file
65
js/notify/notifyInterface.js
Normal file
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* interface defn for notifications. Implementation of this interface
|
||||
* is in notifyImpl.js
|
||||
*
|
||||
* Used by preloadMain.js to create proxy for actual implementation.
|
||||
*
|
||||
* Keep interface here in sync with implementation, in order
|
||||
* to expose methods/props to renderer.
|
||||
*
|
||||
* Note: getters and method calls here return a promise.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
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 notifications
|
||||
* flash {bool} true if notification should flash (default false)
|
||||
* color {string} background color for notification
|
||||
* }
|
||||
*/
|
||||
constructor(title, options) {}
|
||||
|
||||
/**
|
||||
* close notification
|
||||
*/
|
||||
close() {}
|
||||
|
||||
/**
|
||||
* call to clean up ref held by main process to notification.
|
||||
* note: calling close will also invoke destroy.
|
||||
*/
|
||||
destroy() {}
|
||||
|
||||
/**
|
||||
* This returns a promise and is always 'granted'
|
||||
* @return {promise} promise fullfilled with 'granted'
|
||||
*/
|
||||
static get permission() {}
|
||||
|
||||
/**
|
||||
* add event listeners for 'click' and 'close' events
|
||||
*
|
||||
* @param {String} event event to listen for
|
||||
* @param {func} cb callback invoked when event occurs
|
||||
*/
|
||||
addEventListener(event, cb) {}
|
||||
|
||||
/**
|
||||
* remove event listeners for 'click' and 'close' events
|
||||
*
|
||||
* @param {String} event event to stop listening for.
|
||||
* @param {func} cb callback associated with original addEventListener
|
||||
*/
|
||||
removeEventListener(event, cb) {}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
module.exports = Notify;
|
262
js/preload/createProxy.js
Normal file
262
js/preload/createProxy.js
Normal file
@@ -0,0 +1,262 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const apiEnums = require('../enums/api.js');
|
||||
const proxyCmds = apiEnums.proxyCmds;
|
||||
|
||||
/**
|
||||
* Creates and returns a proxy (in renderer process) that will use IPC
|
||||
* with main process where "real" instance is created.
|
||||
*
|
||||
* The constructor is executed synchronously, so take care to not block
|
||||
* processes.
|
||||
*
|
||||
* All method calls will be sent over IPC to main process, evaulated and
|
||||
* result returned back to main process. Method calls return a promise.
|
||||
*
|
||||
* Special method calls: "addEventListener" and "removeEventListener" allow
|
||||
* attaching/detaching to events.
|
||||
*
|
||||
* Getters (e.g., x.y) will return a promise that gets fullfilled
|
||||
* when ipc returns value.
|
||||
*
|
||||
* Setters are synchronously executed (so take care).
|
||||
*
|
||||
* Note: The "real" instance should implement a destroy method (e.g., close) that
|
||||
* should be used to destroy the instance held in main process, otherwise a
|
||||
* memory leak will occur - as renderer can not know when instance is no longer
|
||||
* used.
|
||||
*
|
||||
* @param {Class} ApiClass reference to prototype/class constructor.
|
||||
* @return {object} proxy for ApiClass.
|
||||
*/
|
||||
function createProxy(ApiClass) {
|
||||
return new Proxy(ApiClass, constructorHandler);
|
||||
}
|
||||
|
||||
let id = 1;
|
||||
function uniqueId() {
|
||||
return id++;
|
||||
}
|
||||
|
||||
let constructorHandler = {
|
||||
construct: function(target, argumentsList) {
|
||||
var arg = {
|
||||
className: target.name,
|
||||
constructorArgsArray: argumentsList
|
||||
};
|
||||
|
||||
var objId = ipcRenderer.sendSync(proxyCmds.createObject, arg);
|
||||
|
||||
if (!objId) {
|
||||
throw new Error('can not create obj: ' + target.name);
|
||||
}
|
||||
|
||||
var ProxyClass = new target();
|
||||
ProxyClass._objId = objId;
|
||||
ProxyClass._callbacks = new WeakMap();
|
||||
|
||||
let instanceHandler = {
|
||||
get: instanceGetHandler,
|
||||
set: instanceSetHandler
|
||||
}
|
||||
|
||||
return new Proxy(ProxyClass, instanceHandler);
|
||||
},
|
||||
|
||||
// static getter and method handler
|
||||
get: staticGetHandler
|
||||
}
|
||||
|
||||
function instanceGetHandler(target, name) {
|
||||
// all methods and getters we support should be on the prototype
|
||||
let prototype = Reflect.getPrototypeOf(target);
|
||||
let desc = Object.getOwnPropertyDescriptor(prototype, name);
|
||||
|
||||
// does this have a "getter"
|
||||
if (desc && desc.get) {
|
||||
return getHandler(target, name, false);
|
||||
}
|
||||
// does this have a method
|
||||
if (desc && typeof desc.value === 'function') {
|
||||
if (name === 'addEventListener') {
|
||||
return addEventHandler(target);
|
||||
}
|
||||
|
||||
if (name === 'removeEventListener') {
|
||||
return removeEventHandler(target);
|
||||
}
|
||||
|
||||
return methodHandler(target, name, false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function addEventHandler(target) {
|
||||
return function(eventName, callback) {
|
||||
var callbackId = eventName + uniqueId();
|
||||
var args = {
|
||||
callbackId: callbackId,
|
||||
objId: target._objId,
|
||||
eventName: eventName
|
||||
};
|
||||
ipcRenderer.send(proxyCmds.addEvent, args);
|
||||
|
||||
let callbackFunc = function(arg) {
|
||||
if (arg.callbackId === callbackId) {
|
||||
callback(args.result);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on(proxyCmds.eventCallback, callbackFunc);
|
||||
|
||||
target._callbacks.set(callback, {
|
||||
callbackId: callbackId,
|
||||
callbackbackFunc: callbackFunc
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function removeEventHandler(target) {
|
||||
return function(eventName, callback) {
|
||||
if (target._callbacks && target._callback.has(callback)) {
|
||||
let callbackObj = target._callback.get(callback);
|
||||
|
||||
let args = {
|
||||
eventName: eventName,
|
||||
callbackId: callbackObj.callbackId,
|
||||
objId: target._objId
|
||||
}
|
||||
|
||||
ipcRenderer.removeListener(proxyCmds.eventCallback,
|
||||
callbackObj.callbackbackFunc);
|
||||
|
||||
ipcRenderer.send(proxyCmds.removeEvent, args);
|
||||
|
||||
target._callbacks.delete(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function methodHandler(target, methodName, isStatic) {
|
||||
return function(...argPassedToMethod) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var invokeId = methodName + uniqueId();
|
||||
var args = {
|
||||
invokeId: invokeId,
|
||||
objId: target._objId,
|
||||
methodName: methodName,
|
||||
arguments: argPassedToMethod,
|
||||
isStatic: isStatic,
|
||||
className: target.name
|
||||
}
|
||||
|
||||
if (!isStatic) {
|
||||
args.objId = target._objId;
|
||||
}
|
||||
|
||||
ipcRenderer.on(proxyCmds.invokeResult, resultCallback);
|
||||
ipcRenderer.send(proxyCmds.invokeMethod, args);
|
||||
|
||||
function removeEventListener() {
|
||||
ipcRenderer.removeListener(proxyCmds.invokeResult,
|
||||
resultCallback);
|
||||
}
|
||||
function resultCallback(arg) {
|
||||
if (arg.invokeId === invokeId) {
|
||||
window.clearTimeout(timer);
|
||||
removeEventListener();
|
||||
if (arg.error) {
|
||||
reject('method called failed: ' + arg.error);
|
||||
} else {
|
||||
resolve(arg.returnValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timeout in case we never hear anything back from main process
|
||||
let timer = setTimeout(function() {
|
||||
removeEventListener();
|
||||
reject('timeout_no_reponse');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getHandler(target, property, isStatic) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var getterId = property + uniqueId();
|
||||
var args = {
|
||||
getterId: getterId,
|
||||
getterProperty: property,
|
||||
isStatic: isStatic,
|
||||
className: target.name
|
||||
}
|
||||
|
||||
if (!isStatic) {
|
||||
args.objId = target._objId;
|
||||
}
|
||||
|
||||
ipcRenderer.on(proxyCmds.getResult, resultCallback);
|
||||
ipcRenderer.send(proxyCmds.get, args);
|
||||
|
||||
function removeEventListener() {
|
||||
ipcRenderer.removeListener(proxyCmds.getResult,
|
||||
resultCallback);
|
||||
}
|
||||
function resultCallback(arg) {
|
||||
if (arg.getterId === getterId) {
|
||||
window.clearTimeout(timer);
|
||||
removeEventListener();
|
||||
if (arg.error) {
|
||||
reject('getter called failed: ' + arg.error);
|
||||
} else {
|
||||
resolve(arg.returnValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timeout in case we never hear anything back from main process
|
||||
let timer = setTimeout(function() {
|
||||
removeEventListener();
|
||||
reject('timeout_no_reponse');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function instanceSetHandler(target, property, value) {
|
||||
let prototype = Reflect.getPrototypeOf(target);
|
||||
let desc = Object.getOwnPropertyDescriptor(prototype, property);
|
||||
|
||||
if (desc && desc.set) {
|
||||
var args = {
|
||||
objId: target._objId,
|
||||
setterProperty: property,
|
||||
setterValue: value
|
||||
}
|
||||
|
||||
ipcRenderer.sendSync(proxyCmds.set, args);
|
||||
}
|
||||
}
|
||||
|
||||
function staticGetHandler(target, name) {
|
||||
// all methods and getters we support should be on the prototype
|
||||
let desc = Object.getOwnPropertyDescriptor(target, name);
|
||||
|
||||
// does this have a static "getter"
|
||||
if (desc && desc.get) {
|
||||
return getHandler(target, name, true);
|
||||
}
|
||||
// does this have a static method
|
||||
if (desc && typeof desc.value === 'function') {
|
||||
return methodHandler(target, name, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
module.exports = createProxy
|
@@ -19,9 +19,13 @@ const apiEnums = require('../enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
const apiName = apiEnums.apiName;
|
||||
|
||||
const notifyInterface = require('../notify/notifyInterface.js');
|
||||
const createProxy = require('./createProxy.js');
|
||||
|
||||
// hold ref so doesn't get GC'ed
|
||||
const local = {
|
||||
ipcRenderer: ipcRenderer
|
||||
ipcRenderer: ipcRenderer,
|
||||
|
||||
};
|
||||
|
||||
// throttle calls to this func to at most once per sec, called on leading edge.
|
||||
@@ -57,6 +61,12 @@ window.SYM_API = {
|
||||
throttledSetBadgeCount(count);
|
||||
},
|
||||
|
||||
/**
|
||||
* provides api similar to html5 Notification, see details
|
||||
* in notify/notifyInterface.js
|
||||
*/
|
||||
Notification: createProxy(notifyInterface),
|
||||
|
||||
/**
|
||||
* allows JS to register a logger that can be used by electron main process.
|
||||
* @param {Object} logger function that can be called accepting
|
||||
|
@@ -11,6 +11,7 @@ const getGuid = require('./utils/getGuid.js');
|
||||
const log = require('./log.js')
|
||||
const logLevels = require('./enums/logLevels.js');
|
||||
|
||||
|
||||
// show dialog when certificate errors occur
|
||||
require('./dialogs/showCertError.js');
|
||||
|
||||
@@ -24,7 +25,7 @@ let isOnline = true;
|
||||
// different preload script for main window and child windows.
|
||||
// note: these files are generated by browserify prebuild process.
|
||||
const preloadMainScript = path.join(__dirname, 'preload/_preloadMain.js');
|
||||
const preloadChildScript = path.join(__dirname, 'preloda/_preloadChild.js');
|
||||
const preloadChildScript = path.join(__dirname, 'preload/_preloadChild.js');
|
||||
|
||||
function addWindowKey(key, browserWin) {
|
||||
windows[ key ] = browserWin;
|
||||
|
@@ -9,6 +9,9 @@
|
||||
"dev:mac": "ELECTRON_DEV=true npm run start",
|
||||
"dev:win": "SET ELECTRON_DEV=true && npm run start",
|
||||
"start": "npm run browserify-preload && electron .",
|
||||
"demo:mac": "ELECTRON_DEV=true npm run start:demo",
|
||||
"demo:win": "SET ELECTRON_DEV=true && npm run start:demo",
|
||||
"start:demo": "npm run browserify-preload && electron . --url=file://$(pwd)/demo/index.html",
|
||||
"dist-mac": "npm run prebuild && build --mac",
|
||||
"dist-win": "npm run prebuild && build --win --x64",
|
||||
"dist-win-x86": "npm run prebuild && build --win --ia32",
|
||||
@@ -16,7 +19,7 @@
|
||||
"unpacked-win-x86": "npm run prebuild && build --win --ia32 --dir",
|
||||
"prebuild": "npm run lint && npm run test && npm run browserify-preload",
|
||||
"lint": "eslint js/**",
|
||||
"test": "jest --coverage tests/*.test.js",
|
||||
"test": "jest --coverage --testPathPattern tests",
|
||||
"browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron js/preload/preloadMain.js && browserify -o js/preload/_preloadChild.js -x electron js/preload/preloadChild.js"
|
||||
},
|
||||
"build": {
|
||||
|
Reference in New Issue
Block a user