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
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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import {
|
||||
MouseButtonMacroAction,
|
||||
MoveMouseMacroAction,
|
||||
ScrollMouseMacroAction,
|
||||
MacroSubAction
|
||||
} from '../../../../../config-serializer/config-items/macro-action';
|
||||
import { Tab } from '../../../../popover/tab';
|
||||
|
||||
type MouseMacroAction = MouseButtonMacroAction | MoveMouseMacroAction | ScrollMouseMacroAction;
|
||||
|
||||
enum TabName {
|
||||
Move,
|
||||
Scroll,
|
||||
Click,
|
||||
Hold,
|
||||
Release
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'macro-mouse-tab',
|
||||
templateUrl: './macro-mouse.component.html',
|
||||
styleUrls: [
|
||||
'../../macro-action-editor.component.scss',
|
||||
'./macro-mouse.component.scss'
|
||||
],
|
||||
host: { 'class': 'macro__mouse' }
|
||||
})
|
||||
export class MacroMouseTabComponent implements OnInit {
|
||||
@Input() macroAction: MouseMacroAction;
|
||||
@ViewChild('tab') selectedTab: Tab;
|
||||
|
||||
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
|
||||
TabName = TabName;
|
||||
/* tslint:enable:variable-name */
|
||||
activeTab: TabName;
|
||||
buttonLabels: string[];
|
||||
private selectedButtons: boolean[];
|
||||
|
||||
constructor() {
|
||||
this.buttonLabels = ['Left', 'Middle', 'Right'];
|
||||
this.selectedButtons = Array(this.buttonLabels.length).fill(false);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.macroAction) {
|
||||
this.macroAction = new MouseButtonMacroAction();
|
||||
this.macroAction.action = MacroSubAction.press;
|
||||
}
|
||||
const tabName = this.getTabName(this.macroAction);
|
||||
this.selectTab(tabName);
|
||||
const buttonActions = [TabName.Click, TabName.Hold, TabName.Release];
|
||||
if (buttonActions.includes(this.activeTab)) {
|
||||
this.selectedButtons = (<MouseButtonMacroAction>this.macroAction).getMouseButtons();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.ngOnInit();
|
||||
}
|
||||
|
||||
selectTab(tab: TabName): void {
|
||||
this.activeTab = tab;
|
||||
|
||||
if (tab === this.getTabName(this.macroAction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (tab) {
|
||||
case TabName.Scroll:
|
||||
this.macroAction = new ScrollMouseMacroAction();
|
||||
break;
|
||||
case TabName.Move:
|
||||
this.macroAction = new MoveMouseMacroAction();
|
||||
break;
|
||||
default:
|
||||
this.macroAction = new MouseButtonMacroAction();
|
||||
this.macroAction.action = this.getAction(tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setMouseClick(index: number): void {
|
||||
this.selectedButtons[index] = !this.selectedButtons[index];
|
||||
(<MouseButtonMacroAction>this.macroAction).setMouseButtons(this.selectedButtons);
|
||||
}
|
||||
|
||||
hasButton(index: number): boolean {
|
||||
return this.selectedButtons[index];
|
||||
}
|
||||
|
||||
getAction(tab: TabName): MacroSubAction {
|
||||
switch (tab) {
|
||||
case TabName.Click:
|
||||
return MacroSubAction.press;
|
||||
case TabName.Hold:
|
||||
return MacroSubAction.hold;
|
||||
case TabName.Release:
|
||||
return MacroSubAction.release;
|
||||
default:
|
||||
throw new Error(`Invalid tab name: ${TabName[tab]}`);
|
||||
}
|
||||
}
|
||||
|
||||
getTabName(action: MouseMacroAction): TabName {
|
||||
if (action instanceof MouseButtonMacroAction) {
|
||||
if (!action.action || action.isOnlyPressAction()) {
|
||||
return TabName.Click;
|
||||
} else if (action.isOnlyHoldAction()) {
|
||||
return TabName.Hold;
|
||||
} else if (action.isOnlyReleaseAction()) {
|
||||
return TabName.Release;
|
||||
}
|
||||
} else if (action instanceof MoveMouseMacroAction) {
|
||||
return TabName.Move;
|
||||
} else if (action instanceof ScrollMouseMacroAction) {
|
||||
return TabName.Scroll;
|
||||
}
|
||||
return TabName.Move;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MacroTextTabComponent } from './macro-text.component';
|
||||
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<h4>Type text</h4>
|
||||
<p>Input the text you want to type with this macro action.</p>
|
||||
<textarea #macroTextInput name="macro-text" (change)="onTextChange()" class="macro__text-input">{{ macroAction.text }}</textarea>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.macro__text-input {
|
||||
width: 100%;
|
||||
min-height: 10rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
OnInit,
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
Renderer,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { TextMacroAction } from '../../../../../config-serializer/config-items/macro-action';
|
||||
|
||||
@Component({
|
||||
selector: 'macro-text-tab',
|
||||
templateUrl: './macro-text.component.html',
|
||||
styleUrls: ['./macro-text.component.scss'],
|
||||
host: { 'class': 'macro__text' }
|
||||
})
|
||||
export class MacroTextTabComponent implements OnInit, AfterViewInit {
|
||||
@Input() macroAction: TextMacroAction;
|
||||
@ViewChild('macroTextInput') input: ElementRef;
|
||||
|
||||
constructor(private renderer: Renderer) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.macroAction) {
|
||||
this.macroAction = new TextMacroAction();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.renderer.invokeElementMethod(this.input.nativeElement, 'focus');
|
||||
}
|
||||
|
||||
onTextChange() {
|
||||
this.macroAction.text = this.input.nativeElement.value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<ng-template [ngIf]="macro">
|
||||
<macro-header
|
||||
[macro]="macro"
|
||||
[isNew]="isNew"
|
||||
></macro-header>
|
||||
<macro-list
|
||||
[macro]="macro"
|
||||
(add)="addAction($event.macroId, $event.action)"
|
||||
(edit)="editAction($event.macroId, $event.index, $event.action)"
|
||||
(delete)="deleteAction($event.macroId, $event.index, $event.action)"
|
||||
(reorder)="reorderAction($event.macroId, $event.oldIndex, $event.newIndex)"
|
||||
></macro-list>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="!macro" class="not-found">
|
||||
There is no macro with id {{ route.params.select('id') | async }}.
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.not-found {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import 'rxjs/add/operator/pluck';
|
||||
|
||||
import { Macro } from '../../../config-serializer/config-items/macro';
|
||||
import { MacroAction } from '../../../config-serializer/config-items/macro-action/macro-action';
|
||||
|
||||
import { MacroActions } from '../../../store/actions';
|
||||
import { AppState } from '../../../store/index';
|
||||
import { getMacro } from '../../../store/reducers/user-configuration';
|
||||
|
||||
@Component({
|
||||
selector: 'macro-edit',
|
||||
templateUrl: './macro-edit.component.html',
|
||||
styleUrls: ['./macro-edit.component.scss'],
|
||||
host: {
|
||||
'class': 'container-fluid'
|
||||
}
|
||||
})
|
||||
export class MacroEditComponent implements OnDestroy {
|
||||
macro: Macro;
|
||||
isNew: boolean;
|
||||
|
||||
private subscription: Subscription;
|
||||
constructor(private store: Store<AppState>, public route: ActivatedRoute) {
|
||||
this.subscription = route
|
||||
.params
|
||||
.pluck<{}, string>('id')
|
||||
.switchMap((id: string) => store.let(getMacro(+id)))
|
||||
.subscribe((macro: Macro) => {
|
||||
this.macro = macro;
|
||||
});
|
||||
|
||||
this.isNew = this.route.snapshot.params['empty'] === 'new';
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
addAction(macroId: number, action: MacroAction) {
|
||||
this.store.dispatch(MacroActions.addMacroAction(macroId, action));
|
||||
}
|
||||
|
||||
editAction(macroId: number, index: number, action: MacroAction) {
|
||||
this.store.dispatch(MacroActions.saveMacroAction(macroId, index, action));
|
||||
}
|
||||
|
||||
deleteAction(macroId: number, index: number, action: MacroAction) {
|
||||
this.store.dispatch(MacroActions.deleteMacroAction(macroId, index, action));
|
||||
}
|
||||
|
||||
reorderAction(macroId: number, oldIndex: number, newIndex: number) {
|
||||
this.store.dispatch(MacroActions.reorderMacroAction(macroId, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<uhk-header>
|
||||
<div class="row">
|
||||
<h1 class="col-xs-12 pane-title">
|
||||
<i class="fa fa-play"></i>
|
||||
<input #macroName cancelable
|
||||
class="pane-title__name"
|
||||
type="text"
|
||||
(change)="editMacroName($event.target.value)"
|
||||
(keyup.enter)="macroName.blur()"
|
||||
/>
|
||||
<i class="glyphicon glyphicon-trash macro__remove pull-right" title=""
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
html="true"
|
||||
data-original-title="<span class='text-nowrap'>Delete macro</span>"
|
||||
(click)="removeMacro()"
|
||||
></i>
|
||||
<i class="fa fa-files-o macro__duplicate pull-right" title=""
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
data-original-title="Duplicate macro"
|
||||
(click)="duplicateMacro()"
|
||||
></i>
|
||||
</h1>
|
||||
</div>
|
||||
</uhk-header>
|
||||
@@ -0,0 +1,43 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.macro {
|
||||
&__remove {
|
||||
font-size: 0.75em;
|
||||
top: 8px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $icon-hover-delete;
|
||||
}
|
||||
}
|
||||
|
||||
&__duplicate {
|
||||
font-size: 0.75em;
|
||||
top: 7px;
|
||||
margin-right: 15px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $icon-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pane-title {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&__name {
|
||||
border: none;
|
||||
border-bottom: 2px dotted #999;
|
||||
padding: 0;
|
||||
margin: 0 0.25rem;
|
||||
width: 330px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { Macro } from '../../../config-serializer/config-items/macro';
|
||||
|
||||
import { MacroActions } from '../../../store/actions';
|
||||
import { AppState } from '../../../store/index';
|
||||
|
||||
@Component({
|
||||
selector: 'macro-header',
|
||||
templateUrl: './macro-header.component.html',
|
||||
styleUrls: ['./macro-header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MacroHeaderComponent implements AfterViewInit, OnChanges {
|
||||
@Input() macro: Macro;
|
||||
@Input() isNew: boolean;
|
||||
@ViewChild('macroName') macroName: ElementRef;
|
||||
|
||||
constructor(private store: Store<AppState>, private renderer: Renderer2) { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.isNew) {
|
||||
this.setFocusOnName();
|
||||
}
|
||||
if (changes['macro']) {
|
||||
this.setName();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.isNew) {
|
||||
this.setFocusOnName();
|
||||
}
|
||||
}
|
||||
|
||||
removeMacro() {
|
||||
this.store.dispatch(MacroActions.removeMacro(this.macro.id));
|
||||
}
|
||||
|
||||
duplicateMacro() {
|
||||
this.store.dispatch(MacroActions.duplicateMacro(this.macro));
|
||||
}
|
||||
|
||||
editMacroName(name: string) {
|
||||
if (name.length === 0) {
|
||||
this.setName();
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(MacroActions.editMacroName(this.macro.id, name));
|
||||
}
|
||||
|
||||
private setFocusOnName() {
|
||||
this.macroName.nativeElement.select();
|
||||
}
|
||||
|
||||
private setName(): void {
|
||||
this.renderer.setProperty(this.macroName.nativeElement, 'value', this.macro.name);
|
||||
}
|
||||
|
||||
}
|
||||
8
packages/uhk-web/src/app/components/macro/index.ts
Normal file
8
packages/uhk-web/src/app/components/macro/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './edit/macro-edit.component';
|
||||
export * from './list/macro-list.component';
|
||||
export * from './header/macro-header.component';
|
||||
export * from './macro.routes';
|
||||
export * from './not-found';
|
||||
export * from './item';
|
||||
export * from './action-editor';
|
||||
export * from './action-editor/tab';
|
||||
1
packages/uhk-web/src/app/components/macro/item/index.ts
Normal file
1
packages/uhk-web/src/app/components/macro/item/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MacroItemComponent } from './macro-item.component';
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="list-group-item action--item" [class.is-editing]="editing">
|
||||
<span *ngIf="movable" class="glyphicon glyphicon-option-vertical action--movable" aria-hidden="true"></span>
|
||||
<div class="action--item--wrap" [class.pointer]="editable" (click)="editAction()">
|
||||
<icon [name]="iconName"></icon>
|
||||
<div class="action--title">{{ title }}</div>
|
||||
<icon *ngIf="editable && macroAction && !editing" name="pencil"></icon>
|
||||
</div>
|
||||
<icon *ngIf="deletable" name="trash" (click)="deleteAction()"></icon>
|
||||
</div>
|
||||
<div class="list-group-item macro-action-editor__container"
|
||||
[@toggler]="((editable && editing) || newItem) ? 'active' : 'inactive'">
|
||||
<macro-action-editor
|
||||
[macroAction]="macroAction"
|
||||
(cancel)="cancelEdit()"
|
||||
(save)="saveEditedAction($event)">
|
||||
</macro-action-editor>
|
||||
</div>
|
||||
@@ -0,0 +1,86 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
:host {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
|
||||
&.macro-item:first-of-type {
|
||||
.list-group-item {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.macro-item:last-of-type {
|
||||
.list-group-item {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.gu-transit {
|
||||
opacity: 0.2;
|
||||
|
||||
.list-group-item {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
&--item {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
icon {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.is-editing {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&--wrap {
|
||||
justify-content: space-between;
|
||||
|
||||
&.pointer {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $icon-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--title {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--movable {
|
||||
&:hover {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.macro-action-editor__container {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { Component, Input, Output, EventEmitter, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
import { KeyModifiers } from '../../../config-serializer/config-items/key-modifiers';
|
||||
import {
|
||||
DelayMacroAction,
|
||||
KeyMacroAction,
|
||||
MacroAction,
|
||||
MouseButtonMacroAction,
|
||||
MoveMouseMacroAction,
|
||||
ScrollMouseMacroAction,
|
||||
TextMacroAction
|
||||
} from '../../../config-serializer/config-items/macro-action';
|
||||
|
||||
import { MapperService } from '../../../services/mapper.service';
|
||||
|
||||
@Component({
|
||||
animations: [
|
||||
trigger('toggler', [
|
||||
state('inactive', style({
|
||||
height: '0px'
|
||||
})),
|
||||
state('active', style({
|
||||
height: '*'
|
||||
})),
|
||||
transition('inactive <=> active', animate('500ms ease-out'))
|
||||
])
|
||||
],
|
||||
selector: 'macro-item',
|
||||
templateUrl: './macro-item.component.html',
|
||||
styleUrls: ['./macro-item.component.scss'],
|
||||
host: { 'class': 'macro-item' }
|
||||
})
|
||||
export class MacroItemComponent implements OnInit, OnChanges {
|
||||
@Input() macroAction: MacroAction;
|
||||
@Input() editable: boolean;
|
||||
@Input() deletable: boolean;
|
||||
@Input() movable: boolean;
|
||||
|
||||
@Output() save = new EventEmitter<MacroAction>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
@Output() edit = new EventEmitter<void>();
|
||||
@Output() delete = new EventEmitter<void>();
|
||||
|
||||
title: string;
|
||||
iconName: string;
|
||||
editing: boolean;
|
||||
newItem: boolean = false;
|
||||
|
||||
constructor(private mapper: MapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.updateView();
|
||||
if (!this.macroAction) {
|
||||
this.editing = true;
|
||||
this.newItem = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['macroAction']) {
|
||||
this.updateView();
|
||||
}
|
||||
}
|
||||
|
||||
saveEditedAction(editedAction: MacroAction): void {
|
||||
this.macroAction = editedAction;
|
||||
this.editing = false;
|
||||
this.updateView();
|
||||
this.save.emit(editedAction);
|
||||
}
|
||||
|
||||
editAction(): void {
|
||||
if (!this.editable || this.editing) {
|
||||
this.cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
this.editing = true;
|
||||
this.edit.emit();
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.editing = false;
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
deleteAction(): void {
|
||||
this.delete.emit();
|
||||
}
|
||||
|
||||
private updateView(): void {
|
||||
if (!this.macroAction) {
|
||||
this.title = 'New macro action';
|
||||
} else if (this.macroAction instanceof DelayMacroAction) {
|
||||
// Delay
|
||||
this.iconName = 'clock';
|
||||
const action: DelayMacroAction = this.macroAction as DelayMacroAction;
|
||||
const delay = action.delay > 0 ? action.delay / 1000 : 0;
|
||||
this.title = `Delay of ${delay}s`;
|
||||
} else if (this.macroAction instanceof TextMacroAction) {
|
||||
// Write text
|
||||
const action: TextMacroAction = this.macroAction as TextMacroAction;
|
||||
this.iconName = 'font';
|
||||
this.title = `Write text: ${action.text}`;
|
||||
} else if (this.macroAction instanceof KeyMacroAction) {
|
||||
// Key pressed/held/released
|
||||
const action: KeyMacroAction = this.macroAction as KeyMacroAction;
|
||||
this.setKeyActionContent(action);
|
||||
} else if (this.macroAction instanceof MouseButtonMacroAction) {
|
||||
// Mouse button clicked/held/released
|
||||
const action: MouseButtonMacroAction = this.macroAction as MouseButtonMacroAction;
|
||||
this.setMouseButtonActionContent(action);
|
||||
} else if (this.macroAction instanceof MoveMouseMacroAction || this.macroAction instanceof ScrollMouseMacroAction) {
|
||||
// Mouse moved or scrolled
|
||||
this.setMouseMoveScrollActionContent(this.macroAction);
|
||||
} else {
|
||||
this.title = this.macroAction.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
private setKeyActionContent(action: KeyMacroAction): void {
|
||||
if (!action.hasScancode() && !action.hasModifiers()) {
|
||||
this.title = 'Invalid keypress';
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.isPressAction()) {
|
||||
// Press key
|
||||
this.iconName = 'hand-pointer';
|
||||
this.title = 'Press key: ';
|
||||
} else if (action.isHoldAction()) {
|
||||
// Hold key
|
||||
this.iconName = 'hand-rock';
|
||||
this.title = 'Hold key: ';
|
||||
} else if (action.isReleaseAction()) {
|
||||
// Release key
|
||||
this.iconName = 'hand-paper';
|
||||
this.title = 'Release key: ';
|
||||
}
|
||||
|
||||
if (action.hasScancode()) {
|
||||
const scancode: string = (this.mapper.scanCodeToText(action.scancode, action.type) || ['Unknown']).join(' ');
|
||||
if (scancode) {
|
||||
this.title += scancode;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.hasModifiers()) {
|
||||
// Press/hold/release modifiers
|
||||
for (let i = KeyModifiers.leftCtrl; i <= KeyModifiers.rightGui; i <<= 1) {
|
||||
if (action.isModifierActive(i)) {
|
||||
this.title += ' ' + KeyModifiers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setMouseMoveScrollActionContent(action: MacroAction): void {
|
||||
let typedAction: any;
|
||||
if (action instanceof MoveMouseMacroAction) {
|
||||
// Move mouse pointer
|
||||
this.iconName = 'mouse-pointer';
|
||||
this.title = 'Move pointer';
|
||||
typedAction = this.macroAction as MoveMouseMacroAction;
|
||||
} else {
|
||||
// Scroll mouse
|
||||
this.iconName = 'mouse-pointer';
|
||||
this.title = 'Scroll';
|
||||
typedAction = this.macroAction as ScrollMouseMacroAction;
|
||||
}
|
||||
|
||||
let needAnd: boolean;
|
||||
if (Math.abs(typedAction.x) !== 0) {
|
||||
this.title += ` by ${Math.abs(typedAction.x)}px ${typedAction.x > 0 ? 'left' : 'right'}`;
|
||||
needAnd = true;
|
||||
}
|
||||
if (Math.abs(typedAction.y) !== 0) {
|
||||
this.title += ` ${needAnd ? 'and' : 'by'} ${Math.abs(typedAction.y)}px ${typedAction.y > 0 ? 'down' : 'up'}`;
|
||||
}
|
||||
}
|
||||
|
||||
private setMouseButtonActionContent(action: MouseButtonMacroAction): void {
|
||||
// Press/hold/release mouse buttons
|
||||
if (action.isOnlyPressAction()) {
|
||||
this.iconName = 'mouse-pointer';
|
||||
this.title = 'Click mouse button: ';
|
||||
} else if (action.isOnlyHoldAction()) {
|
||||
this.iconName = 'hand-rock';
|
||||
this.title = 'Hold mouse button: ';
|
||||
} else if (action.isOnlyReleaseAction()) {
|
||||
this.iconName = 'hand-paper';
|
||||
this.title = 'Release mouse button: ';
|
||||
}
|
||||
|
||||
const buttonLabels: string[] = ['Left', 'Middle', 'Right'];
|
||||
const selectedButtons: boolean[] = action.getMouseButtons();
|
||||
const selectedButtonLabels: string[] = [];
|
||||
selectedButtons.forEach((isSelected, idx) => {
|
||||
if (isSelected && buttonLabels[idx]) {
|
||||
selectedButtonLabels.push(buttonLabels[idx]);
|
||||
}
|
||||
});
|
||||
this.title += selectedButtonLabels.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="row list-container">
|
||||
<div class="col-xs-10 col-xs-offset-1 list-group">
|
||||
<div class="macro-actions-container" [dragula]="'macroActions'" [dragulaModel]="macro.macroActions">
|
||||
<macro-item *ngFor="let macroAction of macro.macroActions; let macroActionIndex = index"
|
||||
[macroAction]="macroAction"
|
||||
[editable]="true"
|
||||
[deletable]="true"
|
||||
[movable]="true"
|
||||
(save)="saveAction($event, macroActionIndex)"
|
||||
(edit)="editAction(macroActionIndex)"
|
||||
(cancel)="cancelAction()"
|
||||
(delete)="deleteAction(macroAction, macroActionIndex)"
|
||||
[attr.data-index]="macroActionIndex"
|
||||
></macro-item>
|
||||
|
||||
<macro-item *ngIf="showNew"
|
||||
[@togglerNew]="showNew ? 'active' : 'inactive'"
|
||||
[macroAction]="newMacro"
|
||||
[editable]="true"
|
||||
[deletable]="false"
|
||||
[movable]="false"
|
||||
(save)="addNewAction($event)"
|
||||
(cancel)="hideNewAction()"
|
||||
></macro-item>
|
||||
</div>
|
||||
<div class="list-group add-new__action-container" [@toggler]="(!showNew) ? 'active' : 'inactive'">
|
||||
<div class="list-group-item action--item add-new__action-item no-reorder clearfix">
|
||||
<a class="add-new__action-item--link" (click)="showNewAction()">
|
||||
<i class="fa fa-plus"></i> Add macro action
|
||||
</a>
|
||||
<a class="add-new__action-item--link">
|
||||
<i class="fa fa fa-circle"></i> Add captured keystroke
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,153 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.list-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.action {
|
||||
&--edit__form {
|
||||
background-color: #fff;
|
||||
margin-left: -0.5rem;
|
||||
margin-right: -15px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
&--item {
|
||||
padding-left: 8px;
|
||||
|
||||
&.active,
|
||||
&.active:hover {
|
||||
background-color: white;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
border-color: black;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-group {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.macro__name {
|
||||
border-bottom: 2px dotted #999;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.macro-settings {
|
||||
border: 1px solid black;
|
||||
border-top-color: #999;
|
||||
z-index: 100;
|
||||
|
||||
.helper {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 13px;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
.action--item.active.callout,
|
||||
.macro-settings.callout {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.macro-actions-container {
|
||||
margin-bottom: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.list-group-item .move-handle:hover {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.flex-button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.add-new__action-container {
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.add-new__action-item {
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-top: 0;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--link {
|
||||
width: 50%;
|
||||
float: left;
|
||||
padding: 10px 5px;
|
||||
text-align: center;
|
||||
color: $icon-hover;
|
||||
|
||||
&:first-of-type {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-circle {
|
||||
color: #c00;
|
||||
}
|
||||
}
|
||||
|
||||
// Dragula styles
|
||||
.gu {
|
||||
&-mirror {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
z-index: 9999;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-unselectable {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Component, EventEmitter, Input, Output, QueryList, ViewChildren, forwardRef } from '@angular/core';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
import { DragulaService } from 'ng2-dragula/ng2-dragula';
|
||||
|
||||
import { Macro } from '../../../config-serializer/config-items/macro';
|
||||
import { MacroAction } from '../../../config-serializer/config-items/macro-action';
|
||||
import { MacroItemComponent } from '../item';
|
||||
|
||||
@Component({
|
||||
animations: [
|
||||
trigger('toggler', [
|
||||
state('inactive', style({
|
||||
height: '0px'
|
||||
})),
|
||||
state('active', style({
|
||||
height: '*'
|
||||
})),
|
||||
transition('inactive <=> active', animate('500ms ease-out'))
|
||||
]),
|
||||
trigger('togglerNew', [
|
||||
state('void', style({
|
||||
height: '0px'
|
||||
})),
|
||||
state('active', style({
|
||||
height: '*'
|
||||
})),
|
||||
transition(':enter', animate('500ms ease-out')),
|
||||
transition(':leave', animate('500ms ease-out'))
|
||||
])
|
||||
],
|
||||
selector: 'macro-list',
|
||||
templateUrl: './macro-list.component.html',
|
||||
styleUrls: ['./macro-list.component.scss'],
|
||||
viewProviders: [DragulaService]
|
||||
})
|
||||
export class MacroListComponent {
|
||||
@Input() macro: Macro;
|
||||
@ViewChildren(forwardRef(() => MacroItemComponent)) macroItems: QueryList<MacroItemComponent>;
|
||||
|
||||
@Output() add = new EventEmitter();
|
||||
@Output() edit = new EventEmitter();
|
||||
@Output() delete = new EventEmitter();
|
||||
@Output() reorder = new EventEmitter();
|
||||
|
||||
newMacro: Macro = undefined;
|
||||
showNew: boolean = false;
|
||||
private activeEdit: number = undefined;
|
||||
private dragIndex: number;
|
||||
|
||||
constructor(dragulaService: DragulaService) {
|
||||
/* tslint:disable:no-unused-variable: Used by Dragula. */
|
||||
dragulaService.setOptions('macroActions', {
|
||||
moves: function (el: any, container: any, handle: any) {
|
||||
return handle.className.includes('action--movable');
|
||||
}
|
||||
});
|
||||
|
||||
dragulaService.drag.subscribe((value: any) => {
|
||||
this.dragIndex = +value[1].getAttribute('data-index');
|
||||
});
|
||||
|
||||
dragulaService.drop.subscribe((value: any) => {
|
||||
if (value[4]) {
|
||||
this.reorder.emit({
|
||||
macroId: this.macro.id,
|
||||
oldIndex: this.dragIndex,
|
||||
newIndex: +value[4].getAttribute('data-index')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showNewAction() {
|
||||
this.hideActiveEditor();
|
||||
|
||||
this.newMacro = undefined;
|
||||
this.showNew = true;
|
||||
}
|
||||
|
||||
hideNewAction() {
|
||||
this.showNew = false;
|
||||
}
|
||||
|
||||
addNewAction(macroAction: MacroAction) {
|
||||
this.add.emit({
|
||||
macroId: this.macro.id,
|
||||
action: macroAction
|
||||
});
|
||||
|
||||
this.newMacro = undefined;
|
||||
this.showNew = false;
|
||||
}
|
||||
|
||||
editAction(index: number) {
|
||||
// Hide other editors when clicking edit button of a macro action
|
||||
this.hideActiveEditor();
|
||||
this.showNew = false;
|
||||
this.activeEdit = index;
|
||||
}
|
||||
|
||||
cancelAction() {
|
||||
this.activeEdit = undefined;
|
||||
}
|
||||
|
||||
saveAction(macroAction: MacroAction, index: number) {
|
||||
this.edit.emit({
|
||||
macroId: this.macro.id,
|
||||
index: index,
|
||||
action: macroAction
|
||||
});
|
||||
|
||||
this.hideActiveEditor();
|
||||
}
|
||||
|
||||
deleteAction(macroAction: MacroAction, index: number) {
|
||||
this.delete.emit({
|
||||
macroId: this.macro.id,
|
||||
index: index,
|
||||
action: macroAction
|
||||
});
|
||||
|
||||
this.hideActiveEditor();
|
||||
}
|
||||
|
||||
private hideActiveEditor() {
|
||||
if (this.activeEdit !== undefined) {
|
||||
this.macroItems.toArray()[this.activeEdit].cancelEdit();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
packages/uhk-web/src/app/components/macro/macro.routes.ts
Normal file
20
packages/uhk-web/src/app/components/macro/macro.routes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { MacroEditComponent } from './edit/macro-edit.component';
|
||||
import { MacroNotFoundComponent, MacroNotFoundGuard } from './not-found';
|
||||
|
||||
export const macroRoutes: Routes = [
|
||||
{
|
||||
path: 'macro',
|
||||
component: MacroNotFoundComponent,
|
||||
canActivate: [MacroNotFoundGuard]
|
||||
},
|
||||
{
|
||||
path: 'macro/:id',
|
||||
component: MacroEditComponent
|
||||
},
|
||||
{
|
||||
path: 'macro/:id/:empty',
|
||||
component: MacroEditComponent
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export { MacroNotFoundComponent } from './macro-not-found.component';
|
||||
export { MacroNotFoundGuard } from './macro-not-found-guard.service';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import 'rxjs/add/operator/let';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { AppState } from '../../../store/index';
|
||||
import { getMacros } from '../../../store/reducers/user-configuration';
|
||||
import { Macro } from '../../../config-serializer/config-items/macro';
|
||||
|
||||
@Injectable()
|
||||
export class MacroNotFoundGuard implements CanActivate {
|
||||
|
||||
constructor(private store: Store<AppState>, private router: Router) { }
|
||||
|
||||
canActivate(): Observable<boolean> {
|
||||
return this.store
|
||||
.let(getMacros())
|
||||
.map((macros: Macro[]) => {
|
||||
const hasMacros = macros.length > 0;
|
||||
if (hasMacros) {
|
||||
this.router.navigate(['/macro', macros[0].id]);
|
||||
}
|
||||
return !hasMacros;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="container-fluid">
|
||||
<uhk-header>
|
||||
<h1> </h1>
|
||||
</uhk-header>
|
||||
<div class="not-found">
|
||||
You don't have any macros. Try to add one!
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.not-found {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'macro-not-found',
|
||||
templateUrl: './macro-not-found.component.html',
|
||||
styleUrls: ['./macro-not-found.component.scss']
|
||||
})
|
||||
export class MacroNotFoundComponent { }
|
||||
@@ -0,0 +1 @@
|
||||
export * from './missing-device.component';
|
||||
@@ -0,0 +1 @@
|
||||
<uhk-message title="Cannot find your UHK" subtitle="Please plug it in!"></uhk-message>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import 'rxjs/add/operator/ignoreElements';
|
||||
import 'rxjs/add/operator/takeWhile';
|
||||
|
||||
@Component({
|
||||
selector: 'missing-device',
|
||||
templateUrl: './missing-device.component.html'
|
||||
})
|
||||
export class MissingDeviceComponent {
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './notification.component';
|
||||
@@ -0,0 +1,3 @@
|
||||
<span class="text">Keymap removed</span>
|
||||
<a href="#" class="action action--undo">Undo</a>
|
||||
<span class="dismiss">×</span>
|
||||
@@ -0,0 +1,38 @@
|
||||
:host {
|
||||
padding: 1rem 1.5rem;
|
||||
box-shadow: 0 0 0 1px #000;
|
||||
border-radius: 0.5rem;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 10000;
|
||||
background-color: #333;
|
||||
color: #eee;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
color: #5bc0de;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: #5bc0de;
|
||||
}
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
color: #ccc;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrls: ['./notification.component.scss']
|
||||
})
|
||||
export class NotificationComponent {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
1
packages/uhk-web/src/app/components/popover/index.ts
Normal file
1
packages/uhk-web/src/app/components/popover/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './popover.component';
|
||||
@@ -0,0 +1,82 @@
|
||||
<div class="popover"
|
||||
#popover
|
||||
[@popover]="animationState"
|
||||
[ngClass]="{'leftArrow': leftArrow, 'rightArrow': rightArrow}"
|
||||
[style.top.px]="topPosition"
|
||||
[style.left.px]="leftPosition"
|
||||
>
|
||||
<div class="arrowCustom"></div>
|
||||
<div class="popover-title menu-tabs">
|
||||
<ul class="nav nav-tabs popover-menu">
|
||||
<li #keypress [class.active]="activeTab === tabName.Keypress" (click)="selectTab(tabName.Keypress)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-keyboard-o"></i>
|
||||
<span>Keypress</span>
|
||||
</a>
|
||||
</li>
|
||||
<li #layer [class.active]="activeTab === tabName.Layer" (click)="selectTab(tabName.Layer)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-clone"></i>
|
||||
<span>Layer</span>
|
||||
</a>
|
||||
</li>
|
||||
<li #mouse [class.active]="activeTab === tabName.Mouse" (click)="selectTab(tabName.Mouse)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-mouse-pointer"></i>
|
||||
<span>Mouse</span>
|
||||
</a>
|
||||
</li>
|
||||
<li #macro [class.active]="activeTab === tabName.Macro" (click)="selectTab(tabName.Macro)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-play"></i>
|
||||
<span>Macro</span>
|
||||
</a>
|
||||
</li>
|
||||
<li #keymap [class.active]="activeTab === tabName.Keymap" (click)="selectTab(tabName.Keymap)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-keyboard-o"></i>
|
||||
<span>Keymap</span>
|
||||
</a>
|
||||
</li>
|
||||
<li #none [class.active]="activeTab === tabName.None" (click)="selectTab(tabName.None)">
|
||||
<a class="menu-tabs--item">
|
||||
<i class="fa fa-ban"></i>
|
||||
<span>None</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div [ngSwitch]="activeTab">
|
||||
<keypress-tab #tab *ngSwitchCase="tabName.Keypress" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
[longPressEnabled]="true"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></keypress-tab>
|
||||
<layer-tab #tab *ngSwitchCase="tabName.Layer" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
[currentLayer]="currentLayer"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></layer-tab>
|
||||
<mouse-tab #tab *ngSwitchCase="tabName.Mouse" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></mouse-tab>
|
||||
<macro-tab #tab *ngSwitchCase="tabName.Macro" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></macro-tab>
|
||||
<keymap-tab #tab *ngSwitchCase="tabName.Keymap" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
[keymaps]="keymaps$ | async"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></keymap-tab>
|
||||
<none-tab #tab *ngSwitchCase="tabName.None" class="popover-content"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></none-tab>
|
||||
</div>
|
||||
<div class="popover-action">
|
||||
<button class="btn btn-sm btn-default" type="button" (click)="onCancelClick()"> Cancel </button>
|
||||
<button class="btn btn-sm btn-primary" [class.disabled]="!keyActionValid" type="button" (click)="onRemapKey()"> Remap Key </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popover-overlay" [class.display]="visible" (click)="onOverlay()"></div>
|
||||
@@ -0,0 +1,135 @@
|
||||
.popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
max-width: 568px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
|
||||
&.leftArrow {
|
||||
.arrowCustom {
|
||||
transform: none;
|
||||
left: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&.rightArrow {
|
||||
.arrowCustom {
|
||||
transform: none;
|
||||
right: 22px;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .container-fluid {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.arrowCustom {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 41px;
|
||||
height: 16px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 21px solid transparent;
|
||||
border-right: 21px solid transparent;
|
||||
border-bottom: 17px solid rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 20px solid transparent;
|
||||
border-right: 20px solid transparent;
|
||||
border-bottom: 16px solid #f7f7f7;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-action {
|
||||
padding: 8px 14px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
background-color: #f7f7f7;
|
||||
border-top: 1px solid #ebebeb;
|
||||
border-radius: 0 0 5px 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.popover-title {
|
||||
&.menu-tabs {
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
display: block;
|
||||
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: flex;
|
||||
|
||||
.menu-tabs--item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.popover-overlay {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
transition: background 200ms ease-out, height 0ms 200ms linear;
|
||||
|
||||
&.display {
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
transition: background 200ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-item {
|
||||
position: relative;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&.keymap-name--wrapper {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.layout-segment-code {
|
||||
height: 2rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
}
|
||||
200
packages/uhk-web/src/app/components/popover/popover.component.ts
Normal file
200
packages/uhk-web/src/app/components/popover/popover.component.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import 'rxjs/add/operator/combineLatest';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import {
|
||||
KeyAction,
|
||||
KeystrokeAction,
|
||||
MouseAction,
|
||||
PlayMacroAction,
|
||||
SwitchKeymapAction,
|
||||
SwitchLayerAction
|
||||
} from '../../config-serializer/config-items/key-action';
|
||||
import { Keymap } from '../../config-serializer/config-items/keymap';
|
||||
|
||||
import { Tab } from './tab/tab';
|
||||
|
||||
import { AppState } from '../../store';
|
||||
import { getKeymaps } from '../../store/reducers/user-configuration';
|
||||
|
||||
enum TabName {
|
||||
Keypress,
|
||||
Layer,
|
||||
Mouse,
|
||||
Macro,
|
||||
Keymap,
|
||||
None
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'popover',
|
||||
templateUrl: './popover.component.html',
|
||||
styleUrls: ['./popover.component.scss'],
|
||||
animations: [
|
||||
trigger('popover', [
|
||||
state('closed', style({
|
||||
transform: 'translateY(30px)',
|
||||
visibility: 'hidden',
|
||||
opacity: 0
|
||||
})),
|
||||
state('opened', style({
|
||||
transform: 'translateY(0)',
|
||||
visibility: 'visible',
|
||||
opacity: 1
|
||||
})),
|
||||
transition('opened => closed', [
|
||||
animate('200ms ease-out', keyframes([
|
||||
style({ transform: 'translateY(0)', visibility: 'visible', opacity: 1, offset: 0 }),
|
||||
style({ transform: 'translateY(30px)', visibility: 'hidden', opacity: 0, offset: 1 })
|
||||
]))
|
||||
]),
|
||||
transition('closed => opened', [
|
||||
style({
|
||||
visibility: 'visible'
|
||||
}),
|
||||
animate('200ms ease-out', keyframes([
|
||||
style({ transform: 'translateY(30px)', opacity: 0, offset: 0 }),
|
||||
style({ transform: 'translateY(0)', opacity: 1, offset: 1 })
|
||||
]))
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class PopoverComponent implements OnChanges {
|
||||
@Input() defaultKeyAction: KeyAction;
|
||||
@Input() currentKeymap: Keymap;
|
||||
@Input() currentLayer: number;
|
||||
@Input() keyPosition: ClientRect;
|
||||
@Input() wrapPosition: ClientRect;
|
||||
@Input() visible: boolean;
|
||||
|
||||
@Output() cancel = new EventEmitter<any>();
|
||||
@Output() remap = new EventEmitter<KeyAction>();
|
||||
|
||||
@ViewChild('tab') selectedTab: Tab;
|
||||
@ViewChild('popover') popoverHost: ElementRef;
|
||||
|
||||
tabName = TabName;
|
||||
keyActionValid: boolean;
|
||||
activeTab: TabName;
|
||||
keymaps$: Observable<Keymap[]>;
|
||||
leftArrow: boolean = false;
|
||||
rightArrow: boolean = false;
|
||||
topPosition: number = 0;
|
||||
leftPosition: number = 0;
|
||||
animationState: string;
|
||||
|
||||
private readonly currentKeymap$ = new BehaviorSubject<Keymap>(undefined);
|
||||
|
||||
constructor(store: Store<AppState>) {
|
||||
this.animationState = 'closed';
|
||||
this.keymaps$ = store.let(getKeymaps())
|
||||
.combineLatest(this.currentKeymap$)
|
||||
.map(([keymaps, currentKeymap]: [Keymap[], Keymap]) =>
|
||||
keymaps.filter((keymap: Keymap) => currentKeymap.abbreviation !== keymap.abbreviation)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(change: SimpleChanges) {
|
||||
if (this.keyPosition && this.wrapPosition && (change['keyPosition'] || change['wrapPosition'])) {
|
||||
this.calculatePosition();
|
||||
}
|
||||
|
||||
if (change['defaultKeyAction']) {
|
||||
let tab: TabName;
|
||||
|
||||
if (this.defaultKeyAction instanceof KeystrokeAction) {
|
||||
tab = TabName.Keypress;
|
||||
} else if (this.defaultKeyAction instanceof SwitchLayerAction) {
|
||||
tab = TabName.Layer;
|
||||
} else if (this.defaultKeyAction instanceof MouseAction) {
|
||||
tab = TabName.Mouse;
|
||||
} else if (this.defaultKeyAction instanceof PlayMacroAction) {
|
||||
tab = TabName.Macro;
|
||||
} else if (this.defaultKeyAction instanceof SwitchKeymapAction) {
|
||||
tab = TabName.Keymap;
|
||||
} else {
|
||||
tab = TabName.None;
|
||||
}
|
||||
|
||||
this.selectTab(tab);
|
||||
}
|
||||
|
||||
if (change['visible']) {
|
||||
if (change['visible'].currentValue) {
|
||||
this.animationState = 'opened';
|
||||
} else {
|
||||
this.animationState = 'closed';
|
||||
}
|
||||
}
|
||||
|
||||
if (change.currentKeymap) {
|
||||
this.currentKeymap$.next(this.currentKeymap);
|
||||
}
|
||||
}
|
||||
|
||||
onCancelClick(): void {
|
||||
this.cancel.emit(undefined);
|
||||
}
|
||||
|
||||
onRemapKey(): void {
|
||||
if (this.keyActionValid) {
|
||||
try {
|
||||
const keyAction = this.selectedTab.toKeyAction();
|
||||
this.remap.emit(keyAction);
|
||||
} catch (e) {
|
||||
// TODO: show error dialog
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('keydown.escape')
|
||||
onEscape(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
selectTab(tab: TabName): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
onOverlay() {
|
||||
this.cancel.emit(undefined);
|
||||
}
|
||||
|
||||
private calculatePosition() {
|
||||
const offsetLeft: number = this.wrapPosition.left + 265; // 265 is a width of the side menu with a margin
|
||||
const popover: HTMLElement = this.popoverHost.nativeElement;
|
||||
let newLeft: number = this.keyPosition.left + (this.keyPosition.width / 2);
|
||||
|
||||
this.leftArrow = newLeft < offsetLeft;
|
||||
this.rightArrow = (newLeft + popover.offsetWidth) > offsetLeft + this.wrapPosition.width;
|
||||
|
||||
if (this.leftArrow) {
|
||||
newLeft = this.keyPosition.left;
|
||||
} else if (this.rightArrow) {
|
||||
newLeft = this.keyPosition.left - popover.offsetWidth + this.keyPosition.width;
|
||||
} else {
|
||||
newLeft -= popover.offsetWidth / 2;
|
||||
}
|
||||
|
||||
// 7 is a space between a bottom key position and a popover
|
||||
this.topPosition = this.keyPosition.top + this.keyPosition.height + 7 + window.scrollY;
|
||||
this.leftPosition = newLeft;
|
||||
}
|
||||
}
|
||||
7
packages/uhk-web/src/app/components/popover/tab/index.ts
Normal file
7
packages/uhk-web/src/app/components/popover/tab/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './tab';
|
||||
export { KeymapTabComponent } from './keymap';
|
||||
export { KeypressTabComponent } from './keypress';
|
||||
export { LayerTabComponent } from './layer';
|
||||
export { MacroTabComponent } from './macro';
|
||||
export { MouseTabComponent } from './mouse';
|
||||
export { NoneTabComponent } from './none';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './keymap-tab.component';
|
||||
@@ -0,0 +1,22 @@
|
||||
<ng-template [ngIf]="keymapOptions.length === 0">
|
||||
<span> No keymaps are available to choose from. Create a keymap first! </span>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="keymapOptions.length > 0">
|
||||
<div>
|
||||
<b>Switch to keymap:</b>
|
||||
<select2
|
||||
[data]="keymapOptions"
|
||||
[value]="selectedKeymap?.abbreviation || -1"
|
||||
(valueChanged)="onChange($event)"
|
||||
[width]="'100%'"
|
||||
></select2>
|
||||
</div>
|
||||
<div>
|
||||
<div class="empty" *ngIf="!selectedKeymap?.abbreviation">
|
||||
<img src="assets/images/base-layer--blank.svg">
|
||||
</div>
|
||||
<svg-keyboard *ngIf="selectedKeymap?.abbreviation"
|
||||
[moduleConfig]="selectedKeymap.layers[0].modules">
|
||||
</svg-keyboard>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,43 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> span {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
|
||||
b {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
select2 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user