ucap-electron/projects/notify-window/src/lib/services/notify-window.service.ts

508 lines
14 KiB
TypeScript
Raw Normal View History

2020-03-27 17:41:40 +09:00
import url from 'url';
import fse from 'fs-extra';
2020-08-10 14:04:56 +09:00
import * as Electron from 'electron';
2020-03-27 17:41:40 +09:00
import log from 'electron-log';
import { WebContentsChannel } from '@ucap/electron-core';
import { AnimationQueue } from '../utils/animation-queue';
import {
NotifyWindowOptions,
DefaultNotifyWindowOptions
} from '../models/notify-window-options';
import { NotifyWindow } from '../models/notify-window';
import { NotifyWindowEventType } from '../types/event.type';
import { Channel } from '../types/channel.type';
const onClickNotifyWindow = 'onClickNotifyWindow';
const onCloseNotifyWindow = 'onCloseNotifyWindow';
interface ENPoint {
x: number;
y: number;
}
interface ENDimension {
width: number;
height: number;
}
class BrowserWindowPooler {
2020-08-10 14:04:56 +09:00
private readonly inactiveWindows: Electron.BrowserWindow[];
2020-03-27 17:41:40 +09:00
constructor(private readonly minSize: number) {
this.minSize = 0 > this.minSize ? 0 : this.minSize;
this.inactiveWindows = [];
}
2020-08-10 14:04:56 +09:00
get length() {
2020-03-27 17:41:40 +09:00
return this.inactiveWindows.length;
}
2020-08-10 14:04:56 +09:00
push(...items: Electron.BrowserWindow[]): number {
2020-03-27 17:41:40 +09:00
const length = this.inactiveWindows.push(...items);
if (this.minSize < length) {
this.start();
}
return length;
}
2020-08-10 14:04:56 +09:00
pop(): Electron.BrowserWindow {
2020-03-27 17:41:40 +09:00
if (!this.inactiveWindows || 0 === this.inactiveWindows.length) {
return undefined;
}
return this.inactiveWindows.pop();
}
closeAll() {
while (this.inactiveWindows.length > 0) {
const w = this.inactiveWindows.pop();
w.close();
}
}
private start() {
setTimeout(() => {
while (this.minSize < this.inactiveWindows.length) {
const w = this.inactiveWindows.pop();
w.close();
}
}, 3000);
}
}
export class NotifyWindowService {
private animationQueue: AnimationQueue;
private customOptions: NotifyWindowOptions;
private nextInsertPosition: ENPoint;
private totalDimension: ENDimension;
private firstPosition: ENPoint;
private lowerRightCornerPosition: ENPoint;
private maxVisibleNotifications: number;
2020-08-10 14:04:56 +09:00
private activeNotifications: Electron.BrowserWindow[];
2020-03-27 17:41:40 +09:00
private browserWindowPooler: BrowserWindowPooler;
private notificationQueue: NotifyWindow[];
private closedNotifications: Map<number, boolean>;
private latestId: number;
private templateUrl: string;
constructor(options?: NotifyWindowOptions) {
this.customOptions = {
...DefaultNotifyWindowOptions
};
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
this.setup();
this.setupEvents();
}
2020-08-10 14:04:56 +09:00
set options(options: NotifyWindowOptions) {
2020-03-27 17:41:40 +09:00
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
this.calcDimensions();
}
2020-08-10 14:04:56 +09:00
get options(): NotifyWindowOptions {
2020-03-27 17:41:40 +09:00
return this.customOptions;
}
2020-08-10 14:04:56 +09:00
set templatePath(templatePath: string) {
2020-03-27 17:41:40 +09:00
if (!!templatePath) {
this.customOptions.templatePath = templatePath;
this.updateTemplatePath();
}
}
2020-08-10 14:04:56 +09:00
get templatePath(): string {
2020-03-27 17:41:40 +09:00
if (!this.templateUrl) {
this.updateTemplatePath();
}
return this.templateUrl;
}
notify(notification: NotifyWindow): number {
notification.id = this.latestId++;
this.animationQueue.push({
context: this,
func: this.showNotification,
args: [notification]
});
return notification.id;
}
dispose(): void {
this.animationQueue.clear();
2020-08-10 14:04:56 +09:00
this.activeNotifications.forEach((window) => window.close());
2020-03-27 17:41:40 +09:00
this.browserWindowPooler.closeAll();
}
closeAll(): void {
this.animationQueue.clear();
2020-08-10 14:04:56 +09:00
this.activeNotifications.forEach((window) => window.close());
2020-03-27 17:41:40 +09:00
this.browserWindowPooler.closeAll();
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.browserWindowPooler = new BrowserWindowPooler(
2020-08-10 14:04:56 +09:00
this.options.browserWindowPool.min
2020-03-27 17:41:40 +09:00
);
this.notificationQueue = [];
this.closedNotifications = new Map();
this.latestId = 0;
this.animationQueue = new AnimationQueue();
2020-08-10 14:04:56 +09:00
const display = Electron.screen.getPrimaryDisplay();
2020-03-27 17:41:40 +09:00
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 =
2020-08-10 14:04:56 +09:00
this.options.browserWindowPool.max < this.maxVisibleNotifications
? this.options.browserWindowPool.max
2020-03-27 17:41:40 +09:00
: this.maxVisibleNotifications;
}
private setupEvents(): void {
const self = this;
2020-08-10 14:04:56 +09:00
Electron.ipcMain.on(
2020-03-27 17:41:40 +09:00
Channel.close,
2020-08-10 14:04:56 +09:00
(
event: Electron.IpcMainEvent,
windowId: number,
sNotification: string
) => {
const notification: NotifyWindow = JSON.parse(sNotification);
2020-03-27 17:41:40 +09:00
const onClose = self.buildCloseNotification(
2020-08-10 14:04:56 +09:00
Electron.BrowserWindow.fromId(windowId),
2020-03-27 17:41:40 +09:00
notification
);
self.buildCloseNotificationSafely(onClose)('close');
}
);
2020-08-10 14:04:56 +09:00
Electron.ipcMain.on(
2020-03-27 17:41:40 +09:00
Channel.click,
2020-08-10 14:04:56 +09:00
(
event: Electron.IpcMainEvent,
windowId: number,
sNotification: string
) => {
const notification: NotifyWindow = JSON.parse(sNotification);
2020-03-27 17:41:40 +09:00
if (!!notification.url) {
2020-08-10 14:04:56 +09:00
Electron.shell.openExternal(notification.url);
2020-03-27 17:41:40 +09:00
}
2020-08-10 14:04:56 +09:00
const notificationWindow = Electron.BrowserWindow.fromId(windowId);
2020-03-27 17:41:40 +09:00
if (notificationWindow && notificationWindow[onClickNotifyWindow]) {
const onClose = self.buildCloseNotification(
2020-08-10 14:04:56 +09:00
Electron.BrowserWindow.fromId(windowId),
2020-03-27 17:41:40 +09:00
notification
);
notificationWindow[onClickNotifyWindow]({
type: NotifyWindowEventType.Click,
id: notification.id,
close: self.buildCloseNotificationSafely(onClose)
});
delete notificationWindow[onClickNotifyWindow];
}
}
);
}
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 {
fse.statSync(this.customOptions.templatePath).isFile();
this.templateUrl = url.format({
pathname: this.customOptions.templatePath,
protocol: 'file:',
slashes: true
});
} catch (e) {
log.error(
'electron-notify: Could not find template ("' +
this.customOptions.templatePath +
'").'
);
log.error(
'electron-notify: To use a different template you need to correct the config.templatePath or simply adapt config.htmlTemplate'
);
}
}
private showNotification(notification: NotifyWindow): Promise<any> {
const self = this;
return new Promise<any>((resolve, reject) => {
if (this.activeNotifications.length < this.maxVisibleNotifications) {
2020-08-10 14:04:56 +09:00
self.getWindow().then((notificationWindow) => {
2020-03-27 17:41:40 +09:00
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: NotifyWindowEventType.Show,
id: notification.id,
close: onCloseNotificationSafely
});
}
if (!!notification.onClick) {
notificationWindow[onClickNotifyWindow] = notification.onClick;
} else {
delete notificationWindow[onClickNotifyWindow];
}
if (!!notification.onClose) {
notificationWindow[onCloseNotifyWindow] = notification.onClose;
} else {
delete notificationWindow[onCloseNotifyWindow];
}
notificationWindow.webContents.send(
Channel.browserWindowSetContents,
2020-08-10 14:04:56 +09:00
JSON.stringify(notification)
2020-03-27 17:41:40 +09:00
);
notificationWindow.showInactive();
resolve(notificationWindow);
});
} else {
self.notificationQueue.push(notification);
resolve();
}
});
}
private buildCloseNotification(
2020-08-10 14:04:56 +09:00
notificationWindow: Electron.BrowserWindow,
2020-03-27 17:41:40 +09:00
notification: NotifyWindow,
timeoutIdFunc?: () => number
) {
const self = this;
return (e: NotifyWindowEventType): Promise<void> => {
if (notificationWindow.isDestroyed()) {
return;
}
if (self.closedNotifications.has(notification.id)) {
self.closedNotifications.delete(notification.id);
2020-08-10 14:04:56 +09:00
return new Promise<void>((resolve) => {
2020-03-27 17:41:40 +09:00
resolve();
});
} else {
self.closedNotifications.set(notification.id, true);
}
if (!!notificationWindow[onCloseNotifyWindow]) {
notificationWindow[onCloseNotifyWindow]({
type: e,
id: notification.id
});
delete notificationWindow[onCloseNotifyWindow];
}
notificationWindow.webContents.send(Channel.reset);
if (!!timeoutIdFunc) {
clearTimeout(timeoutIdFunc());
}
const i = self.activeNotifications.indexOf(notificationWindow);
self.activeNotifications.splice(i, 1);
self.browserWindowPooler.push(notificationWindow);
notificationWindow.hide();
self.checkForQueuedNotifications();
return self.moveOneDown(i);
};
}
private buildCloseNotificationSafely(
onClose: (e: NotifyWindowEventType) => 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()]
});
}
}
2020-08-10 14:04:56 +09:00
private getWindow(): Promise<Electron.BrowserWindow> {
2020-03-27 17:41:40 +09:00
const slef = this;
2020-08-10 14:04:56 +09:00
return new Promise<Electron.BrowserWindow>((resolve, reject) => {
if (0 < slef.browserWindowPooler.length) {
2020-03-27 17:41:40 +09:00
resolve(slef.browserWindowPooler.pop());
} else {
const windowProperties = slef.customOptions.defaultWindow;
windowProperties.width = slef.customOptions.width;
windowProperties.height = slef.customOptions.height;
2020-08-10 14:04:56 +09:00
const notificationWindow = new Electron.BrowserWindow({
2020-03-27 17:41:40 +09:00
...windowProperties,
title: 'Notification'
});
notificationWindow.setVisibleOnAllWorkspaces(true);
2020-08-10 14:04:56 +09:00
notificationWindow.loadURL(slef.templatePath);
2020-03-27 17:41:40 +09:00
notificationWindow.webContents.on(
2020-08-10 14:04:56 +09:00
WebContentsChannel.didFinishLoad,
2020-03-27 17:41:40 +09:00
() => {
// Done
notificationWindow.webContents.send(
Channel.loadConfig,
slef.customOptions
);
resolve(notificationWindow);
}
);
notificationWindow.webContents.on(
2020-08-10 14:04:56 +09:00
WebContentsChannel.devtoolsOpened,
2020-03-27 17:41:40 +09:00
() => {
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(
2020-08-10 14:04:56 +09:00
aryNotificationPos.map(async (index) => {
2020-03-27 17:41:40 +09:00
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);
});
}
}