From 121807a65a76f79e19052b0ad61258779f611c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Kiss?= Date: Thu, 22 Jun 2017 14:22:54 +0200 Subject: [PATCH] Add 'New update available' dialog to the electron version (#299) * build(tsconfig): Rename root tsconfig.json -> tsconfig.base.json * feat(auto-update): Add update dialog When new update available than new message will visible of the top of the screen with 2 buttons 'Update' and 'Close'. - Update button: Update the application (close and restart) - Close button: Hide the updatePanel * fix(auto-update): Add types to the event methods * style: Add comma after SafeStylePipe import I forgot add the comma when I rebased the branch * fix(auto-update): Use electron-is-dev package to detect dev build I removed the isDev() function from the shared util library because it is electron specific code. * ci: Change osx_image: xcode8.3 Recommended after the last travis upgrade * feat(auto-update): Add auto update settings page and save config save on electron platform * ci: Fix osx image * ci: Upgrade the electron builder -> 19.6.1 The builder now use the 2 package.json structure and build only the necessary dependencies. --- .travis.yml | 2 +- electron/src/app.module.ts | 25 +++- electron/src/app/app.component.html | 6 + electron/src/app/app.component.ts | 21 ++- .../privilege-checker.component.ts | 3 +- .../src/components/update-available/index.ts | 1 + .../update-available.component.html | 5 + .../update-available.component.scss | 5 + .../update-available.component.ts | 13 ++ .../src/custom_types/electron-is-dev.d.ts | 1 + electron/src/dev-extension.ts | 22 ++++ electron/src/electron-main.ts | 124 +++++++++++++++++- electron/src/package.json | 20 +++ .../services/app-update-renderer.service.ts | 75 +++++++++++ ...electron-datastorage-repository.service.ts | 37 +++++- .../src/store/actions/app-update.action.ts | 40 ++++++ electron/src/store/actions/app.action.ts | 22 ++++ .../src/store/effects/app-update.effect.ts | 32 +++++ electron/src/store/effects/app.effect.ts | 24 ++++ electron/src/store/effects/index.ts | 2 + electron/src/store/index.ts | 41 ++++++ .../src/store/reducers/app-update.reducer.ts | 37 ++++++ electron/src/store/reducers/app.reducer.ts | 21 +++ electron/src/tsconfig-electron-main.json | 2 +- electron/src/tsconfig.json | 3 +- electron/src/webpack.config.js | 4 + package.json | 19 ++- scripts/release.js | 114 +++++++++------- .../auto-update-settings.html | 36 +++++ .../auto-update-settings.ts | 35 +++++ .../settings/settings.component.html | 13 +- .../components/settings/settings.component.ts | 36 ++++- shared/src/models/auto-update-settings.ts | 4 + .../datastorage-repository.service.ts | 5 + .../local-datastorage-repository.service.ts | 25 ++-- .../src/store/actions/auto-update-settings.ts | 74 +++++++++++ .../src/store/effects/auto-update-settings.ts | 42 ++++++ shared/src/store/effects/index.ts | 1 + shared/src/store/effects/user-config.ts | 20 ++- shared/src/store/index.ts | 10 ++ .../store/reducers/auto-update-settings.ts | 50 +++++++ shared/src/store/reducers/index.ts | 6 +- shared/src/store/reducers/preset.ts | 2 +- shared/src/tsconfig.json | 26 +--- shared/src/util/index.ts | 7 + shared/src/util/ipcEvents.ts | 20 +++ tsconfig.json => tsconfig.base.json | 4 +- web/src/app.module.ts | 18 ++- web/src/tsconfig.json | 2 +- 49 files changed, 1028 insertions(+), 129 deletions(-) create mode 100644 electron/src/components/update-available/index.ts create mode 100644 electron/src/components/update-available/update-available.component.html create mode 100644 electron/src/components/update-available/update-available.component.scss create mode 100644 electron/src/components/update-available/update-available.component.ts create mode 100644 electron/src/custom_types/electron-is-dev.d.ts create mode 100644 electron/src/dev-extension.ts create mode 100644 electron/src/package.json create mode 100644 electron/src/services/app-update-renderer.service.ts create mode 100644 electron/src/store/actions/app-update.action.ts create mode 100644 electron/src/store/actions/app.action.ts create mode 100644 electron/src/store/effects/app-update.effect.ts create mode 100644 electron/src/store/effects/app.effect.ts create mode 100644 electron/src/store/effects/index.ts create mode 100644 electron/src/store/index.ts create mode 100644 electron/src/store/reducers/app-update.reducer.ts create mode 100644 electron/src/store/reducers/app.reducer.ts create mode 100644 shared/src/components/auto-update-settings/auto-update-settings.html create mode 100644 shared/src/components/auto-update-settings/auto-update-settings.ts create mode 100644 shared/src/models/auto-update-settings.ts create mode 100644 shared/src/store/actions/auto-update-settings.ts create mode 100644 shared/src/store/effects/auto-update-settings.ts create mode 100644 shared/src/store/reducers/auto-update-settings.ts create mode 100644 shared/src/util/ipcEvents.ts rename tsconfig.json => tsconfig.base.json (90%) diff --git a/.travis.yml b/.travis.yml index f4d87f7f..94804bdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ matrix: fast_finish: true include: - os: osx - + osx_image: xcode8.3 - os: linux env: CC=clang CXX=clang++ npm_config_clang=1 compiler: clang diff --git a/electron/src/app.module.ts b/electron/src/app.module.ts index 83004c04..f80c8d2d 100644 --- a/electron/src/app.module.ts +++ b/electron/src/app.module.ts @@ -67,6 +67,7 @@ import { SvgKeyboardWrapComponent } from './shared/components/svg/wrap'; import { appRoutingProviders, routing } from './app/app.routes'; import { AppComponent } from './app/app.component'; import { MainAppComponent } from './main-app'; +import { UpdateAvailableComponent } from './components/update-available/update-available.component'; import { CancelableDirective } from './shared/directives'; import { SafeStylePipe } from './shared/pipes'; @@ -76,7 +77,8 @@ import { MapperService } from './shared/services/mapper.service'; import { SvgModuleProviderService } from './shared/services/svg-module-provider.service'; import { UhkDeviceService } from './services/uhk-device.service'; -import { KeymapEffects, MacroEffects, UserConfigEffects} from './shared/store/effects'; +import { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects'; +import { ApplicationEffect, AppUpdateEffect } from './store/effects'; import { KeymapEditGuard } from './shared/components/keymap/edit'; import { MacroNotFoundGuard } from './shared/components/macro/not-found'; @@ -88,7 +90,9 @@ import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service'; import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service'; import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service'; -import { reducer } from '../../shared/src/store/reducers/index'; +import { AppUpdateRendererService } from './services/app-update-renderer.service'; +import { reducer } from './store'; +import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings'; @NgModule({ declarations: [ @@ -143,7 +147,9 @@ import { reducer } from '../../shared/src/store/reducers/index'; PrivilegeCheckerComponent, UhkMessageComponent, CancelableDirective, - SafeStylePipe + SafeStylePipe, + UpdateAvailableComponent, + AutoUpdateSettings ], imports: [ BrowserModule, @@ -163,7 +169,10 @@ import { reducer } from '../../shared/src/store/reducers/index'; Select2Module, EffectsModule.runAfterBootstrap(KeymapEffects), EffectsModule.runAfterBootstrap(MacroEffects), - EffectsModule.runAfterBootstrap(UserConfigEffects) + EffectsModule.runAfterBootstrap(UserConfigEffects), + EffectsModule.runAfterBootstrap(AutoUpdateSettingsEffects), + EffectsModule.run(ApplicationEffect), + EffectsModule.run(AppUpdateEffect) ], providers: [ UhkDeviceConnectedGuard, @@ -177,9 +186,11 @@ import { reducer } from '../../shared/src/store/reducers/index'; MacroNotFoundGuard, CaptureService, UhkDeviceService, - {provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService}, - DefaultUserConfigurationService + { provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService }, + DefaultUserConfigurationService, + AppUpdateRendererService ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { +} diff --git a/electron/src/app/app.component.html b/electron/src/app/app.component.html index 0680b43f..032f6141 100644 --- a/electron/src/app/app.component.html +++ b/electron/src/app/app.component.html @@ -1 +1,7 @@ + + + diff --git a/electron/src/app/app.component.ts b/electron/src/app/app.component.ts index ccce4074..7c028ec0 100644 --- a/electron/src/app/app.component.ts +++ b/electron/src/app/app.component.ts @@ -1,4 +1,9 @@ import { Component, ViewEncapsulation } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Rx'; + +import { AppState, getShowAppUpdateAvailable } from '../store'; +import { DoNotUpdateAppAction, UpdateAppAction } from '../store/actions/app-update.action'; @Component({ selector: 'app', @@ -6,4 +11,18 @@ import { Component, ViewEncapsulation } from '@angular/core'; styleUrls: ['app.component.scss'], encapsulation: ViewEncapsulation.None }) -export class AppComponent { } +export class AppComponent { + showUpdateAvailable$: Observable; + + constructor(private store: Store) { + this.showUpdateAvailable$ = store.select(getShowAppUpdateAvailable); + } + + updateApp() { + this.store.dispatch(new UpdateAppAction()); + } + + doNotUpdateApp() { + this.store.dispatch(new DoNotUpdateAppAction()); + } +} diff --git a/electron/src/components/privilege-checker/privilege-checker.component.ts b/electron/src/components/privilege-checker/privilege-checker.component.ts index 3128a185..b16aba51 100644 --- a/electron/src/components/privilege-checker/privilege-checker.component.ts +++ b/electron/src/components/privilege-checker/privilege-checker.component.ts @@ -1,3 +1,4 @@ +/// import { Component } from '@angular/core'; import { Router } from '@angular/router'; @@ -13,7 +14,7 @@ import { remote } from 'electron'; import * as path from 'path'; import * as sudo from 'sudo-prompt'; -import { UhkDeviceService } from './../../services/uhk-device.service'; +import { UhkDeviceService } from '../../services/uhk-device.service'; @Component({ selector: 'privilege-checker', diff --git a/electron/src/components/update-available/index.ts b/electron/src/components/update-available/index.ts new file mode 100644 index 00000000..f68c4f14 --- /dev/null +++ b/electron/src/components/update-available/index.ts @@ -0,0 +1 @@ +export { UpdateAvailableComponent } from './update-available.component'; diff --git a/electron/src/components/update-available/update-available.component.html b/electron/src/components/update-available/update-available.component.html new file mode 100644 index 00000000..b655f74b --- /dev/null +++ b/electron/src/components/update-available/update-available.component.html @@ -0,0 +1,5 @@ +
+ New version available. + + +
diff --git a/electron/src/components/update-available/update-available.component.scss b/electron/src/components/update-available/update-available.component.scss new file mode 100644 index 00000000..0e591654 --- /dev/null +++ b/electron/src/components/update-available/update-available.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + justify-content: center; + margin: 0.5rem; +} diff --git a/electron/src/components/update-available/update-available.component.ts b/electron/src/components/update-available/update-available.component.ts new file mode 100644 index 00000000..685dba9b --- /dev/null +++ b/electron/src/components/update-available/update-available.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-update-available', + templateUrl: './update-available.component.html', + styleUrls: ['./update-available.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class UpdateAvailableComponent { + @Input() showUpdateAvailable: boolean = false; + @Output() updateApp = new EventEmitter(); + @Output() doNotUpdateApp = new EventEmitter(); +} diff --git a/electron/src/custom_types/electron-is-dev.d.ts b/electron/src/custom_types/electron-is-dev.d.ts new file mode 100644 index 00000000..df6a02c1 --- /dev/null +++ b/electron/src/custom_types/electron-is-dev.d.ts @@ -0,0 +1 @@ +declare module 'electron-is-dev'; diff --git a/electron/src/dev-extension.ts b/electron/src/dev-extension.ts new file mode 100644 index 00000000..3cf765f7 --- /dev/null +++ b/electron/src/dev-extension.ts @@ -0,0 +1,22 @@ +/// + +/* + * Install DevTool extensions when Electron is in development mode + */ +import { app } from 'electron'; +import * as isDev from 'electron-is-dev'; + +if (isDev) { + + app.once('ready', () => { + + const { default: installExtension, REDUX_DEVTOOLS } = require('electron-devtools-installer'); + + installExtension(REDUX_DEVTOOLS) + .then((name: string) => console.log(`Added Extension: ${name}`)) + .catch((err: any) => console.log('An error occurred: ', err)); + + require('electron-debug')({ showDevTools: true }); + }); + +} diff --git a/electron/src/electron-main.ts b/electron/src/electron-main.ts index 58181aac..2bcc8bf2 100644 --- a/electron/src/electron-main.ts +++ b/electron/src/electron-main.ts @@ -1,10 +1,27 @@ -import { BrowserWindow, app } from 'electron'; +/// + +import { app, BrowserWindow, ipcMain } from 'electron'; +import { autoUpdater } from 'electron-updater'; +import * as log from 'electron-log'; import * as path from 'path'; +import { ProgressInfo } from 'electron-builder-http/out/ProgressCallbackTransform'; +import { VersionInfo } from 'electron-builder-http/out/publishOptions'; +import * as settings from 'electron-settings'; +import * as isDev from 'electron-is-dev'; + +import { IpcEvents } from './shared/util'; +import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service'; + +// import './dev-extension'; +// require('electron-debug')({ showDevTools: true, enabled: true }); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let win: Electron.BrowserWindow; +log.transports.file.level = 'debug'; +autoUpdater.logger = log; + function createWindow() { // Create the browser window. win = new BrowserWindow({ @@ -35,6 +52,9 @@ function createWindow() { // when you should delete the corresponding element. win = null; }); + + win.webContents.on('did-finish-load', () => { + }); } // This method will be called when Electron has finished @@ -51,6 +71,10 @@ app.on('window-all-closed', () => { } }); +app.on('will-quit', () => { + saveFirtsRun(); +}); + app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. @@ -61,3 +85,101 @@ app.on('activate', () => { // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here + +// ========================================================================= +// Auto update events +// ========================================================================= +function checkForUpdate() { + if (isDev) { + const msg = 'Application update is not working in dev mode.'; + log.info(msg); + sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg); + return; + } + + if (isFirstRun()) { + const msg = 'Application update is skipping at first run.'; + log.info(msg); + sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg); + return; + } + + autoUpdater.allowPrerelease = allowPreRelease(); + autoUpdater.checkForUpdates(); +} + +autoUpdater.on('checking-for-update', () => { + sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate); +}); + +autoUpdater.on('update-available', (ev: any, info: VersionInfo) => { + autoUpdater.downloadUpdate(); + sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info); +}); + +autoUpdater.on('update-not-available', (ev: any, info: VersionInfo) => { + sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info); +}); + +autoUpdater.on('error', (ev: any, err: Error) => { + sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateError, err); +}); + +autoUpdater.on('download-progress', (progressObj: ProgressInfo) => { + sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloadProgress, progressObj); +}); + +autoUpdater.on('update-downloaded', (ev: any, info: VersionInfo) => { + sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info); +}); + +ipcMain.on(IpcEvents.autoUpdater.updateAndRestart, () => autoUpdater.quitAndInstall(true)); + +ipcMain.on(IpcEvents.app.appStarted, () => { + if (checkForUpdateAtStartup()) { + checkForUpdate(); + } +}); + +ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => checkForUpdate()); + +function isFirstRun() { + if (!settings.has('firstRunVersion')) { + return true; + } + const firstRunVersion = settings.get('firstRunVersion'); + log.info(`firstRunVersion: ${firstRunVersion}`); + log.info(`package.version: ${app.getVersion()}`); + + return firstRunVersion !== app.getVersion(); +} + +function saveFirtsRun() { + settings.set('firstRunVersion', app.getVersion()); +} + +function sendIpcToWindow(message: string, arg?: any) { + log.info('sendIpcToWindow:', message, arg); + if (!win || win.isDestroyed()) { + return; + } + + win.webContents.send(message, arg); +} + +function allowPreRelease() { + const settings = getAutoUpdateSettings(); + + return settings && settings.usePreReleaseUpdate; +} + +function checkForUpdateAtStartup() { + const settings = getAutoUpdateSettings(); + + return settings && settings.checkForUpdateOnStartUp; +} + +function getAutoUpdateSettings() { + const storageService = new ElectronDataStorageRepositoryService(); + return storageService.getAutoUpdateSettings(); +} diff --git a/electron/src/package.json b/electron/src/package.json new file mode 100644 index 00000000..2df162ad --- /dev/null +++ b/electron/src/package.json @@ -0,0 +1,20 @@ +{ + "name": "uhk-agent", + "main": "electron-main.js", + "version": "0.0.0", + "description": "Agent is the configuration application of the Ultimate Hacking Keyboard.", + "author": "Ultimate Gadget Laboratories", + "repository": { + "type": "git", + "url": "git@github.com:UltimateHackingKeyboard/agent.git" + }, + "license": "GPL-3.0", + "engines": { + "node": ">=6.9.5 <7.0.0", + "npm": ">=3.10.7 <4.0.0" + }, + "dependencies": { + "usb": "git+https://github.com/aktary/node-usb.git" + } +} + diff --git a/electron/src/services/app-update-renderer.service.ts b/electron/src/services/app-update-renderer.service.ts new file mode 100644 index 00000000..22ef08ab --- /dev/null +++ b/electron/src/services/app-update-renderer.service.ts @@ -0,0 +1,75 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Action, Store } from '@ngrx/store'; +import { ipcRenderer } from 'electron'; + +import { IpcEvents } from '../shared/util'; +import { AppState } from '../store'; +import { UpdateDownloadedAction } from '../store/actions/app-update.action'; +import { CheckForUpdateFailedAction, CheckForUpdateSuccessAction } from '../shared/store/actions/auto-update-settings'; + +/** + * This service handle the application update events in the electron renderer process. + * + * The class contains parameters with 'any' type, because the relevant type definitions in + * import { ProgressInfo } from 'electron-builder-http/out/ProgressCallbackTransform'; + * import { VersionInfo } from 'electron-builder-http/out/publishOptions'; + * but, typescript allow import these if import 'electron-updater' too, but I i don't want to import + * the updater in renderer process. + */ +@Injectable() +export class AppUpdateRendererService { + constructor(private store: Store, + private zone: NgZone) { + this.registerEvents(); + } + + sendAppStarted() { + ipcRenderer.send(IpcEvents.app.appStarted); + } + + sendUpdateAndRestartApp() { + ipcRenderer.send(IpcEvents.autoUpdater.updateAndRestart); + } + + checkForUpdate() { + ipcRenderer.send(IpcEvents.autoUpdater.checkForUpdate); + } + + private registerEvents() { + ipcRenderer.on(IpcEvents.autoUpdater.updateAvailable, (event: string, arg: any) => { + this.writeUpdateState(IpcEvents.autoUpdater.updateAvailable, arg); + }); + + ipcRenderer.on(IpcEvents.autoUpdater.updateNotAvailable, () => { + this.writeUpdateState(IpcEvents.autoUpdater.updateNotAvailable); + this.dispachStoreAction(new CheckForUpdateSuccessAction('No update available')); + }); + + ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateError, (event: string, arg: any) => { + this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateError, arg); + this.dispachStoreAction(new CheckForUpdateFailedAction(arg)); + }); + + ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateDownloadProgress, (event: string, arg: any) => { + this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateDownloadProgress, arg); + }); + + ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateDownloaded, (event: string, arg: any) => { + this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateDownloaded, arg); + this.dispachStoreAction(new UpdateDownloadedAction()); + }); + + ipcRenderer.on(IpcEvents.autoUpdater.checkForUpdateNotAvailable, (event: string, arg: any) => { + this.writeUpdateState(IpcEvents.autoUpdater.checkForUpdateNotAvailable, arg); + this.dispachStoreAction(new CheckForUpdateFailedAction(arg)); + }); + } + + private dispachStoreAction(action: Action) { + this.zone.run(() => this.store.dispatch(action)); + } + + private writeUpdateState(event: any, arg?: any) { + console.log({ event, arg }); + } +} diff --git a/electron/src/services/electron-datastorage-repository.service.ts b/electron/src/services/electron-datastorage-repository.service.ts index 9cd0f066..42c5449a 100644 --- a/electron/src/services/electron-datastorage-repository.service.ts +++ b/electron/src/services/electron-datastorage-repository.service.ts @@ -1,13 +1,36 @@ -import { UserConfiguration } from '../shared/config-serializer/config-items/UserConfiguration'; +import * as storage from 'electron-settings'; -export class ElectronDataStorageRepositoryService { - getConfig(): UserConfiguration { - // TODO implement load logic - return; +import { UserConfiguration } from '../shared/config-serializer/config-items/UserConfiguration'; +import { DataStorageRepositoryService } from '../shared/services/datastorage-repository.service'; +import { AutoUpdateSettings } from '../shared/models/auto-update-settings'; + +export class ElectronDataStorageRepositoryService implements DataStorageRepositoryService { + static getValue(key: string): any { + const value = storage.get(key); + if (!value) { + return null; + } + + return JSON.parse(value); + } + + static saveValue(key: string, value: any) { + storage.set(key, JSON.stringify(value)); + } + + getConfig(): UserConfiguration { + return ElectronDataStorageRepositoryService.getValue('user-config'); } - /* tslint:disable:no-unused-variable */ saveConfig(config: UserConfiguration): void { - // TODO implement save logic + ElectronDataStorageRepositoryService.saveValue('user-config', config.toJsonObject()); + } + + getAutoUpdateSettings(): AutoUpdateSettings { + return ElectronDataStorageRepositoryService.getValue('auto-update-settings'); + } + + saveAutoUpdateSettings(settings: AutoUpdateSettings): void { + ElectronDataStorageRepositoryService.saveValue('auto-update-settings', settings); } } diff --git a/electron/src/store/actions/app-update.action.ts b/electron/src/store/actions/app-update.action.ts new file mode 100644 index 00000000..316f2b5d --- /dev/null +++ b/electron/src/store/actions/app-update.action.ts @@ -0,0 +1,40 @@ +import { Action } from '@ngrx/store'; +import { type } from '../../shared/util/'; + +const PREFIX = '[app-update] '; + +// tslint:disable-next-line:variable-name +export const ActionTypes = { + UPDATE_AVAILABLE: type(PREFIX + 'update available'), + UPDATE_APP: type(PREFIX + 'update app'), + DO_NOT_UPDATE_APP: type(PREFIX + 'do not update app'), + UPDATE_DOWNLOADED: type(PREFIX + 'update downloaded'), + UPDATING: type(PREFIX + 'updating') +}; + +export class UpdateAvailableAction implements Action { + type = ActionTypes.UPDATE_AVAILABLE; +} + +export class UpdateAppAction implements Action { + type = ActionTypes.UPDATE_APP; +} + +export class DoNotUpdateAppAction implements Action { + type = ActionTypes.DO_NOT_UPDATE_APP; +} + +export class UpdateDownloadedAction implements Action { + type = ActionTypes.UPDATE_DOWNLOADED; +} + +export class UpdatingAction implements Action { + type = ActionTypes.UPDATING; +} + +export type Actions + = UpdateAvailableAction + | UpdateAppAction + | DoNotUpdateAppAction + | UpdateDownloadedAction + | UpdatingAction; diff --git a/electron/src/store/actions/app.action.ts b/electron/src/store/actions/app.action.ts new file mode 100644 index 00000000..63620373 --- /dev/null +++ b/electron/src/store/actions/app.action.ts @@ -0,0 +1,22 @@ +import { Action } from '@ngrx/store'; +import { type } from '../../shared/util/'; + +const PREFIX = '[app] '; + +// tslint:disable-next-line:variable-name +export const ActionTypes = { + APP_BOOTSRAPPED: type(PREFIX + 'bootstrapped'), + APP_STARTED: type(PREFIX + 'started') +}; + +export class AppBootsrappedAction implements Action { + type = ActionTypes.APP_BOOTSRAPPED; +} + +export class AppStartedAction implements Action { + type = ActionTypes.APP_STARTED; +} + +export type Actions + = AppStartedAction + | AppBootsrappedAction; diff --git a/electron/src/store/effects/app-update.effect.ts b/electron/src/store/effects/app-update.effect.ts new file mode 100644 index 00000000..aef916cf --- /dev/null +++ b/electron/src/store/effects/app-update.effect.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Action } from '@ngrx/store'; +import { Actions, Effect } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/first'; + +import { ActionTypes } from '../actions/app-update.action'; +import { ActionTypes as AutoUpdateActionTypes } from '../../shared/store/actions/auto-update-settings'; +import { AppUpdateRendererService } from '../../services/app-update-renderer.service'; + +@Injectable() +export class AppUpdateEffect { + @Effect({ dispatch: false }) + appStart$: Observable = this.actions$ + .ofType(ActionTypes.UPDATE_APP) + .first() + .do(() => { + this.appUpdateRendererService.sendUpdateAndRestartApp(); + }); + + @Effect({ dispatch: false }) checkForUpdate$: Observable = this.actions$ + .ofType(AutoUpdateActionTypes.CHECK_FOR_UPDATE_NOW) + .do(() => { + this.appUpdateRendererService.checkForUpdate(); + }); + + constructor(private actions$: Actions, + private appUpdateRendererService: AppUpdateRendererService) { + } + +} diff --git a/electron/src/store/effects/app.effect.ts b/electron/src/store/effects/app.effect.ts new file mode 100644 index 00000000..89e82cbb --- /dev/null +++ b/electron/src/store/effects/app.effect.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { Action } from '@ngrx/store'; +import { Effect, Actions } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; + +import * as app from '../actions/app.action'; +import { AppUpdateRendererService } from '../../services/app-update-renderer.service'; + +@Injectable() +export class ApplicationEffect { + @Effect() + appStart$: Observable = this.actions$ + .ofType(app.ActionTypes.APP_BOOTSRAPPED) + .startWith(new app.AppStartedAction()) + .delay(3000) // wait 3 sec to mainRenderer subscribe all events + .do(() => { + this.appUpdateRendererService.sendAppStarted(); + }); + + constructor( + private actions$: Actions, + private appUpdateRendererService: AppUpdateRendererService) { } +} diff --git a/electron/src/store/effects/index.ts b/electron/src/store/effects/index.ts new file mode 100644 index 00000000..292459d8 --- /dev/null +++ b/electron/src/store/effects/index.ts @@ -0,0 +1,2 @@ +export { AppUpdateEffect } from './app-update.effect'; +export { ApplicationEffect } from './app.effect'; diff --git a/electron/src/store/index.ts b/electron/src/store/index.ts new file mode 100644 index 00000000..08f2ed7f --- /dev/null +++ b/electron/src/store/index.ts @@ -0,0 +1,41 @@ +/// +import { createSelector } from 'reselect'; +import { compose } from '@ngrx/core/compose'; +import { storeFreeze } from 'ngrx-store-freeze'; +import { ActionReducer, combineReducers } from '@ngrx/store'; +import { routerReducer } from '@ngrx/router-store'; +import * as isDev from 'electron-is-dev'; + +import { AppState as CommonState } from '../shared/store'; +import * as fromApp from './reducers/app.reducer'; +import * as fromAppUpdate from './reducers/app-update.reducer'; +import { autoUpdateReducer, presetReducer, userConfigurationReducer } from '../shared/store/reducers'; + +export interface AppState extends CommonState { + app: fromApp.State; + appUpdate: fromAppUpdate.State; +} + +const reducers = { + userConfiguration: userConfigurationReducer, + presetKeymaps: presetReducer, + router: routerReducer, + app: fromApp.reducer, + appUpdate: fromAppUpdate.reducer, + autoUpdateSettings: autoUpdateReducer +}; + +const developmentReducer: ActionReducer = compose(storeFreeze, combineReducers)(reducers); +const productionReducer: ActionReducer = combineReducers(reducers); + +export function reducer(state: any, action: any) { + if (isDev) { + return developmentReducer(state, action); + } else { + return productionReducer(state, action); + } +} + +export const appUpdateState = (state: AppState) => state.appUpdate; + +export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable); diff --git a/electron/src/store/reducers/app-update.reducer.ts b/electron/src/store/reducers/app-update.reducer.ts new file mode 100644 index 00000000..de7fe6da --- /dev/null +++ b/electron/src/store/reducers/app-update.reducer.ts @@ -0,0 +1,37 @@ +import { Actions, ActionTypes } from '../actions/app-update.action'; + +export interface State { + updateAvailable: boolean; + updateDownloaded: boolean; + doNotUpdateApp: boolean; +} + +const initialState: State = { + updateAvailable: false, + updateDownloaded: false, + doNotUpdateApp: false +}; + +export function reducer(state = initialState, action: Actions) { + switch (action.type) { + case ActionTypes.UPDATE_AVAILABLE: { + const newState = Object.assign({}, state); + newState.updateAvailable = true; + return newState; + } + case ActionTypes.UPDATE_DOWNLOADED: { + const newState = Object.assign({}, state); + newState.updateDownloaded = true; + return newState; + } + case ActionTypes.DO_NOT_UPDATE_APP: { + const newState = Object.assign({}, state); + newState.doNotUpdateApp = true; + return newState; + } + default: + return state; + } +} + +export const getShowAppUpdateAvailable = (state: State) => state.updateDownloaded && !state.doNotUpdateApp; diff --git a/electron/src/store/reducers/app.reducer.ts b/electron/src/store/reducers/app.reducer.ts new file mode 100644 index 00000000..c4d6b356 --- /dev/null +++ b/electron/src/store/reducers/app.reducer.ts @@ -0,0 +1,21 @@ +import { Actions, ActionTypes } from '../actions/app.action'; + +export interface State { + started: boolean; +} + +const initialState: State = { + started: false +}; + +export function reducer(state = initialState, action: Actions) { + switch (action.type) { + case ActionTypes.APP_STARTED: { + const newState = Object.assign({}, state); + newState.started = true; + return newState; + } + default: + return state; + } +} diff --git a/electron/src/tsconfig-electron-main.json b/electron/src/tsconfig-electron-main.json index a83953aa..afe7859b 100644 --- a/electron/src/tsconfig-electron-main.json +++ b/electron/src/tsconfig-electron-main.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "files": [ "electron-main.ts" ] diff --git a/electron/src/tsconfig.json b/electron/src/tsconfig.json index 8a661e67..20e558c5 100644 --- a/electron/src/tsconfig.json +++ b/electron/src/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "exclude": [ - "../dist", "electron-main.ts", "webpack.config.*" ] diff --git a/electron/src/webpack.config.js b/electron/src/webpack.config.js index 9753255c..ed8de2e3 100644 --- a/electron/src/webpack.config.js +++ b/electron/src/webpack.config.js @@ -80,6 +80,10 @@ module.exports = { { from: 'node_modules/usb', to: 'vendor/usb' + }, + { + from: 'electron/src/package.json', + to: 'package.json' } ] ), diff --git a/package.json b/package.json index 0ba074fc..3ab78bf1 100644 --- a/package.json +++ b/package.json @@ -16,17 +16,23 @@ "@ngrx/store-devtools": "3.2.4", "@ngrx/store-log-monitor": "3.0.2", "@types/core-js": "0.9.35", + "@types/electron-devtools-installer": "^2.0.2", + "@types/electron-settings": "^3.0.0", "@types/file-saver": "0.0.1", "@types/jquery": "3.2.1", "@types/node": "^6.0.78", "@types/usb": "^1.1.3", "angular2-template-loader": "0.6.2", "copy-webpack-plugin": "^4.0.1", + "devtron": "^1.4.0", "electron": "1.6.11", - "electron-builder": "^19.4.2", + "electron-builder": "19.6.1", + "electron-debug": "^1.1.0", + "electron-devtools-installer": "^2.2.0", "electron-rebuild": "^1.5.7", "expose-loader": "^0.7.1", "html-loader": "0.4.5", + "jsonfile": "3.0.0", "node-sass": "^4.5.2", "npm-run-all": "4.0.2", "path": "^0.12.7", @@ -58,12 +64,18 @@ "buffer": "^5.0.6", "core-js": "2.4.1", "dragula": "^3.7.2", + "electron-is-dev": "0.1.2", + "electron-log": "2.2.6", + "electron-settings": "3.0.14", + "electron-updater": "2.2.0", "filesaver.js": "^0.2.0", "font-awesome": "^4.6.3", "jquery": "3.2.1", "json-loader": "^0.5.4", "ng2-dragula": "1.5.0", "ng2-select2": "1.0.0-beta.10", + "ngrx-store-freeze": "^0.1.9", + "reselect": "3.0.1", "rxjs": "^5.4.1", "select2": "^4.0.3", "sudo-prompt": "^7.0.0", @@ -76,7 +88,7 @@ "postinstall": "run-p build:usb \"symlink -- -i\" ", "test": "cd ./test-serializer && node ./test-serializer.js", "lint": "run-s -scn lint:ts lint:style", - "lint:ts": "tslint \"electron/**/*.ts\" \"web/**/*.ts\" \"shared/**/*.ts\" \"test-serializer/**/*.ts\"", + "lint:ts": "tslint \"electron/src/**/*.ts\" \"web/src/**/*.ts\" \"shared/**/*.ts\" \"test-serializer/**/*.ts\"", "lint:style": "stylelint \"electron/**/*.scss\" \"web/**/*.scss\" \"shared/**/*.scss\" --syntax scss", "build": "run-p build:web build:electron", "build:web": "webpack --config \"web/src/webpack.config.js\"", @@ -91,6 +103,7 @@ "symlink": "node ./tools/symlinker", "standard-version": "standard-version", "pack": "node ./scripts/release.js", - "release": "node ./scripts/release.js" + "install:build-deps": "cd electron/dist && npm i", + "release": "npm run install:build-deps && node ./scripts/release.js" } } diff --git a/scripts/release.js b/scripts/release.js index 47b17e1a..3219ea21 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -1,111 +1,129 @@ 'use strict'; +const jsonfile = require('jsonfile'); const TEST_BUILD = false; // set true if you would like to test on your local machince if (!process.env.CI && !TEST_BUILD) { - console.error('Create release only on CI server') - process.exit(1) + console.error('Create release only on CI server'); + process.exit(1); } -let branchName = '' -let pullRequestNr = '' -let gitTag = '' -let repoName = '' +let branchName = ''; +let pullRequestNr = ''; +let gitTag = ''; +let repoName = ''; if (process.env.TRAVIS) { - branchName = process.env.TRAVIS_BRANCH - pullRequestNr = process.env.TRAVIS_PULL_REQUEST - gitTag = process.env.TRAVIS_TAG - repoName = process.env.TRAVIS_REPO_SLUG + branchName = process.env.TRAVIS_BRANCH; + pullRequestNr = process.env.TRAVIS_PULL_REQUEST; + gitTag = process.env.TRAVIS_TAG; + repoName = process.env.TRAVIS_REPO_SLUG; } else if (process.env.APPVEYOR) { - branchName = process.env.APPVEYOR_REPO_BRANCH - pullRequestNr = process.env.APPVEYOR_PULL_REQUEST_NUMBER - gitTag = process.env.APPVEYOR_REPO_TAG_NAME - repoName = process.env.APPVEYOR_REPO_NAME + branchName = process.env.APPVEYOR_REPO_BRANCH; + pullRequestNr = process.env.APPVEYOR_PULL_REQUEST_NUMBER; + gitTag = process.env.APPVEYOR_REPO_TAG_NAME; + repoName = process.env.APPVEYOR_REPO_NAME; } -console.log({ branchName, pullRequestNr, gitTag, repoName }) +console.log({branchName, pullRequestNr, gitTag, repoName}); -// TODO(Robi): Remove the comment after success tests -const isReleaseCommit = TEST_BUILD || branchName === gitTag && repoName === 'UltimateHackingKeyboard/agent' +const isReleaseCommit = TEST_BUILD || branchName === gitTag && repoName === 'UltimateHackingKeyboard/agent'; if (!isReleaseCommit) { - console.log('It is not a release task. Skipping publish.') + console.log('It is not a release task. Skipping publish.'); process.exit(0) } -const fs = require('fs-extra') -const cp = require('child_process') -const path = require('path') -const builder = require("electron-builder") -const Platform = builder.Platform +const fs = require('fs-extra'); +const cp = require('child_process'); +const path = require('path'); +const builder = require("electron-builder"); +const Platform = builder.Platform; +const electron_build_folder = path.join(__dirname, '../electron/dist'); -let sha = '' +let sha = ''; if (process.env.TRAVIS) { - sha = process.env.TRAVIS_COMMIT + sha = process.env.TRAVIS_COMMIT; } else if (process.env.APPVEYOR) { - sha = process.env.APPVEYOR_REPO_COMMIT + sha = process.env.APPVEYOR_REPO_COMMIT; } -let target = '' +let target = ''; +let artifactName = 'UHK.Agent-${version}-${os}'; if (process.platform === 'darwin') { - target = Platform.MAC.createTarget() + target = Platform.MAC.createTarget(); + artifactName += '.${ext}'; } else if (process.platform === 'win32') { - target = Platform.WINDOWS.createTarget() + target = Platform.WINDOWS.createTarget(); + artifactName += '-${arch}.${ext}'; } else if (process.platform === 'linux') { - target = Platform.LINUX.createTarget() + target = Platform.LINUX.createTarget(); + artifactName += '.${ext}'; } else { - console.error(`I dunno how to publish a release for ${process.platform} :(`) - process.exit(1) + console.error(`I dunno how to publish a release for ${process.platform} :(`); + process.exit(1); } if (process.platform === 'darwin') { // TODO: Remove comment when macOS certificates boughted and exported - //require('./setup-macos-keychain').registerKeyChain() + //require('./setup-macos-keychain').registerKeyChain(); } -let version = '' +let version = ''; if (TEST_BUILD || gitTag) { - version = gitTag + const jsonVersion = require('../package.json').version; + version = gitTag; + updateVersionNumberIn2rndPackageJson(jsonVersion); builder.build({ dir: true, targets: target, appMetadata: { - main: 'electron/dist/electron-main.js', + main: 'electron-main.js', name: 'UHK Agent', author: { - name: 'Ultimate Gaget Laboratories' + name: 'Ultimate Gadget Laboratories' }, + version: jsonVersion }, config: { + directories: { + app: electron_build_folder + }, appId: 'com.ultimategadgetlabs.uhk.agent', productName: 'UHK Agent', mac: { - category: 'public.app-category.utilities', + category: 'public.app-category.utilities' }, publish: 'github', + artifactName, files: [ - '!**/*', - 'electron/dist/**/*', - 'node_modules/**/*' + '**/*' ] - }, }) .then(() => { - console.log('Packing success.') + console.log('Packing success.'); }) .catch((error) => { - console.error(`${error}`) - process.exit(1) + console.error(`${error}`); + process.exit(1); }) } else { - console.log('No git tag') + console.log('No git tag'); // TODO: Need it? - version = sha.substr(0, 8) - process.exit(1) + version = sha.substr(0, 8); + process.exit(1); +} + +function updateVersionNumberIn2rndPackageJson(version) { + const jsonPath = path.join(__dirname,'../electron/dist/package.json'); + const json = require(jsonPath); + + json.version = version; + + jsonfile.writeFileSync(jsonPath, json, {spaces: 2}) } diff --git a/shared/src/components/auto-update-settings/auto-update-settings.html b/shared/src/components/auto-update-settings/auto-update-settings.html new file mode 100644 index 00000000..525deecf --- /dev/null +++ b/shared/src/components/auto-update-settings/auto-update-settings.html @@ -0,0 +1,36 @@ +
+
+
+ +
+ +
+ +
+
+ +
+

