refactore: create feature modules (#387)

* add @angular/cli to the project

* increase nodejs version -> 8.2.1

* add lerna

* merge web and shared module

* move electron module into packages as uhk-agent

Electron agent functionality is not working

* delete symlinker

* convert private properties to public of component if used in html

* revert uhk-message.component

* fix component path

* fix the correct name of the uhk-message.component.scss

* building web and electron module

* delete uhk-renderer package

* handle device connect disconnect state

* add privilege detection

* fix set privilege functionality

* turn back download keymap functionality

* add bootstrap, select2 js and fix null pointer exception

* turn back upload data to keyboard

* fix send keymap

* fix test-serializer

* add missing package.json

* merging

* fix appveyor build

* fix linting

* turn back electron storage service

* commit the missing electron-datastorage-repository

* update node to 8.3.0 in .nvmrc and log node version in appveyor build

* set exact version number in appveyor build

* vertical align privilege and missing device components

* set back node version to 8 in appveyor

* move node-usb dependency from usb dir to root

maybe it is fix the appveyor build

* revert usb to root

* fix electron builder script

* fix electron builder script

* turn off electron devtools

* remove CTRL+U functionality

* fix CTRL+o

* fix lint error

* turnoff store freeze

* start process when got `Error: EPERM: operation not permitted` error

* move files from root usb dir -> packages/usb
This commit is contained in:
Róbert Kiss
2017-08-19 20:02:17 +02:00
committed by László Monda
parent 97770f67c0
commit 0f558e4132
524 changed files with 25606 additions and 5036 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
node_modules
npm-debug.log
*-debug.log
.vscode
dist
.idea

2
.nvmrc
View File

@@ -1 +1 @@
8.1.4
8.3.0

View File

@@ -20,11 +20,13 @@ install:
- ps: Install-Product node $env:nodejs_version
- set CI=true
- set PATH=%APPDATA%\npm;%PATH%
- npm install
- node -v
- npm -v
- appveyor-retry npm install
test_script:
- npm run build
- npm run build:test
- appveyor-retry npm run build
- appveyor-retry npm run build:test
- npm run lint
- npm run test
- npm run release

View File

@@ -1,2 +0,0 @@
### Before build:
Add symbolic link from shared/src to electron/src/shared with **npm run symlink -- -i**

View File

@@ -1 +0,0 @@
shared

View File

@@ -1,7 +0,0 @@
<app-update-available *ngIf="showUpdateAvailable$ | async"
(updateApp)="updateApp()"
(doNotUpdateApp)="doNotUpdateApp()">
</app-update-available>
<router-outlet></router-outlet>
<notifier-container></notifier-container>

View File

@@ -1,6 +0,0 @@
app {
display: block;
min-height: 100vh;
height: 100vh;
width: 100%;
}

View File

@@ -1,28 +0,0 @@
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 '../shared/store/actions/app-update.action';
@Component({
selector: 'app',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
showUpdateAvailable$: Observable<boolean>;
constructor(private store: Store<AppState>) {
this.showUpdateAvailable$ = store.select(getShowAppUpdateAvailable);
}
updateApp() {
this.store.dispatch(new UpdateAppAction());
}
doNotUpdateApp() {
this.store.dispatch(new DoNotUpdateAppAction());
}
}

View File

@@ -1,35 +0,0 @@
import { ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MissingDeviceComponent } from './../components/missing-device/missing-device.component';
import { PrivilegeCheckerComponent } from './../components/privilege-checker';
import { MainAppComponent } from './../main-app/main-app.component';
import { mainAppRoutes } from './../main-app/main-app.routes';
import { UhkDeviceConnectedGuard } from './../services/uhk-device-connected.guard';
import { UhkDeviceDisconnectedGuard } from './../services/uhk-device-disconnected.guard';
import { UhkDeviceInitializedGuard } from './../services/uhk-device-initialized.guard';
import { UhkDeviceUninitializedGuard } from './../services/uhk-device-uninitialized.guard';
const appRoutes: Routes = [
{
path: 'detection',
component: MissingDeviceComponent,
canActivate: [UhkDeviceDisconnectedGuard]
},
{
path: 'privilege',
component: PrivilegeCheckerComponent,
canActivate: [UhkDeviceConnectedGuard, UhkDeviceUninitializedGuard]
},
{
path: '',
component: MainAppComponent,
canActivate: [UhkDeviceInitializedGuard],
children: mainAppRoutes
}
];
export const appRoutingProviders: any[] = [];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true });

View File

@@ -1 +0,0 @@
export * from './app.component';

View File

@@ -1 +0,0 @@
export { KeymapEditComponent } from './keymap-edit.component';

View File

@@ -1,98 +0,0 @@
import { Component, HostListener, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import '@ngrx/core/add/operator/select';
import { Store } from '@ngrx/store';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/withLatestFrom';
import { Keymap } from '../../../shared/config-serializer/config-items/keymap';
import { UhkBuffer } from '../../../shared/config-serializer/uhk-buffer';
import { AppState } from '../../../shared/store';
import { SvgKeyboardWrapComponent } from '../../../shared/components/svg/wrap';
import { KeymapEditComponent as SharedKeymapEditComponent } from '../../../shared/components/keymap/edit';
import { UhkDeviceService } from '../../../services/uhk-device.service';
import { getUserConfiguration } from '../../../shared/store/reducers/user-configuration';
@Component({
selector: 'keymap-edit',
templateUrl: '../../../shared/components/keymap/edit/keymap-edit.component.html',
styleUrls: ['../../../shared/components/keymap/edit/keymap-edit.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class KeymapEditComponent extends SharedKeymapEditComponent {
@ViewChild(SvgKeyboardWrapComponent) wrap: SvgKeyboardWrapComponent;
constructor(
store: Store<AppState>,
route: ActivatedRoute,
private uhkDevice: UhkDeviceService
) {
super(store, route);
}
@HostListener('window:keydown.control.u', ['$event'])
onCtrlU(event: KeyboardEvent): void {
console.log('ctrl + u pressed');
event.preventDefault();
event.stopPropagation();
this.sendLayer();
}
@HostListener('window:keydown.control.i', ['$event'])
onCtrlI(event: KeyboardEvent): void {
console.log('ctrl + i pressed');
event.preventDefault();
event.stopPropagation();
this.sendKeymap();
}
private sendLayer(): void {
const currentLayer: number = this.wrap.getSelectedLayer();
this.keymap$
.first()
.map(keymap => keymap.layers[currentLayer])
.withLatestFrom(this.store.let(getUserConfiguration()))
.map(([layer, userConfig]) => {
const uhkBuffer = new UhkBuffer();
layer.toBinary(uhkBuffer, userConfig);
return uhkBuffer.getBufferContent();
})
.switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer))
.do(response => console.log('Sending layer finished', response))
.switchMap(() => this.uhkDevice.applyConfig())
.subscribe(
response => console.log('Applying layer finished', response),
error => console.error('Error during uploading layer', error),
() => console.log('Layer has been sucessfully uploaded')
);
}
private sendKeymap(): void {
this.keymap$
.first()
.withLatestFrom(this.store.let(getUserConfiguration()))
.map(([keymap, userConfig]) => {
const uhkBuffer = new UhkBuffer();
keymap.toBinary(uhkBuffer, userConfig);
return uhkBuffer.getBufferContent();
})
.switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer))
.do(response => console.log('Sending keymap finished', response))
.switchMap(() => this.uhkDevice.applyConfig())
.subscribe(
response => console.log('Applying keymap finished', response),
error => console.error('Error during uploading keymap', error),
() => console.log('Keymap has been sucessfully uploaded')
);
}
}

View File

@@ -1 +0,0 @@
export * from './keymap.routes';

View File

