electron notification is implemented

This commit is contained in:
병준 박 2019-11-09 17:29:02 +09:00
parent c40bd2484d
commit d35227ff3d
53 changed files with 1677 additions and 123 deletions

View File

@ -1445,6 +1445,76 @@
} }
} }
} }
},
"ucap-webmessenger-electron-notification": {
"projectType": "library",
"root": "projects/ucap-webmessenger-electron-notification",
"sourceRoot": "projects/ucap-webmessenger-electron-notification/src",
"prefix": "ucap-electron-notification",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/ucap-webmessenger-electron-notification/tsconfig.lib.json",
"project": "projects/ucap-webmessenger-electron-notification/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/ucap-webmessenger-electron-notification/src/test.ts",
"tsConfig": "projects/ucap-webmessenger-electron-notification/tsconfig.spec.json",
"karmaConfig": "projects/ucap-webmessenger-electron-notification/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/ucap-webmessenger-electron-notification/tsconfig.lib.json",
"projects/ucap-webmessenger-electron-notification/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"ucap-webmessenger-electron-core": {
"projectType": "library",
"root": "projects/ucap-webmessenger-electron-core",
"sourceRoot": "projects/ucap-webmessenger-electron-core/src",
"prefix": "ucap-electron-core",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/ucap-webmessenger-electron-core/tsconfig.lib.json",
"project": "projects/ucap-webmessenger-electron-core/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/ucap-webmessenger-electron-core/src/test.ts",
"tsConfig": "projects/ucap-webmessenger-electron-core/tsconfig.spec.json",
"karmaConfig": "projects/ucap-webmessenger-electron-core/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/ucap-webmessenger-electron-core/tsconfig.lib.json",
"projects/ucap-webmessenger-electron-core/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
} }
}, },
"defaultProject": "ucap-webmessenger-app" "defaultProject": "ucap-webmessenger-app"

View File

