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

4
packages/test-serializer/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
uhk-config.bin
uhk-config-serialized.json
uhk-config-serialized.bin
test-serializer.js

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

@@ -0,0 +1,45 @@
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('../uhk-web/src/app/config-serializer/user-config.json'));
const config1Js = userConfig;
const config1Ts: UserConfiguration = new UserConfiguration().fromJsonObject(config1Js);
const config1Buffer = new UhkBuffer();
config1Ts.toBinary(config1Buffer);
const config1BufferContent = config1Buffer.getBufferContent();
fs.writeFileSync('user-config.bin', config1BufferContent);
config1Buffer.offset = 0;
console.log();
const config2Ts = new UserConfiguration().fromBinary(config1Buffer);
console.log('\n');
const config2Js = config2Ts.toJsonObject();
const config2Buffer = new UhkBuffer();
config2Ts.toBinary(config2Buffer);
fs.writeFileSync('user-config-serialized.json', JSON.stringify(config2Js, undefined, 4));
const config2BufferContent = config1Buffer.getBufferContent();
fs.writeFileSync('user-config-serialized.bin', config2BufferContent);
console.log('\n');
let returnValue = 0;
try {
assert.deepEqual(config1Js, config2Js);
console.log('JSON configurations are identical.');
} catch (error) {
console.log('JSON configurations differ.');
returnValue = 1;
}
const buffersContentsAreEqual: boolean = Buffer.compare(config1BufferContent, config2BufferContent) === 0;
if (buffersContentsAreEqual) {
console.log('Binary configurations are identical.');
} else {
console.log('Binary configurations differ.');
returnValue += 2;
}
process.exit(returnValue);

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"typeRoots": [
"../node_modules/@types"
],
"types": [
"node"
]
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,28 @@
// var webpack = require("webpack");
module.exports = {
entry: {
main: __dirname + '/test-serializer.ts'
},
target: 'node',
output: {
path: __dirname,
filename: "test-serializer.js"
},
resolve: {
extensions: ['.webpack.js', '.web.js', '.ts', '.js'],
modules: ['node_modules']
},
module: {
rules: [
{ test: /\.ts$/, loader: 'ts-loader', exclude: /node_modules/ }
]
},
plugins: [
// new webpack.optimize.UglifyJsPlugin({ minimize: true }),
],
node: {
fs: "empty"
}
}

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 @@
declare module 'command-line-args';

View File

@@ -0,0 +1 @@
declare module 'electron-is-dev';

View File

@@ -0,0 +1,22 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/*
* Install DevTool extensions when Electron is in development mode
*/
import { app } from 'electron';
import * as isDev from 'electron-is-dev';
if (isDev) {
app.once('ready', () => {
const { default: installExtension, REDUX_DEVTOOLS } = require('electron-devtools-installer');
installExtension(REDUX_DEVTOOLS)
.then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: any) => console.log('An error occurred: ', err));
require('electron-debug')({ showDevTools: true });
});
}

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

@@ -0,0 +1,19 @@
{
"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": {
"node-hid": "0.5.4"
}
}

View File

@@ -0,0 +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';

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,3 @@
export interface CommandLineArgs {
addons: 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,17 @@
import { Action } from '@ngrx/store';
export enum NotificationType {
Default = 'default',
Success = 'success',
Error = 'error',
Warning = 'warning',
Info = 'info',
Undoable = 'undoable'
}
export interface Notification {
type: NotificationType;
title?: string;
message: string;
extra?: Action;
}

View File

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

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
@Injectable()
export class LogService {
error(...args: any[]): void {
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

@@ -0,0 +1,5 @@
export namespace Constants {
export const VENDOR_ID = 0x1D50;
export const PRODUCT_ID = 0x6122;
export const MAX_PAYLOAD_SIZE = 64;
}

View File

@@ -0,0 +1,39 @@
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 {
return camelCasedText.replace(/^[a-z]|[A-Z]/g, function (v, i) {
return i === 0 ? v.toUpperCase() : ' ' + v.toLowerCase();
});
}
export function capitalizeFirstLetter(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1);
}
/**
* This function coerces a string into a string literal type.
* Using tagged union types in TypeScript 2.0, this enables
* powerful typechecking of our reducers.
*
* Since every action label passes through this function it
* is a good place to ensure all of our action labels
* are unique.
*/
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"`);
}
typeCache[<string>label] = true;
return <T>label;
}
export function runInElectron() {
return window && (<any>window).process && (<any>window).process.type;
}

View File

@@ -0,0 +1,31 @@
class App {
public static readonly appStarted = 'app-started';
public static readonly getAppStartInfo = 'app-get-start-info';
public static readonly getAppStartInfoReply = 'app-get-start-info-reply';
}
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';
}
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

@@ -0,0 +1,72 @@
/* GitHub ribbon */
.github-fork-ribbon {
background-color: #a00;
overflow: hidden;
white-space: nowrap;
position: fixed;
right: -50px;
bottom: 40px;
z-index: 2000;
/* stylelint-disable indentation */
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-box-shadow: 0 0 10px #888;
-moz-box-shadow: 0 0 10px #888;
box-shadow: 0 0 10px #888;
/* stylelint-enable indentation */
a {
border: 1px solid #faa;
color: #fff;
display: block;
font: bold 81.25% 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 1px 0;
padding: 10px 50px;
text-align: center;
text-decoration: none;
text-shadow: 0 0 5px #444;
}
}
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

