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:
committed by
László Monda
parent
97770f67c0
commit
0f558e4132
4
packages/test-serializer/.gitignore
vendored
Normal file
4
packages/test-serializer/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
uhk-config.bin
|
||||
uhk-config-serialized.json
|
||||
uhk-config-serialized.bin
|
||||
test-serializer.js
|
||||
25
packages/test-serializer/package.json
Normal file
25
packages/test-serializer/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
45
packages/test-serializer/test-serializer.ts
Normal file
45
packages/test-serializer/test-serializer.ts
Normal 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);
|
||||
14
packages/test-serializer/tsconfig.json
Normal file
14
packages/test-serializer/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
packages/test-serializer/user-config-serialized.bin
Normal file
BIN
packages/test-serializer/user-config-serialized.bin
Normal file
Binary file not shown.
1550
packages/test-serializer/user-config-serialized.json
Normal file
1550
packages/test-serializer/user-config-serialized.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/test-serializer/user-config.bin
Normal file
BIN
packages/test-serializer/user-config.bin
Normal file
Binary file not shown.
28
packages/test-serializer/webpack.config.js
Normal file
28
packages/test-serializer/webpack.config.js
Normal 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
2604
packages/uhk-agent/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
packages/uhk-agent/package.json
Normal file
36
packages/uhk-agent/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/uhk-agent/src/custom_types/command-line-args.d.ts
vendored
Normal file
1
packages/uhk-agent/src/custom_types/command-line-args.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'command-line-args';
|
||||
1
packages/uhk-agent/src/custom_types/electron-is-dev.d.ts
vendored
Normal file
1
packages/uhk-agent/src/custom_types/electron-is-dev.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'electron-is-dev';
|
||||
22
packages/uhk-agent/src/dev-extension.ts
Normal file
22
packages/uhk-agent/src/dev-extension.ts
Normal 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 });
|
||||
});
|
||||
|
||||
}
|
||||
117
packages/uhk-agent/src/electron-main.ts
Normal file
117
packages/uhk-agent/src/electron-main.ts
Normal 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
|
||||
6
packages/uhk-agent/src/manifest.json
Normal file
6
packages/uhk-agent/src/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "UHK-Agent",
|
||||
"version": "1.0",
|
||||
"devtools_page": "foo.html",
|
||||
"default_locale": "en"
|
||||
}
|
||||
19
packages/uhk-agent/src/package.json
Normal file
19
packages/uhk-agent/src/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/uhk-agent/src/polyfills.ts
Normal file
5
packages/uhk-agent/src/polyfills.ts
Normal 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';
|
||||
110
packages/uhk-agent/src/services/app-update.service.ts
Normal file
110
packages/uhk-agent/src/services/app-update.service.ts
Normal 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 };
|
||||
}
|
||||
|
||||
}
|
||||
28
packages/uhk-agent/src/services/app.service.ts
Normal file
28
packages/uhk-agent/src/services/app.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
167
packages/uhk-agent/src/services/device.service.ts
Normal file
167
packages/uhk-agent/src/services/device.service.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
4
packages/uhk-agent/src/services/logger.service.ts
Normal file
4
packages/uhk-agent/src/services/logger.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as log from 'electron-log';
|
||||
log.transports.file.level = 'debug';
|
||||
|
||||
export const logger = log;
|
||||
16
packages/uhk-agent/src/services/main-service-base.ts
Normal file
16
packages/uhk-agent/src/services/main-service-base.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
57
packages/uhk-agent/src/services/sudo.service.ts
Normal file
57
packages/uhk-agent/src/services/sudo.service.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
1
packages/uhk-agent/src/vendor.ts
Normal file
1
packages/uhk-agent/src/vendor.ts
Normal file
@@ -0,0 +1 @@
|
||||
import 'sudo-prompt';
|
||||
21
packages/uhk-agent/tsconfig.json
Normal file
21
packages/uhk-agent/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
45
packages/uhk-agent/webpack.config.js
Normal file
45
packages/uhk-agent/webpack.config.js
Normal 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
|
||||
}
|
||||
|
||||
};
|
||||
3
packages/uhk-common/index.ts
Normal file
3
packages/uhk-common/index.ts
Normal 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
29
packages/uhk-common/package-lock.json
generated
Normal 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="
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/uhk-common/package.json
Normal file
17
packages/uhk-common/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
packages/uhk-common/src/models/app-start-info.ts
Normal file
7
packages/uhk-common/src/models/app-start-info.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { CommandLineArgs } from './command-line-args';
|
||||
|
||||
export interface AppStartInfo {
|
||||
commandLineArgs: CommandLineArgs;
|
||||
deviceConnected: boolean;
|
||||
hasPermission: boolean;
|
||||
}
|
||||
3
packages/uhk-common/src/models/command-line-args.ts
Normal file
3
packages/uhk-common/src/models/command-line-args.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface CommandLineArgs {
|
||||
addons: boolean;
|
||||
}
|
||||
4
packages/uhk-common/src/models/index.ts
Normal file
4
packages/uhk-common/src/models/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './command-line-args';
|
||||
export * from './notification';
|
||||
export * from './ipc-response';
|
||||
export * from './app-start-info';
|
||||
4
packages/uhk-common/src/models/ipc-response.ts
Normal file
4
packages/uhk-common/src/models/ipc-response.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class IpcResponse {
|
||||
success: boolean;
|
||||
error?: { message: string };
|
||||
}
|
||||
17
packages/uhk-common/src/models/notification.ts
Normal file
17
packages/uhk-common/src/models/notification.ts
Normal 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;
|
||||
}
|
||||
1
packages/uhk-common/src/services/index.ts
Normal file
1
packages/uhk-common/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './logger.service';
|
||||
20
packages/uhk-common/src/services/logger.service.ts
Normal file
20
packages/uhk-common/src/services/logger.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
packages/uhk-common/src/util/constants.ts
Normal file
5
packages/uhk-common/src/util/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export namespace Constants {
|
||||
export const VENDOR_ID = 0x1D50;
|
||||
export const PRODUCT_ID = 0x6122;
|
||||
export const MAX_PAYLOAD_SIZE = 64;
|
||||
}
|
||||
39
packages/uhk-common/src/util/index.ts
Normal file
39
packages/uhk-common/src/util/index.ts
Normal 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;
|
||||
}
|
||||
31
packages/uhk-common/src/util/ipcEvents.ts
Normal file
31
packages/uhk-common/src/util/ipcEvents.ts
Normal 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;
|
||||
}
|
||||
20
packages/uhk-common/tsconfig.json
Normal file
20
packages/uhk-common/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
69
packages/uhk-web/.angular-cli.json
Normal file
69
packages/uhk-web/.angular-cli.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
28
packages/uhk-web/README.md
Normal file
28
packages/uhk-web/README.md
Normal 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).
|
||||
14
packages/uhk-web/e2e/app.e2e-spec.ts
Normal file
14
packages/uhk-web/e2e/app.e2e-spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
11
packages/uhk-web/e2e/app.po.ts
Normal file
11
packages/uhk-web/e2e/app.po.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
14
packages/uhk-web/e2e/tsconfig.e2e.json
Normal file
14
packages/uhk-web/e2e/tsconfig.e2e.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
packages/uhk-web/index.ts
Normal file
3
packages/uhk-web/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './src/app/web.module';
|
||||
export * from './src/app/app.routes';
|
||||
export * from './src/app/app.component';
|
||||
33
packages/uhk-web/karma.conf.js
Normal file
33
packages/uhk-web/karma.conf.js
Normal 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
10654
packages/uhk-web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
103
packages/uhk-web/package.json
Normal file
103
packages/uhk-web/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
28
packages/uhk-web/protractor.conf.js
Normal file
28
packages/uhk-web/protractor.conf.js
Normal 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 } }));
|
||||
}
|
||||
};
|
||||
17
packages/uhk-web/sprite-config.json
Normal file
17
packages/uhk-web/sprite-config.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/uhk-web/src/app/app.component.html
Normal file
13
packages/uhk-web/src/app/app.component.html
Normal 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>
|
||||
72
packages/uhk-web/src/app/app.component.scss
Normal file
72
packages/uhk-web/src/app/app.component.scss
Normal 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;
|
||||
}
|
||||
93
packages/uhk-web/src/app/app.component.ts
Normal file
93
packages/uhk-web/src/app/app.component.ts
Normal 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')
|
||||
);
|
||||
}
|
||||
}
|
||||
42
packages/uhk-web/src/app/app.routes.ts
Normal file
42
packages/uhk-web/src/app/app.routes.ts
Normal 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 });
|
||||
@@ -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...
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
10
packages/uhk-web/src/app/components/add-on/add-on.routes.ts
Normal file
10
packages/uhk-web/src/app/components/add-on/add-on.routes.ts
Normal 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
|
||||
}
|
||||
];
|
||||
2
packages/uhk-web/src/app/components/add-on/index.ts
Normal file
2
packages/uhk-web/src/app/components/add-on/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './add-on.component';
|
||||
export * from './add-on.routes';
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
1
packages/uhk-web/src/app/components/keyboard/index.ts
Normal file
1
packages/uhk-web/src/app/components/keyboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slider';
|
||||
@@ -0,0 +1 @@
|
||||
export { KeyboardSliderComponent } from './keyboard-slider.component';
|
||||
@@ -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 |
@@ -0,0 +1,8 @@
|
||||
svg-keyboard {
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transform: translateX(-101%);
|
||||
user-select: none;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
2
packages/uhk-web/src/app/components/keymap/edit/index.ts
Normal file
2
packages/uhk-web/src/app/components/keymap/edit/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { KeymapEditComponent } from './keymap-edit.component';
|
||||
export { KeymapEditGuard } from './keymap-edit-guard.service';
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.not-found {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
4
packages/uhk-web/src/app/components/keymap/index.ts
Normal file
4
packages/uhk-web/src/app/components/keymap/index.ts
Normal 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';
|
||||
26
packages/uhk-web/src/app/components/keymap/keymap.routes.ts
Normal file
26
packages/uhk-web/src/app/components/keymap/keymap.routes.ts
Normal 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
|
||||
}
|
||||
];
|
||||
1
packages/uhk-web/src/app/components/layers/index.ts
Normal file
1
packages/uhk-web/src/app/components/layers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './layers.component';
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MacroActionEditorComponent } from './macro-action-editor.component';
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MacroDelayTabComponent } from './macro-delay.component';
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { MacroDelayTabComponent } from './delay';
|
||||
export { MacroKeyTabComponent } from './key';
|
||||
export { MacroMouseTabComponent } from './mouse';
|
||||
export { MacroTextTabComponent } from './text';
|
||||
@@ -0,0 +1 @@
|
||||
export { MacroKeyTabComponent } from './macro-key.component';
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MacroMouseTabComponent } from './macro-mouse.component';
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user