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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|