@@ -1,27 +0,0 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
import { UhkDeviceService } from '../../services/uhk-device.service';
@Component({
selector: 'missing-device',
templateUrl: 'missing-device.component.html'
})
export class MissingDeviceComponent {
constructor(uhkDevice: UhkDeviceService, router: Router) {
uhkDevice.isConnected()
.distinctUntilChanged()
.takeWhile(connected => !connected)
.ignoreElements()
.subscribe({
complete: () => {
router.navigate(['/privilege']);
}
});
}
}

View File

@@ -1,135 +0,0 @@
/// <reference path="../../custom_types/sudo-prompt.d.ts"/>
import { Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
import * as isDev from 'electron-is-dev';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
import { remote } from 'electron';
import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { UhkDeviceService } from '../../services/uhk-device.service';
import { LogService } from '../../shared/services/logger.service';
@Component({
selector: 'privilege-checker',
templateUrl: 'privilege-checker.component.html',
styleUrls: ['privilege-checker.component.scss']
})
export class PrivilegeCheckerComponent {
private rootDir: string;
constructor(private router: Router,
private uhkDevice: UhkDeviceService,
private logService: LogService) {
if (isDev) {
this.rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
} else {
this.rootDir = path.dirname(remote.app.getAppPath());
}
this.logService.info('App root dir: ', this.rootDir);
uhkDevice.isConnected()
.distinctUntilChanged()
.takeWhile(connected => connected)
.ignoreElements()
.subscribe({
complete: () => {
router.navigate(['/detection']);
}
});
uhkDevice.isInitialized()
.distinctUntilChanged()
.takeWhile(initialized => !initialized)
.ignoreElements()
.subscribe({
complete: () => {
router.navigate(['/']);
}
});
}
setUpPermissions(): void {
let permissionSetter: Observable<void>;
switch (process.platform) {
case 'linux':
permissionSetter = this.setUpPermissionsOnLinux();
break;
// HID API shouldn't need privilege escalation on Windows
// TODO: If all HID API test success then delete this branch and setUpPermissionsOnWin() method
// case 'win32':
// permissionSetter = this.setUpPermissionsOnWin();
// break;
default:
permissionSetter = Observable.throw('Permissions couldn\'t be set. Invalid platform: ' + process.platform);
break;
}
permissionSetter.subscribe({
error: e => {
console.log(e);
},
complete: () => {
this.logService.info('Permissions has been successfully set');
this.uhkDevice.initialize();
this.router.navigate(['/']);
}
});
}
private setUpPermissionsOnLinux(): Observable<void> {
const subject = new ReplaySubject<void>();
const scriptPath = path.resolve(this.rootDir, 'rules/setup-rules.sh');
const options = {
name: 'Setting UHK access rules'
};
const command = `sh ${scriptPath}`;
console.log(command);
sudo.exec(command, options, (error: any) => {
if (error) {
subject.error(error);
} else {
subject.complete();
}
});
return subject.asObservable();
}
// TODO: If all HID API test success then delete this method.
// and remove zadic-${process.arch}.exe files from windows installer resources
// private setUpPermissionsOnWin(): Observable<void> {
// const subject = new ReplaySubject<void>();
//
// // source code: https://github.com/pbatard/libwdi
// const scriptPath = path.resolve(this.rootDir, `rules/zadic-${process.arch}.exe`);
// const options = {
// name: 'Setting UHK access rules'
// };
// const params = [
// `--vid $\{Constants.VENDOR_ID}`,
// `--pid $\{Constants.PRODUCT_ID}`,
// '--iface 0', // interface ID
// '--usealldevices', // if the device has installed USB driver than overwrite it
// '--noprompt' // return at the end of the installation and not waiting for any user command
// ];
// const paramsString = params.join(' ');
// const command = `"${scriptPath}" ${paramsString}`;
//
// sudo.exec(command, options, (error: any) => {
// if (error) {
// subject.error(error);
// } else {
// subject.complete();
// }
// });
//
// return subject.asObservable();
// }
}

View File

@@ -1,12 +0,0 @@
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'uhk-message',
templateUrl: 'uhk-message.component.html',
styleUrls: ['uhk-message.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UhkMessageComponent {
@Input() title: string;
@Input() subtitle: string;
}

View File

@@ -1,196 +0,0 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/// <reference path="./custom_types/command-line-args.d.ts"/>
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/updateInfo';
import * as settings from 'electron-settings';
import * as isDev from 'electron-is-dev';
import * as commandLineArgs from 'command-line-args';
import { IpcEvents } from './shared/util';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { CommandLineArgs } from './shared/models/command-line-args';
const optionDefinitions = [
{ name: 'addons', type: Boolean, defaultOption: false }
];
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
// import './dev-extension';
require('electron-debug')({ showDevTools: false, 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({
title: 'UHK Agent',
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true
},
icon: 'images/agent-icon.png'
});
win.setMenuBarVisibility(false);
win.maximize();
const indexPath = path.resolve(__dirname, './index.html');
// and load the index.html of the app.
win.loadURL(`file://${indexPath}`);
win.on('page-title-updated', event => {
event.preventDefault();
});
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});
win.webContents.on('did-finish-load', () => {
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
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.
if (win === null) {
createWindow();
}
});
// 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: string) => {
sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateError, err.substr(0, 100));
});
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());
ipcMain.on(IpcEvents.app.getCommandLineArgs, (event: any) => event.sender.send(IpcEvents.app.getCommandLineArgsReply, options));
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 autoUpdateSettings = getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.usePreReleaseUpdate;
}
function checkForUpdateAtStartup() {
const autoUpdateSettings = getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.checkForUpdateOnStartUp;
}
function getAutoUpdateSettings() {
const storageService = new ElectronDataStorageRepositoryService();
return storageService.getAutoUpdateSettings();
}

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ultimate Hacking Keyboard Configurator</title>
<link href="vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link rel="shortcut icon" href="images/favicon.ico">
<script src="polyfills.uhk.js"></script>
<script src="vendor.uhk.js"></script>
<!--<script src="vendor/usb/usb.js"></script>-->
<script>
usb = require('usb');
nodeHid = require('node-hid');
</script>
</head>
<body>
<!-- Google Tag Manager -->
<noscript>
<iframe src="//www.googletagmanager.com/ns.html?id=GTM-PQLCXB" height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PQLCXB');
</script>
<!-- End Google Tag Manager -->
<app></app>
<script src="app.uhk.js"></script> <!-- This should be moved to the head -->
</body>
</html>

View File

@@ -1,2 +0,0 @@
export * from './main-app.component';
export * from './main-app.routes';

View File

@@ -1,6 +0,0 @@
<notification></notification>
<side-menu></side-menu>
<div id="main-content" class="split split-horizontal main-content">
<router-outlet></router-outlet>
</div>
<ngrx-store-log-monitor toggleCommand="alt-t"></ngrx-store-log-monitor>

View File

@@ -1,13 +0,0 @@
import { Routes } from '@angular/router';
import { addOnRoutes } from '../shared/components/add-on';
import { keymapRoutes } from '../components/keymap';
import { macroRoutes } from '../shared/components/macro';
import { settingsRoutes } from '../shared/components/settings';
export const mainAppRoutes: Routes = [
...keymapRoutes,
...macroRoutes,
...addOnRoutes,
...settingsRoutes
];

View File

@@ -1,5 +0,0 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@@ -1,30 +0,0 @@
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 { CommandLineArgs } from '../shared/models/command-line-args';
import { ProcessCommandLineArgsAction } from '../../../shared/src/store/actions/app.action';
@Injectable()
export class AppRendererService {
constructor(private store: Store<AppState>,
private zone: NgZone) {
this.registerEvents();
}
getCommandLineArgs() {
ipcRenderer.send(IpcEvents.app.getCommandLineArgs);
}
private registerEvents() {
ipcRenderer.on(IpcEvents.app.getCommandLineArgsReply, (event: string, arg: CommandLineArgs) => {
this.dispachStoreAction(new ProcessCommandLineArgsAction(arg));
});
}
private dispachStoreAction(action: Action) {
this.zone.run(() => this.store.dispatch(action));
}
}