@ -79,6 +79,16 @@ const mainConfig: webpack.Configuration = {
resolve: { resolve: {
extensions: ['.js', '.ts'], extensions: ['.js', '.ts'],
alias: { alias: {
'@ucap-webmessenger/electron-core': path.resolve(
__dirname,
'..',
'projects/ucap-webmessenger-electron-core/src/public-api'
),
'@ucap-webmessenger/electron-notification': path.resolve(
__dirname,
'..',
'projects/ucap-webmessenger-electron-notification/src/public-api'
),
'@ucap-webmessenger/native': path.resolve( '@ucap-webmessenger/native': path.resolve(
__dirname, __dirname,
'..', '..',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,143 @@
'use strict';
const electron = require('electron');
const ipc = electron.ipcRenderer;
const winId = electron.remote.getCurrentWindow().id;
function setStyle(config) {
// Style it
let notiDoc = global.window.document;
let container = notiDoc.getElementById('container');
let appIcon = notiDoc.getElementById('appIcon');
let image = notiDoc.getElementById('image');
let close = notiDoc.getElementById('close');
let message = notiDoc.getElementById('message');
// 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);
// Style appIcon or hide
if (config.appIcon) {
setStyleOnDomElement(config.defaultStyleAppIcon, appIcon);
appIcon.src = config.appIcon;
} else {
setStyleOnDomElement(
{
display: 'none'
},
appIcon
);
}
// Style image
setStyleOnDomElement(config.defaultStyleImage, image);
// Style close button
setStyleOnDomElement(config.defaultStyleClose, close);
// Remove margin from text p
setStyleOnDomElement(config.defaultStyleText, message);
}
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 global.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 = global.window.document;
// 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);
}
// Close button
let closeButton = notiDoc.getElementById('close');
closeButton.addEventListener('click', function(event) {
event.stopPropagation();
ipc.send('UCAP::ElectronNotification::close', winId, notificationObj);
});
// URL
let container = notiDoc.getElementById('container');
container.addEventListener('click', function() {
ipc.send('UCAP::ElectronNotification::click', winId, notificationObj);
});
}
function setStyleOnDomElement(styleObj, domElement) {
try {
for (let styleAttr in styleObj) {
domElement.style[styleAttr] = styleObj[styleAttr];
}
} catch (e) {
throw new Error(
'electron-notify: Could not set style on domElement',
styleObj,
domElement
);
}
}
function loadConfig(event, conf) {
setStyle(conf || {});
}
function reset() {
let notiDoc = global.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('UCAP::ElectronNotification::BrowserWindowSetContents', setContents);
ipc.on('UCAP::ElectronNotification::loadConfig', loadConfig);
ipc.on('UCAP::ElectronNotification::reset', reset);
function log() {
console.log.apply(console, arguments);
}
delete global.require;
delete global.exports;
delete global.module;

Binary file not shown.

View File

@ -0,0 +1,130 @@
html {
height: 100%;
overflow-y: scroll;
}
body {
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
color: #333;
font-family: '나눔고딕', Malgun Gothic, '맑은고딕', Arial, Dotum, '돋움',
Gulim, '굴림';
font-size: 12px;
line-height: 18px !important;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
ul,
ol {
list-style: none;
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
img {
border: none;
}
a:link,
a:visited,
a:hover,
a:active {
text-decoration: none;
}
.noti_messege {
width: 340px;
height: 100px;
border: 1px solid #666;
background-color: #fff;
box-shadow: 0px 0px 3px 0px #e7e7e7;
}
.info {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 16px 14px;
color: #fff;
}
.btn_close {
position: absolute;
z-index: 1;
right: 6px;
top: 6px;
width: 20px;
height: 20px;
background: url(../image/btn_close_gray.png) no-repeat 50% 50%;
}
.btn_close:hover {
opacity: 0.7;
}
.photo {
position: relative;
top: 0px;
right: 0;
bottom: 0;
left: 0;
margin: 4px 0;
width: 54px;
height: 54px;
border-radius: 50%;
background: #5bc1ff url(../image/img_nophoto_50.png) no-repeat 50% 50%;
border: 2px solid #ddd;
}
.info .profile {
position: absolute;
width: 60px;
text-align: center;
}
.photo img {
overflow: hidden;
width: 50px;
height: 50px;
border-radius: 50px;
}
.noti_messege .info .profile + div {
padding-left: 70px;
position: relative;
line-height: 180%;
height: 100%;
}
.sender {
font-size: 14px;
font-weight: bold;
margin-bottom: 4px;
color: #333;
width: 94%;
}
.sender .name {
color: #2e7fb5;
}
.message {
color: #666;
}
.ellipsis {
display: block;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
overflow: hidden;
}
.ellipsis_row2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-wrap: break-word;
line-height: 1.6em;
height: 3.2em;
}

View File

@ -0,0 +1,50 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<title>[개발]M Messenger - 메시지 알림</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="UTF-8" />
<link
type="text/css"
rel="stylesheet"
href="styles/noti_messege.css"
/>
</HEAD>
<style>
html,
body {
overflow-y: hidden;
overflow-x: hidden;
}
</style>
<BODY>
<div class="noti_messege" id="container">
<div class="info">
<a class="btn_close" id="close"></a>
<div class="profile">
<div class="photo">
<img src="" id="appIcon" />
<img
src=""
id="image"
onerror="this.src='image/img_nophoto_50.png';"
/>
</div>
</div>
<div>
<ul id="text">
<li class="sender ellipsis" id="title">
<span class="name">김 수안무 거북이와 두루미</span>님이 메시지를
보냈습니다.
</li>
<li class="message ellipsis_row2" id="message">
홍길동 대리(솔루션사업팀)홍길동 대리(솔루션사업팀)홍길동
대리(솔루션사업팀)홍길동 대리(솔루션사업팀)
</li>
</ul>
</div>
</div>
</div>
</BODY>
</HTML>

View File

@ -7,6 +7,11 @@ import { EventEmitter } from 'events';
import { now } from '../util/now'; import { now } from '../util/now';
import { registerWindowStateChangedEvents } from '../lib/window-state'; import { registerWindowStateChangedEvents } from '../lib/window-state';
import {
ElectronAppChannel,
ElectronBrowserWindowChannel,
ElectronWebContentsChannel
} from '@ucap-webmessenger/electron-core';
export class AppWindow { export class AppWindow {
private window: BrowserWindow | null = null; private window: BrowserWindow | null = null;
@ -62,11 +67,11 @@ export class AppWindow {
savedWindowState.manage(this.window); savedWindowState.manage(this.window);
let quitting = false; let quitting = false;
app.on('before-quit', () => { app.on(ElectronAppChannel.BeforeQuit, () => {
quitting = true; quitting = true;
}); });
ipcMain.on('will-quit', (event: IpcMainEvent) => { ipcMain.on(ElectronAppChannel.WillQuit, (event: IpcMainEvent) => {
quitting = true; quitting = true;
event.returnValue = true; event.returnValue = true;
}); });
@ -75,7 +80,7 @@ export class AppWindow {
// lets us activate quickly and keep all our interesting logic in the // lets us activate quickly and keep all our interesting logic in the
// renderer. // renderer.
if (__DARWIN__) { if (__DARWIN__) {
this.window.on('close', e => { this.window.on(ElectronBrowserWindowChannel.Close, e => {
if (!quitting) { if (!quitting) {
e.preventDefault(); e.preventDefault();
} }
@ -92,8 +97,8 @@ export class AppWindow {
// //
// can be tidied up once https://github.com/electron/electron/issues/12971 // can be tidied up once https://github.com/electron/electron/issues/12971
// has been confirmed as resolved // has been confirmed as resolved
this.window.once('ready-to-show', () => { this.window.once(ElectronBrowserWindowChannel.ReadyToShow, () => {
this.window.on('unmaximize', () => { this.window.on(ElectronBrowserWindowChannel.Unmaximize, () => {
setTimeout(() => { setTimeout(() => {
const bounds = this.window.getBounds(); const bounds = this.window.getBounds();
bounds.width += 1; bounds.width += 1;
@ -109,26 +114,30 @@ export class AppWindow {
public load(): void { public load(): void {
let startLoad = 0; let startLoad = 0;
this.window.webContents.once('did-start-loading', () => { this.window.webContents.once(
ElectronWebContentsChannel.DidStartLoading,
() => {
this._rendererReadyTime = null; this._rendererReadyTime = null;
this._loadTime = null; this._loadTime = null;
startLoad = now(); startLoad = now();
}); }
);
this.window.webContents.once(
ElectronWebContentsChannel.DidFinishLoad,
() => {
this.window.webContents.setVisualZoomLevelLimits(1, 1);
this.window.webContents.once('did-finish-load', () => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
this.window.webContents.openDevTools(); this.window.webContents.openDevTools();
} }
this._loadTime = now() - startLoad; this._loadTime = now() - startLoad;
}); }
);
this.window.webContents.on('did-finish-load', () => { this.window.webContents.on(ElectronWebContentsChannel.DidFailLoad, () => {
this.window.webContents.setVisualZoomLevelLimits(1, 1);
});
this.window.webContents.on('did-fail-load', () => {
this.window.webContents.openDevTools(); this.window.webContents.openDevTools();
this.window.show(); this.window.show();
}); });
@ -158,7 +167,7 @@ export class AppWindow {
} }
public onClose(fn: () => void) { public onClose(fn: () => void) {
this.window.on('closed', fn); this.window.on(ElectronBrowserWindowChannel.Closed, fn);
} }
/** /**

View File

@ -7,12 +7,22 @@ import * as fs from 'fs';
import { AppWindow } from './app/AppWindow'; import { AppWindow } from './app/AppWindow';
import { now } from './util/now'; import { now } from './util/now';
import { showUncaughtException } from './crash/show-uncaught-exception'; import { showUncaughtException } from './crash/show-uncaught-exception';
import { Channel } from '@ucap-webmessenger/native-electron';
import {
UpdaterChannel,
FileChannel,
IdleStateChannel,
NotificationChannel
} from '@ucap-webmessenger/native-electron';
import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification';
import { root } from './util/root'; import { root } from './util/root';
import { DefaultFolder } from './lib/default-folder'; import { DefaultFolder } from './lib/default-folder';
import { FileUtil } from './lib/file-util'; import { FileUtil } from './lib/file-util';
import { IdleChecker } from './lib/idle-checker'; import { IdleChecker } from './lib/idle-checker';
import { NotificationRequest } from '@ucap-webmessenger/native';
import { ElectronAppChannel } from '@ucap-webmessenger/electron-core';
let appWindow: AppWindow | null = null; let appWindow: AppWindow | null = null;
@ -23,6 +33,9 @@ type OnDidLoadFn = (window: AppWindow) => void;
let onDidLoadFns: Array<OnDidLoadFn> | null = []; let onDidLoadFns: Array<OnDidLoadFn> | null = [];
let preventQuit = false; let preventQuit = false;
let notificationService: ElectronNotificationService | null;
function handleUncaughtException(error: Error) { function handleUncaughtException(error: Error) {
preventQuit = true; preventQuit = true;
@ -57,7 +70,7 @@ const gotSingleInstanceLock = app.requestSingleInstanceLock();
isDuplicateInstance = !gotSingleInstanceLock; isDuplicateInstance = !gotSingleInstanceLock;
let idle: IdleChecker | null; let idle: IdleChecker | null;
app.on('second-instance', (event, args, workingDirectory) => { app.on(ElectronAppChannel.SecondInstance, (event, args, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window. // Someone tried to run a second instance, we should focus our window.
if (appWindow) { if (appWindow) {
if (appWindow.isMinimized()) { if (appWindow.isMinimized()) {
@ -132,7 +145,7 @@ function createWindow() {
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on('ready', () => { app.on(ElectronAppChannel.Ready, () => {
if (isDuplicateInstance) { if (isDuplicateInstance) {
return; return;
} }
@ -141,6 +154,30 @@ app.on('ready', () => {
createWindow(); createWindow();
notificationService = new ElectronNotificationService({
width: 340,
height: 100,
padding: 0,
borderRadius: 0,
// appIcon: iconPath,
displayTime: 5000,
defaultStyleContainer: {},
defaultStyleAppIcon: { display: 'none' },
defaultStyleImage: {},
defaultStyleClose: {},
defaultStyleText: {}
});
notificationService.options.defaultWindow.webPreferences.preload = path.join(
__dirname,
'resources/notification/preload.js'
);
notificationService.templatePath = path.join(
__dirname,
'resources/notification/template.html'
);
ipcMain.on('uncaught-exception', (event: IpcMainEvent, error: Error) => { ipcMain.on('uncaught-exception', (event: IpcMainEvent, error: Error) => {
handleUncaughtException(error); handleUncaughtException(error);
}); });
@ -155,7 +192,7 @@ app.on('ready', () => {
}); });
// Quit when all windows are closed. // Quit when all windows are closed.
app.on('window-all-closed', () => { app.on(ElectronAppChannel.WindowAllClosed, () => {
// On OS X it is common for applications and their menu bar // On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q // to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
@ -163,7 +200,7 @@ app.on('window-all-closed', () => {
} }
}); });
app.on('activate', () => { app.on(ElectronAppChannel.Activate, () => {
onDidLoad(window => { onDidLoad(window => {
window.show(); window.show();
}); });
@ -179,11 +216,11 @@ function onDidLoad(fn: OnDidLoadFn) {
} }
} }
ipcMain.on(Channel.checkForUpdates, (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(UpdaterChannel.Check, (event: IpcMainEvent, ...args: any[]) => {
event.returnValue = false; event.returnValue = false;
}); });
ipcMain.on(Channel.readFile, (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(FileChannel.ReadFile, (event: IpcMainEvent, ...args: any[]) => {
try { try {
fse.readFile(root(args[0]), (err, data) => { fse.readFile(root(args[0]), (err, data) => {
if (!!err) { if (!!err) {
@ -197,7 +234,9 @@ ipcMain.on(Channel.readFile, (event: IpcMainEvent, ...args: any[]) => {
} }
}); });
ipcMain.on(Channel.saveFile, async (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(
FileChannel.SaveFile,
async (event: IpcMainEvent, ...args: any[]) => {
try { try {
const buffer: Buffer = args[0]; const buffer: Buffer = args[0];
const fileName: string = args[1]; const fileName: string = args[1];
@ -217,17 +256,51 @@ ipcMain.on(Channel.saveFile, async (event: IpcMainEvent, ...args: any[]) => {
} catch (error) { } catch (error) {
event.returnValue = undefined; event.returnValue = undefined;
} }
}); }
);
ipcMain.on(Channel.idleStateStart, (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(
IdleStateChannel.StartCheck,
(event: IpcMainEvent, ...args: any[]) => {
if (!!idle) { if (!!idle) {
idle.destoryChecker(); idle.destoryChecker();
idle = null; idle = null;
} }
idle = new IdleChecker(appWindow.browserWindow); // default 10min idle = new IdleChecker(appWindow.browserWindow); // default 10min
idle.startChecker(); idle.startChecker();
}); }
);
ipcMain.on(Channel.showNotify, (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(
console.log('Channel.showNotify', args); NotificationChannel.Notify,
}); (event: IpcMainEvent, ...args: any[]) => {
const noti: NotificationRequest = args[0];
notificationService.notify({
title: noti.title,
text: noti.contents,
image:
noti.image ||
path.join(__dirname, 'resources/notification/image/img_nophoto_50.png'),
sound: noti.useSound
? path.join(
'file://',
__dirname,
'resources/notification/sound/messageAlarm.mp3'
)
: '',
onClick: () => {
console.log('onClick');
}
});
console.log('Channel.notify', noti);
}
);
ipcMain.on(
NotificationChannel.CloseAllNotify,
(event: IpcMainEvent, ...args: any[]) => {
console.log('Channel.closeAllNotify', args);
}
);

View File

@ -1,5 +1,5 @@
import { powerMonitor, BrowserWindow } from 'electron'; import { powerMonitor, BrowserWindow } from 'electron';
import { Channel } from '@ucap-webmessenger/native-electron'; import { IdleStateChannel } from '@ucap-webmessenger/native-electron';
import { setInterval } from 'timers'; import { setInterval } from 'timers';
export enum IdleType { export enum IdleType {
@ -28,13 +28,13 @@ export class IdleChecker {
if (this.status === IdleType.ACTIVE) { if (this.status === IdleType.ACTIVE) {
this.status = IdleType.IDLE; this.status = IdleType.IDLE;
// TODO :: USER_STATUS change away // TODO :: USER_STATUS change away
this.window.webContents.send(Channel.idleStateChanged, this.status); this.window.webContents.send(IdleStateChannel.Changed, this.status);
} }
} else { } else {
if (this.status === IdleType.IDLE) { if (this.status === IdleType.IDLE) {
this.status = IdleType.ACTIVE; this.status = IdleType.ACTIVE;
// TODO :: USER_STATUS chage online // TODO :: USER_STATUS chage online
this.window.webContents.send(Channel.idleStateChanged, this.status); this.window.webContents.send(IdleStateChannel.Changed, this.status);
} }
} }
} }

View File

@ -1,6 +1,7 @@
import { BrowserWindow } from 'electron'; import { BrowserWindow } from 'electron';
import { WindowState } from '@ucap-webmessenger/native'; import { WindowState } from '@ucap-webmessenger/native';
import { Channel } from '@ucap-webmessenger/native-electron'; import { WindowStateChannel } from '@ucap-webmessenger/native-electron';
import { ElectronBrowserWindowChannel } from '@ucap-webmessenger/electron-core';
export function getWindowState(window: Electron.BrowserWindow): WindowState { export function getWindowState(window: Electron.BrowserWindow): WindowState {
if (window.isFullScreen()) { if (window.isFullScreen()) {
@ -17,26 +18,30 @@ export function getWindowState(window: Electron.BrowserWindow): WindowState {
} }
export function registerWindowStateChangedEvents(window: BrowserWindow) { export function registerWindowStateChangedEvents(window: BrowserWindow) {
window.on('enter-full-screen', () => window.on(ElectronBrowserWindowChannel.EnterFullScreen, () =>
sendWindowStateEvent(window, WindowState.FullScreen) sendWindowStateEvent(window, WindowState.FullScreen)
); );
window.on('leave-full-screen', () => window.on(ElectronBrowserWindowChannel.LeaveFullScreen, () =>
sendWindowStateEvent(window, WindowState.Normal) sendWindowStateEvent(window, WindowState.Normal)
); );
window.on('maximize', () => window.on(ElectronBrowserWindowChannel.Maximize, () =>
sendWindowStateEvent(window, WindowState.Maximized) sendWindowStateEvent(window, WindowState.Maximized)
); );
window.on('minimize', () => window.on(ElectronBrowserWindowChannel.Minimize, () =>
sendWindowStateEvent(window, WindowState.Minimized) sendWindowStateEvent(window, WindowState.Minimized)
); );
window.on('unmaximize', () => window.on(ElectronBrowserWindowChannel.Unmaximize, () =>
sendWindowStateEvent(window, WindowState.Normal) sendWindowStateEvent(window, WindowState.Normal)
); );
window.on('restore', () => sendWindowStateEvent(window, WindowState.Normal)); window.on(ElectronBrowserWindowChannel.Restore, () =>
window.on('hide', () => sendWindowStateEvent(window, WindowState.Hidden)); sendWindowStateEvent(window, WindowState.Normal)
window.on('show', () => { );
window.on(ElectronBrowserWindowChannel.Hide, () =>
sendWindowStateEvent(window, WindowState.Hidden)
);
window.on(ElectronBrowserWindowChannel.Show, () => {
// because the app can be maximized before being closed - which will restore it // because the app can be maximized before being closed - which will restore it
// maximized on the next launch - this function should inspect the current state // maximized on the next launch - this function should inspect the current state
// rather than always assume it is a 'normal' launch // rather than always assume it is a 'normal' launch
@ -45,5 +50,5 @@ export function registerWindowStateChangedEvents(window: BrowserWindow) {
} }
function sendWindowStateEvent(window: BrowserWindow, windowState: WindowState) { function sendWindowStateEvent(window: BrowserWindow, windowState: WindowState) {
window.webContents.send(Channel.windowStateChanged, windowState); window.webContents.send(WindowStateChannel.Changed, windowState);
} }

View File

@ -13,6 +13,12 @@
"types": ["node"], "types": ["node"],
"lib": ["es2017", "es2016", "es2015", "dom"], "lib": ["es2017", "es2016", "es2015", "dom"],
"paths": { "paths": {
"@ucap-webmessenger/electron-core": [
"../projects/ucap-webmessenger-electron-core/src/public-api"
],
"@ucap-webmessenger/electron-notification": [
"../projects/ucap-webmessenger-electron-notification/src/public-api"
],
"@ucap-webmessenger/native": [ "@ucap-webmessenger/native": [
"../projects/ucap-webmessenger-native/src/public-api" "../projects/ucap-webmessenger-native/src/public-api"
], ],

50
package-lock.json generated
View File

@ -3101,15 +3101,6 @@
"integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
"dev": true "dev": true
}, },
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"async-each": { "async-each": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
@ -8061,6 +8052,15 @@
"once": "^1.4.0" "once": "^1.4.0"
}, },
"dependencies": { "dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"istanbul-lib-coverage": { "istanbul-lib-coverage": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
@ -11239,6 +11239,15 @@
"mkdirp": "^0.5.1" "mkdirp": "^0.5.1"
}, },
"dependencies": { "dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"debug": { "debug": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
@ -13255,6 +13264,17 @@
"requires": { "requires": {
"async": "^2.5.0", "async": "^2.5.0",
"loader-utils": "^1.1.0" "loader-utils": "^1.1.0"
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
}
} }
}, },
"source-map-resolve": { "source-map-resolve": {
@ -13519,6 +13539,15 @@
"lodash": "^4.17.14" "lodash": "^4.17.14"
}, },
"dependencies": { "dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"debug": { "debug": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
@ -14116,8 +14145,7 @@
"tslib": { "tslib": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
"dev": true
}, },
"tslint": { "tslint": {
"version": "5.15.0", "version": "5.15.0",

View File

@ -6,6 +6,7 @@
"start": "npm-run-all -p start:renderer start:main", "start": "npm-run-all -p start:renderer start:main",
"start:main": "wait-on http-get://localhost:4200/ && npm run build:main:dev && electron --nolazy --inspect-brk=9229 .", "start:main": "wait-on http-get://localhost:4200/ && npm run build:main:dev && electron --nolazy --inspect-brk=9229 .",
"start:renderer": "ng serve", "start:renderer": "ng serve",
"start:web": "cross-env UCAP_ENV=WEB ng serve",
"start:production": "npm run build:renderer && npm run build:main:prod && electron --nolazy --inspect-brk=9229 .", "start:production": "npm run build:renderer && npm run build:main:prod && electron --nolazy --inspect-brk=9229 .",
"build:renderer": "cross-env NODE_ENV=production ng build --base-href ./", "build:renderer": "cross-env NODE_ENV=production ng build --base-href ./",
"build:main:dev": "cross-env NODE_ENV=development TS_NODE_PROJECT='./config/tsconfig.webpack.json' parallel-webpack --config=config/main.webpack.config.ts", "build:main:dev": "cross-env NODE_ENV=development TS_NODE_PROJECT='./config/tsconfig.webpack.json' parallel-webpack --config=config/main.webpack.config.ts",
@ -15,7 +16,9 @@
"e2e": "ng e2e" "e2e": "ng e2e"
}, },
"private": true, "private": true,
"dependencies": {}, "dependencies": {
"tslib": "^1.10.0"
},
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^8.2.0", "@angular-builders/custom-webpack": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.14", "@angular-devkit/build-angular": "~0.803.14",

View File

@ -0,0 +1,24 @@
# UcapWebmessengerElectronCore
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.11.
## Code scaffolding
Run `ng generate component component-name --project ucap-webmessenger-electron-core` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ucap-webmessenger-electron-core`.
> Note: Don't forget to add `--project ucap-webmessenger-electron-core` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build ucap-webmessenger-electron-core` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build ucap-webmessenger-electron-core`, go to the dist folder `cd dist/ucap-webmessenger-electron-core` and run `npm publish`.
## Running unit tests
Run `ng test ucap-webmessenger-electron-core` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/ucap-webmessenger-electron-core'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ucap-webmessenger-electron-core",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,8 @@
{
"name": "@ucap-webmessenger/electron-core",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.11",
"@angular/core": "^8.2.11"
}
}

View File

@ -0,0 +1,37 @@
export enum ElectronAppChannel {
WillFinishLaunching = 'will-finish-launching',
Ready = 'ready',
WindowAllClosed = 'window-all-closed',
BeforeQuit = 'before-quit',
WillQuit = 'will-quit',
Quit = 'quit',
OpenFile = 'open-file',
OpenUrl = 'open-url',
Activate = 'activate',
ContinueActivity = 'continue-activity',
WillContinueActivity = 'will-continue-activity',
ContinueActivityError = 'continue-activity-error',
ActivityWasContinued = 'activity-was-continued',
SecondInstance = 'second-instance'
}
export enum ElectronBrowserWindowChannel {
EnterFullScreen = 'enter-full-screen',
LeaveFullScreen = 'leave-full-screen',
Maximize = 'maximize',
Minimize = 'minimize',
Unmaximize = 'unmaximize',
Restore = 'restore',
Hide = 'hide',
Show = 'show',
Close = 'close',
Closed = 'closed',
ReadyToShow = 'ready-to-show'
}
export enum ElectronWebContentsChannel {
DevtoolsOpened = 'devtools-opened',
DidStartLoading = 'did-start-loading',
DidFinishLoad = 'did-finish-load',
DidFailLoad = 'did-fail-load'
}

View File

@ -0,0 +1,5 @@
/*
* Public API Surface of ucap-webmessenger-electron-core
*/
export * from './lib/types/channel.type';

View File

@ -0,0 +1,21 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"ucapElectronCore",
"camelCase"
],
"component-selector": [
true,
"element",
"ucap-electron-core",
"kebab-case"
]
}
}

View File

@ -0,0 +1,24 @@
# UcapWebmessengerElectronNotification
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.11.
## Code scaffolding
Run `ng generate component component-name --project ucap-webmessenger-electron-notification` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ucap-webmessenger-electron-notification`.
> Note: Don't forget to add `--project ucap-webmessenger-electron-notification` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build ucap-webmessenger-electron-notification` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build ucap-webmessenger-electron-notification`, go to the dist folder `cd dist/ucap-webmessenger-electron-notification` and run `npm publish`.
## Running unit tests
Run `ng test ucap-webmessenger-electron-notification` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/ucap-webmessenger-electron-notification'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ucap-webmessenger-electron-notification",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,5 @@
{
"name": "@ucap-webmessenger/electron-notification",
"version": "0.0.1",
"peerDependencies": {}
}

View File

@ -0,0 +1,113 @@
import * as path from 'path';
import { BrowserWindowConstructorOptions } from 'electron';
export interface ElectronNotificationOptions {
width?: number;
height?: number;
padding?: number;
borderRadius?: number;
displayTime?: number;
animationSteps?: number;
animationStepMs?: number;
animateInParallel?: boolean;
appIcon?: string;
pathToModule?: string;
logging?: boolean;
defaultStyleContainer?: {
[attribute: string]: any;
};
defaultStyleAppIcon?: {
[attribute: string]: any;
};
defaultStyleImage?: {
[attribute: string]: any;
};
defaultStyleClose?: {
[attribute: string]: any;
};
defaultStyleText?: {
[attribute: string]: any;
};
defaultWindow?: BrowserWindowConstructorOptions;
templatePath?: string;
htmlTemplate?: string;
}
export const DefaultElectronNotificationOptions: ElectronNotificationOptions = {
width: 300,
height: 65,
padding: 10,
borderRadius: 5,
displayTime: 5000,
animationSteps: 5,
animationStepMs: 20,
appIcon: null,
pathToModule: '',
logging: true,
defaultStyleContainer: {
backgroundColor: '#f0f0f0',
overflow: 'hidden',
padding: 8,
border: '1px solid #CCC',
fontFamily: 'Arial',
fontSize: 12,
position: 'relative',
lineHeight: '15px'
},
defaultStyleAppIcon: {
overflow: 'hidden',
float: 'left',
height: 40,
width: 40,
marginRight: 10
},
defaultStyleImage: {
overflow: 'hidden',
float: 'right',
height: 40,
width: 40,
marginLeft: 10
},
defaultStyleClose: {
position: 'absolute',
top: 1,
right: 3,
fontSize: 11,
color: '#CCC'
},
defaultStyleText: {
margin: 0,
overflow: 'hidden',
cursor: 'default'
},
defaultWindow: {
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
show: false,
frame: false,
transparent: true,
acceptFirstMouse: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
webSecurity: false,
allowRunningInsecureContent: true
}
},
htmlTemplate:
'<html>\n' +
'<head></head>\n' +
'<body style="overflow: hidden; -webkit-user-select: none;">\n' +
'<div id="container">\n' +
' <img src="" id="appIcon" />\n' +
' <img src="" id="image" />\n' +
' <div id="text">\n' +
' <b id="title"></b>\n' +
' <p id="message"></p>\n' +
' </div>\n' +
' <div id="close">X</div>\n' +
'</div>\n' +
'</body>\n' +
'</html>'
};

View File

@ -0,0 +1,20 @@
import { ElectronNotificationEventType } from '../types/event.type';
export interface ElectronNotificationEvent {
type: ElectronNotificationEventType;
id: number;
close?: (reason: any) => void;
}
export interface ElectronNotification {
id?: number;
displayTime?: number;
title?: string;
text?: string;
image?: string;
url?: string;
sound?: string;
onClick?: (e: ElectronNotificationEvent) => void;
onShow?: (e: ElectronNotificationEvent) => void;
onClose?: (e: ElectronNotificationEvent) => void;
}

View File

@ -0,0 +1,461 @@
import * as path from 'path';
import * as url from 'url';
import { AnimationQueue } from '../utils/animation-queue';
import {
ElectronNotificationOptions,
DefaultElectronNotificationOptions
} from '../models/electron-notification-options';
import { screen, BrowserWindow, ipcMain, IpcMainEvent, shell } from 'electron';
import { ElectronNotification } from '../models/electron-notification';
import { ElectronNotificationEventType } from '../types/event.type';
import { Channel } from '../types/channel.type';
import { ElectronWebContentsChannel } from '@ucap-webmessenger/electron-core';
const onClickElectronNotification = 'onClickElectronNotification';
const onCloseElectronNotification = 'onCloseElectronNotification';
interface ENPoint {
x: number;
y: number;
}
interface ENDimension {
width: number;
height: number;
}
export class ElectronNotificationService {
private animationQueue: AnimationQueue;
private customOptions: ElectronNotificationOptions;
private nextInsertPosition: ENPoint;
private totalDimension: ENDimension;
private firstPosition: ENPoint;
private lowerRightCornerPosition: ENPoint;
private maxVisibleNotifications: number;
private activeNotifications: BrowserWindow[];
private inactiveWindows: BrowserWindow[];
private notificationQueue: ElectronNotification[];
private closedNotifications: Map<number, boolean>;
private latestId: number;
private templateUrl: string;
constructor(options?: ElectronNotificationOptions) {
this.customOptions = {
...DefaultElectronNotificationOptions
};
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
this.setup();
this.setupEvents();
}
set options(options: ElectronNotificationOptions) {
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
this.calcDimensions();
}
get options(): ElectronNotificationOptions {
return this.customOptions;
}
set templatePath(templatePath: string) {
if (!!templatePath) {
this.customOptions.templatePath = templatePath;
this.updateTemplatePath();
}
}
get templatePath(): string {
if (!this.templateUrl) {
this.updateTemplatePath();
}
return this.templateUrl;
}
notify(notification: ElectronNotification): number {
notification.id = this.latestId++;
this.animationQueue.push({
context: this,
func: this.showNotification,
args: [notification]
});
return notification.id;
}
dispose(): void {
this.animationQueue.clear();
this.activeNotifications.forEach(window => window.close());
this.inactiveWindows.forEach(window => window.close());
}
closeAll(): void {
this.animationQueue.clear();
this.activeNotifications.forEach(window => window.close());
this.inactiveWindows.forEach(window => window.close());
this.setup();
}
private setup(): void {
this.nextInsertPosition = { x: 0, y: 0 };
this.totalDimension = { width: 0, height: 0 };
this.firstPosition = { x: 0, y: 0 };
this.activeNotifications = [];
this.inactiveWindows = [];
this.notificationQueue = [];
this.closedNotifications = new Map();
this.latestId = 0;
this.animationQueue = new AnimationQueue();
const display = screen.getPrimaryDisplay();
this.lowerRightCornerPosition = {
x: display.bounds.x + display.workArea.x + display.workAreaSize.width,
y: display.bounds.y + display.workArea.y + display.workAreaSize.height
};
this.calcDimensions();
this.maxVisibleNotifications = Math.floor(
display.workAreaSize.height / this.totalDimension.height
);
this.maxVisibleNotifications =
7 < this.maxVisibleNotifications ? 7 : this.maxVisibleNotifications;
}
private setupEvents(): void {
const self = this;
ipcMain.on(
Channel.close,
(
event: IpcMainEvent,
windowId: number,
notification: ElectronNotification
) => {
const onClose = self.buildCloseNotification(
BrowserWindow.fromId(windowId),
notification
);
self.buildCloseNotificationSafely(onClose)('close');
}
);
ipcMain.on(
Channel.click,
(
event: IpcMainEvent,
windowId: number,
notification: ElectronNotification
) => {
if (!!notification.url) {
shell.openExternal(notification.url);
}
const notificationWindow = BrowserWindow.fromId(windowId);
if (
notificationWindow &&
notificationWindow[onClickElectronNotification]
) {
const onClose = self.buildCloseNotification(
BrowserWindow.fromId(windowId),
notification
);
notificationWindow[onClickElectronNotification]({
type: ElectronNotificationEventType.Click,
id: notification.id,
close: self.buildCloseNotificationSafely(onClose)
});
delete notificationWindow[onClickElectronNotification];
}
}
);
}
private calcDimensions() {
this.totalDimension = {
width: this.customOptions.width + this.customOptions.padding,
height: this.customOptions.height + this.customOptions.padding
};
this.firstPosition = {
x: this.lowerRightCornerPosition.x - this.totalDimension.width,
y: this.lowerRightCornerPosition.y - this.totalDimension.height
};
this.nextInsertPosition = {
x: this.firstPosition.x,
y: this.firstPosition.y
};
}
private calcInsertPosition() {
if (this.activeNotifications.length < this.maxVisibleNotifications) {
this.nextInsertPosition.y =
this.lowerRightCornerPosition.y -
this.totalDimension.height * (this.activeNotifications.length + 1);
}
}
private updateTemplatePath() {
try {
import('fs')
.then(fs => {
fs.statSync(this.customOptions.templatePath).isFile();
this.templateUrl = url.format({
pathname: this.customOptions.templatePath,
protocol: 'file:',
slashes: true
});
})
.catch(reason => {
throw reason;
});
} catch (e) {
console.log(
'electron-notify: Could not find template ("' +
this.customOptions.templatePath +
'").'
);
console.log(
'electron-notify: To use a different template you need to correct the config.templatePath or simply adapt config.htmlTemplate'
);
}
}
private showNotification(notification: ElectronNotification): Promise<any> {
const self = this;
return new Promise<any>((resolve, reject) => {
if (this.activeNotifications.length < this.maxVisibleNotifications) {
self.getWindow().then(notificationWindow => {
self.calcInsertPosition();
notificationWindow.setPosition(
self.nextInsertPosition.x,
self.nextInsertPosition.y
);
self.activeNotifications.push(notificationWindow);
const displayTime = !!notification.displayTime
? notification.displayTime
: self.customOptions.displayTime;
let timeoutId: any;
const onClose = self.buildCloseNotification(
notificationWindow,
notification,
() => timeoutId
);
const onCloseNotificationSafely = self.buildCloseNotificationSafely(
onClose
);
timeoutId = setTimeout(() => {
if (notificationWindow.isDestroyed()) {
return;
}
onCloseNotificationSafely('timeout');
}, displayTime);
if (!!notification.onShow) {
notification.onShow({
type: ElectronNotificationEventType.Show,
id: notification.id,
close: onCloseNotificationSafely
});
}
if (!!notification.onClose) {
notificationWindow[onClickElectronNotification] =
notification.onClick;
} else {
delete notificationWindow[onClickElectronNotification];
}
if (!!notification.onClose) {
notificationWindow[onCloseElectronNotification] =
notification.onClose;
} else {
delete notificationWindow[onCloseElectronNotification];
}
notificationWindow.webContents.send(
Channel.browserWindowSetContents,
notification
);
notificationWindow.showInactive();
resolve(notificationWindow);
});
} else {
self.notificationQueue.push(notification);
resolve();
}
});
}
private buildCloseNotification(
notificationWindow: BrowserWindow,
notification: ElectronNotification,
timeoutIdFunc?: () => number
) {
const self = this;
return (e: ElectronNotificationEventType): Promise<void> => {
if (notificationWindow.isDestroyed()) {
return;
}
if (self.closedNotifications.has(notification.id)) {
self.closedNotifications.delete(notification.id);
return new Promise<void>(resolve => {
resolve();
});
} else {
self.closedNotifications.set(notification.id, true);
}
if (!!notificationWindow[onCloseElectronNotification]) {
notificationWindow[onCloseElectronNotification]({
type: e,
id: notification.id
});
delete notificationWindow[onCloseElectronNotification];
}
notificationWindow.webContents.send(Channel.reset);
if (!!timeoutIdFunc) {
clearTimeout(timeoutIdFunc());
}
const i = self.activeNotifications.indexOf(notificationWindow);
self.activeNotifications.splice(i, 1);
self.inactiveWindows.push(notificationWindow);
notificationWindow.hide();
self.checkForQueuedNotifications();
return self.moveOneDown(i);
};
}
private buildCloseNotificationSafely(
onClose: (e: ElectronNotificationEventType) => any
) {
const self = this;
return (reason: any) => {
if (!reason) {
reason = 'closedByAPI';
}
self.animationQueue.push({
context: self,
func: onClose,
args: [reason]
});
};
}
private checkForQueuedNotifications(): void {
if (
0 < this.notificationQueue.length &&
this.activeNotifications.length < this.maxVisibleNotifications
) {
this.animationQueue.push({
context: this,
func: this.showNotification,
args: [this.notificationQueue.shift()]
});
}
}
private getWindow(): Promise<BrowserWindow> {
const slef = this;
return new Promise<BrowserWindow>((resolve, reject) => {
if (0 < slef.inactiveWindows.length) {
resolve(slef.inactiveWindows.pop());
} else {
const windowProperties = slef.customOptions.defaultWindow;
windowProperties.width = slef.customOptions.width;
windowProperties.height = slef.customOptions.height;
const notificationWindow = new BrowserWindow(windowProperties);
notificationWindow.setVisibleOnAllWorkspaces(true);
notificationWindow.loadURL(slef.templatePath);
notificationWindow.webContents.on(
ElectronWebContentsChannel.DidFinishLoad,
() => {
// Done
notificationWindow.webContents.send(
Channel.loadConfig,
slef.customOptions
);
resolve(notificationWindow);
}
);
notificationWindow.webContents.on(
ElectronWebContentsChannel.DevtoolsOpened,
() => {
notificationWindow.webContents.closeDevTools();
}
);
}
});
}
private moveOneDown(startPos: number): Promise<void> {
const self = this;
return new Promise<void>(async (resolve, reject) => {
if (startPos >= self.activeNotifications.length || -1 === startPos) {
resolve();
return;
}
const aryNotificationPos: number[] = [];
for (let i = startPos; i < self.activeNotifications.length; i++) {
aryNotificationPos.push(i);
}
await Promise.all(
aryNotificationPos.map(async index => {
await self.moveNotificationAnimation(index);
})
);
resolve();
});
}
private moveNotificationAnimation(index: number): Promise<void> {
const self = this;
return new Promise<void>((resolve, reject) => {
const notificationWindow = self.activeNotifications[index];
const newY =
self.lowerRightCornerPosition.y -
self.totalDimension.height * (index + 1);
const startY = notificationWindow.getPosition()[1];
const step = (newY - startY) / self.customOptions.animationSteps;
let curStep = 1;
const animationInterval = setInterval(() => {
// Abort condition
if (curStep === self.customOptions.animationSteps) {
notificationWindow.setPosition(self.firstPosition.x, newY);
clearInterval(animationInterval);
return resolve();
}
// Move one step down
notificationWindow.setPosition(
self.firstPosition.x,
Math.trunc(startY + curStep * step)
);
curStep++;
}, self.customOptions.animationStepMs);
});
}
}

View File

@ -0,0 +1,7 @@
export enum Channel {
close = 'UCAP::ElectronNotification::close',
click = 'UCAP::ElectronNotification::click',
loadConfig = 'UCAP::ElectronNotification::loadConfig',
reset = 'UCAP::ElectronNotification::reset',
browserWindowSetContents = 'UCAP::ElectronNotification::BrowserWindowSetContents'
}

View File

@ -0,0 +1,5 @@
export enum ElectronNotificationEventType {
Show = 'Show',
Click = 'Click',
Close = 'Close'
}

View File

@ -0,0 +1,42 @@
export interface AnimationQueueObject {
context: any;
func: (...args: any[]) => Promise<any>;
args: any[];
}
export class AnimationQueue {
private running = false;
private queue: AnimationQueueObject[] = [];
push(o: AnimationQueueObject): void {
if (this.running) {
this.queue.push(o);
} else {
this.running = true;
this.animate(o);
}
}
animate(o: AnimationQueueObject): void {
const self = this;
try {
(o.func.apply(o.context, o.args) as Promise<any>)
.then(() => {
if (self.queue.length > 0) {
self.animate.call(self, self.queue.shift());
} else {
self.running = false;
}
})
.catch(reason => {
console.log(reason);
});
} catch (e) {
console.log(e);
}
}
clear(): void {
this.queue = [];
}
}

View File

@ -0,0 +1,13 @@
/*
* Public API Surface of ucap-webmessenger-electron-notification
*/
export * from './lib/models/electron-notification-options';
export * from './lib/models/electron-notification';
export * from './lib/services/electron-notification.service';
export * from './lib/types/channel.type';
export * from './lib/types/event.type';
export * from './lib/utils/animation-queue';

View File

@ -0,0 +1,21 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"ucapElectronNotification",
"camelCase"
],
"component-selector": [
true,
"element",
"ucap-electron-notification",
"kebab-case"
]
}
}

View File

@ -3,14 +3,15 @@ import { Observable } from 'rxjs';
import { import {
NativeService, NativeService,
WindowState, WindowState,
NotiRequest, NotificationRequest,
WindowIdle WindowIdle
} from '@ucap-webmessenger/native'; } from '@ucap-webmessenger/native';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
export class BrowserNativeService implements NativeService { export class BrowserNativeService implements NativeService {
showNotify(noti: NotiRequest): void {} notify(noti: NotificationRequest): void {}
closeAllNotify(): void {}
checkForUpdates(): Observable<boolean> { checkForUpdates(): Observable<boolean> {
return new Observable<boolean>(subscriber => { return new Observable<boolean>(subscriber => {

View File

@ -5,11 +5,17 @@ import { Observable, Subject } from 'rxjs';
import { import {
NativeService, NativeService,
WindowState, WindowState,
NotiRequest, NotificationRequest,
WindowIdle WindowIdle
} from '@ucap-webmessenger/native'; } from '@ucap-webmessenger/native';
import { Channel } from '../types/channel.type';
import { share } from 'rxjs/operators'; import { share } from 'rxjs/operators';
import {
NotificationChannel,
UpdaterChannel,
FileChannel,
WindowStateChannel,
IdleStateChannel
} from '../types/channel.type';
export class ElectronNativeService implements NativeService { export class ElectronNativeService implements NativeService {
private windowStateChangedSubject: Subject<WindowState> | null = null; private windowStateChangedSubject: Subject<WindowState> | null = null;
@ -18,22 +24,18 @@ export class ElectronNativeService implements NativeService {
private idleStateChangedSubject: Subject<WindowIdle> | null = null; private idleStateChangedSubject: Subject<WindowIdle> | null = null;
private idleStateChanged$: Observable<WindowIdle> | null = null; private idleStateChanged$: Observable<WindowIdle> | null = null;
showNotify(noti: NotiRequest): void { notify(noti: NotificationRequest): void {
ipcRenderer.send( ipcRenderer.send(NotificationChannel.Notify, noti);
Channel.showNotify, }
noti.roomSeq,
noti.title, closeAllNotify(): void {
noti.contents, ipcRenderer.send(NotificationChannel.CloseAllNotify);
noti.image,
noti.useSound,
noti.interval
);
} }
checkForUpdates(): Observable<boolean> { checkForUpdates(): Observable<boolean> {
return new Observable<boolean>(subscriber => { return new Observable<boolean>(subscriber => {
try { try {
subscriber.next(ipcRenderer.sendSync(Channel.checkForUpdates)); subscriber.next(ipcRenderer.sendSync(UpdaterChannel.Check));
} catch (error) { } catch (error) {
subscriber.error(error); subscriber.error(error);
} finally { } finally {
@ -43,13 +45,13 @@ export class ElectronNativeService implements NativeService {
} }
showImageViewer(): void { showImageViewer(): void {
ipcRenderer.send(Channel.showImageViewer); ipcRenderer.send(FileChannel.ShowImageViewer);
} }
readFile(path: string): Observable<Buffer> { readFile(path: string): Observable<Buffer> {
return new Observable<Buffer>(subscriber => { return new Observable<Buffer>(subscriber => {
try { try {
subscriber.next(ipcRenderer.sendSync(Channel.readFile, path)); subscriber.next(ipcRenderer.sendSync(FileChannel.ReadFile, path));
} catch (error) { } catch (error) {
subscriber.error(error); subscriber.error(error);
} finally { } finally {
@ -66,7 +68,7 @@ export class ElectronNativeService implements NativeService {
return new Observable<string>(subscriber => { return new Observable<string>(subscriber => {
try { try {
subscriber.next( subscriber.next(
ipcRenderer.sendSync(Channel.saveFile, buffer, fileName, path) ipcRenderer.sendSync(FileChannel.SaveFile, buffer, fileName, path)
); );
} catch (error) { } catch (error) {
subscriber.error(error); subscriber.error(error);
@ -85,9 +87,8 @@ export class ElectronNativeService implements NativeService {
} }
ipcRenderer.on( ipcRenderer.on(
Channel.windowStateChanged, WindowStateChannel.Changed,
(event: IpcRendererEvent, windowState: WindowState) => { (event: IpcRendererEvent, windowState: WindowState) => {
console.log('windowStateChanged', windowState);
this.windowStateChangedSubject.next(windowState); this.windowStateChangedSubject.next(windowState);
} }
); );
@ -133,10 +134,10 @@ export class ElectronNativeService implements NativeService {
.pipe(share()); .pipe(share());
} }
ipcRenderer.send(Channel.idleStateStart, 'start'); ipcRenderer.send(IdleStateChannel.StartCheck);
ipcRenderer.on( ipcRenderer.on(
Channel.idleStateChanged, IdleStateChannel.Changed,
(event: IpcRendererEvent, idleState: WindowIdle) => { (event: IpcRendererEvent, idleState: WindowIdle) => {
this.idleStateChangedSubject.next(idleState); this.idleStateChangedSubject.next(idleState);
} }

View File

@ -1,11 +1,23 @@
export enum Channel { export enum NotificationChannel {
windowStateChanged = 'window-state-changed', Notify = 'UCAP::notification::notify',
idleStateChanged = 'window-idle-state-changed', CloseAllNotify = 'UCAP::notification::closeAllNotify'
idleStateStart = 'window-idle-state-check-start', }
showNotify = 'UCAP::showNotify', export enum UpdaterChannel {
checkForUpdates = 'UCAP::checkForUpdates', Check = 'UCAP::updater::check'
showImageViewer = 'UCAP::showImageViewer', }
saveFile = 'UCAP::saveFile',
readFile = 'UCAP::readFile' export enum FileChannel {
ShowImageViewer = 'UCAP::file::showImageViewer',
SaveFile = 'UCAP::file::saveFile',
ReadFile = 'UCAP::file::readFile'
}
export enum WindowStateChannel {
Changed = 'UCAP::windowState::windowStateChanged'
}
export enum IdleStateChannel {
Changed = 'UCAP::idleState::changed',
StartCheck = 'UCAP::idleState::startCheck'
} }

View File

@ -0,0 +1,8 @@
export interface NotificationRequest {
roomSeq: string;
title: string;
contents: string;
image: string;
useSound: boolean;
interval?: number;
}

View File

@ -2,9 +2,11 @@ import { Observable } from 'rxjs';
import { WindowState } from '../types/window-state.type'; import { WindowState } from '../types/window-state.type';
import { WindowIdle } from '../types/window-idle.type'; import { WindowIdle } from '../types/window-idle.type';
import { NotificationRequest } from '../models/notification';
export interface NativeService { export interface NativeService {
showNotify(noti: NotiRequest): void; notify(noti: NotificationRequest): void;
closeAllNotify(): void;
checkForUpdates(): Observable<boolean>; checkForUpdates(): Observable<boolean>;
@ -20,12 +22,3 @@ export interface NativeService {
idleStateChanged(): Observable<WindowIdle>; idleStateChanged(): Observable<WindowIdle>;
} }
export interface NotiRequest {
roomSeq: string;
title: string;
contents: string;
image: string;
useSound: boolean;
interval?: number;
}

View File

@ -2,6 +2,8 @@
* Public API Surface of ucap-webmessenger-native * Public API Surface of ucap-webmessenger-native
*/ */
export * from './lib/models/notification';
export * from './lib/services/native.service'; export * from './lib/services/native.service';
export * from './lib/types/token'; export * from './lib/types/token';

View File

@ -125,6 +125,12 @@
], ],
"@ucap-webmessenger/util": [ "@ucap-webmessenger/util": [
"projects/ucap-webmessenger-util/src/public-api" "projects/ucap-webmessenger-util/src/public-api"
],
"@ucap-webmessenger/electron-core": [
"projects/ucap-webmessenger-electron-core/src/public-api"
],
"@ucap-webmessenger/electron-notification": [
"projects/ucap-webmessenger-electron-notification/src/public-api"
] ]
} }
}, },