2019-11-09 17:29:02 +09:00
|
|
|
import * as path from 'path';
|
|
|
|
import * as url from 'url';
|
2019-11-11 15:53:39 +09:00
|
|
|
import * as fse from 'fs-extra';
|
2019-11-09 17:29:02 +09:00
|
|
|
|
|
|
|
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 {
|
2019-11-11 15:53:39 +09:00
|
|
|
fse.statSync(this.customOptions.templatePath).isFile();
|
|
|
|
|
|
|
|
this.templateUrl = url.format({
|
|
|
|
pathname: this.customOptions.templatePath,
|
|
|
|
protocol: 'file:',
|
|
|
|
slashes: true
|
|
|
|
});
|
2019-11-09 17:29:02 +09:00
|
|
|
} 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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|