View File

@@ -1,22 +0,0 @@
import { CanActivate, Router } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import { UhkDeviceService } from './uhk-device.service';
@Injectable()
export class UhkDeviceInitializedGuard implements CanActivate {
constructor(private uhkDevice: UhkDeviceService, private router: Router) { }
canActivate(): Observable<boolean> {
return this.uhkDevice.isInitialized()
.do(initialized => {
if (!initialized) {
this.router.navigate(['/detection']);
}
});
}
}

View File

@@ -1,24 +0,0 @@
import { CanActivate, Router } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import { UhkDeviceService } from './uhk-device.service';
@Injectable()
export class UhkDeviceUninitializedGuard implements CanActivate {
constructor(private uhkDevice: UhkDeviceService, private router: Router) { }
canActivate(): Observable<boolean> {
return this.uhkDevice.isInitialized()
.do(initialized => {
if (initialized) {
this.router.navigate(['/']);
}
})
.map(initialized => !initialized);
}
}

View File

@@ -1,141 +0,0 @@
import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/do';
import { Device, findByIds, InEndpoint, Interface, on, OutEndpoint } from 'usb';
import { LogService } from '../shared/services/logger.service';
import { Constants } from '../shared/util';
import { UhkDeviceService } from './uhk-device.service';
@Injectable()
export class UhkLibUsbApiService extends UhkDeviceService implements OnDestroy {
private device: Device;
static isUhkDevice(device: Device) {
return device.deviceDescriptor.idVendor === Constants.VENDOR_ID &&
device.deviceDescriptor.idProduct === Constants.PRODUCT_ID;
}
constructor(zone: NgZone, protected logService: LogService) {
super(logService);
this.initialize();
// The change detection doesn't work properly if the callbacks are called outside Angular Zone
on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device)));
on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device)));
}
initialize(): void {
if (this.initialized$.getValue()) {
return;
}
this.device = findByIds(Constants.VENDOR_ID, Constants.PRODUCT_ID);
this.connected$.next(!!this.device);
if (!this.device) {
return;
}
try {
this.device.open();
this.deviceOpened$.next(true);
} catch (error) {
this.logService.error(error);
return;
}
const usbInterface: Interface = this.device.interface(0);
// https://github.com/tessel/node-usb/issues/147
// The function 'isKernelDriverActive' is not available on Windows and not even needed.
if (usbInterface.isKernelDriverActive()) {
usbInterface.detachKernelDriver();
}
this.messageIn$ = Observable.create((subscriber: Subscriber<Buffer>) => {
const inEndPoint: InEndpoint = <InEndpoint>usbInterface.endpoints[0];
this.logService.info('Try to read');
inEndPoint.transfer(Constants.MAX_PAYLOAD_SIZE, (error: string, receivedBuffer: Buffer) => {
if (error) {
this.logService.error('reading error', error);
subscriber.error(error);
} else {
this.logService.info('read data', receivedBuffer);
subscriber.next(receivedBuffer);
subscriber.complete();
}
});
});
const outEndPoint: OutEndpoint = <OutEndpoint>usbInterface.endpoints[1];
const outSending = this.messageOut$.concatMap(senderPackage => {
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
this.logService.info('transfering', senderPackage.buffer);
outEndPoint.transfer(senderPackage.buffer, error => {
if (error) {
this.logService.error('transfering errored', error);
subscriber.error(error);
} else {
this.logService.info('transfering finished');
subscriber.complete();
}
});
})).concat(this.messageIn$)
.do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
.catch((error: string) => {
senderPackage.observer.error(error);
return Observable.empty<void>();
});
}).publish();
this.outSubscription = outSending.connect();
this.initialized$.next(true);
}
hasPermissions(): Observable<boolean> {
return this.isConnected()
.combineLatest(this.deviceOpened$)
.map((latest: boolean[]) => {
const connected = latest[0];
const opened = latest[1];
if (!connected) {
return false;
} else if (opened) {
return true;
}
try {
this.device.open();
} catch (error) {
return false;
}
this.device.close();
return true;
});
}
onDeviceAttach(device: Device) {
if (!UhkLibUsbApiService.isUhkDevice(device)) {
return;
}
// Ugly hack: device is not openable (on Windows) right after the attach
Observable.timer(100)
.first()
.subscribe(() => this.initialize());
}
onDeviceDetach(device: Device) {
if (!UhkLibUsbApiService.isUhkDevice(device)) {
return;
}
this.disconnect();
}
}

View File

@@ -1,26 +0,0 @@
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 '../../shared/store/actions/app.action';
import { AppUpdateRendererService } from '../../services/app-update-renderer.service';
import { AppRendererService } from '../../services/app-renderer.service';
@Injectable()
export class ApplicationEffect {
@Effect()
appStart$: Observable<Action> = this.actions$
.ofType(app.ActionTypes.APP_BOOTSRAPPED)
.startWith(new app.AppStartedAction())
.do(() => {
this.appUpdateRendererService.sendAppStarted();
this.appRendererService.getCommandLineArgs();
});
constructor(
private actions$: Actions,
private appUpdateRendererService: AppUpdateRendererService,
private appRendererService: AppRendererService) { }
}

View File

@@ -1,2 +0,0 @@
export { AppUpdateEffect } from './app-update.effect';
export { ApplicationEffect } from './app.effect';

View File

@@ -1,34 +0,0 @@
/// <reference path="../custom_types/electron-is-dev.d.ts"/>
import { createSelector } from 'reselect';
import { compose } from '@ngrx/core/compose';
import { storeFreeze } from 'ngrx-store-freeze';
import { ActionReducer, combineReducers } from '@ngrx/store';
import * as isDev from 'electron-is-dev';
import { AppState as CommonState } from '../shared/store';
import * as fromAppUpdate from './reducers/app-update.reducer';
import { reducer as CommonReducer } from '../shared/store/reducers';
export interface AppState extends CommonState {
appUpdate: fromAppUpdate.State;
}
const reducers = {
...CommonReducer,
appUpdate: fromAppUpdate.reducer
};
const developmentReducer: ActionReducer<AppState> = compose(storeFreeze, combineReducers)(reducers);
const productionReducer: ActionReducer<AppState> = 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);

View File

@@ -1,6 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"files": [
"electron-main.ts"
]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"exclude": [
"electron-main.ts",
"webpack.config.*"
]
}

View File

@@ -1,3 +0,0 @@
import './shared/vendor.ts';
import 'sudo-prompt';

View File

@@ -1,30 +0,0 @@
//var webpack = require("webpack");
var path = require('path');
var rootDir = path.resolve(__dirname, '../');
module.exports = {
entry: [ path.resolve(rootDir, 'src/electron-main.ts')],
output: {
path: rootDir + "/dist",
filename: "electron-main.js"
},
target: 'electron-main',
devtool: 'source-map',
resolve: {
extensions: ['.webpack.js', '.web.js', '.ts', '.js'],
modules: ["node_modules"]
},
module: {
rules: [
{ test: /\.ts$/, loader: 'ts-loader?' + JSON.stringify({ configFileName: 'tsconfig-electron-main.json' }), exclude: /node_modules/ },
]},
plugins: [
// new webpack.optimize.UglifyJsPlugin({ minimize: true })
],
node: {
__dirname: false,
__filename: false
}
}

View File

