project initialized

This commit is contained in:
richard-loafle 2020-03-27 17:41:40 +09:00
commit e1d820434e
54 changed files with 7822 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/docs
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"trailingComma": "none",
"tabWidth": 2,
"singleQuote": true
}

9
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"recommendations": [
"msjsdiag.debugger-for-chrome",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-tslint-plugin",
"VisualStudioExptTeam.vscodeintellicode"
]
}

7
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

14
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.autoClosingBrackets": "languageDefined",
"editor.trimAutoWhitespace": true,
"files.trimTrailingWhitespace": true,
"files.trimFinalNewlines": true,
"files.watcherExclude": {
"**/dist": true
},
"debug.node.autoAttach": "on"
}

15
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build:main:dev",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

6003
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "ucap-electron",
"version": "0.0.0",
"private": true,
"scripts": {
"clean:all": "rimraf dist/*",
"build:all": "npm-run-all -s build:core build:common build:notify-window build:updater-window",
"build:core": "node ./scripts/build.js core",
"build:common": "node ./scripts/build.js common",
"build:notify-window": "node ./scripts/build.js notify-window",
"build:updater-window": "node ./scripts/build.js updater-window",
"publish:all": "npm-run-all -s publish:core publish:notify-window publish:updater-window",
"publish:core": "cd ./dist/core && npm publish",
"publish:common": "cd ./dist/common && npm publish",
"publish:notify-window": "cd ./dist/core && npm publish",
"publish:updater-window": "cd ./dist/core && npm publish"
},
"dependencies": {},
"devDependencies": {
"@tsed/core": "^5.44.11",
"@tsed/di": "^5.44.11",
"@types/fs-extra": "^8.1.0",
"@types/node": "^12.11.1",
"@ucap/electron-core": "file:dist/core/ucap-electron-core-0.0.1.tgz",
"concurrently": "^5.1.0",
"electron": "^8.1.1",
"electron-log": "^4.1.0",
"fs-extra": "^8.1.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"rxjs": "^6.5.4",
"terser-webpack-plugin": "^2.3.5",
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",
"tslib": "^1.11.1",
"tslint": "^6.1.0",
"typedoc": "^0.16.11",
"typescript": "^3.8.3",
"webpack": "^4.42.0",
"webpack-merge": "^4.2.2",
"webpack-node-externals": "^1.7.2"
}
}

View File

@ -0,0 +1,15 @@
{
"name": "@ucap/electron-common",
"version": "0.0.1",
"publishConfig": {
"registry": "http://10.81.13.221:8081/nexus/repository/npm-ucap/"
},
"scripts": {},
"dependencies": {
"@tsed/core": "^5.44.11",
"@tsed/di": "^5.44.11",
"electron": "^8.0.0",
"rxjs": "^6.5.4"
},
"devDependencies": {}
}

View File

@ -0,0 +1,91 @@
import { Type, constructorOf } from '@tsed/core';
import {
GlobalProviders,
InjectorService,
ProviderScope,
registerProvider
} from '@tsed/di';
import * as Electron from 'electron';
import { AppOptions } from './decorators/app-settings';
import { AppSettingsService } from './services/app-settings.service';
import { ElectronApp } from './decorators/electron-app';
export abstract class App {
readonly injector: InjectorService;
private startedAt = new Date();
constructor(settings: Partial<AppOptions> = {}) {
// create injector with initial configuration
this.injector = this.createInjector(this.getConfiguration(this, settings));
this.createElectronApp(this.injector);
}
get settings(): AppSettingsService {
return this.injector.settings as AppSettingsService;
}
get electronApp(): ElectronApp {
return this.injector.get<ElectronApp>(ElectronApp)!;
}
static async bootstrap(
module: Type<App>,
settings: Partial<AppOptions> = {}
): Promise<App> {
const app = new module(settings);
return app;
}
async start(): Promise<any> {
try {
} catch (err) {
return Promise.reject(err);
}
}
private createInjector(settings: Partial<AppOptions> = {}) {
const injector = new InjectorService();
injector.settings = this.createSettingsService(injector);
// injector.logger = $log;
// @ts-ignore
injector.settings.set(settings);
/* istanbul ignore next */
if (injector.settings.env === 'test') {
injector.logger.stop();
}
return injector;
}
private createSettingsService(injector: InjectorService): AppSettingsService {
const provider = GlobalProviders.get(AppSettingsService)!.clone();
provider.instance = injector.invoke<AppSettingsService>(provider.useClass);
injector.addProvider(AppSettingsService, provider);
return provider.instance as any;
}
private getConfiguration(module: any, configuration: any = {}) {
const provider = GlobalProviders.get(constructorOf(module))!;
return { ...provider.configuration, ...configuration };
}
private createElectronApp(injector: InjectorService): void {
injector.forkProvider(ElectronApp);
}
}
registerProvider({
provide: ElectronApp,
scope: ProviderScope.SINGLETON,
global: true,
useValue: Electron.app
});