{{version}}

+
+
+ + + +
+ {{message}} +
+
+
diff --git a/shared/src/components/auto-update-settings/auto-update-settings.ts b/shared/src/components/auto-update-settings/auto-update-settings.ts new file mode 100644 index 00000000..e344aeae --- /dev/null +++ b/shared/src/components/auto-update-settings/auto-update-settings.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +import { State } from '../../store/reducers/auto-update-settings'; + +@Component({ + selector: 'auto-update-settings', + templateUrl: './auto-update-settings.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AutoUpdateSettings { + + @Input() version: string; + @Input() settings: State; + @Input() checkingForUpdate: boolean; + @Input() message: string; + + @Output() toggleCheckForUpdateOnStartUp = new EventEmitter(); + @Output() toggleUsePreReleaseUpdate = new EventEmitter(); + @Output() checkForUpdate = new EventEmitter(); + + constructor() { + } + + emitCheckForUpdateOnStartUp(value: boolean) { + this.toggleCheckForUpdateOnStartUp.emit(value); + } + + emitUsePreReleaseUpdate(value: boolean) { + this.toggleUsePreReleaseUpdate.emit(value); + } + + emitCheckForUpdate() { + this.checkForUpdate.emit(); + } +} diff --git a/shared/src/components/settings/settings.component.html b/shared/src/components/settings/settings.component.html index 9fd90dd6..0195b444 100644 --- a/shared/src/components/settings/settings.component.html +++ b/shared/src/components/settings/settings.component.html @@ -4,4 +4,15 @@ Settings -To be done... \ No newline at end of file +
+ To be done... +
+ + diff --git a/shared/src/components/settings/settings.component.ts b/shared/src/components/settings/settings.component.ts index 78d7c210..8a2179a9 100644 --- a/shared/src/components/settings/settings.component.ts +++ b/shared/src/components/settings/settings.component.ts @@ -1,4 +1,15 @@ import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; + +import { runInElectron } from '../../util/index'; +import { AppState, getAutoUpdateMessage, getAutoUpdateSettings, getCheckingForUpdate } from '../../store'; +import { + CheckForUpdateNowAction, + ToggleCheckForUpdateOnStartupAction, + TogglePreReleaseFlagAction +} from '../../store/actions/auto-update-settings'; +import { AutoUpdateSettings } from '../../models/auto-update-settings'; @Component({ selector: 'settings', @@ -9,5 +20,28 @@ import { Component } from '@angular/core'; } }) export class SettingsComponent { - constructor() { } + runInElectron = runInElectron(); + // TODO: From where do we get the version number? The electron gives back in main process, but the web... + version = '1.0.0'; + autoUpdateSettings$: Observable; + checkingForUpdate$: Observable; + autoUpdateMessage$: Observable; + + constructor(private store: Store) { + this.autoUpdateSettings$ = store.select(getAutoUpdateSettings); + this.checkingForUpdate$ = store.select(getCheckingForUpdate); + this.autoUpdateMessage$ = store.select(getAutoUpdateMessage); + } + + toogleCheckForUpdateOnStartUp(value: boolean) { + this.store.dispatch(new ToggleCheckForUpdateOnStartupAction(value)); + } + + toogleUsePreReleaseUpdate(value: boolean) { + this.store.dispatch(new TogglePreReleaseFlagAction(value)); + } + + checkForUpdate() { + this.store.dispatch(new CheckForUpdateNowAction()); + } } diff --git a/shared/src/models/auto-update-settings.ts b/shared/src/models/auto-update-settings.ts new file mode 100644 index 00000000..0a40d562 --- /dev/null +++ b/shared/src/models/auto-update-settings.ts @@ -0,0 +1,4 @@ +export interface AutoUpdateSettings { + checkForUpdateOnStartUp: boolean; + usePreReleaseUpdate: boolean; +} diff --git a/shared/src/services/datastorage-repository.service.ts b/shared/src/services/datastorage-repository.service.ts index 8f5b61ec..2d2baa7c 100644 --- a/shared/src/services/datastorage-repository.service.ts +++ b/shared/src/services/datastorage-repository.service.ts @@ -1,12 +1,17 @@ import { InjectionToken } from '@angular/core'; import { UserConfiguration } from '../config-serializer/config-items/UserConfiguration'; +import { AutoUpdateSettings } from '../models/auto-update-settings'; export interface DataStorageRepositoryService { getConfig(): UserConfiguration; saveConfig(config: UserConfiguration): void; + + getAutoUpdateSettings(): AutoUpdateSettings; + + saveAutoUpdateSettings(settings: AutoUpdateSettings): void; } export let DATA_STORAGE_REPOSITORY = new InjectionToken('dataStorage-repository'); diff --git a/shared/src/services/local-datastorage-repository.service.ts b/shared/src/services/local-datastorage-repository.service.ts index f0b27c80..4872f386 100644 --- a/shared/src/services/local-datastorage-repository.service.ts +++ b/shared/src/services/local-datastorage-repository.service.ts @@ -1,27 +1,26 @@ import { Injectable } from '@angular/core'; + import { UserConfiguration } from '../config-serializer/config-items/UserConfiguration'; import { DataStorageRepositoryService } from './datastorage-repository.service'; -import { DefaultUserConfigurationService } from './default-user-configuration.service'; +import { State as AutoUpdateSettings } from '../store/reducers/auto-update-settings'; @Injectable() export class LocalDataStorageRepositoryService implements DataStorageRepositoryService { - constructor(private defaultUserConfigurationService: DefaultUserConfigurationService) { } getConfig(): UserConfiguration { - const configJsonString = localStorage.getItem('config'); - let config: UserConfiguration; - - if (configJsonString) { - const configJsonObject = JSON.parse(configJsonString); - if (configJsonObject.dataModelVersion === this.defaultUserConfigurationService.getDefault().dataModelVersion) { - config = new UserConfiguration().fromJsonObject(configJsonObject); - } - } - - return config; + return JSON.parse(localStorage.getItem('config')); } saveConfig(config: UserConfiguration): void { localStorage.setItem('config', JSON.stringify(config.toJsonObject())); } + + getAutoUpdateSettings(): AutoUpdateSettings { + return JSON.parse(localStorage.getItem('auto-update-settings')); + } + + saveAutoUpdateSettings(settings: AutoUpdateSettings): void { + localStorage.setItem('auto-update-settings', JSON.stringify(settings)); + } + } diff --git a/shared/src/store/actions/auto-update-settings.ts b/shared/src/store/actions/auto-update-settings.ts new file mode 100644 index 00000000..bdb30abe --- /dev/null +++ b/shared/src/store/actions/auto-update-settings.ts @@ -0,0 +1,74 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../util'; +import { AutoUpdateSettings } from '../../models/auto-update-settings'; + +const PREFIX = '[app-update-config] '; + +// tslint:disable-next-line:variable-name +export const ActionTypes = { + TOGGLE_CHECK_FOR_UPDATE_ON_STARTUP: type(PREFIX + 'Check for update on startup'), + CHECK_FOR_UPDATE_NOW: type(PREFIX + 'Check for update now'), + CHECK_FOR_UPDATE_SUCCESS: type(PREFIX + 'Check for update success'), + CHECK_FOR_UPDATE_FAILED: type(PREFIX + 'Check for update faild'), + TOGGLE_PRE_RELEASE_FLAG: type(PREFIX + 'Toggle pre release update flag'), + LOAD_AUTO_UPDATE_SETTINGS: type(PREFIX + 'Load auto update settings'), + LOAD_AUTO_UPDATE_SETTINGS_SUCCESS: type(PREFIX + 'Load auto update settings success'), + SAVE_AUTO_UPDATE_SETTINGS_SUCCESS: type(PREFIX + 'Save auto update settings success') +}; + +export class ToggleCheckForUpdateOnStartupAction implements Action { + type = ActionTypes.TOGGLE_CHECK_FOR_UPDATE_ON_STARTUP; + + constructor(public payload: boolean) { + } +} + +export class CheckForUpdateNowAction implements Action { + type = ActionTypes.CHECK_FOR_UPDATE_NOW; +} + +export class CheckForUpdateSuccessAction implements Action { + type = ActionTypes.CHECK_FOR_UPDATE_SUCCESS; + constructor(public payload?: string) { + } +} + +export class CheckForUpdateFailedAction implements Action { + type = ActionTypes.CHECK_FOR_UPDATE_FAILED; + + constructor(public payload: any) { + } +} + +export class TogglePreReleaseFlagAction implements Action { + type = ActionTypes.TOGGLE_PRE_RELEASE_FLAG; + + constructor(public payload: boolean) { + } +} + +export class LoadAutoUpdateSettingsAction implements Action { + type = ActionTypes.LOAD_AUTO_UPDATE_SETTINGS_SUCCESS; +} + +export class LoadAutoUpdateSettingsSuccessAction implements Action { + type = ActionTypes.LOAD_AUTO_UPDATE_SETTINGS_SUCCESS; + + constructor(public payload: AutoUpdateSettings) { + } +} + +export class SaveAutoUpdateSettingsSuccessAction implements Action { + type = ActionTypes.SAVE_AUTO_UPDATE_SETTINGS_SUCCESS; +} + +export type Actions + = ToggleCheckForUpdateOnStartupAction + | CheckForUpdateNowAction + | CheckForUpdateSuccessAction + | CheckForUpdateFailedAction + | TogglePreReleaseFlagAction + | LoadAutoUpdateSettingsAction + | LoadAutoUpdateSettingsSuccessAction + | SaveAutoUpdateSettingsSuccessAction; diff --git a/shared/src/store/effects/auto-update-settings.ts b/shared/src/store/effects/auto-update-settings.ts new file mode 100644 index 00000000..c505741d --- /dev/null +++ b/shared/src/store/effects/auto-update-settings.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@angular/core'; +import { Actions, Effect } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; +import { Action, Store } from '@ngrx/store'; + +import { + ActionTypes, + LoadAutoUpdateSettingsAction, + LoadAutoUpdateSettingsSuccessAction, + SaveAutoUpdateSettingsSuccessAction +} from '../actions/auto-update-settings'; +import { DATA_STORAGE_REPOSITORY, DataStorageRepositoryService } from '../../services/datastorage-repository.service'; +import { AppState, getAutoUpdateSettings } from '../index'; +import { initialState, State } from '../reducers/auto-update-settings'; +import { AutoUpdateSettings } from '../../models/auto-update-settings'; + +@Injectable() +export class AutoUpdateSettingsEffects { + @Effect() loadUserConfig$: Observable = this.actions$ + .ofType(ActionTypes.LOAD_AUTO_UPDATE_SETTINGS) + .startWith(new LoadAutoUpdateSettingsAction()) + .switchMap(() => { + let settings: AutoUpdateSettings = this.dataStorageRepository.getAutoUpdateSettings(); + if (!settings) { + settings = initialState; + } + return Observable.of(new LoadAutoUpdateSettingsSuccessAction(settings)); + }); + + @Effect() saveAutoUpdateConfig$: Observable = this.actions$ + .ofType(ActionTypes.TOGGLE_CHECK_FOR_UPDATE_ON_STARTUP, ActionTypes.TOGGLE_PRE_RELEASE_FLAG) + .withLatestFrom(this.store.select(getAutoUpdateSettings)) + .map(([action, config]) => { + this.dataStorageRepository.saveAutoUpdateSettings(config); + return new SaveAutoUpdateSettingsSuccessAction(); + }); + + constructor(private actions$: Actions, + @Inject(DATA_STORAGE_REPOSITORY) private dataStorageRepository: DataStorageRepositoryService, + private store: Store) { + } +} diff --git a/shared/src/store/effects/index.ts b/shared/src/store/effects/index.ts index f1b3e30c..71a99489 100644 --- a/shared/src/store/effects/index.ts +++ b/shared/src/store/effects/index.ts @@ -1,3 +1,4 @@ export * from './keymap'; export * from './macro'; export * from './user-config'; +export * from './auto-update-settings'; diff --git a/shared/src/store/effects/user-config.ts b/shared/src/store/effects/user-config.ts index 60cb20b4..4251d4f2 100644 --- a/shared/src/store/effects/user-config.ts +++ b/shared/src/store/effects/user-config.ts @@ -30,18 +30,28 @@ export class UserConfigEffects { .ofType(ActionTypes.LOAD_USER_CONFIG) .startWith(new LoadUserConfigAction()) .switchMap(() => { - let config: UserConfiguration = this.dataStorageRepository.getConfig(); + const configJsonObject = this.dataStorageRepository.getConfig(); + let config: UserConfiguration; + + if (configJsonObject) { + if (configJsonObject.dataModelVersion === this.defaultUserConfigurationService.getDefault().dataModelVersion) { + config = new UserConfiguration().fromJsonObject(configJsonObject); + } + } + if (!config) { config = this.defaultUserConfigurationService.getDefault(); } + return Observable.of(new LoadUserConfigSuccessAction(config)); }); @Effect() saveUserConfig$: Observable = this.actions$ - .ofType(KeymapActions.ADD, KeymapActions.DUPLICATE, KeymapActions.EDIT_NAME, KeymapActions.EDIT_ABBR, - KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY, KeymapActions.CHECK_MACRO, - MacroActions.ADD, MacroActions.DUPLICATE, MacroActions.EDIT_NAME, MacroActions.REMOVE, MacroActions.ADD_ACTION, - MacroActions.SAVE_ACTION, MacroActions.DELETE_ACTION, MacroActions.REORDER_ACTION) + .ofType( + KeymapActions.ADD, KeymapActions.DUPLICATE, KeymapActions.EDIT_NAME, KeymapActions.EDIT_ABBR, + KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY, KeymapActions.CHECK_MACRO, + MacroActions.ADD, MacroActions.DUPLICATE, MacroActions.EDIT_NAME, MacroActions.REMOVE, MacroActions.ADD_ACTION, + MacroActions.SAVE_ACTION, MacroActions.DELETE_ACTION, MacroActions.REORDER_ACTION) .withLatestFrom(this.store.select(getUserConfiguration)) .map(([action, config]) => { this.dataStorageRepository.saveConfig(config); diff --git a/shared/src/store/index.ts b/shared/src/store/index.ts index 7ccc0839..885f5287 100644 --- a/shared/src/store/index.ts +++ b/shared/src/store/index.ts @@ -1,10 +1,20 @@ +import { createSelector } from 'reselect'; + import { Keymap } from '../config-serializer/config-items/Keymap'; import { UserConfiguration } from '../config-serializer/config-items/UserConfiguration'; +import * as autoUpdate from './reducers/auto-update-settings'; // State interface for the application export interface AppState { userConfiguration: UserConfiguration; presetKeymaps: Keymap[]; + autoUpdateSettings: autoUpdate.State; } export const getUserConfiguration = (state: AppState) => state.userConfiguration; + +export const appUpdateState = (state: AppState) => state.autoUpdateSettings; + +export const getAutoUpdateSettings = createSelector(appUpdateState, autoUpdate.getUpdateSettings); +export const getCheckingForUpdate = createSelector(appUpdateState, autoUpdate.checkingForUpdate); +export const getAutoUpdateMessage = createSelector(appUpdateState, autoUpdate.getMessage); diff --git a/shared/src/store/reducers/auto-update-settings.ts b/shared/src/store/reducers/auto-update-settings.ts new file mode 100644 index 00000000..24c7a9c1 --- /dev/null +++ b/shared/src/store/reducers/auto-update-settings.ts @@ -0,0 +1,50 @@ +import { Action } from '@ngrx/store'; +import { ActionTypes } from '../actions/auto-update-settings'; +import { AutoUpdateSettings } from '../../models/auto-update-settings'; + +export interface State extends AutoUpdateSettings { + checkingForUpdate: boolean; + message?: string; +} + +export const initialState: State = { + checkForUpdateOnStartUp: false, + usePreReleaseUpdate: false, + checkingForUpdate: false +}; + +export function reducer(state = initialState, action: Action): State { + switch (action.type) { + case ActionTypes.TOGGLE_CHECK_FOR_UPDATE_ON_STARTUP: { + return Object.assign({}, state, { checkForUpdateOnStartUp: action.payload }); + } + + case ActionTypes.TOGGLE_PRE_RELEASE_FLAG: { + return Object.assign({}, state, { usePreReleaseUpdate: action.payload }); + } + + case ActionTypes.LOAD_AUTO_UPDATE_SETTINGS_SUCCESS: { + return Object.assign({}, action.payload); + } + + case ActionTypes.CHECK_FOR_UPDATE_NOW: { + return Object.assign({}, state, { checkingForUpdate: true, message: null }); + } + + case ActionTypes.CHECK_FOR_UPDATE_SUCCESS: + case ActionTypes.CHECK_FOR_UPDATE_FAILED: { + return Object.assign({}, state, { checkingForUpdate: false, message: action.payload }); + } + + default: + return state; + } +} + +export const getUpdateSettings = (state: State) => ({ + checkForUpdateOnStartUp: state.checkForUpdateOnStartUp, + usePreReleaseUpdate: state.usePreReleaseUpdate +}); + +export const checkingForUpdate = (state: State) => state.checkingForUpdate; +export const getMessage = (state: State) => state.message; diff --git a/shared/src/store/reducers/index.ts b/shared/src/store/reducers/index.ts index d4be10de..4885edb7 100644 --- a/shared/src/store/reducers/index.ts +++ b/shared/src/store/reducers/index.ts @@ -2,10 +2,14 @@ import { routerReducer } from '@ngrx/router-store'; import userConfigurationReducer from './user-configuration'; import presetReducer from './preset'; +import { reducer as autoUpdateReducer } from './auto-update-settings'; + +export { userConfigurationReducer, presetReducer, autoUpdateReducer }; // All reducers that are used in application export const reducer = { userConfiguration: userConfigurationReducer, presetKeymaps: presetReducer, - router: routerReducer + router: routerReducer, + autoUpdateSettings: autoUpdateReducer }; diff --git a/shared/src/store/reducers/preset.ts b/shared/src/store/reducers/preset.ts index f19044f7..40027961 100644 --- a/shared/src/store/reducers/preset.ts +++ b/shared/src/store/reducers/preset.ts @@ -8,7 +8,7 @@ const initialState: Keymap[] = []; export default function(state = initialState, action: Action): Keymap[] { switch (action.type) { case KeymapActions.LOAD_KEYMAPS_SUCCESS: { - return Object.assign(state, action.payload); + return action.payload; } default: diff --git a/shared/src/tsconfig.json b/shared/src/tsconfig.json index b4153557..9536a0f4 100644 --- a/shared/src/tsconfig.json +++ b/shared/src/tsconfig.json @@ -1,27 +1,3 @@ { - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "removeComments": false, - "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true, - "typeRoots": [ - "../../node_modules/@types" - ], - "types": [ - "node", - "jquery", - "core-js", - "select2", - "file-saver" - ] - }, - "exclude": [ - "node_modules", - "config-serializer" - ] + "extends": "../../tsconfig.base.json" } diff --git a/shared/src/util/index.ts b/shared/src/util/index.ts index 4496def2..b25fbecc 100644 --- a/shared/src/util/index.ts +++ b/shared/src/util/index.ts @@ -20,6 +20,7 @@ export function capitalizeFirstLetter(text: string): string { */ const typeCache: { [label: string]: boolean } = {}; + export function type(label: T | ''): T { if (typeCache[label]) { throw new Error(`Action type "${label}" is not unique"`); @@ -29,3 +30,9 @@ export function type(label: T | ''): T { return label; } + +export { IpcEvents } from './ipcEvents'; + +export function runInElectron() { + return window && (window).process && (window).process.type; +} diff --git a/shared/src/util/ipcEvents.ts b/shared/src/util/ipcEvents.ts new file mode 100644 index 00000000..5d528ea7 --- /dev/null +++ b/shared/src/util/ipcEvents.ts @@ -0,0 +1,20 @@ +class App { + public static readonly appStarted = 'app-started'; +} + +class AutoUpdate { + public static readonly checkingForUpdate = 'checking-for-update'; + public static readonly updateAvailable = 'update-available'; + public static readonly updateNotAvailable = 'update-not-available'; + public static readonly autoUpdateError = 'auto-update-error'; + public static readonly autoUpdateDownloaded = 'update-downloaded'; + public static readonly autoUpdateDownloadProgress = 'auto-update-download-progress'; + public static readonly updateAndRestart = 'update-and-restart'; + public static readonly checkForUpdate = 'check-for-update'; + public static readonly checkForUpdateNotAvailable = 'check-for-update-not-available'; +} + +export class IpcEvents { + public static readonly app = App; + public static readonly autoUpdater = AutoUpdate; +} diff --git a/tsconfig.json b/tsconfig.base.json similarity index 90% rename from tsconfig.json rename to tsconfig.base.json index a903d777..2226a65a 100644 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -11,8 +11,8 @@ "suppressImplicitAnyIndexErrors": true }, "exclude": [ - "./node_modules", + "./dist", ".electron/dist", - "./dist" + "node_modules" ] } diff --git a/web/src/app.module.ts b/web/src/app.module.ts index d4249305..6db46ee0 100644 --- a/web/src/app.module.ts +++ b/web/src/app.module.ts @@ -60,7 +60,7 @@ import { } from './shared/components/svg/keys'; import { SvgModuleComponent } from './shared/components/svg/module'; import { SvgKeyboardWrapComponent } from './shared/components/svg/wrap'; -import { MainAppComponent, appRoutingProviders, routing } from './main-app'; +import { appRoutingProviders, MainAppComponent, routing } from './main-app'; import { CancelableDirective } from './shared/directives'; import { SafeStylePipe } from './shared/pipes'; @@ -69,14 +69,15 @@ import { CaptureService } from './shared/services/capture.service'; import { MapperService } from './shared/services/mapper.service'; import { SvgModuleProviderService } from './shared/services/svg-module-provider.service'; -import { KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects'; +import { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects'; import { KeymapEditGuard } from './shared/components/keymap/edit'; import { MacroNotFoundGuard } from './shared/components/macro/not-found'; import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service'; import { LocalDataStorageRepositoryService } from './shared/services/local-datastorage-repository.service'; import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service'; -import { reducer } from '../../shared/src/store/reducers/index'; +import { reducer } from './shared/store/reducers/index'; +import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings'; @NgModule({ declarations: [ @@ -127,7 +128,8 @@ import { reducer } from '../../shared/src/store/reducers/index'; SettingsComponent, KeyboardSliderComponent, CancelableDirective, - SafeStylePipe + SafeStylePipe, + AutoUpdateSettings ], imports: [ BrowserModule, @@ -147,7 +149,8 @@ import { reducer } from '../../shared/src/store/reducers/index'; Select2Module, EffectsModule.runAfterBootstrap(KeymapEffects), EffectsModule.runAfterBootstrap(MacroEffects), - EffectsModule.runAfterBootstrap(UserConfigEffects) + EffectsModule.runAfterBootstrap(UserConfigEffects), + EffectsModule.runAfterBootstrap(AutoUpdateSettingsEffects) ], providers: [ SvgModuleProviderService, @@ -156,9 +159,10 @@ import { reducer } from '../../shared/src/store/reducers/index'; KeymapEditGuard, MacroNotFoundGuard, CaptureService, - {provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService}, + { provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService }, DefaultUserConfigurationService ], bootstrap: [MainAppComponent] }) -export class AppModule { } +export class AppModule { +} diff --git a/web/src/tsconfig.json b/web/src/tsconfig.json index 3dd8c5da..8cd79cac 100644 --- a/web/src/tsconfig.json +++ b/web/src/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "exclude": [ "config-serializer" ]