@@ -1,109 +0,0 @@
const webpack = require("webpack");
const SvgStore = require('webpack-svgstore-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
const ContextReplacementPlugin = require("webpack/lib/ContextReplacementPlugin");
const webpackHelper = require('../../scripts/webpack-helper');
const rootDir = path.resolve(__dirname, '../');
module.exports = {
entry: {
polyfills: path.resolve(rootDir, 'src/shared/polyfills.ts'),
vendor: path.resolve(rootDir, 'src/vendor.ts'),
app: path.resolve(rootDir, 'src/main.ts')
},
output: {
path: rootDir + "/dist",
filename: "[name].uhk.js"
},
target: 'electron-renderer',
externals: {
usb: 'usb',
'node-hid': 'nodeHid'
},
devtool: 'source-map',
resolve: {
extensions: ['.webpack.js', '.web.js', '.ts', '.js'],
modules: ["node_modules"],
alias: {
jquery: 'jquery/dist/jquery.min.js',
select2: 'select2/dist/js/select2.full.min.js',
'file-saver': 'filesaver.js/FileSaver.min.js'
}
},
module: {
rules: [
{ test: /\.ts$/, use: ['ts-loader', 'angular2-template-loader'], exclude: /node_modules/ },
{ test: /\.html$/, loader: 'html-loader?attrs=false' },
{
test: /\.scss$/,
exclude: /node_modules/,
use: ['raw-loader', 'sass-loader']
},
{ test: /jquery/, loader: 'expose-loader?$!expose-loader?jQuery' },
{ test: require.resolve("usb"), loader: "expose-loader?usb" },
{ test: require.resolve("node-hid"), loader: "expose-loader?node-hid" }
]
},
plugins: [
// new webpack.optimize.UglifyJsPlugin({ minimize: true })
new SvgStore({
svgoOptions: {
plugins: [
{ removeTitle: true }
]
}
}),
new CopyWebpackPlugin(
[
{
from: path.join(__dirname, 'index.html'), flatten: true
},
{
from: 'node_modules/font-awesome/css/font-awesome.min.css',
to: 'vendor/font-awesome/css/font-awesome.min.css'
},
{
from: 'node_modules/font-awesome/fonts',
to: 'vendor/font-awesome/fonts'
},
{
from: 'node_modules/bootstrap/dist/',
to: 'vendor/bootstrap'
},
{
from: 'images',
to: 'images'
},
{
from: 'rules',
to: 'rules'
},
{
from: 'node_modules/usb',
to: 'vendor/usb'
},
{
from: 'electron/src/package.json',
to: 'package.json'
}
]
),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
}),
new CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)@angular/,
webpackHelper.root(__dirname, './src') // location of your src
)
]
};

7
lerna.json Normal file
View File

@@ -0,0 +1,7 @@
{
"lerna": "2.0.0",
"packages": [
"packages/*"
],
"version": "1.0.0"
}

11222
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,108 +14,100 @@
"npm": ">=5.1.0 <6.0.0"
},
"devDependencies": {
"@ngrx/store-devtools": "3.2.4",
"@ngrx/store-log-monitor": "3.0.2",
"@types/bootstrap": "^3.3.35",
"@types/core-js": "0.9.42",
"@angular/cli": "^1.3.0-rc.5",
"@angular/compiler-cli": "^4.3.3",
"@angular/language-service": "^4.3.3",
"@types/electron-devtools-installer": "^2.0.2",
"@types/electron-settings": "^3.0.0",
"@types/file-saver": "0.0.1",
"@types/jquery": "^3.2.11",
"@types/node": "^8.0.20",
"@types/file-saver": "~0.0.1",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/jquery": "^3.2.9",
"@types/node": "~6.0.60",
"@types/node-hid": "^0.5.2",
"@types/usb": "^1.1.3",
"angular2-template-loader": "0.6.2",
"angular-notifier": "^2.0.0",
"autoprefixer": "^6.5.3",
"buffer": "^5.0.6",
"codelyzer": "~3.0.1",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
"css-loader": "^0.28.1",
"cssnano": "^3.10.0",
"devtron": "^1.4.0",
"electron": "1.7.5",
"electron-builder": "^19.21.0",
"electron-debug": "^1.4.0",
"electron-builder": "19.6.1",
"electron-debug": "^1.3.0",
"electron-devtools-installer": "^2.2.0",
"electron-rebuild": "^1.6.0",
"expose-loader": "^0.7.3",
"html-loader": "^0.5.1",
"jsonfile": "^3.0.1",
"node-sass": "^4.5.2",
"npm-run-all": "4.0.2",
"path": "^0.12.7",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.1",
"sass-loader": "^6.0.6",
"standard-version": "^4.2.0",
"stylelint": "^8.0.0",
"ts-loader": "^2.3.2",
"tslint": "^5.6.0",
"webpack": "^3.5.4",
"webpack-dev-server": "^2.7.1",
"webpack-svgstore-plugin": "4.0.1"
},
"dependencies": {
"@angular/animations": "^4.3.4",
"@angular/common": "^4.3.4",
"@angular/compiler": "^4.3.4",
"@angular/core": "^4.3.4",
"@angular/forms": "^4.3.4",
"@angular/platform-browser": "^4.3.4",
"@angular/platform-browser-dynamic": "^4.3.4",
"@angular/router": "^4.3.4",
"@ngrx/core": "^1.2.0",
"@ngrx/effects": "^2.0.4",
"@ngrx/router-store": "^1.2.6",
"@ngrx/store": "^2.2.3",
"angular-notifier": "^2.0.0",
"bootstrap": "^3.3.7",
"browser-stdout": "^1.3.0",
"buffer": "^5.0.7",
"command-line-args": "^4.0.7",
"core-js": "^2.5.0",
"dragula": "^3.7.2",
"electron-is-dev": "^0.3.0",
"electron-log": "^2.2.7",
"electron-rebuild": "^1.5.11",
"electron-settings": "^3.1.1",
"electron-updater": "^2.8.7",
"filesaver.js": "^0.2.0",
"font-awesome": "^4.7.0",
"jquery": "^3.2.1",
"json-loader": "^0.5.7",
"ng2-dragula": "^1.5.0",
"ng2-select2": "^1.0.0-beta.10",
"ngrx-store-freeze": "^0.1.9",
"node-hid": "^0.5.4",
"exports-loader": "^0.6.3",
"file-loader": "^0.10.0",
"istanbul-instrumenter-loader": "^2.0.0",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"json-loader": "^0.5.4",
"jsonfile": "3.0.1",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"lerna": "^2.0.0",
"less-loader": "^4.0.2",
"mkdirp": "^0.5.1",
"npm-run-all": "^4.0.2",
"postcss-loader": "^1.3.3",
"postcss-url": "^5.1.2",
"protractor": "~5.1.2",
"raw-loader": "^0.5.1",
"reselect": "^3.0.1",
"rxjs": "^5.4.3",
"select2": "^4.0.3",
"sudo-prompt": "^7.1.1",
"rimraf": "^2.6.1",
"rxjs": "^5.4.2",
"sass-loader": "^6.0.3",
"script-loader": "^0.7.0",
"source-map-loader": "^0.2.0",
"standard-version": "^4.2.0",
"style-loader": "^0.13.1",
"stylelint": "^7.13.0",
"stylus-loader": "^3.0.1",
"svg-sprite": "^1.3.7",
"ts-loader": "^2.3.1",
"ts-node": "~3.0.4",
"tslint": "~5.5.0",
"typescript": "^2.4.2",
"usb": "git+https://github.com/tessel/node-usb.git#1.3.0",
"url-loader": "^0.5.7",
"webpack": "^2.4.1",
"webpack-dev-server": "~2.4.5",
"webpack-svgstore-plugin": "^4.0.1",
"xml-loader": "^1.2.1",
"zone.js": "^0.8.16"
"zone.js": "^0.8.14"
},
"scripts": {
"postinstall": "run-p \"symlink -- -i\" ",
"test": "cd ./test-serializer && node ./test-serializer.js",
"postinstall": "lerna bootstrap",
"test": "lerna exec --scope test-serializer npm test",
"lint": "run-s -scn lint:ts lint:style",
"lint:ts": "run-p -sn lint:ts:electron-main lint:ts:electron-renderer lint:ts:web lint:ts:test-serializer",
"lint:ts:electron-main": "tslint --type-check --project ./electron/src/tsconfig-electron-main.json",
"lint:ts:electron-renderer": "tslint --type-check --project ./electron/src/tsconfig.json",
"lint:ts:web": "tslint --type-check --project ./web/src/tsconfig.json",
"lint:ts:test-serializer": "tslint --type-check --project ./test-serializer/tsconfig.json",
"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\"",
"build:electron": "run-s -sn build:electron:main build:electron:app install:build-deps",
"build:electron:main": "webpack --config \"electron/src/webpack.config.electron-main.js\"",
"build:electron:app": "webpack --config \"electron/src/webpack.config.js\"",
"build:usb": "electron-rebuild -w usb,node-hid -p -m electron/dist",
"build:test": "webpack --config \"test-serializer/webpack.config.js\"",
"server:web": "webpack-dev-server --config \"web/src/webpack.config.js\" --content-base \"./web/dist\"",
"server:electron": "webpack --config \"electron/src/webpack.config.js\" --watch",
"electron": "electron electron/dist/electron-main.js",
"symlink": "node ./tools/symlinker",
"lint:ts:electron-main": "tslint --type-check --project ./packages/uhk-agent/tsconfig.json",
"lint:ts:electron-renderer": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.renderer.json",
"lint:ts:web": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.app.json",
"lint:ts:test-serializer": "tslint --type-check --project ./packages/test-serializer/tsconfig.json",
"lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss",
"build": "run-s build:web build:electron",
"build:web": "lerna exec --scope uhk-web npm run build",
"build:electron": "run-s -sn build:electron:renderer build:electron:main",
"build:electron:main": "lerna exec --scope uhk-agent npm run build",
"build:electron:renderer": "lerna exec --scope uhk-web npm run build:renderer",
"build:test": "lerna exec --scope test-serializer npm run build",
"server:web": "lerna exec --scope uhk-web npm start",
"server:electron": "lerna exec --scope uhk-web npm run server:renderer",
"electron": "lerna exec --scope uhk-agent npm start",
"standard-version": "standard-version",
"pack": "node ./scripts/release.js",
"install:build-deps": "cd electron/dist && npm i && cd .. && npm run build:usb",
"sprites": "node ./scripts/generate-svg-sprites",
"release": "node ./scripts/release.js",
"clean": "rimraf ./node_modules ./electron/dist"
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist package-lock.json"
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "test-serializer",
"main": "test-serializer.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": ">=8.1.0 <9.0.0",
"npm": ">=5.1.0 <6.0.0"
},
"dependencies": {
},
"devDependencies": {
"uhk-web": "^1.0.0"
},
"scripts": {
"build": "webpack",
"test": "node ./test-serializer.js"
}
}

