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.
This commit is contained in:
Róbert Kiss
2017-06-22 14:22:54 +02:00
committed by László Monda
parent 2598109f8c
commit 121807a65a
49 changed files with 1028 additions and 129 deletions

View File

@@ -0,0 +1,36 @@
<div class="row">
<div class="col-xs-12">
<div class="checkbox">
<label>
<input type="checkbox"
[checked]="settings.checkForUpdateOnStartUp"
(change)="emitCheckForUpdateOnStartUp($event.target.checked)"> Automatically check for update on
application start
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
[checked]="settings.usePreReleaseUpdate"
(change)="emitUsePreReleaseUpdate($event.target.checked)"> Allow alpha / pre release
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Version:</label>
<div class="col-sm-10">
<p class="form-control-static">{{version}}</p>
</div>
</div>
<button class="btn btn-link" (click)="emitCheckForUpdate()">
Check for update
<span *ngIf="checkingForUpdate"
class="fa fa-spinner fa-spin"></span>
</button>
<div>
{{message}}
</div>
</div>
</div>

View File

@@ -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<boolean>();
@Output() toggleUsePreReleaseUpdate = new EventEmitter<boolean>();
@Output() checkForUpdate = new EventEmitter();
constructor() {
}
emitCheckForUpdateOnStartUp(value: boolean) {
this.toggleCheckForUpdateOnStartUp.emit(value);
}
emitUsePreReleaseUpdate(value: boolean) {
this.toggleUsePreReleaseUpdate.emit(value);
}
emitCheckForUpdate() {
this.checkForUpdate.emit();
}
}

View File

@@ -4,4 +4,15 @@
<span class="macro__name pane-title__name">Settings</span>
</h1>
</div>
To be done...
<div *ngIf="!runInElectron">
To be done...
</div>
<auto-update-settings *ngIf="runInElectron"
[version]="version"
[settings]="autoUpdateSettings$ | async"
[checkingForUpdate]="checkingForUpdate$ | async"
[message]="autoUpdateMessage$ | async"
(toggleCheckForUpdateOnStartUp)="toogleCheckForUpdateOnStartUp($event)"
(toggleUsePreReleaseUpdate)="toogleUsePreReleaseUpdate($event)"
(checkForUpdate)="checkForUpdate()">
</auto-update-settings>

View File

@@ -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<AutoUpdateSettings>;
checkingForUpdate$: Observable<boolean>;
autoUpdateMessage$: Observable<string>;
constructor(private store: Store<AppState>) {
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());
}
}

View File

@@ -0,0 +1,4 @@
export interface AutoUpdateSettings {
checkForUpdateOnStartUp: boolean;
usePreReleaseUpdate: boolean;
}

View File

@@ -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');

View File

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

View File

@@ -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;

View File

@@ -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<Action> = 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<Action> = 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<AppState>) {
}
}

View File

@@ -1,3 +1,4 @@
export * from './keymap';
export * from './macro';
export * from './user-config';
export * from './auto-update-settings';

View File

@@ -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<Action> = 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);

View File

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

View File

@@ -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;

View File

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

View File

@@ -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:

View File

@@ -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"
}

View File

@@ -20,6 +20,7 @@ export function capitalizeFirstLetter(text: string): string {
*/
const typeCache: { [label: string]: boolean } = {};
export function type<T>(label: T | ''): T {
if (typeCache[<string>label]) {
throw new Error(`Action type "${label}" is not unique"`);
@@ -29,3 +30,9 @@ export function type<T>(label: T | ''): T {
return <T>label;
}
export { IpcEvents } from './ipcEvents';
export function runInElectron() {
return window && (<any>window).process && (<any>window).process.type;
}

View File

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