View File

@ -0,0 +1,10 @@
import { Type } from '@tsed/core';
import { IModuleOptions, Module } from '@tsed/di';
export interface AppOptions extends IModuleOptions {
bootstrap: Type<any>;
}
export function AppSettings(settings: Partial<AppOptions> = {}): any {
return Module({ ...settings, root: true });
}

View File

@ -0,0 +1,14 @@
import { Type } from '@tsed/core';
import { Inject } from '@tsed/di';
import * as Electron from 'electron';
export type ElectronApp = Electron.App;
export function ElectronApp(
target: Type<any>,
targetKey: string,
// tslint:disable-next-line: ban-types
descriptor: TypedPropertyDescriptor<Function> | number
) {
return Inject(ElectronApp)(target, targetKey, descriptor);
}

View File

@ -0,0 +1,16 @@
import {
DIConfiguration,
Injectable,
ProviderScope,
ProviderType
} from '@tsed/di';
@Injectable({
scope: ProviderScope.SINGLETON,
global: true
})
export class AppSettingsService extends DIConfiguration {
constructor() {
super();
}
}

View File

@ -0,0 +1,10 @@
/*
* Public API Surface of common
*/
export * from './lib/app/decorators/app-settings';
export * from './lib/app/decorators/electron-app';
export * from './lib/app/services/app-settings.service';
export * from './lib/app/app';

View File

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

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.lib.json"
}

View File