View File

@@ -1,10 +1,10 @@
import { UserConfiguration } from '../shared/src/config-serializer/config-items/user-configuration';
import { UhkBuffer } from '../shared/src/config-serializer/uhk-buffer';
import { UserConfiguration } from '../uhk-web/src/app/config-serializer/config-items/user-configuration';
import { UhkBuffer } from '../uhk-web/src/app/config-serializer/uhk-buffer';
const assert = require('assert');
const fs = require('fs');
const userConfig = JSON.parse(fs.readFileSync('../shared/src/config-serializer/user-config.json'));
const userConfig = JSON.parse(fs.readFileSync('../uhk-web/src/app/config-serializer/user-config.json'));
const config1Js = userConfig;
const config1Ts: UserConfiguration = new UserConfiguration().fromJsonObject(config1Js);

View File

@@ -8,8 +8,7 @@
"../node_modules/@types"
],
"types": [
"node",
"core-js"
"node"
]
}
}

2604
packages/uhk-agent/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"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": ">=8.1.0 <9.0.0",
"npm": ">=5.1.0 <6.0.0"
},
"dependencies": {
"command-line-args": "4.0.6",
"electron": "1.7.5",
"electron-is-dev": "0.1.2",
"electron-log": "2.2.6",
"electron-rebuild": "1.6.0",
"electron-settings": "3.0.14",
"electron-updater": "2.2.0",
"node-hid": "0.5.4",
"sudo-prompt": "^7.0.0"
},
"devDependencies": {
"uhk-common": "^1.0.0"
},
"scripts": {
"start": "electron ./dist/electron-main.js",
"build": "webpack && npm run install:build-deps && npm run build:usb",
"build:usb": "electron-rebuild -w node-hid -p -m ./dist",
"install:build-deps": "cd ./dist && npm i"
}
}

View File

@@ -0,0 +1,117 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/// <reference path="./custom_types/command-line-args.d.ts"/>
import './polyfills';
import { app, BrowserWindow, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import * as path from 'path';
import * as url from 'url';
import * as commandLineArgs from 'command-line-args';
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { CommandLineArgs } from 'uhk-common';
import { DeviceService } from './services/device.service';
import { logger } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service';
import { AppService } from './services/app.service';
import { SudoService } from './services/sudo.service';
const optionDefinitions = [
{ name: 'addons', type: Boolean, defaultOption: false }
];
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
// 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;
autoUpdater.logger = logger;
let deviceService: DeviceService;
let appUpdateService: AppUpdateService;
let appService: AppService;
let sudoService: SudoService;
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
title: 'UHK Agent',
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true
},
icon: 'assets/images/agent-icon.png'
});
win.setMenuBarVisibility(false);
win.maximize();
deviceService = new DeviceService(logger, win);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options);
sudoService = new SudoService(logger);
// and load the index.html of the app.
win.loadURL(url.format({
pathname: path.join(__dirname, 'renderer/index.html'),
protocol: 'file:',
slashes: true
}));
win.on('page-title-updated', (event: any) => {
event.preventDefault();
});
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
deviceService = null;
appUpdateService = null;
appService = null;
sudoService = null;
});
win.webContents.on('did-finish-load', () => {
});
win.webContents.on('crashed', (event: any) => {
logger.error(event);
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
if (appUpdateService) {
appUpdateService.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.
if (win === null) {
createWindow();
}
});
// 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

View File

@@ -0,0 +1,6 @@
{
"name": "UHK-Agent",
"version": "1.0",
"devtools_page": "foo.html",
"default_locale": "en"
}

View File

@@ -14,8 +14,6 @@
"npm": ">=5.1.0 <6.0.0"
},
"dependencies": {
"node-hid": "0.5.4",
"usb": "git+https://github.com/aktary/node-usb.git"
"node-hid": "0.5.4"
}
}

View File

@@ -1,4 +1,5 @@
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/array';
import 'core-js/es7/object';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';

View File