@@ -0,0 +1,93 @@
import { Component, HostListener, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
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: './app.component.html',
styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MainAppComponent {
showUpdateAvailable$: Observable<boolean>;
deviceConnected$: Observable<boolean>;
runningInElectron$: Observable<boolean>;
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'])
onCtrlO(event: KeyboardEvent): void {
console.log('ctrl + o pressed');
event.preventDefault();
event.stopPropagation();
this.sendUserConfiguration();
}
@HostListener('window:keydown.alt.j', ['$event'])
onAltJ(event: KeyboardEvent): void {
event.preventDefault();
event.stopPropagation();
this.store
.let(getUserConfiguration())
.first()
.subscribe(userConfiguration => {
const asString = JSON.stringify(userConfiguration.toJsonObject());
const asBlob = new Blob([asString], { type: 'text/plain' });
saveAs(asBlob, 'UserConfiguration.json');
});
}
@HostListener('window:keydown.alt.b', ['$event'])
onAltB(event: KeyboardEvent): void {
event.preventDefault();
event.stopPropagation();
this.store
.let(getUserConfiguration())
.first()
.map(userConfiguration => {
const uhkBuffer = new UhkBuffer();
userConfiguration.toBinary(uhkBuffer);
return new Blob([uhkBuffer.getBufferContent()]);
})
.subscribe(blob => saveAs(blob, 'UserConfiguration.bin'));
}
private sendUserConfiguration(): void {
this.store
.let(getUserConfiguration())
.first()
.map(userConfiguration => {
const uhkBuffer = new UhkBuffer();
userConfiguration.toBinary(uhkBuffer);
return uhkBuffer.getBufferContent();
})
.subscribe(
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

@@ -0,0 +1,7 @@
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-puzzle-piece"></i>
<span class="macro__name pane-title__name">{{ name$ | async }}</span>
</h1>
</div>
To be done...

View File

@@ -0,0 +1,5 @@
:host {
width: 100%;
height: 100%;
display: block;
}

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/pluck';
@Component({
selector: 'add-on',
templateUrl: './add-on.component.html',
styleUrls: ['./add-on.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class AddOnComponent {
name$: Observable<string>;
constructor(route: ActivatedRoute) {
this.name$ = route
.params
.pluck<{}, string>('name');
}
}

View File

@@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { AddOnComponent } from './add-on.component';
export const addOnRoutes: Routes = [
{
path: 'add-on/:name',
component: AddOnComponent
}
];

View File

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

View File

@@ -0,0 +1,32 @@
<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>
</div>

View File

@@ -0,0 +1,34 @@
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 | undefined;
@Input() checkingForUpdate: boolean;
@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

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

View File

@@ -0,0 +1 @@
export { KeyboardSliderComponent } from './keyboard-slider.component';

View File

@@ -0,0 +1,13 @@
<svg-keyboard *ngFor="let layer of layers; let index = index; trackBy: trackKeyboard"
[@layerState]="layerAnimationState[index]"
[moduleConfig]="layer.modules"
[keybindAnimationEnabled]="keybindAnimationEnabled"
[halvesSplit]="halvesSplit"
[capturingEnabled]="capturingEnabled"
[selectedKey]="selectedKey"
[selected]="selectedKey?.layerId === index"
(keyClick)="keyClick.emit($event)"
(keyHover)="keyHover.emit($event)"
(capture)="capture.emit($event)"
>
</svg-keyboard>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1,8 @@
svg-keyboard {
width: 95%;
max-width: 1400px;
position: absolute;
left: 0;
transform: translateX(-101%);
user-select: none;
}

View File

@@ -0,0 +1,105 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
import { Layer } from '../../../config-serializer/config-items/layer';
type AnimationKeyboard =
'leftIn' |
'leftOut' |
'rightIn' |
'rightOut';
@Component({
selector: 'keyboard-slider',
templateUrl: './keyboard-slider.component.html',
styleUrls: ['./keyboard-slider.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
// We use 101%, because there was still a trace of the keyboard in the screen when animation was done
animations: [
trigger('layerState', [
state('leftIn, rightIn', style({
transform: 'translateX(-50%)',
left: '50%'
})),
state('leftOut', style({
transform: 'translateX(-101%)',
left: '0'
})),
state('rightOut', style({
transform: 'translateX(0)',
left: '101%'
})),
transition('leftOut => leftIn, rightOut => leftIn', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(0%)', left: '101%', offset: 0 }),
style({ transform: 'translateX(-50%)', left: '50%', offset: 1 })
]))
]),
transition('leftIn => leftOut, rightIn => leftOut', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-50%)', left: '50%', offset: 0 }),
style({ transform: 'translateX(-101%)', left: '0%', offset: 1 })
]))
]),
transition('* => rightIn', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-101%)', left: '0%', offset: 0 }),
style({ transform: 'translateX(-50%)', left: '50%', offset: 1 })
]))
]),
transition('* => rightOut', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-50%)', left: '50%', offset: 0 }),
style({ transform: 'translateX(0%)', left: '101%', offset: 1 })
]))
]),
transition(':leave', [
animate('2000ms ease-out', keyframes([
style({ opacity: 1, offset: 0 }),
style({ opacity: 0, offset: 1 })
]))
])
])
]
})
export class KeyboardSliderComponent implements OnChanges {
@Input() layers: Layer[];
@Input() currentLayer: number;
@Input() keybindAnimationEnabled: boolean;
@Input() capturingEnabled: boolean;
@Input() halvesSplit: boolean;
@Input() selectedKey: { layerId: number, moduleId: number, keyId: number };
@Output() keyClick = new EventEmitter();
@Output() keyHover = new EventEmitter();
@Output() capture = new EventEmitter();
layerAnimationState: AnimationKeyboard[];
ngOnChanges(changes: SimpleChanges) {
if (changes['layers']) {
this.layerAnimationState = this.layers.map<AnimationKeyboard>(() => 'leftOut');
this.layerAnimationState[this.currentLayer] = 'leftIn';
}
const layerChange = changes['currentLayer'];
if (layerChange) {
const prevValue = layerChange.isFirstChange() ? layerChange.currentValue : layerChange.previousValue;
this.onLayerChange(prevValue, layerChange.currentValue);
}
}
trackKeyboard(index: number) {
return index;
}
onLayerChange(oldIndex: number, index: number): void {
if (index > oldIndex) {
this.layerAnimationState[oldIndex] = 'leftOut';
this.layerAnimationState[index] = 'leftIn';
} else {
this.layerAnimationState[oldIndex] = 'rightOut';
this.layerAnimationState[index] = 'rightIn';
}
}
}