@ -0,0 +1,9 @@
{
"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,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -0,0 +1,7 @@
{
"dest": "../../dist/common",
"docDest": "../../docs/common",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,10 @@
{
"name": "@ucap/electron-core",
"version": "0.0.1",
"publishConfig": {
"registry": "http://10.81.13.221:8081/nexus/repository/npm-ucap/"
},
"scripts": {},
"dependencies": {},
"devDependencies": {}
}

View File

@ -0,0 +1,39 @@
export enum AppChannel {
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 BrowserWindowChannel {
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',
Focus = 'focus',
Blur = 'blur'
}
export enum WebContentsChannel {
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 core
*/
export * from './lib/types/channel.type';

View File

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

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.lib.json"
}

View File

@ -0,0 +1,9 @@
{
"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,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -0,0 +1,7 @@
{
"dest": "../../dist/core",
"docDest": "../../docs/core",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "@ucap/electron-notify-window",
"version": "0.0.1",
"publishConfig": {
"registry": "http://10.81.13.221:8081/nexus/repository/npm-ucap/"
},
"scripts": {},
"dependencies": {
"@ucap/electron-core": "~0.0.1",
"electron": "^8.0.0",
"electron-log": "^4.1.0",
"fs-extra": "^8.1.0",
"rxjs": "^6.5.4"
},
"devDependencies": {}
}

View File

@ -0,0 +1,120 @@
import * as path from 'path';
import { BrowserWindowConstructorOptions } from 'electron';
export interface NotifyWindowOptions {
width?: number;
height?: number;
padding?: number;
borderRadius?: number;
displayTime?: number;
animationSteps?: number;
animationStepMs?: number;
animateInParallel?: boolean;
appIcon?: string;
pathToModule?: string;
logging?: boolean;
browserWindowPool?: {
min?: number;
max?: number;
};
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 DefaultNotifyWindowOptions: NotifyWindowOptions = {
width: 300,
height: 65,
padding: 10,
borderRadius: 5,
displayTime: 5000,
animationSteps: 5,
animationStepMs: 20,
appIcon: null,
pathToModule: '',
logging: true,
browserWindowPool: {
min: 0,
max: 7
},
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 { NotifyWindowEventType } from '../types/event.type';
export interface NotifyWindowEvent {
type: NotifyWindowEventType;
id: number;
close?: (reason?: any) => void;
}
export interface NotifyWindow {
id?: number;
displayTime?: number;
title?: string;
text?: string;
image?: string;
url?: string;
sound?: string;
onClick?: (e: NotifyWindowEvent) => void;
onShow?: (e: NotifyWindowEvent) => void;
onClose?: (e: NotifyWindowEvent) => void;
}

View File

@ -0,0 +1,497 @@
import url from 'url';
import fse from 'fs-extra';
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 { screen, BrowserWindow, ipcMain, IpcMainEvent, shell } from 'electron';
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 {
private readonly inactiveWindows: BrowserWindow[];
constructor(private readonly minSize: number) {
this.minSize = 0 > this.minSize ? 0 : this.minSize;
this.inactiveWindows = [];
}
getLength() {
return this.inactiveWindows.length;
}
push(...items: BrowserWindow[]): number {
const length = this.inactiveWindows.push(...items);
if (this.minSize < length) {
this.start();
}
return length;
}
pop(): BrowserWindow {
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;
private activeNotifications: BrowserWindow[];
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();
}
setOptions(options: NotifyWindowOptions) {
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
this.calcDimensions();
}
getOptions(): NotifyWindowOptions {
return this.customOptions;
}
setTemplatePath(templatePath: string) {
if (!!templatePath) {
this.customOptions.templatePath = templatePath;
this.updateTemplatePath();
}
}
getTemplatePath(): string {
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();
this.activeNotifications.forEach(window => window.close());
this.browserWindowPooler.closeAll();
}
closeAll(): void {
this.animationQueue.clear();
this.activeNotifications.forEach(window => window.close());
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(
this.getOptions().browserWindowPool.min
);
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 =
this.getOptions().browserWindowPool.max < this.maxVisibleNotifications
? this.getOptions().browserWindowPool.max
: this.maxVisibleNotifications;
}
private setupEvents(): void {
const self = this;
ipcMain.on(
Channel.close,
(event: IpcMainEvent, windowId: number, notification: NotifyWindow) => {
const onClose = self.buildCloseNotification(
BrowserWindow.fromId(windowId),
notification
);
self.buildCloseNotificationSafely(onClose)('close');
}
);
ipcMain.on(
Channel.click,
(event: IpcMainEvent, windowId: number, notification: NotifyWindow) => {
if (!!notification.url) {
shell.openExternal(notification.url);
}
const notificationWindow = BrowserWindow.fromId(windowId);
if (notificationWindow && notificationWindow[onClickNotifyWindow]) {
const onClose = self.buildCloseNotification(
BrowserWindow.fromId(windowId),
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) {
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: 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,
notification
);
notificationWindow.showInactive();
resolve(notificationWindow);
});
} else {
self.notificationQueue.push(notification);
resolve();
}
});
}
private buildCloseNotification(
notificationWindow: BrowserWindow,
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);
return new Promise<void>(resolve => {
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()]
});
}
}
private getWindow(): Promise<BrowserWindow> {
const slef = this;
return new Promise<BrowserWindow>((resolve, reject) => {
if (0 < slef.browserWindowPooler.getLength()) {
resolve(slef.browserWindowPooler.pop());
} else {
const windowProperties = slef.customOptions.defaultWindow;
windowProperties.width = slef.customOptions.width;
windowProperties.height = slef.customOptions.height;
const notificationWindow = new BrowserWindow({
...windowProperties,
title: 'Notification'
});
notificationWindow.setVisibleOnAllWorkspaces(true);
notificationWindow.loadURL(slef.getTemplatePath());
notificationWindow.webContents.on(
WebContentsChannel.DidFinishLoad,
() => {
// Done
notificationWindow.webContents.send(
Channel.loadConfig,
slef.customOptions
);
resolve(notificationWindow);
}
);
notificationWindow.webContents.on(
WebContentsChannel.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::electron::notify-window::close',
click = 'ucap::electron::notify-window::click',
loadConfig = 'ucap::electron::notify-window::loadConfig',
reset = 'ucap::electron::notify-window::reset',
browserWindowSetContents = 'ucap::electron::notify-window::browserWindowSetContents'
}

View File

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

View File

@ -0,0 +1,44 @@
import log from 'electron-log';
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 => {
log.error(reason);
});
} catch (e) {
log.error(e);
}
}
clear(): void {
this.queue = [];
}
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.lib.json"
}

View File

@ -0,0 +1,9 @@
{
"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,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -0,0 +1,7 @@
{
"dest": "../../dist/notify-window",
"docDest": "../../docs/notify-window",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "@ucap/electron-updater-window",
"version": "0.0.1",
"publishConfig": {
"registry": "http://10.81.13.221:8081/nexus/repository/npm-ucap/"
},
"scripts": {},
"dependencies": {
"@ucap/electron-core": "~0.0.1",
"electron": "^8.0.0",
"electron-log": "^4.1.0",
"fs-extra": "^8.1.0",
"rxjs": "^6.5.4"
},
"devDependencies": {}
}

View File

@ -0,0 +1,21 @@
import { BrowserWindowConstructorOptions } from 'electron';
export interface UpdaterWindowOptions extends BrowserWindowConstructorOptions {
templatePath?: string;
onReady?: () => void;
onAcceptUpdate?: () => void;
onDenyUpdate?: () => void;
onCancelDownload?: () => void;
}
export const DefaultUpdaterWindowOptions: UpdaterWindowOptions = {
width: 500,
height: 160,
frame: false,
skipTaskbar: true,
alwaysOnTop: true,
maximizable: false,
webPreferences: {
nodeIntegration: true
}
};

View File

@ -0,0 +1,129 @@
import { BrowserWindow, ipcMain } from 'electron';
import url from 'url';
import fse from 'fs-extra';
import log from 'electron-log';
import {
UpdaterWindowOptions,
DefaultUpdaterWindowOptions
} from '../models/updater-window-options';
import { Channel } from '../types/channel.type';
import { WebContentsChannel } from '@ucap/electron-core';
export class UpdaterWindowService {
private customOptions: UpdaterWindowOptions;
private browserWindow: BrowserWindow;
private templateUrl: string;
constructor(options: UpdaterWindowOptions) {
this.customOptions = {
...DefaultUpdaterWindowOptions
};
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
}
setOptions(options: UpdaterWindowOptions) {
if (!!options) {
this.customOptions = {
...this.customOptions,
...options
};
}
}
getOptions(): UpdaterWindowOptions {
return this.customOptions;
}
setTemplatePath(templatePath: string) {
if (!!templatePath) {
this.customOptions.templatePath = templatePath;
this.updateTemplatePath();
}
}
getTemplatePath(): string {
if (!this.templateUrl) {
this.updateTemplatePath();
}
return this.templateUrl;
}
show(versionInfo: { installed: string; latest: string }) {
this.browserWindow = new BrowserWindow(this.customOptions);
this.browserWindow.loadURL(this.getTemplatePath());
this.browserWindow.on('closed', () => {
this.browserWindow = null;
});
this.browserWindow.webContents.on(WebContentsChannel.DidFinishLoad, () => {
if (process.env.NODE_ENV === 'development') {
this.browserWindow.webContents.openDevTools();
}
if (!!this.customOptions.onReady) {
this.customOptions.onReady();
}
this.browserWindow.webContents.send(
Channel.browserWindowSetContents,
versionInfo
);
});
ipcMain.on(Channel.acceptUpdate, this._acceptUpdateHandler.bind(this));
ipcMain.on(Channel.denyUpdate, this._denyUpdateHandler.bind(this));
ipcMain.on(Channel.cancelDownload, this._cancelDownloadHandler.bind(this));
}
setDownloadValue(value: number, total: number) {
this.browserWindow.webContents.send(Channel.downloadProcess, value, total);
}
setDownloadComplete() {
this.browserWindow.webContents.send(Channel.downloadComplete);
}
close() {
if (!this.browserWindow || this.browserWindow.isDestroyed()) {
return;
}
this.browserWindow.destroy();
}
_acceptUpdateHandler() {
if (!!this.customOptions.onAcceptUpdate) {
this.customOptions.onAcceptUpdate();
}
}
_denyUpdateHandler() {
if (!!this.customOptions.onDenyUpdate) {
this.customOptions.onDenyUpdate();
}
}
_cancelDownloadHandler() {
if (!!this.customOptions.onCancelDownload) {
this.customOptions.onCancelDownload();
}
}
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-update-window: Could not find template ("' +
this.customOptions.templatePath +
'").'
);
}
}
}

View File

@ -0,0 +1,10 @@
export enum Channel {
acceptUpdate = 'ucap::electron::updater-window::acceptUpdate',
denyUpdate = 'ucap::electron::updater-window::denyUpdate',
cancelDownload = 'ucap::electron::updater-window::cancelDownload',
downloadProcess = 'ucap::electron::updater-window::downloadProcess',
downloadComplete = 'ucap::electron::updater-window::downloadComplete',
browserWindowSetContents = 'ucap::electron::updater-window::browserWindowSetContents'
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.lib.json"
}

View File

@ -0,0 +1,9 @@
{
"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,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -0,0 +1,7 @@
{
"dest": "../../dist/updater-window",
"docDest": "../../docs/updater-window",
"lib": {
"entryFile": "src/public-api.ts"
}
}

351
scripts/build.js Normal file
View File

@ -0,0 +1,351 @@
const { execSync } = require('child_process');
const path = require('path');
const fse = require('fs-extra');
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const webpackNodeExternals = require('webpack-node-externals');
const webpackNodeTerser = require('terser-webpack-plugin');
async function buildForProduction(args) {
const rootPath = path.join(__dirname, '..');
const projectName = args[0];
const projectPath = path.join(rootPath, 'projects', projectName);
const packageJson = require(path.join(projectPath, 'package.json'));
const ucapPackageJson = require(path.join(projectPath, 'ucap-package.json'));
const distPath = path.join(projectPath, ucapPackageJson.dest);
const docPath = path.join(projectPath, ucapPackageJson.docDest);
const webpackConfig = (overrideConfig, compilerOptions) => {
const commonConfig = {
name: projectName,
target: 'node',
mode: 'production',
context: path.join(projectPath, 'src'),
entry: path.join(projectPath, ucapPackageJson.lib.entryFile),
output: {
path: path.join(distPath, 'bundles'),
filename: `${projectName}.umd.js`,
libraryTarget: 'umd',
library: projectName,
umdNamedDefine: true
},
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
modules: ['node_modules', 'src']
},
externals: [webpackNodeExternals()],
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
configFile: path.join(projectPath, 'tsconfig.lib.prod.json'),
compilerOptions: !!compilerOptions
? compilerOptions
: {
declaration: false,
module: 'ES2015',
target: 'es5'
}
}
}
],
exclude: /node_modules/
}
]
}
};
return webpackMerge(commonConfig, overrideConfig);
};
const clean = () => {
return new Promise((resolve, reject) => {
const _distPath = path.join(distPath);
console.log(`${projectName}: cleaning started [${_distPath}]`);
execSync(`rimraf ${_distPath}`, {
stdio: 'inherit'
});
console.log(`${projectName}: cleaning complete`);
resolve();
});
};
const genDeclaration = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: generating of declaration started`);
execSync(
`tsc --project ${path.join(
projectPath,
'tsconfig.lib.prod.json'
)} --module commonjs --target es5 --outDir ${path.join(
distPath,
'lib'
)} --emitDeclarationOnly true --declarationDir ${path.join(distPath)}`,
{ stdio: 'inherit' }
);
console.log(`${projectName}: generating of declaration complete`);
resolve();
});
};
const genEs5 = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: generating of es5 started`);
execSync(
`tsc --project ${path.join(
projectPath,
'tsconfig.lib.prod.json'
)} --module es2015 --target es5 --outDir ${path.join(
distPath,
'es5'
)} --declaration false --sourceMap false --inlineSourceMap true`,
{ stdio: 'inherit' }
);
console.log(`${projectName}: generating of es5 complete`);
resolve();
});
};
const genEs2015 = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: generating of es2015 started`);
execSync(
`tsc --project ${path.join(
projectPath,
'tsconfig.lib.prod.json'
)} --module es2015 --target es2015 --outDir ${path.join(
distPath,
'es2015'
)} --declaration false --sourceMap false --inlineSourceMap true`,
{ stdio: 'inherit' }
);
console.log(`${projectName}: generating of es2015 complete`);
resolve();
});
};
const genBundles = () => {
return new Promise(async (resolve, reject) => {
console.log(`${projectName}: generating of bundle[umd] started`);
await new Promise((resolve, reject) => {
webpack(
webpackConfig({ optimization: { minimize: false } }),
(err, status) => {
if (!!err) {
console.err(err);
reject(err);
return;
}
resolve();
}
);
}).catch(reason => {
reject(reason);
});
await new Promise((resolve, reject) => {
webpack(
webpackConfig({
output: {
filename: `${projectName}.umd.min.js`
},
optimization: { minimizer: [new webpackNodeTerser()] }
}),
(err, status) => {
if (!!err) {
console.err(err);
reject(err);
return;
}
resolve();
}
);
}).catch(reason => {
reject(reason);
});
console.log(`${projectName}: generating of bundle[umd] complete`);
resolve();
});
};
const genFes5 = () => {
return new Promise(async (resolve, reject) => {
console.log(`${projectName}: generating of fes5 started`);
await new Promise((resolve, reject) => {
webpack(
webpackConfig(
{
output: {
path: path.join(distPath, 'fes5'),
filename: `${projectName}.js`,
library: projectName
},
optimization: { minimize: false }
},
{
declaration: false,
module: 'es2015',
target: 'es5'
}
),
(err, status) => {
if (!!err) {
console.err(err);
reject(err);
return;
}
resolve();
}
);
}).catch(reason => {
reject(reason);
});
console.log(`${projectName}: generating of fes5 complete`);
resolve();
});
};
const genFes2015 = () => {
return new Promise(async (resolve, reject) => {
console.log(`${projectName}: generating of fes2015 started`);
await new Promise((resolve, reject) => {
webpack(
webpackConfig(
{
output: {
path: path.join(distPath, 'fes2015'),
filename: `${projectName}.js`,
library: projectName
},
optimization: { minimize: false }
},
{
declaration: false,
module: 'es2015',
target: 'es2015'
}
),
(err, status) => {
if (!!err) {
console.err(err);
reject(err);
return;
}
resolve();
}
);
}).catch(reason => {
reject(reason);
});
console.log(`${projectName}: generating of fes2015 complete`);
resolve();
});
};
const genPackageJson = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: generating of package.json started`);
const projectpackageJson = {
...packageJson,
main: `bundles/${projectName}.umd.js`,
module: `fes5/${projectName}.js`,
es2015: `fes2015/${projectName}.js`,
esm5: `es5/public-api.js`,
esm2015: `es2016/public-api.js`,
fesm5: `fes5/${projectName}.js`,
fesm2015: `fes2015/${projectName}.js`,
typings: `public-api.d.ts`,
sideEffects: false
};
fse.writeFileSync(
path.join(distPath, 'package.json'),
JSON.stringify(projectpackageJson)
);
console.log(`${projectName}: generating of package.json complete`);
resolve();
});
};
const installPackage = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: installation for local started`);
process.chdir(path.join(distPath));
execSync(`npm pack`, { stdio: 'inherit' });
process.chdir(path.join(rootPath));
const projectVersion = require(path.join(distPath, 'package.json'))
.version;
execSync(
`npm install -D ${path.join(
distPath,
`ucap-electron-${projectName}-${projectVersion}.tgz`
)}`,
{
stdio: 'inherit'
}
);
execSync(
`rimraf ${path.join(
distPath,
`ucap-electron-${projectName}-${projectVersion}.tgz`
)}`,
{
stdio: 'inherit'
}
);
console.log(`${projectName}: installation for local complete`);
resolve();
});
};
const genDoc = () => {
return new Promise((resolve, reject) => {
console.log(`${projectName}: generating of doc started`);
execSync(`rimraf ${path.join(docPath)}`, {
stdio: 'inherit'
});
execSync(
`typedoc --out ${path.join(docPath)} ${path.join(projectPath, 'src')}`,
{ stdio: 'inherit' }
);
console.log(`${projectName}: generating of doc complete`);
resolve();
});
};
await clean();
await genDeclaration();
await genEs5();
await genEs2015();
await genBundles();
await genFes5();
await genFes2015();
await genPackageJson();
await installPackage();
await genDoc();
}
buildForProduction(process.argv.slice(2));

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"esModuleInterop": true,
"target": "es5",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"],
"paths": {}
}
}

54
tslint.json Normal file
View File

@ -0,0 +1,54 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"import-blacklist": [true, "rxjs/Rx"],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [true, 140],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-empty": false,
"no-inferrable-types": [true, "ignore-params"],
"no-non-null-assertion": false,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [true, "single"],
"trailing-comma": false,
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
}
}