@@ -0,0 +1,110 @@
import { ipcMain, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
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, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
export class AppUpdateService extends MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow,
private app: Electron.App) {
super(logService, win);
this.initListeners();
logService.info('AppUpdateService init success');
}
saveFirtsRun() {
settings.set('firstRunVersion', this.app.getVersion());
}
private initListeners() {
autoUpdater.on('checking-for-update', () => {
this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
});
autoUpdater.on('update-available', (ev: any, info: VersionInfo) => {
autoUpdater.downloadUpdate();
this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
});
autoUpdater.on('update-not-available', (ev: any, info: VersionInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info);
});
autoUpdater.on('error', (ev: any, err: string) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateError, err.substr(0, 100));
});
autoUpdater.on('download-progress', (progressObj: ProgressInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloadProgress, progressObj);
});
autoUpdater.on('update-downloaded', (ev: any, info: VersionInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
});
ipcMain.on(IpcEvents.autoUpdater.updateAndRestart, () => autoUpdater.quitAndInstall(true));
ipcMain.on(IpcEvents.app.appStarted, () => {
if (this.checkForUpdateAtStartup()) {
this.checkForUpdate();
}
});
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => this.checkForUpdate());
}
private checkForUpdate() {
if (isDev) {
const msg = 'Application update is not working in dev mode.';
this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
if (this.isFirstRun()) {
const msg = 'Application update is skipping at first run.';
this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
autoUpdater.allowPrerelease = this.allowPreRelease();
autoUpdater.checkForUpdates();
}
private isFirstRun() {
if (!settings.has('firstRunVersion')) {
return true;
}
const firstRunVersion = settings.get('firstRunVersion');
this.logService.info(`firstRunVersion: ${firstRunVersion}`);
this.logService.info(`package.version: ${this.app.getVersion()}`);
return firstRunVersion !== this.app.getVersion();
}
private allowPreRelease() {
const autoUpdateSettings = this.getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.usePreReleaseUpdate;
}
private checkForUpdateAtStartup() {
const autoUpdateSettings = this.getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.checkForUpdateOnStartUp;
}
private getAutoUpdateSettings() {
// const storageService = new ElectronDataStorageRepositoryService();
// return storageService.getAutoUpdateSettings();
return { checkForUpdateOnStartUp: false, usePreReleaseUpdate: false };
}
}

View File

@@ -0,0 +1,28 @@
import { ipcMain, BrowserWindow } from 'electron';
import { CommandLineArgs, IpcEvents, AppStartInfo, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
import { DeviceService } from './device.service';
export class AppService extends MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow,
private deviceService: DeviceService,
private options: CommandLineArgs) {
super(logService, win);
ipcMain.on(IpcEvents.app.getAppStartInfo, this.handleAppStartInfo.bind(this));
logService.info('AppService init success');
}
private handleAppStartInfo(event: Electron.Event) {
this.logService.info('getStartInfo');
const response: AppStartInfo = {
commandLineArgs: this.options,
deviceConnected: this.deviceService.isConnected,
hasPermission: this.deviceService.hasPermission()
};
this.logService.info('getStartInfo response:', response);
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);
}
}

View File

@@ -0,0 +1,167 @@
import { ipcMain, BrowserWindow } from 'electron';
import { Constants, IpcEvents, LogService, IpcResponse } from 'uhk-common';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Device, devices, HID } from 'node-hid';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/distinctUntilChanged';
enum Command {
UploadConfig = 8,
ApplyConfig = 9
}
export class DeviceService {
private static convertBufferToIntArray(buffer: Buffer): number[] {
return Array.prototype.slice.call(buffer, 0);
}
private pollTimer$: Subscription;
private connected: boolean = false;
constructor(private logService: LogService,
private win: Electron.BrowserWindow) {
this.pollUhkDevice();
ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this));
logService.info('DeviceService init success');
}
public get isConnected(): boolean {
return this.connected;
}
public hasPermission(): boolean {
try {
const devs = devices();
return true;
} catch (err) {
this.logService.error('[DeviceService] hasPermission', err);
}
return false;
}
/**
* HID API not support device attached and detached event.
* This method check the keyboard is attached to the computer or not.
* Every second check the HID device list.
*/
private pollUhkDevice(): void {
this.pollTimer$ = Observable.interval(1000)
.startWith(0)
.map(() => {
return devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.PRODUCT_ID);
})
.distinctUntilChanged()
.do((connected: boolean) => {
this.connected = connected;
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, connected);
this.logService.info(`Device connection state changed to: ${connected}`);
})
.subscribe();
}
private saveUserConfiguration(event: Electron.Event, json: string): void {
const response = new IpcResponse();
try {
const buffer: Buffer = new Buffer(JSON.parse(json).data);
const fragments = this.getTransferBuffers(buffer);
const device = this.getDevice();
device.read((err, data) => {
if (err) {
this.logService.error('Send data to device err:', err);
}
this.logService.debug('send data to device response:', data.toString());
});
for (const fragment of fragments) {
const transferData = this.getTransferData(fragment);
this.logService.debug('Fragment: ', JSON.stringify(transferData));
device.write(transferData);
}
const applyBuffer = new Buffer([Command.ApplyConfig]);
const applyTransferData = this.getTransferData(applyBuffer);
this.logService.debug('Fragment: ', JSON.stringify(applyTransferData));
device.write(applyTransferData);
response.success = true;
this.logService.info('transferring finished');
}
catch (error) {
this.logService.error('transferring error', error);
response.error = { message: error.message };
}
event.sender.send(IpcEvents.device.saveUserConfigurationReply, response);
}
private getTransferData(buffer: Buffer): number[] {
const data = DeviceService.convertBufferToIntArray(buffer);
// if data start with 0 need to add additional leading zero because HID API remove it.
// https://github.com/node-hid/node-hid/issues/187
if (data.length > 0 && data[0] === 0) {
data.unshift(0);
}
// From HID API documentation:
// http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
// The first byte of data[] must contain the Report ID.
// For devices which only support a single report, this must be set to 0x0.
data.unshift(0);
return data;
}
private getTransferBuffers(configBuffer: Buffer): Buffer[] {
const fragments: Buffer[] = [];
const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4;
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
? MAX_SENDING_PAYLOAD_SIZE
: configBuffer.length - offset;
const header = new Buffer([Command.UploadConfig, length, offset & 0xFF, offset >> 8]);
fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)]));
}
return fragments;
}
/**
* Return the 0 interface of the keyboard.
* @returns {HID}
*/
private getDevice(): HID {
try {
const devs = devices();
this.logService.silly('Available devices:', devs);
const dev = devs.find((x: Device) =>
x.vendorId === Constants.VENDOR_ID &&
x.productId === Constants.PRODUCT_ID &&
((x.usagePage === 128 && x.usage === 129) || x.interface === 0));
if (!dev) {
this.logService.info('[DeviceService] UHK Device not found:');
return null;
}
const device = new HID(dev.path);
this.logService.info('Used device:', dev);
return device;
}
catch (err) {
this.logService.error('Can not create device:', err);
}
return null;
}
}

View File

@@ -0,0 +1,4 @@
import * as log from 'electron-log';
log.transports.file.level = 'debug';
export const logger = log;

View File

@@ -0,0 +1,16 @@
import { LogService } from 'uhk-common';
export class MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow) {}
protected sendIpcToWindow(message: string, arg?: any) {
this.logService.info('sendIpcToWindow:', message, arg);
if (!this.win || this.win.isDestroyed()) {
return;
}
this.win.webContents.send(message, arg);
}
}

View File

@@ -0,0 +1,57 @@
import { ipcMain, app } from 'electron';
import * as isDev from 'electron-is-dev';
import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { IpcEvents, LogService, IpcResponse } from 'uhk-common';
export class SudoService {
private rootDir: string;
constructor(private logService: LogService) {
if (isDev) {
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../..');
} else {
this.rootDir = path.dirname(app.getAppPath());
}
this.logService.info('[SudoService] App root dir: ', this.rootDir);
ipcMain.on(IpcEvents.device.setPrivilegeOnLinux, this.setPrivilege.bind(this));
}
private setPrivilege(event: Electron.Event) {
switch (process.platform) {
case 'linux':
this.setPrivilegeOnLinux(event);
break;
default:
const response: IpcResponse = {
success: false,
error: { message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform }
};
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
break;
}
}
private setPrivilegeOnLinux(event: Electron.Event) {
const scriptPath = path.join(this.rootDir, 'rules/setup-rules.sh');
const options = {
name: 'Setting UHK access rules'
};
const command = `sh ${scriptPath}`;
console.log(command);
sudo.exec(command, options, (error: any) => {
const response = new IpcResponse();
if (error) {
response.success = false;
response.error = error;
} else {
response.success = true;
}
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
});
}
}