View File

@@ -0,0 +1,35 @@
<h1>
<i class="fa fa-keyboard-o"></i>
<span>Add new keymap</span>
</h1>
<div class="keymap__search clearfix">
<div class="input-group">
<span class="input-group-addon" id="sizing-addon1">
<i class="fa fa-search"></i>
</span>
<input type="text" class="form-control" placeholder="Search ..." (input)="filterKeyboards($event.target.value)">
</div>
<div class="keymap__search_amount">
{{ (presets$ | async).length }} / {{ (presetsAll$ | async).length }} keymaps shown
</div>
</div>
<div class="keymap__list">
<div #keyboard class="keymap__list_item" *ngFor="let keymap of presets$ | async">
<h2>{{ keymap.name }}</h2>
<p class="keymap__description">
{{ keymap.description }}
</p>
<svg-keyboard-wrap
[keymap]="keymap"
[popoverEnabled]="false"
[tooltipEnabled]="true"
>
</svg-keyboard-wrap>
<div class="btn-group btn-group-lg">
<button class="btn btn-default" (click)="addKeymap(keymap)">Add keymap</button>
</div>
</div>
</div>
<div *ngIf="(presets$ | async).length === 0">
Sorry, no keyboard found under this search query.
</div>

View File

@@ -0,0 +1,61 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
}
.uhk__layer-switcher--wrapper {
position: relative;
&:before {
content: attr(data-title);
display: inline-block;
position: absolute;
bottom: -0.3em;
right: 100%;
font-size: 2.4rem;
padding-right: 0.25em;
margin: 0;
}
}
.keymap {
&__search {
margin-top: 10px;
.input-group {
width: 100%;
max-width: 350px;
float: left;
}
&_amount {
float: left;
margin: 7px 0 0 20px;
}
}
&__description {
margin-bottom: 20px;
}
&__list {
margin-top: 40px;
&_item {
margin-bottom: 50px;
}
.btn-group-lg {
margin: 30px 0 0;
width: 100%;
text-align: center;
.btn {
float: none;
padding-left: 50px;
padding-right: 50px;
}
}
}
}

View File