View File

@@ -0,0 +1 @@
import 'sudo-prompt';

View File

@@ -0,0 +1,21 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2016",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2015.iterable",
"es2016",
"dom",
"dom.iterable"
]
}
}

View File

@@ -0,0 +1,45 @@
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const rootDir = __dirname;
module.exports = {
entry: [path.resolve(rootDir, 'src/electron-main.ts')],
output: {
path: rootDir + "/dist",
filename: "electron-main.js"
},
target: 'electron-main',
externals: {
"node-hid": "require('node-hid')"
},
devtool: 'source-map',
resolve: {
extensions: ['.webpack.js', '.web.js', '.ts', '.js'],
modules: ["node_modules"]
},
module: {
rules: [
{ test: /\.tsx?$/, loader: 'ts-loader' , exclude: /node_modules/},
]
},
plugins: [
new CopyWebpackPlugin(
[
{
from: 'src/manifest.json',
to: 'manifest.json'
},
{
from: 'src/package.json',
to: 'package.json'
}
]
)
],
node: {
__dirname: false,
__filename: false
}
};

View File

@@ -0,0 +1,3 @@
export * from './src/util';
export * from './src/models';
export * from './src/services';

29
packages/uhk-common/package-lock.json generated Normal file
View File

@@ -0,0 +1,29 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@angular/core": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-4.3.3.tgz",
"integrity": "sha1-jmp2kUZh20B/otiN0kQcTAFv9iU=",
"requires": {
"tslib": "1.7.1"
}
},
"@ngrx/core": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ngrx/core/-/core-1.2.0.tgz",
"integrity": "sha1-iCtGq6+i4ObYh8txobLC+j5tDcY="
},
"@ngrx/store": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-2.2.3.tgz",
"integrity": "sha1-570RSfHEQgjxzEdENT8PmKDx9Xs="
},
"tslib": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz",
"integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw="
}
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "uhk-common",
"private": true,
"version": "1.0.0",
"description": "Common Library contains the common code for uhk-agent (electron-main) and web (electron-renderer) modules",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@angular/core": "^4.3.3",
"@ngrx/core": "1.2.0",
"@ngrx/store": "^2.2.3"
}
}

View File

@@ -0,0 +1,7 @@
import { CommandLineArgs } from './command-line-args';
export interface AppStartInfo {
commandLineArgs: CommandLineArgs;
deviceConnected: boolean;
hasPermission: boolean;
}

View File

@@ -0,0 +1,4 @@
export * from './command-line-args';
export * from './notification';
export * from './ipc-response';
export * from './app-start-info';

View File

@@ -0,0 +1,4 @@
export class IpcResponse {
success: boolean;
error?: { message: string };
}

View File

@@ -0,0 +1 @@
export * from './logger.service';

View File

@@ -6,6 +6,14 @@ export class LogService {
console.error(args);
}
debug(...args: any[]): void {
console.debug(args);
}
silly(...args: any[]): void {
console.debug(args);
}
info(...args: any[]): void {
console.info(args);
}

View File

@@ -1,6 +1,5 @@
import { Constants } from './constants';
export { Constants };
export { Constants } from './constants';
export { IpcEvents } from './ipcEvents';
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
export function camelCaseToSentence(camelCasedText: string): string {
@@ -35,8 +34,6 @@ 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

@@ -1,7 +1,7 @@
class App {
public static readonly appStarted = 'app-started';
public static readonly getCommandLineArgs = 'app-get-command-line-args';
public static readonly getCommandLineArgsReply = 'app-get-command-line-args-reply';
public static readonly getAppStartInfo = 'app-get-start-info';
public static readonly getAppStartInfoReply = 'app-get-start-info-reply';
}
class AutoUpdate {
@@ -16,7 +16,16 @@ class AutoUpdate {
public static readonly checkForUpdateNotAvailable = 'check-for-update-not-available';
}
class Device {
public static readonly setPrivilegeOnLinux = 'set-privilege-on-linux';
public static readonly setPrivilegeOnLinuxReply = 'set-privilege-on-linux-reply';
public static readonly deviceConnectionStateChanged = 'device-connection-state-changed';
public static readonly saveUserConfiguration = 'device-save-user-configuration';
public static readonly saveUserConfigurationReply = 'device-save-user-configuration-reply';
}
export class IpcEvents {
public static readonly app = App;
public static readonly autoUpdater = AutoUpdate;
public static readonly device = Device;
}

View File

@@ -0,0 +1,20 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2015.iterable",
"dom",
"es2016"
]
}
}

View File

@@ -0,0 +1,69 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "uhk"
},
"apps": [
{
"name": "web",
"root": "src",
"outDir": "./dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main-web.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.min.css",
"styles.scss"
],
"scripts": [
"../node_modules/bootstrap/dist/js/bootstrap.js",
"../node_modules/select2/dist/js/select2.full.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {
},
"serve": {
"port": 8080
}
}
}

View File

@@ -0,0 +1,28 @@
# Web
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.4.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@@ -0,0 +1,14 @@
import { WebPage } from './app.po';
describe('web App', () => {
let page: WebPage;
beforeEach(() => {
page = new WebPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});

View File

@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class WebPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@@ -0,0 +1,3 @@
export * from './src/app/web.module';
export * from './src/app/app.routes';
export * from './src/app/app.component';

View File

@@ -0,0 +1,33 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

10654
packages/uhk-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
{
"name": "uhk-web",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build:renderer": "webpack --config webpack.config.js",
"server:renderer": "webpack --config webpack.config.js --watch"
},
"private": true,
"devDependencies": {
"@angular/animations": "^4.3.3",
"@angular/cli": "^1.3.0-rc.5",
"@angular/common": "^4.3.3",
"@angular/compiler": "^4.3.3",
"@angular/compiler-cli": "^4.3.3",
"@angular/core": "^4.3.3",
"@angular/forms": "^4.3.3",
"@angular/http": "^4.3.3",
"@angular/language-service": "^4.3.3",
"@angular/platform-browser": "^4.3.3",
"@angular/platform-browser-dynamic": "^4.3.3",
"@angular/router": "^4.3.3",
"@ngrx/core": "1.2.0",
"@ngrx/effects": "^2.0.4",
"@ngrx/router-store": "^1.2.6",
"@ngrx/store": "^2.2.3",
"@ngrx/store-devtools": "3.2.4",
"@ngrx/store-log-monitor": "3.0.2",
"@types/electron-devtools-installer": "^2.0.2",
"@types/electron-settings": "^3.0.0",
"@types/file-saver": "0.0.1",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/jquery": "^3.2.9",
"@types/node": "~8.0.19",
"@types/node-hid": "^0.5.2",
"@types/usb": "^1.1.3",
"angular-notifier": "^2.0.0",
"autoprefixer": "^6.5.3",
"bootstrap": "^3.3.7",
"buffer": "^5.0.6",
"circular-dependency-plugin": "^3.0.0",
"codelyzer": "~3.0.1",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
"css-loader": "^0.28.1",
"cssnano": "^3.10.0",
"dragula": "^3.7.2",
"exports-loader": "^0.6.3",
"file-loader": "^0.10.0",
"filesaver.js": "^0.2.0",
"font-awesome": "^4.7.0",
"html-webpack-plugin": "^2.29.0",
"istanbul-instrumenter-loader": "^2.0.0",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"jquery": "3.2.1",
"jsonfile": "3.0.1",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"less-loader": "^4.0.5",
"ng2-dragula": "1.5.0",
"ng2-select2": "1.0.0-beta.10",
"ngrx-store-freeze": "^0.1.9",
"node-hid": "0.5.4",
"postcss-loader": "^1.3.3",
"postcss-url": "^5.1.2",
"protractor": "~5.1.2",
"raw-loader": "^0.5.1",
"reselect": "^3.0.1",
"sass-loader": "^6.0.3",
"script-loader": "^0.7.0",
"select2": "^4.0.3",
"source-map-loader": "^0.2.0",
"style-loader": "^0.13.1",
"stylus-loader": "^3.0.1",
"sudo-prompt": "^7.1.1",
"ts-loader": "^2.3.1",
"ts-node": "~3.0.4",
"uhk-common": "1.0.0",
"url-loader": "^0.5.7",
"usb": "git+https://github.com/aktary/node-usb.git",
"webpack": "~3.4.1",
"webpack-dev-server": "~2.5.1",
"webpack-svgstore-plugin": "^4.0.1",
"xml-loader": "^1.2.1",
"zone.js": "^0.8.14"
},
"dependencies": {
"classlist.js": "^1.1.20150312",
"file-saver": "^1.3.3"
}
}

View File

@@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -0,0 +1,17 @@
{
"dest": "src/assets/",
"shape": {
"id": {
"generator": "function(name, file){return name}"
}
},
"mode": {
"defs": {
"inline": true,
"dest": "./",
"sprite": "compiled_sprite.svg",
"prefix": "icon-%s",
"bust": false
}
}
}

View File

@@ -0,0 +1,13 @@
<app-update-available *ngIf="showUpdateAvailable$ | async"
(updateApp)="updateApp()"
(doNotUpdateApp)="doNotUpdateApp()">
</app-update-available>
<side-menu *ngIf="deviceConnected$ | async"></side-menu>
<div id="main-content" class="main-content">
<router-outlet></router-outlet>
</div>
<div class="github-fork-ribbon" *ngIf="!(runningInElectron$ | async)">
<a class="" href="https://github.com/UltimateHackingKeyboard/agent" title="Fork me on GitHub">Fork me on GitHub</a>
</div>
<notifier-container></notifier-container>

View File

@@ -34,4 +34,39 @@
main-app {
min-height: 100vh;
width: 100%;
display: block;
overflow: hidden;
position: relative;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 26px;
}
.select2 {
&-container {
z-index: 1100;
.scancode--searchterm {
text-align: right;
color: #b7b7b7;
}
}
&-item {
display: flex;
justify-content: space-between;
}
&-results {
text-align: left;
}
}
.nav-pills > li > a {
cursor: pointer;
}
.select2-container--default .select2-dropdown--below .select2-results > .select2-results__options {
max-height: 300px;
}

View File

@@ -1,41 +1,41 @@
import { Component, ViewEncapsulation, HostListener } from '@angular/core';
import { Router } from '@angular/router';
import { Component, HostListener, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { saveAs } from 'file-saver';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/takeWhile';
import { AppState } from '../shared/store';
import { getUserConfiguration } from '../shared/store/reducers/user-configuration';
import { UhkBuffer } from '../shared/config-serializer/uhk-buffer';
import { UhkDeviceService } from '../services/uhk-device.service';
import { DoNotUpdateAppAction, UpdateAppAction } from './store/actions/app-update.action';
import {
AppState,
getShowAppUpdateAvailable,
deviceConnected,
runningInElectron
} from './store';
import { getUserConfiguration } from './store/reducers/user-configuration';
import { UhkBuffer } from './config-serializer/uhk-buffer';
import { SaveConfigurationAction } from './store/actions/device';
@Component({
selector: 'main-app',
templateUrl: './main-app.component.html',
styleUrls: ['../shared/main-app/main-app.component.scss'],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MainAppComponent {
showUpdateAvailable$: Observable<boolean>;
deviceConnected$: Observable<boolean>;
runningInElectron$: Observable<boolean>;
constructor(private uhkDevice: UhkDeviceService, private store: Store<AppState>, router: Router) {
uhkDevice.isInitialized()
.distinctUntilChanged()
.takeWhile(initialized => initialized)
.ignoreElements()
.subscribe({
complete: () => {
router.navigate(['/detection']);
}
});
constructor(private store: Store<AppState>) {
this.showUpdateAvailable$ = store.select(getShowAppUpdateAvailable);
this.deviceConnected$ = store.select(deviceConnected);
this.runningInElectron$ = store.select(runningInElectron);
}
updateApp() {
this.store.dispatch(new UpdateAppAction());
}
doNotUpdateApp() {
this.store.dispatch(new DoNotUpdateAppAction());
}
@HostListener('window:keydown.control.o', ['$event'])
@@ -78,19 +78,16 @@ export class MainAppComponent {
private sendUserConfiguration(): void {
this.store
.let(getUserConfiguration())
.first()
.map(userConfiguration => {
const uhkBuffer = new UhkBuffer();
userConfiguration.toBinary(uhkBuffer);
return uhkBuffer.getBufferContent();
})
.switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer))
.do(response => console.log('Sending user configuration finished', response))
.switchMap(() => this.uhkDevice.applyConfig())
.subscribe(
response => console.log('Applying user configuration finished', response),
error => console.error('Error during uploading user configuration', error),
() => console.log('User configuration has been sucessfully uploaded')
buffer => this.store.dispatch(new SaveConfigurationAction(buffer)),
error => console.error('Error during uploading user configuration', error),
() => console.log('User configuration has been successfully uploaded')
);
}
}

View File

@@ -0,0 +1,42 @@
import { ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { addOnRoutes } from './components/add-on';
import { keymapRoutes } from './components/keymap';
import { macroRoutes } from './components/macro';
import { PrivilegeCheckerComponent } from './components/privilege-checker/privilege-checker.component';
import { MissingDeviceComponent } from './components/missing-device/missing-device.component';
import { UhkDeviceDisconnectedGuard } from './services/uhk-device-disconnected.guard';
import { UhkDeviceConnectedGuard } from './services/uhk-device-connected.guard';
import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized.guard';
import { UhkDeviceInitializedGuard } from './services/uhk-device-initialized.guard';
import { MainPage } from './pages/main-page/main.page';
import { settingsRoutes } from './components/settings/settings.routes';
const appRoutes: Routes = [
{
path: 'detection',
component: MissingDeviceComponent,
canActivate: [UhkDeviceConnectedGuard, UhkDeviceUninitializedGuard]
},
{
path: 'privilege',
component: PrivilegeCheckerComponent,
canActivate: [UhkDeviceInitializedGuard]
},
{
path: '',
component: MainPage,
canActivate: [UhkDeviceDisconnectedGuard],
children: [
...keymapRoutes,
...macroRoutes,
...addOnRoutes,
...settingsRoutes
]
}
];
export const appRoutingProviders: any[] = [];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true });

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/pluck';
@Component({
selector: 'add-on',
@@ -17,6 +18,6 @@ export class AddOnComponent {
constructor(route: ActivatedRoute) {
this.name$ = route
.params
.select<string>('name');
.pluck<{}, string>('name');
}
}

View File

@@ -28,9 +28,5 @@
<span *ngIf="checkingForUpdate"
class="fa fa-spinner fa-spin"></span>
</button>
<div>
{{message}}
</div>
</div>
</div>

View File

@@ -10,7 +10,7 @@ import { State } from '../../store/reducers/auto-update-settings';
export class AutoUpdateSettings {
@Input() version: string;
@Input() settings: State;
@Input() settings: State | undefined;
@Input() checkingForUpdate: boolean;
@Output() toggleCheckForUpdateOnStartUp = new EventEmitter<boolean>();

Some files were not shown because too many files have changed in this diff Show More