@@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/publishReplay';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { KeymapActions } from '../../../store/actions';
@Component({
selector: 'keymap-add',
templateUrl: './keymap-add.component.html',
styleUrls: ['./keymap-add.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class KeymapAddComponent {
presets$: Observable<Keymap[]>;
presetsAll$: Observable<Keymap[]>;
private filterExpression$: BehaviorSubject<string>;
constructor(private store: Store<AppState>) {
this.presetsAll$ = store.select((appState: AppState) => appState.presetKeymaps);
this.filterExpression$ = new BehaviorSubject('');
this.presets$ = this.presetsAll$
.combineLatest(this.filterExpression$, (keymaps: Keymap[], filterExpression: string) => {
return keymaps.filter((keymap: Keymap) => keymap.name.toLocaleLowerCase().includes(filterExpression));
})
.publishReplay(1)
.refCount();
}
filterKeyboards(filterExpression: string) {
this.filterExpression$.next(filterExpression);
}
addKeymap(keymap: Keymap) {
this.store.dispatch(KeymapActions.addKeymap(keymap));
}
}

View File

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

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/switchMap';
import { Store } from '@ngrx/store';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store/index';
import { getKeymaps } from '../../../store/reducers/user-configuration';
@Injectable()
export class KeymapEditGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(): Observable<boolean> {
return this.store
.let(getKeymaps())
.do((keymaps: Keymap[]) => {
const defaultKeymap = keymaps.find(keymap => keymap.isDefault);
if (defaultKeymap) {
this.router.navigate(['/keymap', defaultKeymap.abbreviation]);
}
})
.switchMap(() => Observable.of(false));
}
}

View File

@@ -0,0 +1,8 @@
<ng-template [ngIf]="keymap$ | async">
<keymap-header [keymap]="keymap$ | async" [deletable]="deletable$ | async" (downloadClick)="downloadKeymap()"></keymap-header>
<svg-keyboard-wrap [keymap]="keymap$ | async" [halvesSplit]="keyboardSplit"></svg-keyboard-wrap>
</ng-template>
<div *ngIf="!(keymap$ | async)" class="not-found">
Sorry, there is no keymap with this abbreviation.
</div>

View File

@@ -0,0 +1,11 @@
:host {
width: 100%;
display: block;
}
.not-found {
margin-top: 30px;
font-size: 16px;
text-align: center;
}

View File

@@ -0,0 +1,90 @@
import { Component, HostListener, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import '@ngrx/core/add/operator/select';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishReplay';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/pluck';
import 'rxjs/add/operator/combineLatest';
import { saveAs } from 'file-saver';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { getKeymap, getKeymaps, getUserConfiguration } from '../../../store/reducers/user-configuration';
import 'rxjs/add/operator/pluck';
import { SvgKeyboardWrapComponent } from '../../svg/wrap/svg-keyboard-wrap.component';
@Component({
selector: 'keymap-edit',
templateUrl: './keymap-edit.component.html',
styleUrls: ['./keymap-edit.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class KeymapEditComponent {
@ViewChild(SvgKeyboardWrapComponent) wrap: SvgKeyboardWrapComponent;
keyboardSplit: boolean;
deletable$: Observable<boolean>;
protected keymap$: Observable<Keymap>;
constructor(protected store: Store<AppState>,
route: ActivatedRoute) {
this.keymap$ = route
.params
.pluck<{}, string>('abbr')
.switchMap((abbr: string) => store.let(getKeymap(abbr)))
.publishReplay(1)
.refCount();
this.deletable$ = store.let(getKeymaps())
.map((keymaps: Keymap[]) => keymaps.length > 1);
}
downloadKeymap() {
const exportableJSON$: Observable<string> = this.keymap$
.switchMap(keymap => this.toExportableJSON(keymap))
.map(exportableJSON => JSON.stringify(exportableJSON));
this.keymap$
.combineLatest(exportableJSON$)
.first()
.subscribe(latest => {
const keymap = latest[0];
const exportableJSON = latest[1];
const fileName = keymap.name + '_keymap.json';
saveAs(new Blob([exportableJSON], { type: 'application/json' }), fileName);
});
}
@HostListener('window:keydown.alt.s', ['$event'])
toggleKeyboardSplit() {
this.keyboardSplit = !this.keyboardSplit;
}
private toExportableJSON(keymap: Keymap): Observable<any> {
return this.store
.let(getUserConfiguration())
.first()
.map(userConfiguration => {
return {
site: 'https://ultimatehackingkeyboard.com',
description: 'Ultimate Hacking Keyboard keymap',
keyboardModel: 'UHK60',
dataModelVersion: userConfiguration.dataModelVersion,
objectType: 'keymap',
objectValue: keymap.toJsonObject()
};
});
}
}

View File

@@ -0,0 +1,47 @@
<uhk-header>
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-keyboard-o"></i>
<input #name cancelable
class="keymap__name pane-title__name"
type="text"
(change)="editKeymapName($event.target.value)"
(keyup.enter)="name.blur()"
/> keymap
(<input #abbr cancelable
class="keymap__abbrev pane-title__abbrev"
type="text"
(change)="editKeymapAbbr($event.target.value)"
(keyup.enter)="abbr.blur()"
[attr.maxLength]="3"
/>)
<i class="fa keymap__is-default"
[ngClass]="{'fa-star-o': !keymap.isDefault, 'fa-star': keymap.isDefault}"
data-toggle="tooltip"
data-placement="bottom"
[title]="starTitle"
(click)="setDefault()"
></i>
<i class="glyphicon glyphicon-trash keymap__remove pull-right"
[title]="trashTitle"
[class.disabled]="!deletable"
data-toggle="tooltip"
data-placement="bottom"
html="true"
(click)="removeKeymap()"
></i>
<i class="fa fa-files-o keymap__duplicate pull-right"
title="Duplicate keymap"
data-toggle="tooltip"
data-placement="bottom"
(click)="duplicateKeymap()"
></i>
<i class="fa fa-download keymap__download pull-right"
title="Download keymap"
[html]="true"
data-toggle="tooltip"
data-placement="bottom"
(click)="onDownloadIconClick()"></i>
</h1>
</div>
</uhk-header>

View File

@@ -0,0 +1,83 @@
@import '../../../../styles/variables';
:host {
display: block;
}
.keymap {
&__is-default {
&.fa-star-o {
cursor: pointer;
&:hover {
color: $icon-hover;
}
}
}
&__remove {
font-size: 0.75em;
top: 8px;
&:not(.disabled):hover {
cursor: pointer;
color: $icon-hover-delete;
}
&.disabled {
opacity: 0.25;
}
}
&__duplicate {
font-size: 0.75em;
top: 7px;
margin-right: 15px;
position: relative;
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
}
.keymap__download {
top: 10px;
font-size: 0.8em;
position: relative;
margin-right: 10px;
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
.pane-title {
margin-bottom: 1em;
&__name,
&__abbrev {
border: none;
border-bottom: 2px dotted #999;
padding: 0;
margin: 0 0.25rem;
&:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent;
}
}
&__name {
width: 290px;
text-overflow: ellipsis;
}
&__abbrev {
width: 90px;
text-align: center;
}
}

View File

@@ -0,0 +1,114 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
Output,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { KeymapActions } from '../../../store/actions';
const DEFAULT_TRASH_TITLE = '<span class="text-nowrap">Delete keymap</span>';
@Component({
selector: 'keymap-header',
templateUrl: './keymap-header.component.html',
styleUrls: ['./keymap-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class KeymapHeaderComponent implements OnChanges {
@Input() keymap: Keymap;
@Input() deletable: boolean;
@Output() downloadClick = new EventEmitter<void>();
@ViewChild('name') keymapName: ElementRef;
@ViewChild('abbr') keymapAbbr: ElementRef;
starTitle: string;
trashTitle: string = DEFAULT_TRASH_TITLE;
constructor(private store: Store<AppState>, private renderer: Renderer2) { }
ngOnChanges(changes: SimpleChanges) {
if (changes['keymap']) {
this.setKeymapTitle();
this.setName();
this.setAbbreviation();
}
if (changes['deletable']) {
this.setTrashTitle();
}
}
setDefault() {
if (!this.keymap.isDefault) {
this.store.dispatch(KeymapActions.setDefault(this.keymap.abbreviation));
}
}
removeKeymap() {
if (this.deletable) {
this.store.dispatch(KeymapActions.removeKeymap(this.keymap.abbreviation));
}
}
duplicateKeymap() {
this.store.dispatch(KeymapActions.duplicateKeymap(this.keymap));
}
editKeymapName(name: string) {
if (name.length === 0) {
this.setName();
return;
}
this.store.dispatch(KeymapActions.editKeymapName(this.keymap.abbreviation, name));
}
editKeymapAbbr(newAbbr: string) {
const regexp = new RegExp(/^[a-zA-Z\d]+$/g);
if (newAbbr.length < 1 || newAbbr.length > 3 || !regexp.test(newAbbr)) {
this.setAbbreviation();
return;
}
newAbbr = newAbbr.toUpperCase();
this.store.dispatch(KeymapActions.editKeymapAbbr(this.keymap.name, this.keymap.abbreviation, newAbbr));
}
setKeymapTitle(): void {
this.starTitle = this.keymap.isDefault
? 'This is the default keymap which gets activated when powering the keyboard.'
: 'Makes this keymap the default keymap which gets activated when powering the keyboard.';
}
setTrashTitle(): void {
this.trashTitle = this.deletable
? DEFAULT_TRASH_TITLE
: '<span class="text-nowrap">The last keymap cannot be deleted.</span>';
}
onDownloadIconClick(): void {
this.downloadClick.emit();
}
private setName(): void {
this.renderer.setProperty(this.keymapName.nativeElement, 'value', this.keymap.name);
}
private setAbbreviation() {
this.renderer.setProperty(this.keymapAbbr.nativeElement, 'value', this.keymap.abbreviation);
}
}

View File

@@ -0,0 +1,4 @@
export * from './add/keymap-add.component';
export * from './edit/keymap-edit.component';
export * from './header/keymap-header.component';
export * from './keymap.routes';

View File

@@ -0,0 +1,26 @@
import { Routes } from '@angular/router';
import { KeymapAddComponent } from './add/keymap-add.component';
import { KeymapEditComponent } from './edit';
import { KeymapEditGuard } from './edit';
export const keymapRoutes: Routes = [
{
path: '',
redirectTo: 'keymap',
pathMatch: 'full'
},
{
path: 'keymap',
component: KeymapEditComponent,
canActivate: [KeymapEditGuard]
},
{
path: 'keymap/add',
component: KeymapAddComponent
},
{
path: 'keymap/:abbr',
component: KeymapEditComponent
}
];

View File

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

View File

@@ -0,0 +1,8 @@
<div class="text-center">
<span role="group" class="uhk__layer-switcher--wrapper btn-group btn-group-lg" data-title="Layers: ">
<button type="button" class="btn btn-default" *ngFor="let button of buttons; let index = index" (click)="selectLayer(index)"
[class.btn-primary]="index === current">
{{ button }}
</button>
</span>
</div>

View File

@@ -0,0 +1,36 @@
:host {
display: block;
&.disabled {
button {
cursor: no-drop;
background: rgba(#ccc, 0.43);
pointer-events: none;
&.btn-primary {
background: #7c7c7c;
border-color: #7c7c7c;
}
}
}
}
.uhk {
&__layer-switcher {
&--wrapper {
position: relative;
margin-bottom: 2rem;
&:before {
content: attr(data-title);
display: inline-block;
position: absolute;
bottom: 0.55em;
right: 100%;
font-size: 18px;
padding-right: 0.45em;
margin: 0;
}
}
}
}

View File

@@ -0,0 +1,31 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'layers',
templateUrl: './layers.component.html',
styleUrls: ['./layers.component.scss']
})
export class LayersComponent {
@Input() current: number;
@Output() select = new EventEmitter();
buttons: string[];
constructor() {
this.buttons = ['Base', 'Mod', 'Fn', 'Mouse'];
this.current = 0;
}
selectLayer(index: number) {
if (this.current === index) {
return;
}
this.select.emit({
oldIndex: this.current,
index: index
});
this.current = index;
}
}

View File

@@ -0,0 +1 @@
export { MacroActionEditorComponent } from './macro-action-editor.component';

View File

@@ -0,0 +1,46 @@
<div class="action--editor">
<div class="row">
<div class="col-lg-3 editor__tab-links">
<ul class="nav nav-pills nav-stacked">
<li #macroText [class.active]="activeTab === TabName.Text" (click)="selectTab(TabName.Text)">
<a>
<i class="fa fa-font"></i>
<span>Type text</span>
</a>
</li>
<li #macroKeypress [class.active]="activeTab === TabName.Keypress" (click)="selectTab(TabName.Keypress)">
<a>
<i class="fa fa-keyboard-o"></i>
<span>Key action</span>
</a>
</li>
<li #macroMouse [class.active]="activeTab === TabName.Mouse" (click)="selectTab(TabName.Mouse)">
<a>
<i class="fa fa-mouse-pointer"></i>
<span>Mouse action</span>
</a>
</li>
<li #macroDelay [class.active]="activeTab === TabName.Delay" (click)="selectTab(TabName.Delay)">
<a>
<i class="fa fa-clock-o"></i>
<span>Delay</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-12 col-lg-9 editor__tabs" [ngSwitch]="activeTab">
<macro-text-tab #tab *ngSwitchCase="TabName.Text" [macroAction]="editableMacroAction"></macro-text-tab>
<macro-key-tab #tab *ngSwitchCase="TabName.Keypress" [macroAction]="editableMacroAction"></macro-key-tab>
<macro-mouse-tab #tab *ngSwitchCase="TabName.Mouse" [macroAction]="editableMacroAction"></macro-mouse-tab>
<macro-delay-tab #tab *ngSwitchCase="TabName.Delay" [macroAction]="editableMacroAction"></macro-delay-tab>
</div>
</div>
<div class="row">
<div class="col-xs-12 flex-button-wrapper editor__actions-container">
<div class="editor__actions">
<button class="btn btn-sm btn-default flex-button" type="button" (click)="onCancelClick()"> Cancel </button>
<button class="btn btn-sm btn-primary flex-button" type="button" (click)="onSaveClick()"> Save </button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,88 @@
@import '../../../../styles/variables';
:host {
display: block;
width: 100%;
}
.action--editor {
padding-top: 0;
padding-bottom: 0;
border-radius: 0;
border: 0;
}
.nav {
padding-bottom: 1rem;
li {
a {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
&.selected {
font-style: italic;
}
&:hover {
cursor: pointer;
}
}
&.active {
z-index: 2;
a {
&.selected {
font-style: normal;
}
&:after {
content: '';
display: block;
position: absolute;
width: 0;
height: 0;
top: 0;
right: -4rem;
border-color: transparent transparent transparent $icon-hover;
border-style: solid;
border-width: 2rem;
}
}
}
}
}
.editor {
&__tabs,
&__tab-links {
padding-top: 1rem;
}
&__tabs {
border-left: 1px solid #ddd;
margin-left: -1.6rem;
padding-left: 3rem;
}
&__actions {
float: right;
&-container {
background: #f5f5f5;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 1rem 1.5rem;
}
}
}
.flex-button-wrapper {
display: flex;
flex-direction: row-reverse;
}
.flex-button {
align-self: flex-end;
}

View File

@@ -0,0 +1,98 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import {
MacroAction,
DelayMacroAction,
KeyMacroAction,
ScrollMouseMacroAction,
MoveMouseMacroAction,
MouseButtonMacroAction,
TextMacroAction,
Helper as MacroActionHelper
} from '../../../config-serializer/config-items/macro-action';
import { MacroDelayTabComponent, MacroMouseTabComponent, MacroKeyTabComponent, MacroTextTabComponent } from './tab';
enum TabName {
Keypress,
Text,
Mouse,
Delay
}
@Component({
selector: 'macro-action-editor',
templateUrl: './macro-action-editor.component.html',
styleUrls: ['./macro-action-editor.component.scss'],
host: { 'class': 'macro-action-editor' }
})
export class MacroActionEditorComponent implements OnInit {
@Input() macroAction: MacroAction;
@Output() save = new EventEmitter<MacroAction>();
@Output() cancel = new EventEmitter<void>();
@ViewChild('tab') selectedTab: MacroTextTabComponent | MacroKeyTabComponent | MacroMouseTabComponent | MacroDelayTabComponent;
editableMacroAction: MacroAction;
activeTab: TabName;
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
TabName = TabName;
/* tslint:enable:variable-name */
ngOnInit() {
this.updateEditableMacroAction();
const tab: TabName = this.getTabName(this.editableMacroAction);
this.activeTab = tab;
}
ngOnChanges() {
this.ngOnInit();
}
onCancelClick(): void {
this.cancel.emit();
}
onSaveClick(): void {
try {
// TODO: Refactor after getKeyMacroAction has been added to all tabs
const action = this.selectedTab instanceof MacroKeyTabComponent ?
this.selectedTab.getKeyMacroAction() :
this.selectedTab.macroAction;
this.save.emit(action);
} catch (e) {
// TODO: show error dialog
console.error(e);
}
}
selectTab(tab: TabName): void {
this.activeTab = tab;
if (tab === this.getTabName(this.macroAction)) {
this.updateEditableMacroAction();
} else {
this.editableMacroAction = undefined;
}
}
getTabName(action: MacroAction): TabName {
if (action instanceof DelayMacroAction) {
return TabName.Delay;
} else if (action instanceof TextMacroAction) {
return TabName.Text;
} else if (action instanceof KeyMacroAction) {
return TabName.Keypress;
} else if (action instanceof MouseButtonMacroAction ||
action instanceof MoveMouseMacroAction ||
action instanceof ScrollMouseMacroAction) {
return TabName.Mouse;
}
return undefined;
}
private updateEditableMacroAction() {
const macroAction: MacroAction = this.macroAction ? this.macroAction : new TextMacroAction();
this.editableMacroAction = MacroActionHelper.createMacroAction(macroAction);
}
}

View File

@@ -0,0 +1 @@
export { MacroDelayTabComponent } from './macro-delay.component';

View File

@@ -0,0 +1,26 @@
<div class="macro-delay">
<div class="row">
<div class="col-xs-12">
<h4>Enter delay in seconds</h4>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<input #macroDelayInput
type="number"
min="0"
max="1000"
step="0.1"
placeholder="Delay amount"
class="form-control"
[attr.value]="delay"
(change)="setDelay($event)">
</div>
</div>
<div class="row macro-delay__presets">
<div class="col-xs-12">
<h6>Choose a preset</h6>
<button *ngFor="let delay of presets" class="btn btn-sm btn-default" (click)="setDelay(delay)">{{delay}}s</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
:host {
display: flex;
flex-direction: column;
position: relative;
}
.macro-delay {
&__presets {
margin-top: 1rem;
button {
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
}
}

View File

@@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnInit,
ViewChild
} from '@angular/core';
import { DelayMacroAction } from '../../../../../config-serializer/config-items/macro-action';
const INITIAL_DELAY = 0.5; // In seconds
@Component({
selector: 'macro-delay-tab',
templateUrl: './macro-delay.component.html',
styleUrls: ['./macro-delay.component.scss'],
host: { 'class': 'macro__delay' },
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MacroDelayTabComponent implements OnInit {
@Input() macroAction: DelayMacroAction;
@ViewChild('macroDelayInput') input: ElementRef;
delay: number;
presets: number[] = [0.3, 0.5, 0.8, 1, 2, 3, 4, 5];
constructor() { }
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new DelayMacroAction();
}
this.delay = this.macroAction.delay > 0 ? this.macroAction.delay / 1000 : INITIAL_DELAY;
}
setDelay(value: number): void {
this.delay = value;
this.macroAction.delay = this.delay * 1000;
}
}

View File

@@ -0,0 +1,4 @@
export { MacroDelayTabComponent } from './delay';
export { MacroKeyTabComponent } from './key';
export { MacroMouseTabComponent } from './mouse';
export { MacroTextTabComponent } from './text';

View File

@@ -0,0 +1 @@
export { MacroKeyTabComponent } from './macro-key.component';

View File

@@ -0,0 +1,32 @@
<div class="col-xs-12 macro-key__container">
<div class="col-xs-3 macro-key__types">
<ul class="nav nav-pills nav-stacked">
<li #keyMove [class.active]="activeTab === TabName.Keypress" (click)="selectTab(TabName.Keypress)">
<a>
<i class="fa fa-hand-pointer-o"></i>
<span>Press key</span>
</a>
</li>
<li #keyHold [class.active]="activeTab === TabName.Hold" (click)="selectTab(TabName.Hold)">
<a>
<i class="fa fa-hand-rock-o"></i>
<span>Hold key</span>
</a>
</li>
<li #keyRelease [class.active]="activeTab === TabName.Release" (click)="selectTab(TabName.Release)">
<a>
<i class="fa fa-hand-paper-o"></i>
<span>Release key</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-9 macro-key__action-container">
<div class="macro-key__action">
<h4 *ngIf="activeTab === TabName.Keypress">Press key</h4>
<h4 *ngIf="activeTab === TabName.Hold">Hold key</h4>
<h4 *ngIf="activeTab === TabName.Release">Release key</h4>
<keypress-tab #keypressTab [defaultKeyAction]="defaultKeyAction" [longPressEnabled]="false"></keypress-tab>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
.macro-key {
&__container {
padding: 0;
}
&__types {
margin-left: 0;
padding: 0 0 1rem;
}
&__action {
&-container {
margin-top: -1rem;
padding-top: 1rem;
border-left: 1px solid #ddd;
}
padding-left: 3rem;
padding-bottom: 1rem;
}
}
.fa {
min-width: 14px;
}

View File

@@ -0,0 +1,74 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { KeystrokeAction } from '../../../../../config-serializer/config-items/key-action';
import { KeyMacroAction, MacroSubAction } from '../../../../../config-serializer/config-items/macro-action';
import { KeypressTabComponent, Tab } from '../../../../popover/tab';
enum TabName {
Keypress,
Hold,
Release
}
@Component({
selector: 'macro-key-tab',
templateUrl: './macro-key.component.html',
styleUrls: [
'../../macro-action-editor.component.scss',
'./macro-key.component.scss'
],
host: { 'class': 'macro__mouse' }
})
export class MacroKeyTabComponent implements OnInit {
@Input() macroAction: KeyMacroAction;
@ViewChild('tab') selectedTab: Tab;
@ViewChild('keypressTab') keypressTab: KeypressTabComponent;
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
TabName = TabName;
/* tslint:enable:variable-name */
activeTab: TabName;
defaultKeyAction: KeystrokeAction;
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new KeyMacroAction();
}
this.defaultKeyAction = new KeystrokeAction(<any>this.macroAction);
this.selectTab(this.getTabName(this.macroAction));
}
selectTab(tab: TabName): void {
this.activeTab = tab;
}
getTabName(macroAction: KeyMacroAction): TabName {
if (!macroAction.action) {
return TabName.Keypress;
} else if (macroAction.action === MacroSubAction.hold) {
return TabName.Hold;
} else if (macroAction.action === MacroSubAction.release) {
return TabName.Release;
}
}
getActionType(tab: TabName): MacroSubAction {
switch (tab) {
case TabName.Keypress:
return MacroSubAction.press;
case TabName.Hold:
return MacroSubAction.hold;
case TabName.Release:
return MacroSubAction.release;
default:
throw new Error('Invalid tab type');
}
}
getKeyMacroAction(): KeyMacroAction {
const keyMacroAction = Object.assign(new KeyMacroAction(), this.keypressTab.toKeyAction());
keyMacroAction.action = this.getActionType(this.activeTab);
return keyMacroAction;
}
}

View File

@@ -0,0 +1 @@
export { MacroMouseTabComponent } from './macro-mouse.component';

View File

@@ -0,0 +1,77 @@
<div class="col-xs-12 macro-mouse__container">
<div class="col-xs-3 macro-mouse__types">
<ul class="nav nav-pills nav-stacked">
<li #mouseMove [class.active]="activeTab === TabName.Move" (click)="selectTab(TabName.Move)">
<a>
<i class="fa fa-arrows"></i>
<span>Move pointer</span>
</a>
</li>
<li #mouseScroll [class.active]="activeTab === TabName.Scroll" (click)="selectTab(TabName.Scroll)">
<a>
<i class="fa fa-arrows-v"></i>
<span>Scroll</span>
</a>
</li>
<li #mouseClick [class.active]="activeTab === TabName.Click" (click)="selectTab(TabName.Click)">
<a>
<i class="fa fa-mouse-pointer"></i>
<span>Click button</span>
</a>
</li>
<li #mouseHold [class.active]="activeTab === TabName.Hold" (click)="selectTab(TabName.Hold)">
<a>
<i class="fa fa-hand-rock-o"></i>
<span>Hold button</span>
</a>
</li>
<li #mouseRelease [class.active]="activeTab === TabName.Release" (click)="selectTab(TabName.Release)">
<a>
<i class="fa fa-hand-paper-o"></i>
<span>Release button</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-9 macro-mouse__actions" [ngSwitch]="activeTab">
<div #tab *ngSwitchCase="TabName.Move">
<h4>Move pointer</h4>
<p>Use negative values to move down or left from current position.</p>
<div class="form-horizontal">
<div class="form-group">
<label for="move-mouse-x">X</label>
<input id="move-mouse-x" type="number" class="form-control" [(ngModel)]="macroAction['x']"> pixels
</div>
<div class="form-group">
<label for="move-mouse-y">Y</label>
<input id="move-mouse-y" type="number" class="form-control" [(ngModel)]="macroAction['y']"> pixels
</div>
</div>
</div>
<div #tab *ngSwitchCase="TabName.Scroll">
<h4>Scroll</h4>
<p>Use negative values to move down or left from current position.</p>
<div class="form-horizontal">
<div class="form-group">
<label for="scroll-mouse-x">X</label>
<input id="scroll-mouse-x" type="number" class="form-control" [(ngModel)]="macroAction['x']"> pixels
</div>
<div class="form-group">
<label for="scroll-mouse-y">Y</label>
<input id="scroll-mouse-y" type="number" class="form-control" [(ngModel)]="macroAction['y']"> pixels
</div>
</div>
</div>
<div #tab *ngIf="activeTab === TabName.Click || activeTab === TabName.Hold || activeTab === TabName.Release">
<h4 *ngIf="activeTab === TabName.Click">Click mouse button</h4>
<h4 *ngIf="activeTab === TabName.Hold">Hold mouse button</h4>
<h4 *ngIf="activeTab === TabName.Release">Release mouse button</h4>
<div class="btn-group macro-mouse__buttons">
<button *ngFor="let buttonLabel of buttonLabels; let buttonIndex = index"
class="btn btn-default"
[class.btn-primary]="hasButton(buttonIndex)"
(click)="setMouseClick(buttonIndex)">{{buttonLabel}}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,43 @@
.macro-mouse {
&__container {
padding: 0;
}
&__types {
border-right: 1px solid #ddd;
border-left: 0;
margin-top: -1rem;
margin-left: 0;
padding: 1rem 0;
}
&__actions {
padding-left: 3rem;
padding-bottom: 1rem;
}
&__buttons {
margin-top: 3rem;
margin-bottom: 1rem;
}
}
.fa {
min-width: 14px;
}
.form-horizontal {
.form-group {
margin: 0 0 0.5rem;
}
label {
display: inline-block;
margin-right: 0.5rem;
}
.form-control {
display: inline-block;
width: 60%;
}
}

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