Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8a2f704f | ||
|
|
439886d69f | ||
|
|
b2a37795e3 | ||
|
|
440db56080 | ||
|
|
337e6e6bb6 | ||
|
|
a1aeda3d35 | ||
|
|
c6a83f8c9b | ||
|
|
0f24427628 | ||
|
|
f52dc36a6a | ||
|
|
63a936968d | ||
|
|
cabfde7963 | ||
|
|
79628c2351 | ||
|
|
762fa6f8bf | ||
|
|
a258c097a9 | ||
|
|
41faa98fcd | ||
|
|
c4d7318686 | ||
|
|
9ef11eaa34 | ||
|
|
f34cb2df56 | ||
|
|
83b9f0d1e9 | ||
|
|
7d81cf0c6a | ||
|
|
82b76a9455 | ||
|
|
4ae577f936 | ||
|
|
81a83994ab | ||
|
|
1d3a3c7f5f | ||
|
|
8bb645125d | ||
|
|
9471b31a5d | ||
|
|
ffa52757c9 | ||
|
|
ee53a0df9b | ||
|
|
8e20c85e07 | ||
|
|
65ea786358 | ||
|
|
1035837b3b | ||
|
|
18fc2e6b3f | ||
|
|
fc728697d7 | ||
|
|
cdf3caee9e | ||
|
|
0a4d3a002e | ||
|
|
d11c532ea4 | ||
|
|
1ff51697b1 | ||
|
|
ab8ae31324 | ||
|
|
daa0e723b1 | ||
|
|
609aba856a | ||
|
|
a6678bd537 | ||
|
|
6c4f580fc2 | ||
|
|
ea41661c65 | ||
|
|
c553c7b63b | ||
|
|
e5988aa800 | ||
|
|
ae319c607f | ||
|
|
5d23ad1c9e | ||
|
|
55eef50da7 | ||
|
|
653465f0e0 | ||
|
|
2cf8044987 | ||
|
|
3c056a7255 | ||
|
|
091796d13c | ||
|
|
eb97dd844f | ||
|
|
17693ec8fe | ||
|
|
7c7ce8f50f | ||
|
|
e294727ac5 | ||
|
|
f29d64c803 | ||
|
|
0385b0ce29 | ||
|
|
b526274cd7 | ||
|
|
88c16af4a9 | ||
|
|
05ac9a6832 | ||
|
|
04aa5236c2 | ||
|
|
ec98e4e1c6 | ||
|
|
bb9ece494c | ||
|
|
217e6776ac | ||
|
|
2286218980 | ||
|
|
3d9c83f9f4 | ||
|
|
14ed163238 | ||
|
|
c815de0718 | ||
|
|
6a46556d9e | ||
|
|
f8f820529f | ||
|
|
cac11155e7 | ||
|
|
d20870f11e | ||
|
|
10ceb6c79d | ||
|
|
b38b6fa294 | ||
|
|
94c1d35429 | ||
|
|
e33cef4e89 | ||
|
|
9b815ed9c1 | ||
|
|
1d4bb6113c | ||
|
|
136120b831 | ||
|
|
cd299c06d6 | ||
|
|
e152a36ad7 | ||
|
|
e90544db33 | ||
|
|
7ceca202b4 | ||
|
|
ddc65aa54b | ||
|
|
13ec617d58 | ||
|
|
00c5b69129 | ||
|
|
6ccf005750 |
69
CHANGELOG.md
@@ -6,6 +6,73 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1
|
||||
|
||||
Every Agent version includes the most recent firmware version. See the [firmware changelog](https://github.com/UltimateHackingKeyboard/firmware/blob/master/CHANGELOG.md).
|
||||
|
||||
## [1.2.4] - 2018-06-21
|
||||
|
||||
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.3.1 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Replace Linux x86-64 blhost with a statically linked version which should make firmware updates work on every Linux distro.
|
||||
|
||||
## [1.2.3] - 2018-06-19
|
||||
|
||||
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.3.1 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Add checkboxes for remapping keys on all layers and/or all keymaps.
|
||||
- Add separator line between the keyboard halves.
|
||||
- Add double tap icon for switch layer actions.
|
||||
- Improve the looks and content of the tooltips of the key action popover.
|
||||
- Make the left keyboard half less likely to timeout during firmware update.
|
||||
- Terminate the firmware update process if blhost segfaults.
|
||||
- Replace the Linux x86-64 version of the blhost binary which should not make it segfault anymore.
|
||||
- Make the firmware update log shorter by listing one device per line and not repeating the list of available USB devices.
|
||||
- Make the firmware update help text shorter.
|
||||
|
||||
## [1.2.2] - 2018-05-27
|
||||
|
||||
Firmware: 8.2.**5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.3.**1** | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Offer recovery for bricked right keyboard halfs.
|
||||
- Detect when the hardware configuration of a device is invalid and display a notification. `DEVICEPROTOCOL:PATCH`
|
||||
- Check if the keyboard is in factory reset mode and if so, display a relevant instruction.
|
||||
- Only allow ASCII characters in type text macro actions.
|
||||
- Allow uploading the same file multiple times in a row.
|
||||
- Only send auto update notification when the user initiates the update.
|
||||
- Update the firmware versions on the firmware update page right after firmware updates.
|
||||
- Add a lot of useful instructions to the firmware page to help users update the firmware.
|
||||
- Add the operating system and initial device list to the firmware update log.
|
||||
- Add copy to clipboard button to the top right corner of the firmware update terminal widget.
|
||||
|
||||
## [1.2.1] - 2018-05-12
|
||||
|
||||
Firmware: 8.2.**2** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.2)] | Device Protocol: 4.3.0 | User Config: 4.0.**1** | Hardware Config: 1.0.0
|
||||
|
||||
- Match for the new USB usage page and usage number. This is critical for UHKs flashed with firmware >=8.2.2 to be recognized by Agent on OSX.
|
||||
- Make the config serializer handle long media macro actions. `USERCONFIG:PATCH`
|
||||
- Add note on the macro page explaining that the macro engine of the firmware is not ready yet.
|
||||
- Add an example to the scancode tooltip to better explain users how to invoke non-US characters.
|
||||
|
||||
## [1.2.0] - 2018-04-20
|
||||
|
||||
Firmware: 8.**2.0** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.0)] | Device Protocol: 4.**3.0** | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Tweak the default mouse speed. This was necessary because the last firmware version adjusted speed multipliers. The mouse speed can be reset via the "Reset speeds to default" button of the "Mouse speed" page.
|
||||
- Make the newly added switch-keymap.js script utilize the new UsbCommandId_SwitchKeymap, allowing for programmatic keymap switching. `DEVICEPROTOCOL:MINOR`
|
||||
|
||||
## [1.1.5] - 2018-04-10
|
||||
|
||||
Firmware: 8.1.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Don't allow to run multiple instances of Agent at the same time, but rather focus the already existing Agent window.
|
||||
|
||||
## [1.1.4] - 2018-04-09
|
||||
|
||||
Firmware: 8.1.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Handle privilege escalation gracefully on Linux even without PolicyKit.
|
||||
- Fix application icon path.
|
||||
- Replace application icon with a diagonal gradient based icon that should look better on desktop.
|
||||
- Make saving the configuration more robust, and add a configuration recovery screen.
|
||||
- Reposition the ISO key in the scancode list.
|
||||
|
||||
## [1.1.3] - 2018-04-06
|
||||
|
||||
Firmware: 8.1.**5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
@@ -40,7 +107,7 @@ Firmware: 8.1.**2** [[release](https://github.com/UltimateHackingKeyboard/firmwa
|
||||
|
||||
## [1.1.0] - 2018-01-15
|
||||
|
||||
Firmware: 8.**1**.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.0)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
Firmware: 8.**1**.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.0)] | Device Protocol: 4.**2.0** | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Only accept device, keymap, and macro names upon editing if their trimmed length is non-zero.
|
||||
- Add diagnostics USB scripts, most notably /packages/usb/{get-i2c-health,set-i2c-baud-rate}.js, some utilizing new device protocol commands and properties. `DEVICEPROTOCOL:MINOR`
|
||||
|
||||
19
README.md
@@ -5,17 +5,8 @@
|
||||
|
||||
Agent is the configuration application of the [Ultimate Hacking Keyboard](https://ultimatehackingkeyboard.com/).
|
||||
|
||||
[Give it a whirl!](http://ultimatehackingkeyboard.github.io/agent/)
|
||||
|
||||
## Two builds to rule them all
|
||||
|
||||
It's worth mentioning that Agent has two builds.
|
||||
|
||||
The **electron build** is the desktop application which is meant to be used if you have an actual UHK at hand. It starts with an opening screen which detects your UHK. You cannot get past this screen without connecting a UHK via USB.
|
||||
|
||||
The **web build** is meant to be used for demonstration purposes, so people who don't yet own a UHK can get a feel of Agent and its capabilities in their browser. Eventually, WebUSB support will be added to the web build, making it able to communicate with the UHK. Given the sandboxed nature of browsers, the web build will always lack features that the electron build offers, so this won't make the electron build obsolete.
|
||||
|
||||
The two builds share code as much as possible.
|
||||
* Try out the [web build of Agent](http://ultimatehackingkeyboard.github.io/agent/) in your browser. This is meant to be used for demonstration purposes.
|
||||
* Download the [desktop build of Agent](https://github.com/UltimateHackingKeyboard/agent/releases) from our releases page. Use this if you have an actual UHK at hand, or else you won't get past the opening screen!
|
||||
|
||||
## Building the electron application
|
||||
|
||||
@@ -33,9 +24,9 @@ For everyone else, use the appropriate package manager for your OS.
|
||||
```
|
||||
git clone git@github.com:UltimateHackingKeyboard/agent.git
|
||||
cd agent
|
||||
npm install # to install Node dependencies
|
||||
npm run build:electron # to build the agent
|
||||
npm run electron # to run the newly built agent
|
||||
npm install
|
||||
npm run build
|
||||
npm run electron
|
||||
```
|
||||
|
||||
At this point, Agent should be running on your machine.
|
||||
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 499 B |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 735 B |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 913 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.7 KiB |
2690
package-lock.json
generated
42
package.json
@@ -3,10 +3,10 @@
|
||||
"private": true,
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"main": "electron/dist/electron-main.js",
|
||||
"version": "1.1.3",
|
||||
"firmwareVersion": "8.1.5",
|
||||
"deviceProtocolVersion": "4.2.0",
|
||||
"userConfigVersion": "4.0.0",
|
||||
"version": "1.2.4",
|
||||
"firmwareVersion": "8.2.5",
|
||||
"deviceProtocolVersion": "4.3.1",
|
||||
"userConfigVersion": "4.0.1",
|
||||
"hardwareConfigVersion": "1.0.0",
|
||||
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
|
||||
"repository": {
|
||||
@@ -21,17 +21,20 @@
|
||||
"devDependencies": {
|
||||
"@types/electron-devtools-installer": "2.0.2",
|
||||
"@types/electron-settings": "3.0.0",
|
||||
"@types/fs-extra": "4.0.5",
|
||||
"@types/fs-extra": "5.0.1",
|
||||
"@types/jasmine": "2.6.0",
|
||||
"@types/jquery": "3.3.1",
|
||||
"@types/jsonfile": "4.0.1",
|
||||
"@types/lodash-es": "4.17.0",
|
||||
"@types/node": "8.0.53",
|
||||
"@types/node-hid": "0.5.2",
|
||||
"@types/request": "2.0.8",
|
||||
"@types/usb": "1.1.3",
|
||||
"autoprefixer": "6.5.3",
|
||||
"buffer": "5.0.6",
|
||||
"copyfiles": "^2.0.0",
|
||||
"check-node-version": "^3.2.0",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"copyfiles": "^2.0.0",
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "5.0.5",
|
||||
"decompress": "4.2.0",
|
||||
@@ -47,10 +50,13 @@
|
||||
"electron-updater": "2.21.4",
|
||||
"exports-loader": "0.6.3",
|
||||
"file-loader": "0.10.0",
|
||||
"fs-extra": "4.0.2",
|
||||
"fs-extra": "5.0.0",
|
||||
"gh-pages": "1.1.0",
|
||||
"jsonfile": "4.0.0",
|
||||
"lerna": "2.9.0",
|
||||
"lodash-es": "4.17.4",
|
||||
"mkdirp": "0.5.1",
|
||||
"node-hid": "0.5.7",
|
||||
"npm-run-all": "4.0.2",
|
||||
"pre-commit": "1.2.2",
|
||||
"request": "2.83.0",
|
||||
@@ -60,9 +66,9 @@
|
||||
"svg-sprite": "1.3.7",
|
||||
"ts-loader": "2.3.1",
|
||||
"ts-node": "3.0.4",
|
||||
"tslint": "5.5.0",
|
||||
"typescript": "2.5.2",
|
||||
"webpack": "2.4.1"
|
||||
"tslint": "5.9.1",
|
||||
"typescript": "2.6.2",
|
||||
"webpack": "3.10.0"
|
||||
},
|
||||
"pre-commit": [
|
||||
"precommit-msg"
|
||||
@@ -76,12 +82,13 @@
|
||||
"test:uhk-web": "lerna exec --scope uhk-web npm test",
|
||||
"lint": "run-s -scn lint:ts lint:style",
|
||||
"lint:ts": "run-p -sn lint:ts:electron-main lint:ts:electron-renderer lint:ts:web lint:ts:test-serializer lint:ts:uhk-usb",
|
||||
"lint:ts:electron-main": "tslint --type-check --project ./packages/uhk-agent/tsconfig.json",
|
||||
"lint:ts:electron-renderer": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.renderer.json",
|
||||
"lint:ts:web": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.app.json",
|
||||
"lint:ts:test-serializer": "tslint --type-check --project ./packages/test-serializer/tsconfig.json",
|
||||
"lint:ts:uhk-usb": "tslint --type-check --project ./packages/uhk-usb/tsconfig.json",
|
||||
"lint:ts:electron-main": "tslint --project ./packages/uhk-agent/tsconfig.json",
|
||||
"lint:ts:electron-renderer": "tslint --project ./packages/uhk-web/src/tsconfig.renderer.json",
|
||||
"lint:ts:web": "tslint --project ./packages/uhk-web/src/tsconfig.app.json",
|
||||
"lint:ts:test-serializer": "tslint --project ./packages/test-serializer/tsconfig.json",
|
||||
"lint:ts:uhk-usb": "tslint --project ./packages/uhk-usb/tsconfig.json",
|
||||
"lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss",
|
||||
"prebuild": "check-node-version --package",
|
||||
"build": "run-s build:common build:usb build:web build:electron",
|
||||
"build:web": "lerna exec --scope uhk-web npm run build",
|
||||
"build:electron": "cross-env AOT_BUILD=true run-s -sn build:electron:renderer build:electron:main",
|
||||
@@ -92,11 +99,14 @@
|
||||
"server:web": "lerna exec --scope uhk-web npm start",
|
||||
"server:electron": "lerna exec --scope uhk-web npm run server:renderer",
|
||||
"electron": "lerna exec --scope uhk-agent npm start",
|
||||
"electron:spe": "lerna exec --scope uhk-agent npm run electron:spe",
|
||||
"standard-version": "standard-version",
|
||||
"pack": "node ./scripts/release.js",
|
||||
"sprites": "node ./scripts/generate-svg-sprites",
|
||||
"release": "node ./scripts/release.js",
|
||||
"clean": "lerna exec rimraf ./node_modules ./dist"
|
||||
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist",
|
||||
"predeploy-gh-pages": "run-s build:web",
|
||||
"deploy-gh-pages": "gh-pages -d packages/uhk-web/dist"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
763
packages/uhk-agent/package-lock.json
generated
Normal file
@@ -17,7 +17,7 @@
|
||||
"command-line-args": "4.0.7",
|
||||
"decompress": "4.2.0",
|
||||
"decompress-bzip2": "4.0.0",
|
||||
"node-hid": "0.5.4",
|
||||
"node-hid": "0.5.7",
|
||||
"sudo-prompt": "7.0.0",
|
||||
"tmp": "0.0.33",
|
||||
"uhk-common": "^1.0.0",
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron ./dist/electron-main.js",
|
||||
"electron:spe": "electron ./dist/electron-main.js --spe",
|
||||
"build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-blhost",
|
||||
"build:usb": "electron-rebuild -w node-hid -p -m ./dist",
|
||||
"install:build-deps": "cd ./dist && npm i",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// <reference path="./custom_types/command-line-args.d.ts"/>
|
||||
|
||||
import './polyfills';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
import * as path from 'path';
|
||||
@@ -10,7 +10,7 @@ import * as url from 'url';
|
||||
import * as commandLineArgs from 'command-line-args';
|
||||
import { UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||
import { LogRegExps } from 'uhk-common';
|
||||
import { CommandLineArgs, LogRegExps } from 'uhk-common';
|
||||
import { DeviceService } from './services/device.service';
|
||||
import { logger } from './services/logger.service';
|
||||
import { AppUpdateService } from './services/app-update.service';
|
||||
@@ -18,13 +18,15 @@ import { AppService } from './services/app.service';
|
||||
import { SudoService } from './services/sudo.service';
|
||||
import { UhkBlhost } from '../../uhk-usb/src';
|
||||
import * as isDev from 'electron-is-dev';
|
||||
import { CommandLineInputs } from './models/command-line-inputs';
|
||||
|
||||
const optionDefinitions = [
|
||||
{name: 'addons', type: Boolean}
|
||||
{name: 'addons', type: Boolean},
|
||||
{name: 'spe', type: Boolean}, // simulate privilege escalation error
|
||||
// show 'Lock layer when double tapping this key' checkbox on 'Layer' tab of the config popover
|
||||
{name: 'layer-double-tap', type: Boolean}
|
||||
];
|
||||
|
||||
const options: CommandLineInputs = commandLineArgs(optionDefinitions);
|
||||
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
|
||||
|
||||
// import './dev-extension';
|
||||
// require('electron-debug')({ showDevTools: true, enabled: true });
|
||||
@@ -60,7 +62,25 @@ if (console.debug) {
|
||||
};
|
||||
}
|
||||
|
||||
const isSecondInstance = app.makeSingleInstance(function (commandLine, workingDirectory) {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore();
|
||||
}
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (isSecondInstance) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
if (isSecondInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('[Electron Main] Create new window.');
|
||||
let packagesDir;
|
||||
if (isDev) {
|
||||
@@ -79,17 +99,17 @@ function createWindow() {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: 'assets/images/agent-icon.png'
|
||||
icon: path.join(__dirname, 'renderer/assets/images/agent-app-icon.png')
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
win.maximize();
|
||||
uhkHidDeviceService = new UhkHidDevice(logger);
|
||||
uhkHidDeviceService = new UhkHidDevice(logger, options);
|
||||
uhkBlhost = new UhkBlhost(logger, packagesDir);
|
||||
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations);
|
||||
appUpdateService = new AppUpdateService(logger, win, app);
|
||||
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
|
||||
sudoService = new SudoService(logger);
|
||||
sudoService = new SudoService(logger, options);
|
||||
// and load the index.html of the app.
|
||||
|
||||
win.loadURL(url.format({
|
||||
@@ -133,13 +153,13 @@ app.on('ready', createWindow);
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
if (appUpdateService) {
|
||||
appUpdateService.saveFirtsRun();
|
||||
}
|
||||
app.exit();
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export interface CommandLineInputs {
|
||||
/**
|
||||
* addons menu visible or not
|
||||
*/
|
||||
addons?: boolean;
|
||||
/**
|
||||
* simulate privilege escalation error
|
||||
*/
|
||||
spe?: boolean;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import { IpcEvents, LogService } from 'uhk-common';
|
||||
import { MainServiceBase } from './main-service-base';
|
||||
|
||||
export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
private sendAutoUpdateNotification = false;
|
||||
|
||||
constructor(protected logService: LogService,
|
||||
protected win: Electron.BrowserWindow,
|
||||
private app: Electron.App) {
|
||||
@@ -24,16 +27,21 @@ export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
private initListeners() {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
this.logService.debug('[AppUpdateService] checking for update');
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', async (ev: any, info: UpdateInfo) => {
|
||||
this.logService.debug('[AppUpdateService] update available. Downloading started');
|
||||
await autoUpdater.downloadUpdate();
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (ev: any, info: UpdateInfo) => {
|
||||
if (this.sendAutoUpdateNotification) {
|
||||
this.logService.debug('[AppUpdateService] update not available');
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (ev: any, err: string) => {
|
||||
@@ -51,6 +59,7 @@ export class AppUpdateService extends MainServiceBase {
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (ev: any, info: UpdateInfo) => {
|
||||
this.logService.debug('[AppUpdateService] update downloaded');
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
|
||||
});
|
||||
|
||||
@@ -61,12 +70,15 @@ export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
ipcMain.on(IpcEvents.app.appStarted, () => {
|
||||
if (this.checkForUpdateAtStartup()) {
|
||||
this.sendAutoUpdateNotification = false;
|
||||
this.logService.debug('[AppUpdateService] app started. Automatically check for update.');
|
||||
this.checkForUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => {
|
||||
this.logService.debug('[AppUpdateService] checkForUpdate request from renderer process');
|
||||
this.sendAutoUpdateNotification = true;
|
||||
this.checkForUpdate();
|
||||
});
|
||||
}
|
||||
@@ -75,14 +87,22 @@ export class AppUpdateService extends MainServiceBase {
|
||||
if (isDev) {
|
||||
const msg = '[AppUpdateService] Application update is not working in dev mode.';
|
||||
this.logService.info(msg);
|
||||
|
||||
if (this.sendAutoUpdateNotification) {
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFirstRun()) {
|
||||
const msg = '[AppUpdateService] Application update is skipping at first run.';
|
||||
this.logService.info(msg);
|
||||
|
||||
if (this.sendAutoUpdateNotification) {
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserWindow, ipcMain, shell } from 'electron';
|
||||
import { ipcMain, shell } from 'electron';
|
||||
import { UhkHidDevice } from 'uhk-usb';
|
||||
|
||||
import { AppStartInfo, IpcEvents, LogService } from 'uhk-common';
|
||||
@@ -22,13 +22,15 @@ export class AppService extends MainServiceBase {
|
||||
|
||||
private async handleAppStartInfo(event: Electron.Event) {
|
||||
this.logService.info('[AppService] getAppStartInfo');
|
||||
|
||||
const deviceConnectionState = this.uhkHidDeviceService.getDeviceConnectionState();
|
||||
const response: AppStartInfo = {
|
||||
commandLineArgs: {
|
||||
addons: this.options.addons || false
|
||||
addons: this.options.addons || false,
|
||||
layerDoubleTap: this.options['layer-double-tap'] || false
|
||||
},
|
||||
deviceConnected: this.uhkHidDeviceService.deviceConnected(),
|
||||
hasPermission: this.uhkHidDeviceService.hasPermission()
|
||||
deviceConnected: deviceConnectionState.connected,
|
||||
hasPermission: deviceConnectionState.hasPermission,
|
||||
bootloaderActive: deviceConnectionState.bootloaderActive
|
||||
};
|
||||
this.logService.info('[AppService] getAppStartInfo response:', response);
|
||||
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { ConfigurationReply, DeviceConnectionState, HardwareModules, IpcEvents, IpcResponse, LogService } from 'uhk-common';
|
||||
import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
import {
|
||||
ConfigurationReply,
|
||||
DeviceConnectionState,
|
||||
FirmwareUpgradeIpcResponse,
|
||||
getHardwareConfigFromDeviceResponse,
|
||||
HardwareModules,
|
||||
IpcEvents,
|
||||
IpcResponse,
|
||||
LogService,
|
||||
mapObjectToUserConfigBinaryBuffer,
|
||||
SaveUserConfigurationData
|
||||
} from 'uhk-common';
|
||||
import { deviceConnectionStateComparer, snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { emptyDir } from 'fs-extra';
|
||||
@@ -14,6 +25,7 @@ import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import { saveTmpFirmware } from '../util/save-extract-firmware';
|
||||
import { TmpFirmware } from '../models/tmp-firmware';
|
||||
import { QueueManager } from './queue-manager';
|
||||
import { backupUserConfiguration, getBackupUserConfigurationContent } from '../util/backup-user-confoguration';
|
||||
|
||||
/**
|
||||
* IpcMain pair of the UHK Communication
|
||||
@@ -60,6 +72,15 @@ export class DeviceService {
|
||||
|
||||
ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this));
|
||||
|
||||
ipcMain.on(IpcEvents.device.recoveryDevice, (...args: any[]) => {
|
||||
this.queueManager.add({
|
||||
method: this.recoveryDevice,
|
||||
bind: this,
|
||||
params: args,
|
||||
asynchronous: true
|
||||
});
|
||||
});
|
||||
|
||||
logService.debug('[DeviceService] init success');
|
||||
}
|
||||
|
||||
@@ -73,15 +94,16 @@ export class DeviceService {
|
||||
try {
|
||||
await this.device.waitUntilKeyboardBusy();
|
||||
const result = await this.operations.loadConfigurations();
|
||||
const modules: HardwareModules = {
|
||||
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
|
||||
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
|
||||
};
|
||||
const modules: HardwareModules = await this.getHardwareModules(false);
|
||||
|
||||
const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration);
|
||||
const uniqueId = hardwareConfig.uniqueId;
|
||||
|
||||
response = {
|
||||
success: true,
|
||||
...result,
|
||||
modules
|
||||
modules,
|
||||
backupConfiguration: await getBackupUserConfigurationContent(this.logService, uniqueId)
|
||||
};
|
||||
} catch (error) {
|
||||
response = {
|
||||
@@ -95,16 +117,36 @@ export class DeviceService {
|
||||
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
|
||||
}
|
||||
|
||||
public async getHardwareModules(catchError: boolean): Promise<HardwareModules> {
|
||||
try {
|
||||
await this.device.waitUntilKeyboardBusy();
|
||||
|
||||
return {
|
||||
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
|
||||
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (!catchError) {
|
||||
return err;
|
||||
}
|
||||
|
||||
this.logService.error('[DeviceService] Read hardware modules information failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.stopPollTimer();
|
||||
this.logService.info('[DeviceService] Device connection checker stopped.');
|
||||
}
|
||||
|
||||
public async updateFirmware(event: Electron.Event, args?: Array<string>): Promise<void> {
|
||||
const response = new IpcResponse();
|
||||
const response = new FirmwareUpgradeIpcResponse();
|
||||
|
||||
let firmwarePathData: TmpFirmware;
|
||||
|
||||
try {
|
||||
this.device.resetDeviceCache();
|
||||
this.stopPollTimer();
|
||||
|
||||
if (args && args.length > 0) {
|
||||
@@ -118,10 +160,12 @@ export class DeviceService {
|
||||
}
|
||||
|
||||
response.success = true;
|
||||
response.modules = await this.getHardwareModules(false);
|
||||
} catch (error) {
|
||||
const err = {message: error.message, stack: error.stack};
|
||||
this.logService.error('[DeviceService] updateFirmware error', err);
|
||||
|
||||
response.modules = await this.getHardwareModules(true);
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
@@ -129,6 +173,35 @@ export class DeviceService {
|
||||
await emptyDir(firmwarePathData.tmpDirectory.name);
|
||||
}
|
||||
|
||||
await snooze(500);
|
||||
|
||||
this.pollUhkDevice();
|
||||
|
||||
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
|
||||
}
|
||||
|
||||
public async recoveryDevice(event: Electron.Event): Promise<void> {
|
||||
const response = new FirmwareUpgradeIpcResponse();
|
||||
|
||||
try {
|
||||
this.stopPollTimer();
|
||||
|
||||
await this.operations.updateRightFirmware();
|
||||
|
||||
await snooze(500);
|
||||
|
||||
this.pollUhkDevice();
|
||||
|
||||
response.modules = await this.getHardwareModules(false);
|
||||
response.success = true;
|
||||
} catch (error) {
|
||||
const err = {message: error.message, stack: error.stack};
|
||||
this.logService.error('[DeviceService] updateFirmware error', err);
|
||||
|
||||
response.modules = await this.getHardwareModules(true);
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
await snooze(500);
|
||||
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
|
||||
}
|
||||
@@ -146,26 +219,24 @@ export class DeviceService {
|
||||
|
||||
this.pollTimer$ = Observable.interval(1000)
|
||||
.startWith(0)
|
||||
.map(() => this.device.deviceConnected())
|
||||
.distinctUntilChanged()
|
||||
.do((connected: boolean) => {
|
||||
const response: DeviceConnectionState = {
|
||||
connected,
|
||||
hasPermission: this.device.hasPermission()
|
||||
};
|
||||
|
||||
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, response);
|
||||
this.logService.info('[DeviceService] Device connection state changed to:', response);
|
||||
.map(() => this.device.getDeviceConnectionState())
|
||||
.distinctUntilChanged<DeviceConnectionState>(deviceConnectionStateComparer)
|
||||
.do((state: DeviceConnectionState) => {
|
||||
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
|
||||
this.logService.info('[DeviceService] Device connection state changed to:', state);
|
||||
})
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private async saveUserConfiguration(event: Electron.Event, args: Array<string>): Promise<void> {
|
||||
const response = new IpcResponse();
|
||||
const json = args[0];
|
||||
const data: SaveUserConfigurationData = JSON.parse(args[0]);
|
||||
|
||||
try {
|
||||
await this.operations.saveUserConfiguration(json);
|
||||
await backupUserConfiguration(data);
|
||||
|
||||
const buffer = mapObjectToUserConfigBinaryBuffer(data.configuration);
|
||||
await this.operations.saveUserConfiguration(buffer);
|
||||
|
||||
response.success = true;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import * as sudo from 'sudo-prompt';
|
||||
import { dirSync } from 'tmp';
|
||||
import { emptyDir, copy } from 'fs-extra';
|
||||
|
||||
import { IpcEvents, LogService, IpcResponse } from 'uhk-common';
|
||||
import { CommandLineArgs, IpcEvents, LogService, IpcResponse } from 'uhk-common';
|
||||
|
||||
export class SudoService {
|
||||
private rootDir: string;
|
||||
|
||||
constructor(private logService: LogService) {
|
||||
constructor(private logService: LogService,
|
||||
private options: CommandLineArgs) {
|
||||
if (isDev) {
|
||||
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../../../');
|
||||
} else {
|
||||
@@ -21,6 +22,19 @@ export class SudoService {
|
||||
}
|
||||
|
||||
private async setPrivilege(event: Electron.Event) {
|
||||
if (this.options.spe) {
|
||||
const error = new Error('No polkit authentication agent found.');
|
||||
this.logService.error('[SudoService] Simulate privilege escalation error ', error);
|
||||
|
||||
const response = new IpcResponse();
|
||||
response.success = false;
|
||||
response.error = {message: error.message};
|
||||
|
||||
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
await this.setPrivilegeOnLinux(event);
|
||||
@@ -55,7 +69,7 @@ export class SudoService {
|
||||
if (error) {
|
||||
this.logService.error('[SudoService] Error when set privilege: ', error);
|
||||
response.success = false;
|
||||
response.error = error;
|
||||
response.error = {message: error.message};
|
||||
} else {
|
||||
response.success = true;
|
||||
}
|
||||
|
||||
32
packages/uhk-agent/src/util/backup-user-confoguration.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { app } from 'electron';
|
||||
import { LogService, UserConfiguration, SaveUserConfigurationData } from 'uhk-common';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
export const getBackupUserConfigurationPath = (uniqueId: number): string => {
|
||||
const appDataDir = app.getPath('userData');
|
||||
|
||||
return path.join(appDataDir, `${uniqueId}.json`);
|
||||
};
|
||||
|
||||
export const backupUserConfiguration = (data: SaveUserConfigurationData): Promise<void> => {
|
||||
const backupFilePath = getBackupUserConfigurationPath(data.uniqueId);
|
||||
return fs.writeJSON(backupFilePath, data.configuration, {spaces: 2});
|
||||
};
|
||||
|
||||
export const getBackupUserConfigurationContent = async (logService: LogService, uniqueId: number): Promise<UserConfiguration> => {
|
||||
try {
|
||||
const backupFilePath = getBackupUserConfigurationPath(uniqueId);
|
||||
|
||||
if (await fs.pathExists(backupFilePath)) {
|
||||
const json = await fs.readJSON(backupFilePath);
|
||||
new UserConfiguration().fromJsonObject(json);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logService.error('Can not load backup user configuration for device', {uniqueId, error});
|
||||
}
|
||||
};
|
||||
170
packages/uhk-common/package-lock.json
generated
@@ -18,11 +18,11 @@
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
|
||||
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"requires": {
|
||||
"color-convert": "1.9.0"
|
||||
"color-convert": "1.9.1"
|
||||
}
|
||||
},
|
||||
"arrify": {
|
||||
@@ -36,9 +36,9 @@
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -55,13 +55,13 @@
|
||||
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
|
||||
"integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
|
||||
"integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
|
||||
"requires": {
|
||||
"ansi-styles": "3.2.0",
|
||||
"ansi-styles": "3.2.1",
|
||||
"escape-string-regexp": "1.0.5",
|
||||
"supports-color": "4.4.0"
|
||||
"supports-color": "5.3.0"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz",
|
||||
"integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
|
||||
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
@@ -122,15 +122,15 @@
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
|
||||
"requires": {
|
||||
"lru-cache": "4.1.1",
|
||||
"lru-cache": "4.1.2",
|
||||
"shebang-command": "1.2.0",
|
||||
"which": "1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
|
||||
"integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz",
|
||||
"integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==",
|
||||
"requires": {
|
||||
"pseudomap": "1.0.2",
|
||||
"yallist": "2.1.2"
|
||||
@@ -144,9 +144,9 @@
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"diff": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz",
|
||||
"integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA=="
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.1",
|
||||
@@ -271,9 +271,9 @@
|
||||
"integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
|
||||
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
},
|
||||
"homedir-polyfill": {
|
||||
"version": "1.0.1",
|
||||
@@ -284,9 +284,9 @@
|
||||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
|
||||
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg=="
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz",
|
||||
"integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw=="
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
@@ -382,7 +382,7 @@
|
||||
"requires": {
|
||||
"jasmine": "2.8.0",
|
||||
"ts-node": "3.3.0",
|
||||
"typescript": "2.5.3",
|
||||
"typescript": "2.8.1",
|
||||
"yargs": "8.0.2"
|
||||
}
|
||||
},
|
||||
@@ -432,29 +432,29 @@
|
||||
"integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz",
|
||||
"integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y="
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz",
|
||||
"integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g=="
|
||||
},
|
||||
"mem": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
|
||||
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
|
||||
"requires": {
|
||||
"mimic-fn": "1.1.0"
|
||||
"mimic-fn": "1.2.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
|
||||
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.8"
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
@@ -472,10 +472,10 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
|
||||
"requires": {
|
||||
"hosted-git-info": "2.5.0",
|
||||
"hosted-git-info": "2.6.0",
|
||||
"is-builtin-module": "1.0.0",
|
||||
"semver": "5.4.1",
|
||||
"validate-npm-package-license": "3.0.1"
|
||||
"semver": "5.5.0",
|
||||
"validate-npm-package-license": "3.0.3"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
@@ -1942,18 +1942,26 @@
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
|
||||
"integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw="
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
|
||||
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
|
||||
"requires": {
|
||||
"p-try": "1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"requires": {
|
||||
"p-limit": "1.1.0"
|
||||
"p-limit": "1.2.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||
@@ -2030,9 +2038,9 @@
|
||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
|
||||
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
@@ -2076,22 +2084,32 @@
|
||||
}
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
|
||||
"integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
|
||||
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
|
||||
"requires": {
|
||||
"spdx-license-ids": "1.2.2"
|
||||
"spdx-expression-parse": "3.0.0",
|
||||
"spdx-license-ids": "3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-exceptions": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
|
||||
"integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg=="
|
||||
},
|
||||
"spdx-expression-parse": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
|
||||
"integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
|
||||
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
|
||||
"requires": {
|
||||
"spdx-exceptions": "2.1.0",
|
||||
"spdx-license-ids": "3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
|
||||
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
|
||||
"integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
@@ -2146,11 +2164,11 @@
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
|
||||
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
|
||||
"integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
|
||||
"requires": {
|
||||
"has-flag": "2.0.0"
|
||||
"has-flag": "3.0.0"
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
@@ -2159,14 +2177,14 @@
|
||||
"integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=",
|
||||
"requires": {
|
||||
"arrify": "1.0.1",
|
||||
"chalk": "2.1.0",
|
||||
"diff": "3.4.0",
|
||||
"make-error": "1.3.0",
|
||||
"chalk": "2.3.2",
|
||||
"diff": "3.5.0",
|
||||
"make-error": "1.3.4",
|
||||
"minimist": "1.2.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"source-map-support": "0.4.18",
|
||||
"tsconfig": "6.0.0",
|
||||
"v8flags": "3.0.1",
|
||||
"v8flags": "3.0.2",
|
||||
"yn": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -2202,9 +2220,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz",
|
||||
"integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz",
|
||||
"integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg=="
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.6.0",
|
||||
@@ -2212,20 +2230,20 @@
|
||||
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag="
|
||||
},
|
||||
"v8flags": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz",
|
||||
"integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.2.tgz",
|
||||
"integrity": "sha512-6sgSKoFw1UpUPd3cFdF7QGnrH6tDeBgW1F3v9gy8gLY0mlbiBXq8soy8aQpY6xeeCjH5K+JvC62Acp7gtl7wWA==",
|
||||
"requires": {
|
||||
"homedir-polyfill": "1.0.1"
|
||||
}
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
|
||||
"integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz",
|
||||
"integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==",
|
||||
"requires": {
|
||||
"spdx-correct": "1.0.2",
|
||||
"spdx-expression-parse": "1.0.4"
|
||||
"spdx-correct": "3.0.0",
|
||||
"spdx-expression-parse": "3.0.0"
|
||||
}
|
||||
},
|
||||
"walkdir": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Common Library contains the common code for uhk-agent (electron-main) and web (electron-renderer) modules",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -41,6 +41,7 @@ export class HardwareConfiguration {
|
||||
}
|
||||
|
||||
fromBinary(buffer: UhkBuffer): HardwareConfiguration {
|
||||
try {
|
||||
this.signature = buffer.readString();
|
||||
this.majorVersion = buffer.readUInt8();
|
||||
this.minorVersion = buffer.readUInt8();
|
||||
@@ -51,6 +52,9 @@ export class HardwareConfiguration {
|
||||
this.isVendorModeOn = buffer.readBoolean();
|
||||
this.isIso = buffer.readBoolean();
|
||||
return this;
|
||||
} catch (e) {
|
||||
throw new Error('Please power cycle your keyboard (Invalid hardware configuration: Index out of bounds)');
|
||||
}
|
||||
}
|
||||
|
||||
toJsonObject(): any {
|
||||
|
||||
@@ -13,7 +13,7 @@ export enum KeystrokeActionFlag {
|
||||
|
||||
const KEYSTROKE_ACTION_FLAG_LENGTH = 3;
|
||||
|
||||
interface JsonObjectKeystrokeAction {
|
||||
export interface JsonObjectKeystrokeAction {
|
||||
keyActionType: string;
|
||||
scancode?: number;
|
||||
modifierMask?: number;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { binaryDefaultHelper, jsonDefaultHelper } from '../../../../test/serializer-test-helper';
|
||||
import { SwitchLayerAction } from './switch-layer-action';
|
||||
import { SwitchLayerAction, SwitchLayerMode } from './switch-layer-action';
|
||||
import { keyActionType } from './key-action';
|
||||
|
||||
// TODO: Add null, undefined, empty object, empty buffer test cases
|
||||
describe('switch-layer-action', () => {
|
||||
const action = new SwitchLayerAction(<SwitchLayerAction>{layer: 0, isLayerToggleable: false});
|
||||
const action = new SwitchLayerAction(<SwitchLayerAction>{layer: 0, switchLayerMode: SwitchLayerMode.hold});
|
||||
|
||||
it('should be instantiate', () => {
|
||||
expect(new SwitchLayerAction()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return <SwitchLayerAction layer="0" toggle="false">', () => {
|
||||
expect(action.toString()).toEqual('<SwitchLayerAction layer="0" toggle="false">');
|
||||
it('should return <SwitchLayerAction layer="0" switchLayerMode="hold">', () => {
|
||||
expect(action.toString()).toEqual('<SwitchLayerAction layer="0" switchLayerMode="hold">');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,4 +31,20 @@ describe('switch-layer-action', () => {
|
||||
binaryDefaultHelper(action);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backward compatibility of the "toggle" property ', () => {
|
||||
it('should map toggle=false to SwitchLayerMode.holdAndDoubleTapToggle', () => {
|
||||
const oldAction = new SwitchLayerAction();
|
||||
oldAction.fromJsonObject({keyActionType: keyActionType.SwitchLayerAction, layer: 0, toggle: false});
|
||||
|
||||
expect(oldAction.switchLayerMode).toEqual(SwitchLayerMode.holdAndDoubleTapToggle);
|
||||
});
|
||||
|
||||
it('should map toggle=true to SwitchLayerMode.toggle', () => {
|
||||
const oldAction = new SwitchLayerAction();
|
||||
oldAction.fromJsonObject({keyActionType: keyActionType.SwitchLayerAction, layer: 0, toggle: true});
|
||||
|
||||
expect(oldAction.switchLayerMode).toEqual(SwitchLayerMode.toggle);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,48 @@ export enum LayerName {
|
||||
mouse
|
||||
}
|
||||
|
||||
export enum SwitchLayerMode {
|
||||
holdAndDoubleTapToggle = 'holdAndDoubleTapToggle',
|
||||
toggle = 'toggle',
|
||||
hold = 'hold'
|
||||
}
|
||||
|
||||
export const mapSwitchLayerModeToNumber = (switchLayerMode: SwitchLayerMode): number => {
|
||||
switch (switchLayerMode) {
|
||||
case SwitchLayerMode.holdAndDoubleTapToggle:
|
||||
return 0;
|
||||
|
||||
case SwitchLayerMode.toggle:
|
||||
return 1;
|
||||
|
||||
case SwitchLayerMode.hold:
|
||||
return 2;
|
||||
|
||||
default:
|
||||
throw new Error(`Can not map ${switchLayerMode} to number`);
|
||||
}
|
||||
};
|
||||
|
||||
export const mapNumberToSwitchLayerMode = (value: number): SwitchLayerMode => {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return SwitchLayerMode.holdAndDoubleTapToggle;
|
||||
|
||||
case 1:
|
||||
return SwitchLayerMode.toggle;
|
||||
|
||||
case 2:
|
||||
return SwitchLayerMode.hold;
|
||||
|
||||
default:
|
||||
throw new Error(`Can not map "${value}" to SwitchLayerMode`);
|
||||
}
|
||||
};
|
||||
|
||||
export class SwitchLayerAction extends KeyAction {
|
||||
|
||||
isLayerToggleable: boolean;
|
||||
@assertEnum(SwitchLayerMode)
|
||||
switchLayerMode: SwitchLayerMode;
|
||||
|
||||
@assertEnum(LayerName)
|
||||
layer: LayerName;
|
||||
@@ -20,21 +59,29 @@ export class SwitchLayerAction extends KeyAction {
|
||||
if (!other) {
|
||||
return;
|
||||
}
|
||||
this.isLayerToggleable = other.isLayerToggleable;
|
||||
this.switchLayerMode = other.switchLayerMode;
|
||||
this.layer = other.layer;
|
||||
}
|
||||
|
||||
fromJsonObject(jsonObject: any): SwitchLayerAction {
|
||||
this.assertKeyActionType(jsonObject);
|
||||
this.layer = LayerName[<string>jsonObject.layer];
|
||||
this.isLayerToggleable = jsonObject.toggle;
|
||||
|
||||
// Backward compatibility when "switchLayerMode" was a boolean type as "toggle"
|
||||
if (typeof jsonObject.toggle === 'boolean') {
|
||||
this.switchLayerMode = jsonObject.toggle ? SwitchLayerMode.toggle : SwitchLayerMode.holdAndDoubleTapToggle;
|
||||
}
|
||||
else {
|
||||
this.switchLayerMode = jsonObject.switchLayerMode;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fromBinary(buffer: UhkBuffer): SwitchLayerAction {
|
||||
this.readAndAssertKeyActionId(buffer);
|
||||
this.layer = buffer.readUInt8();
|
||||
this.isLayerToggleable = buffer.readBoolean();
|
||||
this.switchLayerMode = mapNumberToSwitchLayerMode(buffer.readUInt8());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -42,18 +89,18 @@ export class SwitchLayerAction extends KeyAction {
|
||||
return {
|
||||
keyActionType: keyActionType.SwitchLayerAction,
|
||||
layer: LayerName[this.layer],
|
||||
toggle: this.isLayerToggleable
|
||||
switchLayerMode: this.switchLayerMode
|
||||
};
|
||||
}
|
||||
|
||||
toBinary(buffer: UhkBuffer) {
|
||||
buffer.writeUInt8(KeyActionId.SwitchLayerAction);
|
||||
buffer.writeUInt8(this.layer);
|
||||
buffer.writeBoolean(this.isLayerToggleable);
|
||||
buffer.writeUInt8(mapSwitchLayerModeToNumber(this.switchLayerMode));
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<SwitchLayerAction layer="${this.layer}" toggle="${this.isLayerToggleable}">`;
|
||||
return `<SwitchLayerAction layer="${this.layer}" switchLayerMode="${this.switchLayerMode}">`;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
|
||||
@@ -127,7 +127,7 @@ export class Keymap {
|
||||
if (currentLayerId - 1 === baseKeyAction.layer) {
|
||||
if (currentKeyAction instanceof SwitchLayerAction) {
|
||||
if (currentKeyAction.layer === baseKeyAction.layer &&
|
||||
currentKeyAction.isLayerToggleable === baseKeyAction.isLayerToggleable) {
|
||||
currentKeyAction.switchLayerMode === baseKeyAction.switchLayerMode) {
|
||||
continue;
|
||||
}
|
||||
// tslint:disable-next-line: max-line-length
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { assertEnum, assertUInt8 } from '../../assert';
|
||||
import { assertEnum, assertUInt8, assertUInt16 } from '../../assert';
|
||||
import { UhkBuffer } from '../../uhk-buffer';
|
||||
import { KeyModifiers } from '../key-modifiers';
|
||||
import { MacroAction, MacroActionId, MacroKeySubAction, macroActionType } from './macro-action';
|
||||
import { KeystrokeType } from '../key-action';
|
||||
|
||||
interface JsObjectKeyMacroAction {
|
||||
export interface JsObjectKeyMacroAction {
|
||||
macroActionType: string;
|
||||
action: string;
|
||||
type?: string;
|
||||
@@ -20,12 +20,24 @@ export class KeyMacroAction extends MacroAction {
|
||||
@assertEnum(KeystrokeType)
|
||||
type: KeystrokeType;
|
||||
|
||||
@assertUInt8
|
||||
scancode: number;
|
||||
|
||||
@assertUInt8
|
||||
modifierMask: number;
|
||||
|
||||
@assertUInt16
|
||||
private _scancode: number;
|
||||
|
||||
set scancode(scancode: number) {
|
||||
this._scancode = scancode;
|
||||
if (this.type !== KeystrokeType.shortMedia && this.type !== KeystrokeType.longMedia) {
|
||||
return;
|
||||
}
|
||||
this.type = scancode < 256 ? KeystrokeType.shortMedia : KeystrokeType.longMedia;
|
||||
}
|
||||
|
||||
get scancode() {
|
||||
return this._scancode;
|
||||
}
|
||||
|
||||
constructor(other?: KeyMacroAction) {
|
||||
super();
|
||||
if (!other) {
|
||||
@@ -33,7 +45,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
}
|
||||
this.action = other.action;
|
||||
this.type = other.type;
|
||||
this.scancode = other.scancode;
|
||||
this._scancode = other._scancode;
|
||||
this.modifierMask = other.modifierMask;
|
||||
}
|
||||
|
||||
@@ -45,7 +57,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
} else {
|
||||
this.type = KeystrokeType[jsObject.type];
|
||||
}
|
||||
this.scancode = jsObject.scancode;
|
||||
this._scancode = jsObject.scancode;
|
||||
this.modifierMask = jsObject.modifierMask;
|
||||
return this;
|
||||
}
|
||||
@@ -58,7 +70,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
this.type = keyMacroType & 0b11;
|
||||
keyMacroType >>= 2;
|
||||
if (keyMacroType & 0b10) {
|
||||
this.scancode = buffer.readUInt8();
|
||||
this._scancode = this.type === KeystrokeType.longMedia ? buffer.readUInt16() : buffer.readUInt8();
|
||||
}
|
||||
if (keyMacroType & 0b01) {
|
||||
this.modifierMask = buffer.readUInt8();
|
||||
@@ -78,7 +90,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
} else {
|
||||
jsObject.type = KeystrokeType[this.type];
|
||||
}
|
||||
jsObject.scancode = this.scancode;
|
||||
jsObject.scancode = this._scancode;
|
||||
}
|
||||
|
||||
if (this.hasModifiers()) {
|
||||
@@ -98,15 +110,19 @@ export class KeyMacroAction extends MacroAction {
|
||||
|
||||
buffer.writeUInt8(keyMacroType);
|
||||
if (this.hasScancode()) {
|
||||
if (this.type === KeystrokeType.longMedia) {
|
||||
buffer.writeUInt16(this.scancode);
|
||||
} else {
|
||||
buffer.writeUInt8(this.scancode);
|
||||
}
|
||||
}
|
||||
if (this.hasModifiers()) {
|
||||
buffer.writeUInt8(this.modifierMask);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<KeyMacroAction action="${this.action}" scancode="${this.scancode}" modifierMask="${this.modifierMask}">`;
|
||||
return `<KeyMacroAction action="${this.action}" scancode="${this._scancode}" modifierMask="${this.modifierMask}">`;
|
||||
}
|
||||
|
||||
isModifierActive(modifier: KeyModifiers): boolean {
|
||||
@@ -114,7 +130,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
}
|
||||
|
||||
hasScancode(): boolean {
|
||||
return !!this.scancode;
|
||||
return !!this._scancode;
|
||||
}
|
||||
|
||||
hasModifiers(): boolean {
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -121,7 +121,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -151,7 +151,7 @@ describe('keymap', () => {
|
||||
|
||||
expect(inputUserConfig.toJsonObject()).toEqual(expectedJsonConfig);
|
||||
// tslint:disable-next-line: max-line-length
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is not switch layer. <KeystrokeAction type="basic" scancode="44"> will be override with <SwitchLayerAction layer="0" toggle="false">');
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is not switch layer. <KeystrokeAction type="basic" scancode="44"> will be override with <SwitchLayerAction layer="0" switchLayerMode="holdAndDoubleTapToggle">');
|
||||
});
|
||||
|
||||
it('should normalize SwitchLayerAction if non base layer action is other SwitchLayerAction', () => {
|
||||
@@ -262,7 +262,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -274,7 +274,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -304,6 +304,6 @@ describe('keymap', () => {
|
||||
|
||||
expect(inputUserConfig.toJsonObject()).toEqual(expectedJsonConfig);
|
||||
// tslint:disable-next-line: max-line-length
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is different switch layer. <SwitchLayerAction layer="1" toggle="false"> will be override with <SwitchLayerAction layer="0" toggle="false">');
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is different switch layer. <SwitchLayerAction layer="1" switchLayerMode="holdAndDoubleTapToggle"> will be override with <SwitchLayerAction layer="0" switchLayerMode="holdAndDoubleTapToggle">');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ export enum MouseButtons {
|
||||
Right = 1 << 2
|
||||
}
|
||||
|
||||
interface JsObjectMouseButtonMacroAction {
|
||||
export interface JsObjectMouseButtonMacroAction {
|
||||
macroActionType: string;
|
||||
action: string;
|
||||
mouseButtonsMask?: number;
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
{
|
||||
"id": "29",
|
||||
"text": "Z"
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"text": "| ISO"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -314,10 +318,6 @@
|
||||
"id": "69",
|
||||
"text": "F12"
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"text": "| ISO"
|
||||
},
|
||||
{
|
||||
"id": "104",
|
||||
"text": "F13"
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface AppStartInfo {
|
||||
commandLineArgs: CommandLineArgs;
|
||||
deviceConnected: boolean;
|
||||
hasPermission: boolean;
|
||||
bootloaderActive: boolean;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
export interface CommandLineArgs {
|
||||
addons: boolean;
|
||||
/**
|
||||
* addons menu visible or not
|
||||
*/
|
||||
addons?: boolean;
|
||||
/**
|
||||
* simulate privilege escalation error
|
||||
*/
|
||||
spe?: boolean;
|
||||
/**
|
||||
* show 'Lock layer when double tapping this key' checkbox on 'Layer' tab of the config popover
|
||||
* if it false the checkbox invisible and the value of the checkbox = true
|
||||
*/
|
||||
layerDoubleTap?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HardwareModules } from './hardware-modules';
|
||||
import { UserConfiguration } from '../config-serializer/config-items';
|
||||
|
||||
export interface ConfigurationReply {
|
||||
success: boolean;
|
||||
@@ -6,4 +7,5 @@ export interface ConfigurationReply {
|
||||
hardwareConfiguration?: string;
|
||||
modules?: HardwareModules;
|
||||
error?: string;
|
||||
backupConfiguration?: UserConfiguration;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface DeviceConnectionState {
|
||||
connected: boolean;
|
||||
hasPermission: boolean;
|
||||
bootloaderActive: boolean;
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './version-information';
|
||||
export * from './device-connection-state';
|
||||
export * from './hardware-modules';
|
||||
export * from './hardware-module-info';
|
||||
export * from './save-user-configuration-data';
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { HardwareModules } from './hardware-modules';
|
||||
|
||||
export class IpcResponse {
|
||||
success: boolean;
|
||||
error?: { message: string };
|
||||
}
|
||||
|
||||
export class FirmwareUpgradeIpcResponse extends IpcResponse {
|
||||
modules?: HardwareModules;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SaveUserConfigurationData {
|
||||
uniqueId: number;
|
||||
configuration: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export namespace Constants {
|
||||
export const AGENT_GITHUB_URL = 'https://github.com/UltimateHackingKeyboard/agent';
|
||||
export const FIRMWARE_GITHUB_ISSUE_URL = 'https://github.com/UltimateHackingKeyboard/agent/issues/new';
|
||||
}
|
||||
|
||||
38
packages/uhk-common/src/util/helpers.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HardwareConfiguration, UhkBuffer, UserConfiguration } from '../config-serializer';
|
||||
|
||||
export const getHardwareConfigFromDeviceResponse = (json: string): HardwareConfiguration => {
|
||||
const data = JSON.parse(json);
|
||||
const hardwareConfig = new HardwareConfiguration();
|
||||
hardwareConfig.fromBinary(UhkBuffer.fromArray(data));
|
||||
|
||||
if (hardwareConfig.signature === 'FTY') {
|
||||
throw Error('The device is in factory reset mode. Power-cycle the device to use it with Agent!');
|
||||
}
|
||||
|
||||
if (hardwareConfig.signature !== 'UHK') {
|
||||
throw Error('Please power cycle your keyboard (Invalid hardware configuration: Invalid signature)');
|
||||
}
|
||||
|
||||
return hardwareConfig;
|
||||
};
|
||||
|
||||
export const getUserConfigFromDeviceResponse = (json: string): UserConfiguration => {
|
||||
const data = JSON.parse(json);
|
||||
const userConfig = new UserConfiguration();
|
||||
userConfig.fromBinary(UhkBuffer.fromArray(data));
|
||||
|
||||
if (userConfig.userConfigMajorVersion > 0) {
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
throw Error('Invalid user configuration');
|
||||
};
|
||||
|
||||
export const mapObjectToUserConfigBinaryBuffer = (obj: any): Buffer => {
|
||||
const configuration = new UserConfiguration();
|
||||
configuration.fromJsonObject(obj);
|
||||
const buffer = new UhkBuffer();
|
||||
configuration.toBinary(buffer);
|
||||
|
||||
return buffer.getBufferContent();
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
export { IpcEvents } from './ipcEvents';
|
||||
export * from './log';
|
||||
export * from './constants';
|
||||
export * from './helpers';
|
||||
export * from './is-equal-array';
|
||||
|
||||
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
|
||||
export function camelCaseToSentence(camelCasedText: string): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class App {
|
||||
export class App {
|
||||
public static readonly appStarted = 'app-started';
|
||||
public static readonly getAppStartInfo = 'app-get-start-info';
|
||||
public static readonly getAppStartInfoReply = 'app-get-start-info-reply';
|
||||
@@ -6,7 +6,7 @@ class App {
|
||||
public static readonly openUrl = 'open-url';
|
||||
}
|
||||
|
||||
class AutoUpdate {
|
||||
export class AutoUpdate {
|
||||
public static readonly checkingForUpdate = 'checking-for-update';
|
||||
public static readonly updateAvailable = 'update-available';
|
||||
public static readonly updateNotAvailable = 'update-not-available';
|
||||
@@ -18,7 +18,7 @@ class AutoUpdate {
|
||||
public static readonly checkForUpdateNotAvailable = 'check-for-update-not-available';
|
||||
}
|
||||
|
||||
class Device {
|
||||
export class Device {
|
||||
public static readonly setPrivilegeOnLinux = 'set-privilege-on-linux';
|
||||
public static readonly setPrivilegeOnLinuxReply = 'set-privilege-on-linux-reply';
|
||||
public static readonly deviceConnectionStateChanged = 'device-connection-state-changed';
|
||||
@@ -29,6 +29,7 @@ class Device {
|
||||
public static readonly updateFirmware = 'device-update-firmware';
|
||||
public static readonly updateFirmwareReply = 'device-update-firmware-reply';
|
||||
public static readonly startConnectionPoller = 'device-start-connection-poller';
|
||||
public static readonly recoveryDevice = 'device-recovery';
|
||||
}
|
||||
|
||||
export class IpcEvents {
|
||||
|
||||
15
packages/uhk-common/src/util/is-equal-array.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const isEqualArray = (arr1: Array<any>, arr2: Array<any>): boolean => {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const a of arr1) {
|
||||
if (!arr2.some(b => isEqual(a, b))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"declaration": false,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
172
packages/uhk-usb/package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
|
||||
"requires": {
|
||||
"delegates": "1.0.0",
|
||||
"readable-stream": "2.3.3"
|
||||
"readable-stream": "2.3.6"
|
||||
}
|
||||
},
|
||||
"bindings": {
|
||||
@@ -32,11 +32,12 @@
|
||||
"integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw=="
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz",
|
||||
"integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
|
||||
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
|
||||
"requires": {
|
||||
"readable-stream": "2.3.3"
|
||||
"readable-stream": "2.3.6",
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
@@ -59,6 +60,14 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
|
||||
"requires": {
|
||||
"mimic-response": "1.0.0"
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz",
|
||||
@@ -69,10 +78,15 @@
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz",
|
||||
"integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
|
||||
"requires": {
|
||||
"once": "1.4.0"
|
||||
}
|
||||
@@ -113,9 +127,9 @@
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz",
|
||||
"integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4="
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
@@ -130,6 +144,11 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",
|
||||
"integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
@@ -151,14 +170,17 @@
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
|
||||
"integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY="
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
|
||||
},
|
||||
"node-abi": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.1.1.tgz",
|
||||
"integrity": "sha512-6oxV13poCOv7TfGvhsSz6XZWpXeKkdGVh72++cs33OfMh3KAX8lN84dCvmqSETyDXAFcUHtV7eJrgFBoOqZbNQ=="
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.3.0.tgz",
|
||||
"integrity": "sha512-zwm6vU3SsVgw3e9fu48JBaRBCJGIvAgysDsqtf5+vEexFE71bEOtaMWb5zr/zODZNzTPtQlqUUpC79k68Hspow==",
|
||||
"requires": {
|
||||
"semver": "5.5.0"
|
||||
}
|
||||
},
|
||||
"node-hid": {
|
||||
"version": "0.5.7",
|
||||
@@ -166,8 +188,8 @@
|
||||
"integrity": "sha512-dwwpOetL2+MGYgivbO22ML+45ieCGbueWv1rYxRgBoEc2QMp6UF6ZucEkYts1IA3YPWJNkmpGh6dqQ85n19szw==",
|
||||
"requires": {
|
||||
"bindings": "1.3.0",
|
||||
"nan": "2.7.0",
|
||||
"prebuild-install": "2.3.0"
|
||||
"nan": "2.10.0",
|
||||
"prebuild-install": "2.5.1"
|
||||
}
|
||||
},
|
||||
"noop-logger": {
|
||||
@@ -210,62 +232,63 @@
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||
},
|
||||
"prebuild-install": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.3.0.tgz",
|
||||
"integrity": "sha512-gzjq2oHB8oMbzJSsSh9MQ64zrXZGt092/uT4TLZlz2qnrPxpWqp4vYB7LZrDxnlxf5RfbCjkgDI/z0EIVuYzAw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.1.tgz",
|
||||
"integrity": "sha512-3DX9L6pzwc1m1ksMkW3Ky2WLgPQUBiySOfXVl3WZyAeJSyJb4wtoH9OmeRGcubAWsMlLiL8BTHbwfm/jPQE9Ag==",
|
||||
"requires": {
|
||||
"detect-libc": "1.0.3",
|
||||
"expand-template": "1.1.0",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "1.2.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"node-abi": "2.1.1",
|
||||
"node-abi": "2.3.0",
|
||||
"noop-logger": "0.1.1",
|
||||
"npmlog": "4.1.2",
|
||||
"os-homedir": "1.0.2",
|
||||
"pump": "1.0.2",
|
||||
"rc": "1.2.2",
|
||||
"simple-get": "1.4.3",
|
||||
"pump": "2.0.1",
|
||||
"rc": "1.2.6",
|
||||
"simple-get": "2.7.0",
|
||||
"tar-fs": "1.16.0",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"xtend": "4.0.1"
|
||||
"which-pm-runs": "1.0.0"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-1.0.2.tgz",
|
||||
"integrity": "sha1-Oz7mUS+U8OV1U4wXmV+fFpkKXVE=",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
||||
"requires": {
|
||||
"end-of-stream": "1.4.0",
|
||||
"end-of-stream": "1.4.1",
|
||||
"once": "1.4.0"
|
||||
}
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz",
|
||||
"integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz",
|
||||
"integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=",
|
||||
"requires": {
|
||||
"deep-extend": "0.4.2",
|
||||
"ini": "1.3.4",
|
||||
"ini": "1.3.5",
|
||||
"minimist": "1.2.0",
|
||||
"strip-json-comments": "2.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "1.0.7",
|
||||
"process-nextick-args": "2.0.0",
|
||||
"safe-buffer": "5.1.1",
|
||||
"string_decoder": "1.0.3",
|
||||
"string_decoder": "1.1.1",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
@@ -274,6 +297,11 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -284,14 +312,19 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"simple-concat": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
|
||||
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-1.4.3.tgz",
|
||||
"integrity": "sha1-6XVe2kB+ltpAxeUVjJ6jezO+y+s=",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz",
|
||||
"integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==",
|
||||
"requires": {
|
||||
"decompress-response": "3.3.0",
|
||||
"once": "1.4.0",
|
||||
"unzip-response": "1.0.2",
|
||||
"xtend": "4.0.1"
|
||||
"simple-concat": "1.0.0"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
@@ -305,9 +338,9 @@
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
@@ -332,18 +365,29 @@
|
||||
"requires": {
|
||||
"chownr": "1.0.1",
|
||||
"mkdirp": "0.5.1",
|
||||
"pump": "1.0.2",
|
||||
"tar-stream": "1.5.4"
|
||||
"pump": "1.0.3",
|
||||
"tar-stream": "1.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"pump": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz",
|
||||
"integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==",
|
||||
"requires": {
|
||||
"end-of-stream": "1.4.1",
|
||||
"once": "1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz",
|
||||
"integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz",
|
||||
"integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==",
|
||||
"requires": {
|
||||
"bl": "1.2.1",
|
||||
"end-of-stream": "1.4.0",
|
||||
"readable-stream": "2.3.3",
|
||||
"bl": "1.2.2",
|
||||
"end-of-stream": "1.4.1",
|
||||
"readable-stream": "2.3.6",
|
||||
"xtend": "4.0.1"
|
||||
}
|
||||
},
|
||||
@@ -355,16 +399,16 @@
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"unzip-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz",
|
||||
"integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4="
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"which-pm-runs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
|
||||
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs="
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export namespace Constants {
|
||||
export const VENDOR_ID = 0x1D50;
|
||||
export const PRODUCT_ID = 0x6122;
|
||||
export const BOOTLOADER_ID = 0x6120;
|
||||
export const MAX_PAYLOAD_SIZE = 64;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export class UhkBlhost {
|
||||
|
||||
self.logService.debug(`[blhost] FINISHED: ${code}`);
|
||||
|
||||
if (code !== null && code !== 0) {
|
||||
if (code !== 0) {
|
||||
return reject(new Error(`blhost error code:${code}`));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Device, devices, HID } from 'node-hid';
|
||||
import { LogService } from 'uhk-common';
|
||||
import { CommandLineArgs, DeviceConnectionState, isEqualArray, LogService } from 'uhk-common';
|
||||
|
||||
import {
|
||||
ConfigBufferId,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ModuleSlotToId,
|
||||
UsbCommand
|
||||
} from './constants';
|
||||
import { bufferToString, getTransferData, retry, snooze } from './util';
|
||||
import { bufferToString, getTransferData, isUhkDevice, retry, snooze } from './util';
|
||||
|
||||
export const BOOTLOADER_TIMEOUT_MS = 5000;
|
||||
|
||||
@@ -24,10 +24,12 @@ export class UhkHidDevice {
|
||||
* Internal variable that represent the USB UHK device
|
||||
* @private
|
||||
*/
|
||||
private _prevDevices = [];
|
||||
private _device: HID;
|
||||
private _hasPermission = false;
|
||||
|
||||
constructor(private logService: LogService) {
|
||||
constructor(private logService: LogService,
|
||||
private options: CommandLineArgs) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,17 +40,25 @@ export class UhkHidDevice {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public hasPermission(): boolean {
|
||||
if (this.options.spe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this._hasPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.deviceConnected()) {
|
||||
const dev = devices().find((x: Device) => isUhkDevice(x) || x.productId === Constants.BOOTLOADER_ID);
|
||||
|
||||
if (!dev) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this._hasPermission = this.getDevice() !== null;
|
||||
this.close();
|
||||
const device = new HID(dev.path);
|
||||
device.close();
|
||||
|
||||
this._hasPermission = true;
|
||||
|
||||
return this._hasPermission;
|
||||
} catch (err) {
|
||||
@@ -62,15 +72,24 @@ export class UhkHidDevice {
|
||||
* Return with true is an UHK Device is connected to the computer.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public deviceConnected(): boolean {
|
||||
const connected = devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID &&
|
||||
dev.productId === Constants.PRODUCT_ID);
|
||||
public getDeviceConnectionState(): DeviceConnectionState {
|
||||
const devs = devices();
|
||||
const result: DeviceConnectionState = {
|
||||
bootloaderActive: false,
|
||||
connected: false,
|
||||
hasPermission: this.hasPermission()
|
||||
};
|
||||
|
||||
if (!connected) {
|
||||
this._hasPermission = false;
|
||||
for (const dev of devs) {
|
||||
if (isUhkDevice(dev)) {
|
||||
result.connected = true;
|
||||
} else if (dev.vendorId === Constants.VENDOR_ID &&
|
||||
dev.productId === Constants.BOOTLOADER_ID) {
|
||||
result.bootloaderActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
return connected;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,6 +156,10 @@ export class UhkHidDevice {
|
||||
}
|
||||
}
|
||||
|
||||
public resetDeviceCache(): void {
|
||||
this._prevDevices = [];
|
||||
}
|
||||
|
||||
async reenumerate(enumerationMode: EnumerationModes): Promise<void> {
|
||||
const reenumMode = EnumerationModes[enumerationMode].toString();
|
||||
this.logService.debug(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}`);
|
||||
@@ -192,7 +215,7 @@ export class UhkHidDevice {
|
||||
|
||||
async sendKbootCommandToModule(module: ModuleSlotToI2cAddress, command: KbootCommands, maxTry = 1): Promise<any> {
|
||||
let transfer;
|
||||
const moduleName = kbootKommandName(module);
|
||||
const moduleName = kbootCommandName(module);
|
||||
this.logService.debug(`[UhkHidDevice] USB[T]: Send KbootCommand ${moduleName} ${KbootCommands[command].toString()}`);
|
||||
if (command === KbootCommands.idle) {
|
||||
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command]);
|
||||
@@ -228,19 +251,24 @@ export class UhkHidDevice {
|
||||
private connectToDevice(): HID {
|
||||
try {
|
||||
const devs = devices();
|
||||
this.logService.debug('[UhkHidDevice] Available devices:', devs);
|
||||
if (!isEqualArray(this._prevDevices, devs)) {
|
||||
this.logService.debug('[UhkHidDevice] Available devices:');
|
||||
for (const logDevice of devs) {
|
||||
this.logService.debug(JSON.stringify(logDevice));
|
||||
}
|
||||
this._prevDevices = devs;
|
||||
} else {
|
||||
this.logService.debug('[UhkHidDevice] Available devices unchanged');
|
||||
}
|
||||
|
||||
const dev = devs.find((x: Device) =>
|
||||
x.vendorId === Constants.VENDOR_ID &&
|
||||
x.productId === Constants.PRODUCT_ID &&
|
||||
((x.usagePage === 128 && x.usage === 129) || x.interface === 0));
|
||||
const dev = devs.find(isUhkDevice);
|
||||
|
||||
if (!dev) {
|
||||
this.logService.debug('[UhkHidDevice] UHK Device not found:');
|
||||
return null;
|
||||
}
|
||||
const device = new HID(dev.path);
|
||||
this.logService.debug('[UhkHidDevice] Used device:', dev);
|
||||
this.logService.debug('[UhkHidDevice] Used device:', JSON.stringify(dev));
|
||||
return device;
|
||||
}
|
||||
catch (err) {
|
||||
@@ -251,7 +279,7 @@ export class UhkHidDevice {
|
||||
}
|
||||
}
|
||||
|
||||
function kbootKommandName(module: ModuleSlotToI2cAddress): string {
|
||||
function kbootCommandName(module: ModuleSlotToI2cAddress): string {
|
||||
switch (module) {
|
||||
case ModuleSlotToI2cAddress.leftHalf:
|
||||
return 'leftHalf';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from './constants';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { UhkBlhost } from './uhk-blhost';
|
||||
import { UhkHidDevice } from './uhk-hid-device';
|
||||
import { snooze } from './util';
|
||||
@@ -29,6 +30,7 @@ export class UhkOperations {
|
||||
}
|
||||
|
||||
public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) {
|
||||
this.logService.debug(`[UhkOperations] Operating system: ${os.type()} ${os.release()} ${os.arch()}`);
|
||||
this.logService.debug('[UhkOperations] Start flashing right firmware');
|
||||
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`];
|
||||
|
||||
@@ -105,8 +107,6 @@ export class UhkOperations {
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async loadConfiguration(configBufferId: ConfigBufferId): Promise<string> {
|
||||
let response = [];
|
||||
|
||||
const configBufferIdToName = ['HardwareConfig', 'StagingUserConfig', 'ValidatedUserConfig'];
|
||||
const configName = configBufferIdToName[configBufferId];
|
||||
|
||||
@@ -140,7 +140,8 @@ export class UhkOperations {
|
||||
}
|
||||
}
|
||||
}
|
||||
response = convertBufferToIntArray(configBuffer);
|
||||
const response = convertBufferToIntArray(configBuffer);
|
||||
|
||||
return Promise.resolve(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
const errMsg = `[DeviceOperation] ${configName} from eeprom error`;
|
||||
@@ -165,10 +166,10 @@ export class UhkOperations {
|
||||
return configSize;
|
||||
}
|
||||
|
||||
public async saveUserConfiguration(json: string): Promise<void> {
|
||||
public async saveUserConfiguration(buffer: Buffer): Promise<void> {
|
||||
try {
|
||||
this.logService.debug('[DeviceOperation] USB[T]: Write user configuration to keyboard');
|
||||
await this.sendUserConfigToKeyboard(json);
|
||||
await this.sendUserConfigToKeyboard(buffer);
|
||||
this.logService.debug('[DeviceOperation] USB[T]: Write user configuration to EEPROM');
|
||||
await this.device.writeConfigToEeprom(ConfigBufferId.validatedUserConfig);
|
||||
}
|
||||
@@ -246,12 +247,11 @@ export class UhkOperations {
|
||||
|
||||
/**
|
||||
* IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result.
|
||||
* @param {string} json - UserConfiguration in JSON format
|
||||
* @param {Buffer} buffer - UserConfiguration buffer
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async sendUserConfigToKeyboard(json: string): Promise<void> {
|
||||
const buffer: Buffer = new Buffer(JSON.parse(json).data);
|
||||
private async sendUserConfigToKeyboard(buffer: Buffer): Promise<void> {
|
||||
const fragments = getTransferBuffers(UsbCommand.WriteStagingUserConfig, buffer);
|
||||
for (const fragment of fragments) {
|
||||
await this.device.write(fragment);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Device } from 'node-hid';
|
||||
import { DeviceConnectionState, LogService } from 'uhk-common';
|
||||
|
||||
import { Constants, UsbCommand } from './constants';
|
||||
import { LogService } from 'uhk-common';
|
||||
|
||||
export const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -75,7 +77,6 @@ export async function retry(command: Function, maxTry = 3, logService?: LogServi
|
||||
try {
|
||||
// logService.debug(`[retry] try to run FUNCTION:\n ${command}, \n retry: ${retryCount}`);
|
||||
await command();
|
||||
await snooze(100);
|
||||
// logService.debug(`[retry] success FUNCTION:\n ${command}, \n retry: ${retryCount}`);
|
||||
return;
|
||||
} catch (err) {
|
||||
@@ -91,7 +92,23 @@ export async function retry(command: Function, maxTry = 3, logService?: LogServi
|
||||
if (logService) {
|
||||
logService.info(`[retry] failed, but try run FUNCTION:\n ${command}, \n retry: ${retryCount}`);
|
||||
}
|
||||
await snooze(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deviceConnectionStateComparer = (a: DeviceConnectionState, b: DeviceConnectionState): boolean => {
|
||||
return a.hasPermission === b.hasPermission
|
||||
&& a.connected === b.connected
|
||||
&& a.bootloaderActive === b.bootloaderActive;
|
||||
};
|
||||
|
||||
export const isUhkDevice = (dev: Device): boolean => {
|
||||
return dev.vendorId === Constants.VENDOR_ID &&
|
||||
dev.productId === Constants.PRODUCT_ID &&
|
||||
// hidapi can not read the interface number on Mac, so check the usage page and usage
|
||||
((dev.usagePage === 128 && dev.usage === 129) || // Old firmware
|
||||
(dev.usagePage === (0xFF00 | 0x00) && dev.usage === 0x01) || // New firmware
|
||||
dev.interface === 0);
|
||||
};
|
||||
|
||||
6790
packages/uhk-web/package-lock.json
generated
@@ -10,22 +10,24 @@
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"build:renderer": "webpack --config webpack.config.js",
|
||||
"server:renderer": "webpack --config webpack.config.js --watch"
|
||||
"server:renderer": "webpack --config webpack.config.js --watch",
|
||||
"pree2e": "webdriver-manager update --standalone false --gecko false --quiet"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@angular/animations": "4.4.5",
|
||||
"@angular/cli": "1.4.7",
|
||||
"@angular/common": "4.4.5",
|
||||
"@angular/compiler": "4.4.5",
|
||||
"@angular/compiler-cli": "4.4.5",
|
||||
"@angular/core": "4.4.5",
|
||||
"@angular/forms": "4.4.5",
|
||||
"@angular/http": "4.4.5",
|
||||
"@angular/language-service": "4.4.5",
|
||||
"@angular/platform-browser": "4.4.5",
|
||||
"@angular/platform-browser-dynamic": "4.4.5",
|
||||
"@angular/router": "4.4.5",
|
||||
"@angular/animations": "5.2.9",
|
||||
"@angular/cli": "1.7.4",
|
||||
"@angular/common": "5.2.9",
|
||||
"@angular/compiler": "5.2.9",
|
||||
"@angular/compiler-cli": "5.2.9",
|
||||
"@angular/core": "5.2.9",
|
||||
"@angular-devkit/build-optimizer": "0.3.2",
|
||||
"@angular/forms": "5.2.9",
|
||||
"@angular/http": "5.2.9",
|
||||
"@angular/language-service": "5.2.9",
|
||||
"@angular/platform-browser": "5.2.9",
|
||||
"@angular/platform-browser-dynamic": "5.2.9",
|
||||
"@angular/router": "5.2.9",
|
||||
"@ngrx/effects": "4.0.5",
|
||||
"@ngrx/router-store": "4.0.4",
|
||||
"@ngrx/store": "4.0.3",
|
||||
@@ -37,65 +39,43 @@
|
||||
"@types/jasmine": "2.5.53",
|
||||
"@types/jasminewd2": "2.0.2",
|
||||
"@types/jquery": "3.2.9",
|
||||
"@types/lodash-es": "4.17.0",
|
||||
"@types/node-hid": "0.5.2",
|
||||
"@types/usb": "1.1.3",
|
||||
"angular-confirmation-popover": "3.2.0",
|
||||
"angular-notifier": "2.0.0",
|
||||
"autoprefixer": "6.5.3",
|
||||
"autoprefixer": "^7.2.3",
|
||||
"bootstrap": "3.3.7",
|
||||
"buffer": "5.0.6",
|
||||
"circular-dependency-plugin": "3.0.0",
|
||||
"circular-dependency-plugin": "^4.2.1",
|
||||
"codelyzer": "3.0.1",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"css-loader": "0.28.1",
|
||||
"cssnano": "3.10.0",
|
||||
"dragula": "3.7.2",
|
||||
"exports-loader": "0.6.3",
|
||||
"file-loader": "0.10.0",
|
||||
"file-saver": "1.3.3",
|
||||
"font-awesome": "4.7.0",
|
||||
"html-webpack-plugin": "2.29.0",
|
||||
"istanbul-instrumenter-loader": "2.0.0",
|
||||
"html-webpack-plugin": "^2.29.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",
|
||||
"lodash-es": "4.17.4",
|
||||
"ng2-dragula": "1.5.0",
|
||||
"ng2-nouislider": "^1.7.6",
|
||||
"ng2-nouislider": "^1.7.7",
|
||||
"ng2-select2": "1.0.0-beta.10",
|
||||
"ngx-clipboard": "10.0.0",
|
||||
"ngrx-store-freeze": "0.1.9",
|
||||
"node-hid": "0.5.4",
|
||||
"nouislider": "^10.1.0",
|
||||
"postcss-loader": "1.3.3",
|
||||
"postcss-url": "5.1.2",
|
||||
"nouislider": "^11.1.0",
|
||||
"postcss-url": "^7.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",
|
||||
"rxjs": "5.5.8",
|
||||
"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",
|
||||
"typescript": "2.6.2",
|
||||
"uhk-common": "1.0.0",
|
||||
"url-loader": "0.5.7",
|
||||
"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"
|
||||
"zone.js": "0.8.26",
|
||||
"@angular-devkit/core": "0.3.2",
|
||||
"@ngtools/webpack": "1.10.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"classlist.js": "1.1.20150312",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, HostListener, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Action, Store } from '@ngrx/store';
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
saveToKeyboardState
|
||||
} from './store';
|
||||
import { ProgressButtonState } from './store/reducers/progress-button-state';
|
||||
import { SaveUserConfigInBinaryFileAction, SaveUserConfigInJsonFileAction } from './store/actions/user-config';
|
||||
|
||||
@Component({
|
||||
selector: 'main-app',
|
||||
|
||||
@@ -5,17 +5,19 @@ import { deviceRoutes } from './components/device';
|
||||
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 { PrivilegeCheckerComponent } from './components/privilege-checker';
|
||||
import { MissingDeviceComponent } from './components/missing-device';
|
||||
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 { agentRoutes } from './components/agent/agent.routes';
|
||||
import { agentRoutes } from './components/agent';
|
||||
import { LoadingDevicePageComponent } from './pages/loading-page/loading-device.page';
|
||||
import { UhkDeviceLoadingGuard } from './services/uhk-device-loading.guard';
|
||||
import { UhkDeviceLoadedGuard } from './services/uhk-device-loaded.guard';
|
||||
import { RecoveryModeComponent } from './components/device';
|
||||
import { UhkDeviceBootloaderNotActiveGuard } from './services/uhk-device-bootloader-not-active.guard';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
@@ -33,6 +35,11 @@ const appRoutes: Routes = [
|
||||
component: LoadingDevicePageComponent,
|
||||
canActivate: [UhkDeviceLoadedGuard]
|
||||
},
|
||||
{
|
||||
path: 'recovery-device',
|
||||
component: RecoveryModeComponent,
|
||||
canActivate: [UhkDeviceBootloaderNotActiveGuard]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: MainPage,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<input #inputControl
|
||||
cancelable
|
||||
[class]="css"
|
||||
type="text"
|
||||
[disabled]="disabled"
|
||||
[(ngModel)]="model"
|
||||
(blur)="blur()"
|
||||
(focus)="focus()"
|
||||
(keyup.enter)="keyEnter($event)"
|
||||
(keyup)="calculateTextWidth($event.target.value)">
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
forwardRef, HostListener,
|
||||
Input,
|
||||
Renderer2,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import * as util from '../../util';
|
||||
|
||||
const noop = (_: any) => {
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'auto-grow-input',
|
||||
templateUrl: './auto-grow-input.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AutoGrowInputComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AutoGrowInputComponent implements ControlValueAccessor {
|
||||
@Input() maxParentWidthPercent = 1;
|
||||
@Input() css: string;
|
||||
|
||||
@ViewChild('inputControl') inputControl: ElementRef;
|
||||
|
||||
disabled: boolean;
|
||||
|
||||
get model(): string {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
set model(value: string) {
|
||||
if (this._model === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._model = value;
|
||||
}
|
||||
|
||||
private _model: string;
|
||||
private _originalModel: string;
|
||||
private _onChanged = noop;
|
||||
private _onTouched = noop;
|
||||
|
||||
constructor(private _cdRef: ChangeDetectorRef,
|
||||
private _renderer: Renderer2) {
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this._onChanged = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
if (this.disabled === isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disabled = isDisabled;
|
||||
this._cdRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
windowResize(): void {
|
||||
this.calculateTextWidth(this._model);
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
console.log('write', new Date());
|
||||
if (this.model === obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._model = obj;
|
||||
this._originalModel = obj;
|
||||
this.calculateTextWidth(this._model);
|
||||
this._cdRef.markForCheck();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._onTouched(this);
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (!util.isValidName(this._model) || this._model.trim() === this._originalModel) {
|
||||
this._model = this._originalModel;
|
||||
this.calculateTextWidth(this._model);
|
||||
this._cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._originalModel = this._model;
|
||||
this._onChanged(this._model);
|
||||
}
|
||||
|
||||
keyEnter(event): void {
|
||||
event.target.blur();
|
||||
}
|
||||
|
||||
calculateTextWidth(text: string): void {
|
||||
const htmlInput = this.inputControl.nativeElement as HTMLInputElement;
|
||||
const maxWidth = htmlInput.parentElement.parentElement.offsetWidth * this.maxParentWidthPercent;
|
||||
const textWidth = util.getContentWidth(window.getComputedStyle(htmlInput), text);
|
||||
this._renderer.setStyle(htmlInput, 'width', Math.min(maxWidth, textWidth) + 'px');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './auto-grow-input.component';
|
||||
@@ -9,16 +9,14 @@
|
||||
|
||||
<ul class="list-unstyled btn-list">
|
||||
<li>
|
||||
<button class="btn btn-default"
|
||||
<button class="btn btn-primary"
|
||||
(click)="exportUserConfiguration($event)">Export device configuration
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-default btn-file">
|
||||
Import device configuration
|
||||
<input type="file"
|
||||
(change)="changeFile($event)">
|
||||
</label>
|
||||
<file-upload (fileChanged)="changeFile($event)"
|
||||
label="Import device configuration">
|
||||
</file-upload>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-danger"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SaveUserConfigInBinaryFileAction,
|
||||
SaveUserConfigInJsonFileAction
|
||||
} from '../../../store/actions/user-config';
|
||||
import { UploadFileData } from '../../../models/upload-file-data';
|
||||
|
||||
@Component({
|
||||
selector: 'device-settings',
|
||||
@@ -42,16 +43,7 @@ export class DeviceConfigurationComponent {
|
||||
}
|
||||
}
|
||||
|
||||
changeFile(event): void {
|
||||
const files = event.srcElement.files;
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = function () {
|
||||
const arrayBuffer = new Uint8Array(fileReader.result);
|
||||
this.store.dispatch(new LoadUserConfigurationFromFileAction({
|
||||
filename: event.srcElement.value,
|
||||
data: Array.from(arrayBuffer)
|
||||
}));
|
||||
}.bind(this);
|
||||
fileReader.readAsArrayBuffer(files[0]);
|
||||
changeFile(data: UploadFileData): void {
|
||||
this.store.dispatch(new LoadUserConfigurationFromFileAction(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DeviceConfigurationComponent } from './configuration/device-configurati
|
||||
import { DeviceFirmwareComponent } from './firmware/device-firmware.component';
|
||||
import { MouseSpeedComponent } from './mouse-speed/mouse-speed.component';
|
||||
import { LEDBrightnessComponent } from './led-brightness/led-brightness.component';
|
||||
import { RestoreConfigurationComponent } from './restore-configuration/restore-configuration.component';
|
||||
import { RecoveryModeComponent } from './recovery-mode/recovery-mode.component';
|
||||
|
||||
export const deviceRoutes: Routes = [
|
||||
{
|
||||
@@ -29,6 +31,14 @@ export const deviceRoutes: Routes = [
|
||||
{
|
||||
path: 'firmware',
|
||||
component: DeviceFirmwareComponent
|
||||
},
|
||||
{
|
||||
path: 'restore-user-configuration',
|
||||
component: RestoreConfigurationComponent
|
||||
},
|
||||
{
|
||||
path: 'recovery-mode',
|
||||
component: RecoveryModeComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,14 +12,11 @@
|
||||
Firmware {{ hardwareModules.rightModuleInfo.firmwareVersion }} is running on the right keyboard half.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>
|
||||
Please note that the firmware update process may sometimes fail. If if fails then
|
||||
simply retry until it succeeds. If the left half becomes unresponsive after a failed
|
||||
update then retry and follow the instructions displayed during the update to fix it.
|
||||
We'll make the firmware update process more robust.
|
||||
</i>
|
||||
</p>
|
||||
<p>Please note that firmware update doesn't work on Windows 7, Windows Vista, and Windows XP. Use Windows 10, Windows 8, Linux, or OSX instead.</p>
|
||||
|
||||
<p>If the update process fails, disconnect every USB device from your computer including USB hubs, KVM switches, and every USB device. Then connect only your UHK and retry.</p>
|
||||
|
||||
<p>If you tried the above and the update still keeps failing, please <a class="link-github" (click)="openFirmwareGitHubIssuePage($event)">create a GitHub issue</a>, and attach the update log.</p>
|
||||
|
||||
<p>
|
||||
<button class="btn btn-primary"
|
||||
@@ -27,27 +24,17 @@
|
||||
(click)="onUpdateFirmware()">
|
||||
Flash firmware {{ (getAgentVersionInfo$ | async).firmwareVersion }} (bundled with Agent)
|
||||
</button>
|
||||
<label class="btn btn-primary btn-file"
|
||||
[class.disabled]="flashFirmwareButtonDisbabled$ | async">
|
||||
Choose firmware file and flash it
|
||||
<input id="firmware-file-select"
|
||||
type="file"
|
||||
<file-upload [disabled]="flashFirmwareButtonDisbabled$ | async"
|
||||
(fileChanged)="changeFile($event)"
|
||||
accept=".tar.bz2"
|
||||
[disabled]="flashFirmwareButtonDisbabled$ | async"
|
||||
(change)="changeFile($event)">
|
||||
</label>
|
||||
label="Choose firmware file and flash it"></file-upload>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow" #scrollMe>
|
||||
<div class="flex-grow">
|
||||
<xterm [logs]="xtermLog$ | async"></xterm>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button"
|
||||
class="btn btn-primary ok-button"
|
||||
[disabled]="firmwareOkButtonDisabled$ | async"
|
||||
(click)="onOkButtonClick()">OK
|
||||
</button>
|
||||
<div class="flex-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,24 +6,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
background-color: black;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ok-button {
|
||||
min-width: 100px;
|
||||
.link-github {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { HardwareModules, VersionInformation } from 'uhk-common';
|
||||
import { Constants } from 'uhk-common';
|
||||
import { OpenUrlInNewWindowAction } from '../../../store/actions/app';
|
||||
|
||||
import {
|
||||
AppState,
|
||||
firmwareOkButtonDisabled,
|
||||
flashFirmwareButtonDisbabled,
|
||||
getAgentVersionInfo,
|
||||
getHardwareModules,
|
||||
xtermLog
|
||||
} from '../../../store';
|
||||
import { UpdateFirmwareAction, UpdateFirmwareOkButtonAction, UpdateFirmwareWithAction } from '../../../store/actions/device';
|
||||
import { UpdateFirmwareAction, UpdateFirmwareWithAction } from '../../../store/actions/device';
|
||||
import { XtermLog } from '../../../models/xterm-log';
|
||||
import { UploadFileData } from '../../../models/upload-file-data';
|
||||
|
||||
@Component({
|
||||
selector: 'device-firmware',
|
||||
@@ -26,33 +28,20 @@ import { XtermLog } from '../../../models/xterm-log';
|
||||
export class DeviceFirmwareComponent implements OnDestroy {
|
||||
flashFirmwareButtonDisbabled$: Observable<boolean>;
|
||||
xtermLog$: Observable<Array<XtermLog>>;
|
||||
xtermLogSubscription: Subscription;
|
||||
getAgentVersionInfo$: Observable<VersionInformation>;
|
||||
firmwareOkButtonDisabled$: Observable<boolean>;
|
||||
hardwareModulesSubscription: Subscription;
|
||||
hardwareModules: HardwareModules;
|
||||
|
||||
@ViewChild('scrollMe') divElement: ElementRef;
|
||||
|
||||
constructor(private store: Store<AppState>) {
|
||||
this.flashFirmwareButtonDisbabled$ = store.select(flashFirmwareButtonDisbabled);
|
||||
this.xtermLog$ = store.select(xtermLog);
|
||||
this.xtermLogSubscription = this.xtermLog$.subscribe(() => {
|
||||
if (this.divElement && this.divElement.nativeElement) {
|
||||
setTimeout(() => {
|
||||
this.divElement.nativeElement.scrollTop = this.divElement.nativeElement.scrollHeight;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.getAgentVersionInfo$ = store.select(getAgentVersionInfo);
|
||||
this.firmwareOkButtonDisabled$ = store.select(firmwareOkButtonDisabled);
|
||||
this.hardwareModulesSubscription = store.select(getHardwareModules).subscribe(data => {
|
||||
this.hardwareModules = data;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.xtermLogSubscription.unsubscribe();
|
||||
this.hardwareModulesSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -60,22 +49,12 @@ export class DeviceFirmwareComponent implements OnDestroy {
|
||||
this.store.dispatch(new UpdateFirmwareAction());
|
||||
}
|
||||
|
||||
onOkButtonClick(): void {
|
||||
this.store.dispatch(new UpdateFirmwareOkButtonAction());
|
||||
changeFile(data: UploadFileData): void {
|
||||
this.store.dispatch(new UpdateFirmwareWithAction(data.data));
|
||||
}
|
||||
|
||||
changeFile(event): void {
|
||||
const files = event.srcElement.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = function () {
|
||||
const arrayBuffer = new Uint8Array(fileReader.result);
|
||||
this.store.dispatch(new UpdateFirmwareWithAction(Array.prototype.slice.call(arrayBuffer)));
|
||||
}.bind(this);
|
||||
fileReader.readAsArrayBuffer(files[0]);
|
||||
openFirmwareGitHubIssuePage(event): void {
|
||||
event.preventDefault();
|
||||
this.store.dispatch(new OpenUrlInNewWindowAction(Constants.FIRMWARE_GITHUB_ISSUE_URL));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,6 @@ export * from './configuration/device-configuration.component';
|
||||
export * from './firmware/device-firmware.component';
|
||||
export * from './mouse-speed/mouse-speed.component';
|
||||
export * from './led-brightness/led-brightness.component';
|
||||
export * from './restore-configuration/restore-configuration.component';
|
||||
export * from './recovery-mode/recovery-mode.component';
|
||||
export * from './device.routes';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="full-height">
|
||||
<div class="flex-container">
|
||||
<div>
|
||||
|
||||
<h1>
|
||||
<i class="fa fa-wrench"></i>
|
||||
<span>Fix device</span>
|
||||
</h1>
|
||||
<p>
|
||||
Your device seems to be broken. No worries, Agent can fix it.
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-primary"
|
||||
type="button"
|
||||
[disabled]="flashFirmwareButtonDisbabled$ | async"
|
||||
(click)="onRecoveryDevice()">Fix device
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<xterm [logs]="xtermLog$ | async"></xterm>
|
||||
</div>
|
||||
<div class="flex-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { XtermLog } from '../../../models/xterm-log';
|
||||
import { AppState, flashFirmwareButtonDisbabled, xtermLog } from '../../../store';
|
||||
import { RecoveryDeviceAction } from '../../../store/actions/device';
|
||||
|
||||
@Component({
|
||||
selector: 'device-recovery-mode',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './recovery-mode.component.html',
|
||||
styleUrls: ['./recovery-mode.component.scss'],
|
||||
host: {
|
||||
'class': 'container-fluid'
|
||||
}
|
||||
})
|
||||
export class RecoveryModeComponent implements OnInit {
|
||||
flashFirmwareButtonDisbabled$: Observable<boolean>;
|
||||
|
||||
xtermLog$: Observable<Array<XtermLog>>;
|
||||
|
||||
constructor(private store: Store<AppState>) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.flashFirmwareButtonDisbabled$ = this.store.select(flashFirmwareButtonDisbabled);
|
||||
this.xtermLog$ = this.store.select(xtermLog);
|
||||
}
|
||||
|
||||
onRecoveryDevice(): void {
|
||||
this.store.dispatch(new RecoveryDeviceAction());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<h1>
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
<span>Fix configuration</span>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Your on-board device configuration is invalid.
|
||||
</p>
|
||||
<button class="btn btn-primary"
|
||||
*ngIf="state.hasBackupUserConfiguration"
|
||||
[disabled]="state.restoringUserConfiguration"
|
||||
(click)="restoreUserConfiguration()"> Restore the last valid device configuration
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger"
|
||||
*ngIf="!state.hasBackupUserConfiguration"
|
||||
[disabled]="state.restoringUserConfiguration"
|
||||
(click)="resetUserConfiguration()">Reset device configuration
|
||||
</button>
|
||||
@@ -0,0 +1,10 @@
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { AppState, getBackupUserConfigurationState } from '../../../store';
|
||||
import { ResetUserConfigurationAction, RestoreUserConfigurationFromBackupAction } from '../../../store/actions/device';
|
||||
import { RestoreConfigurationState } from '../../../models/restore-configuration-state';
|
||||
|
||||
@Component({
|
||||
selector: 'restore-configuration',
|
||||
templateUrl: './restore-configuration.component.html',
|
||||
styleUrls: ['./restore-configuration.component.scss'],
|
||||
host: {
|
||||
'class': 'container-fluid'
|
||||
}
|
||||
})
|
||||
export class RestoreConfigurationComponent implements OnInit, OnDestroy {
|
||||
state: RestoreConfigurationState;
|
||||
|
||||
private stateSubscription: Subscription;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.stateSubscription) {
|
||||
this.stateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateSubscription = this.store
|
||||
.select(getBackupUserConfigurationState)
|
||||
.subscribe(data => {
|
||||
this.state = data;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
resetUserConfiguration() {
|
||||
this.store.dispatch(new ResetUserConfigurationAction());
|
||||
}
|
||||
|
||||
restoreUserConfiguration(): void {
|
||||
this.store.dispatch(new RestoreUserConfigurationFromBackupAction());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<label class="btn btn-primary btn-file"
|
||||
[class.disabled]="disabled">
|
||||
{{ label }}
|
||||
<input #inputControl
|
||||
type="file"
|
||||
[accept]="accept"
|
||||
[disabled]="disabled"
|
||||
(change)="changeFile($event)">
|
||||
</label>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
import { UploadFileData } from '../../models/upload-file-data';
|
||||
|
||||
@Component({
|
||||
selector: 'file-upload',
|
||||
templateUrl: './file-upload.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileUploadComponent {
|
||||
@Input() label = 'Select file';
|
||||
@Input() disabled: boolean;
|
||||
@Input() accept: string;
|
||||
|
||||
@Output() fileChanged = new EventEmitter<UploadFileData>();
|
||||
|
||||
changeFile(event): void {
|
||||
const files = event.srcElement.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = function () {
|
||||
const arrayBuffer = new Uint8Array(fileReader.result);
|
||||
const target = event.target || event.srcElement || event.currentTarget;
|
||||
target.value = null;
|
||||
this.fileChanged.emit({
|
||||
filename: event.srcElement.value,
|
||||
data: Array.from(arrayBuffer)
|
||||
});
|
||||
}.bind(this);
|
||||
fileReader.readAsArrayBuffer(files[0]);
|
||||
}
|
||||
}
|
||||
1
packages/uhk-web/src/app/components/file-upload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './file-upload.component';
|
||||
@@ -5,6 +5,7 @@
|
||||
<svg-keyboard-wrap [keymap]="keymap$ | async"
|
||||
[halvesSplit]="keyboardSplit"
|
||||
[keyboardLayout]="keyboardLayout$ | async"
|
||||
[allowLayerDoubleTap]="allowLayerDoubleTap$ | async"
|
||||
(descriptionChanged)="descriptionChanged($event)"></svg-keyboard-wrap>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, HostListener, ViewChild } from '@angular/core';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Keymap } from 'uhk-common';
|
||||
@@ -14,9 +14,8 @@ import 'rxjs/add/operator/combineLatest';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { AppState, getKeyboardLayout } from '../../../store';
|
||||
import { allowLayerDoubleTap, AppState, getKeyboardLayout } from '../../../store';
|
||||
import { getKeymap, getKeymaps, getUserConfiguration } from '../../../store/reducers/user-configuration';
|
||||
import { SvgKeyboardWrapComponent } from '../../svg/wrap';
|
||||
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
|
||||
import { KeymapActions } from '../../../store/actions';
|
||||
import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription';
|
||||
@@ -31,13 +30,12 @@ import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription
|
||||
})
|
||||
export class KeymapEditComponent {
|
||||
|
||||
@ViewChild(SvgKeyboardWrapComponent) wrap: SvgKeyboardWrapComponent;
|
||||
|
||||
keyboardSplit: boolean;
|
||||
|
||||
deletable$: Observable<boolean>;
|
||||
keymap$: Observable<Keymap>;
|
||||
keyboardLayout$: Observable<KeyboardLayout>;
|
||||
allowLayerDoubleTap$: Observable<boolean>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
route: ActivatedRoute) {
|
||||
@@ -52,6 +50,7 @@ export class KeymapEditComponent {
|
||||
.map((keymaps: Keymap[]) => keymaps.length > 1);
|
||||
|
||||
this.keyboardLayout$ = store.select(getKeyboardLayout);
|
||||
this.allowLayerDoubleTap$ = store.select(allowLayerDoubleTap);
|
||||
}
|
||||
|
||||
downloadKeymap() {
|
||||
|
||||
@@ -67,7 +67,7 @@ export class MacroKeyTabComponent extends MacroBaseComponent implements OnInit {
|
||||
}
|
||||
|
||||
getKeyMacroAction(): KeyMacroAction {
|
||||
const keyMacroAction = Object.assign(new KeyMacroAction(), this.keypressTab.toKeyAction());
|
||||
const keyMacroAction = new KeyMacroAction(this.keypressTab.toKeyAction() as any);
|
||||
keyMacroAction.action = this.getActionType(this.activeTab);
|
||||
return keyMacroAction;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<div>
|
||||
<h4>Type text</h4>
|
||||
<textarea #macroTextInput name="macro-text" (change)="onTextChange()"
|
||||
(keyup)="validate()" class="macro__text-input">{{ macroAction?.text }}</textarea>
|
||||
<textarea #macroTextInput
|
||||
name="macro-text"
|
||||
(keydown)="onKeydown($event)"
|
||||
(change)="onTextChange()"
|
||||
(keyup)="validate()"
|
||||
(paste)="onPaste($event)"
|
||||
class="macro__text-input">{{ macroAction?.text }}</textarea>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { TextMacroAction } from 'uhk-common';
|
||||
|
||||
import { MacroBaseComponent } from '../macro-base.component';
|
||||
|
||||
const NON_ASCII_REGEXP = /[^\x00-\x7F]/g;
|
||||
|
||||
@Component({
|
||||
selector: 'macro-text-tab',
|
||||
templateUrl: './macro-text.component.html',
|
||||
@@ -36,6 +38,41 @@ export class MacroTextTabComponent extends MacroBaseComponent implements OnInit,
|
||||
this.macroAction.text = this.input.nativeElement.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not allow non ascii character
|
||||
* @param $event
|
||||
*/
|
||||
onKeydown($event: KeyboardEvent): void {
|
||||
if (new RegExp(NON_ASCII_REGEXP).test($event.key)) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove non ascii character from clipboard data
|
||||
* @param $event
|
||||
*/
|
||||
onPaste($event: ClipboardEvent): void {
|
||||
$event.preventDefault();
|
||||
|
||||
const textarea: HTMLTextAreaElement = this.input.nativeElement;
|
||||
const data = $event.clipboardData.getData('text/plain');
|
||||
const text = data && data.replace(NON_ASCII_REGEXP, '') || '';
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = textarea.value || '';
|
||||
const prefix = value.substr(0, textarea.selectionStart);
|
||||
const end = textarea.selectionEnd;
|
||||
const suffix = value.substr(textarea.selectionEnd);
|
||||
textarea.value = prefix + text + suffix;
|
||||
const correction = end === 0 ? 0 : 1;
|
||||
textarea.selectionStart = textarea.selectionEnd = end + text.length - correction;
|
||||
this.macroAction.text = textarea.value;
|
||||
}
|
||||
|
||||
isMacroValid = () => !!this.input.nativeElement.value;
|
||||
|
||||
private init = () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="row list-container">
|
||||
<div class="col-xs-10 col-xs-offset-1 list-group">
|
||||
<p><i>Please note that macro playback is not implemented yet. You can create macros, but they won't have any effect until firmware support is implemented. We're working on this.</i></p>
|
||||
<div class="macro-actions-container" [dragula]="'macroActions'" [dragulaModel]="macro.macroActions">
|
||||
<macro-item *ngFor="let macroAction of macro.macroActions; let macroActionIndex = index"
|
||||
[macroAction]="macroAction"
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
<layer-tab #tab *ngSwitchCase="tabName.Layer" class="popover-content"
|
||||
[defaultKeyAction]="defaultKeyAction"
|
||||
[currentLayer]="currentLayer"
|
||||
[allowLayerDoubleTap]="allowLayerDoubleTap"
|
||||
(validAction)="keyActionValid=$event"
|
||||
></layer-tab>
|
||||
<mouse-tab #tab *ngSwitchCase="tabName.Mouse" class="popover-content"
|
||||
@@ -75,8 +76,42 @@
|
||||
></none-tab>
|
||||
</div>
|
||||
<div class="popover-action">
|
||||
<form class="form-inline d-inline-block popover-action-form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="remapOnAllKeymap"
|
||||
[(ngModel)]="remapOnAllKeymap"> Remap on all keymaps
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="remapOnAllLayer"
|
||||
[(ngModel)]="remapOnAllLayer"> Remap on all layers
|
||||
</label>
|
||||
</div>
|
||||
<div class="d-inline-block">
|
||||
<icon name="question-circle"
|
||||
data-toggle="tooltip"
|
||||
html="true"
|
||||
maxWidth="525"
|
||||
title="<ul class='no-indent text-left'>
|
||||
<li><strong>Default behavior</strong>: Remap the key on the the current layer of the current keymap.</li>
|
||||
<li><strong>Remap on all keymaps</strong>: Remap key on the current layer of all keymaps.</li>
|
||||
<li><strong>Remap on all layers</strong>: Remap key on all layers of the current keymap.</li>
|
||||
<li><strong>Remap on all keymaps + Remap on all layers</strong>: Remap key on all layers of all keymaps.</li>
|
||||
</ul>"
|
||||
data-placement="bottom"></icon>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="d-inline-block pull-right">
|
||||
<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>
|
||||
<button class="btn btn-sm btn-primary" [class.disabled]="!keyActionValid" type="button"
|
||||
(click)="onRemapKey()"> Remap Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popover-overlay" [class.display]="visible" (click)="onOverlay()"></div>
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
background-color: #f7f7f7;
|
||||
border-top: 1px solid #ebebeb;
|
||||
border-radius: 0 0 5px 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.popover-title {
|
||||
@@ -133,3 +132,11 @@
|
||||
margin-top: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-action-form {
|
||||
margin-top: 4px;
|
||||
|
||||
label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ import {
|
||||
SwitchLayerAction
|
||||
} from 'uhk-common';
|
||||
|
||||
import { Tab } from './tab/tab';
|
||||
import { Tab } from './tab';
|
||||
|
||||
import { AppState } from '../../store';
|
||||
import { getKeymaps } from '../../store/reducers/user-configuration';
|
||||
import { KeyActionRemap } from '../../models/key-action-remap';
|
||||
|
||||
enum TabName {
|
||||
Keypress,
|
||||
@@ -79,12 +80,13 @@ export class PopoverComponent implements OnChanges {
|
||||
@Input() defaultKeyAction: KeyAction;
|
||||
@Input() currentKeymap: Keymap;
|
||||
@Input() currentLayer: number;
|
||||
@Input() keyPosition: ClientRect;
|
||||
@Input() wrapPosition: ClientRect;
|
||||
@Input() keyPosition: any;
|
||||
@Input() wrapPosition: any;
|
||||
@Input() visible: boolean;
|
||||
@Input() allowLayerDoubleTap: boolean;
|
||||
|
||||
@Output() cancel = new EventEmitter<any>();
|
||||
@Output() remap = new EventEmitter<KeyAction>();
|
||||
@Output() remap = new EventEmitter<KeyActionRemap>();
|
||||
|
||||
@ViewChild('tab') selectedTab: Tab;
|
||||
@ViewChild('popover') popoverHost: ElementRef;
|
||||
@@ -99,6 +101,9 @@ export class PopoverComponent implements OnChanges {
|
||||
leftPosition: number = 0;
|
||||
animationState: string;
|
||||
|
||||
remapOnAllKeymap: boolean;
|
||||
remapOnAllLayer: boolean;
|
||||
|
||||
private readonly currentKeymap$ = new BehaviorSubject<Keymap>(undefined);
|
||||
|
||||
constructor(store: Store<AppState>) {
|
||||
@@ -138,6 +143,8 @@ export class PopoverComponent implements OnChanges {
|
||||
if (change['visible']) {
|
||||
if (change['visible'].currentValue) {
|
||||
this.animationState = 'opened';
|
||||
this.remapOnAllKeymap = false;
|
||||
this.remapOnAllLayer = false;
|
||||
} else {
|
||||
this.animationState = 'closed';
|
||||
}
|
||||
@@ -155,8 +162,11 @@ export class PopoverComponent implements OnChanges {
|
||||
onRemapKey(): void {
|
||||
if (this.keyActionValid) {
|
||||
try {
|
||||
const keyAction = this.selectedTab.toKeyAction();
|
||||
this.remap.emit(keyAction);
|
||||
this.remap.emit({
|
||||
remapOnAllKeymap: this.remapOnAllKeymap,
|
||||
remapOnAllLayer: this.remapOnAllLayer,
|
||||
action: this.selectedTab.toKeyAction()
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO: show error dialog
|
||||
console.error(e);
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
></select2>
|
||||
<icon name="question-circle"
|
||||
data-toggle="tooltip"
|
||||
title="Looking for a non-US character? Just pick the character of the desired key according to the US layout."
|
||||
html="true"
|
||||
maxWidth="330"
|
||||
title="<p>Looking for a non-US character? Just pick the character of the desired key according to the US layout.</p>
|
||||
<p>Let's say you're a German user and want to map the Ö character. You can see that on US keyboards this is the semicolon key, so choose semicolon in this dropdown.</p>"
|
||||
data-placement="bottom"></icon>
|
||||
<capture-keystroke-button (capture)="onKeysCapture($event)" tabindex="0"></capture-keystroke-button>
|
||||
</div>
|
||||
@@ -46,7 +49,15 @@
|
||||
></select2>
|
||||
<icon name="question-circle"
|
||||
data-toggle="tooltip"
|
||||
title="The secondary role activates when another key gets pressed while holding this key."
|
||||
html="true"
|
||||
maxWidth="620"
|
||||
title="<p class='text-left'>The secondary role activates when another key gets pressed while holding this key.</p>
|
||||
<p class='text-left'>Let's say that the scancode is Escape and the secondary role is Mouse. Then:</p>
|
||||
<ul class='text-left'>
|
||||
<li>Tap this key to trigger Escape. <i>(Primary role)</i></li>
|
||||
<li>Hold this key and press another key to activate the relevant key of the Mouse layer. <i>(Secondary role)</i></li>
|
||||
</ul>
|
||||
<p class='text-left pt-3'>The secondary role can be any layer or modifier.</p>"
|
||||
data-placement="bottom"></icon>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ export class KeypressTabComponent extends Tab implements OnChanges {
|
||||
const scTypePair = this.toScancodeTypePair(this.selectedScancodeOption);
|
||||
keystrokeAction.scancode = scTypePair[0];
|
||||
if (scTypePair[1] === 'media') {
|
||||
keystrokeAction.type = KeystrokeType.shortMedia;
|
||||
keystrokeAction.type = keystrokeAction.scancode > 255 ? KeystrokeType.longMedia : KeystrokeType.shortMedia;
|
||||
} else {
|
||||
keystrokeAction.type = KeystrokeType[scTypePair[1]];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<ng-template [ngIf]="!isNotBase">
|
||||
<div>
|
||||
<div>
|
||||
<select (change)="toggleChanged($event.target.value)">
|
||||
<option *ngFor="let item of toggleData" [value]="item.id" [selected]="toggle === item.id">
|
||||
{{ item.text }}
|
||||
@@ -11,9 +13,19 @@
|
||||
</option>
|
||||
</select>
|
||||
<span [ngSwitch]="toggle">
|
||||
<ng-template [ngSwitchCase]="true">layer by pressing this key.</ng-template>
|
||||
<ng-template [ngSwitchCase]="'toggle'">layer by tapping this key.</ng-template>
|
||||
<ng-template ngSwitchDefault>layer by holding this key.</ng-template>
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="toggle === 'active' && allowLayerDoubleTap">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="lockLayerWhenDoubleTapping"> Lock layer when double tapping this key.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="isNotBase">
|
||||
<span> Layer switching is only possible from the base layer. </span>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Component, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { KeyAction, LayerName, SwitchLayerAction } from 'uhk-common';
|
||||
import { KeyAction, LayerName, SwitchLayerAction, SwitchLayerMode } from 'uhk-common';
|
||||
|
||||
import { Tab } from '../tab';
|
||||
|
||||
export type toggleType = 'active' | 'toggle';
|
||||
|
||||
@Component({
|
||||
selector: 'layer-tab',
|
||||
templateUrl: './layer-tab.component.html',
|
||||
@@ -11,16 +13,17 @@ import { Tab } from '../tab';
|
||||
export class LayerTabComponent extends Tab implements OnChanges {
|
||||
@Input() defaultKeyAction: KeyAction;
|
||||
@Input() currentLayer: number;
|
||||
@Input() allowLayerDoubleTap: boolean;
|
||||
|
||||
@HostBinding('class.no-base') isNotBase: boolean;
|
||||
|
||||
toggleData: { id: boolean, text: string }[] = [
|
||||
toggleData: { id: toggleType, text: string }[] = [
|
||||
{
|
||||
id: false,
|
||||
id: 'active',
|
||||
text: 'Activate'
|
||||
},
|
||||
{
|
||||
id: true,
|
||||
id: 'toggle',
|
||||
text: 'Toggle'
|
||||
}
|
||||
];
|
||||
@@ -40,12 +43,13 @@ export class LayerTabComponent extends Tab implements OnChanges {
|
||||
}
|
||||
];
|
||||
|
||||
toggle: boolean;
|
||||
toggle: toggleType;
|
||||
layer: LayerName;
|
||||
lockLayerWhenDoubleTapping: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.toggle = false;
|
||||
this.toggle = 'active';
|
||||
this.layer = LayerName.mod;
|
||||
}
|
||||
|
||||
@@ -71,14 +75,39 @@ export class LayerTabComponent extends Tab implements OnChanges {
|
||||
}
|
||||
|
||||
const switchLayerAction: SwitchLayerAction = <SwitchLayerAction>keyAction;
|
||||
this.toggle = switchLayerAction.isLayerToggleable;
|
||||
switch (switchLayerAction.switchLayerMode) {
|
||||
case SwitchLayerMode.holdAndDoubleTapToggle: {
|
||||
this.toggle = 'active';
|
||||
this.lockLayerWhenDoubleTapping = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case SwitchLayerMode.hold: {
|
||||
this.toggle = 'active';
|
||||
this.lockLayerWhenDoubleTapping = false;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
this.toggle = 'toggle';
|
||||
this.lockLayerWhenDoubleTapping = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.layer = switchLayerAction.layer;
|
||||
return true;
|
||||
}
|
||||
|
||||
toKeyAction(): SwitchLayerAction {
|
||||
const keyAction = new SwitchLayerAction();
|
||||
keyAction.isLayerToggleable = this.toggle;
|
||||
if (this.toggle === 'toggle') {
|
||||
keyAction.switchLayerMode = SwitchLayerMode.toggle;
|
||||
} else if (!this.allowLayerDoubleTap || this.lockLayerWhenDoubleTapping) {
|
||||
keyAction.switchLayerMode = SwitchLayerMode.holdAndDoubleTapToggle;
|
||||
} else {
|
||||
keyAction.switchLayerMode = SwitchLayerMode.hold;
|
||||
}
|
||||
|
||||
keyAction.layer = this.layer;
|
||||
if (!this.keyActionValid()) {
|
||||
throw new Error('KeyAction is invalid!');
|
||||
@@ -86,8 +115,8 @@ export class LayerTabComponent extends Tab implements OnChanges {
|
||||
return keyAction;
|
||||
}
|
||||
|
||||
toggleChanged(value: string) {
|
||||
this.toggle = value === 'true';
|
||||
toggleChanged(value: toggleType) {
|
||||
this.toggle = value;
|
||||
}
|
||||
|
||||
layerChanged(value: number) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<span> No macros are available to choose from. Create a macro first! </span>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="macroOptions.length > 0">
|
||||
<p><i>Please note that macro playback is not implemented yet. You can bind macros, but they don't have any effect.</i></p>
|
||||
<p><i>Please note that macro playback is not implemented yet. You can bind macros, but they won't have any effect until firmware support is implemented. We're working on this.</i></p>
|
||||
<div class="macro-selector">
|
||||
<b> Play macro: </b>
|
||||
<select2 [data]="macroOptions" [value]="macroOptions[selectedMacroIndex].id" (valueChanged)="onChange($event)" [width]="'100%'"></select2>
|
||||
|
||||
@@ -1,4 +1,39 @@
|
||||
<span class="privilege-checker-wrapper">
|
||||
<uhk-message header="Cannot talk to your UHK" subtitle="Your UHK has been detected, but its permissions are not set up yet, so Agent can't talk to it."></uhk-message>
|
||||
<button class="btn btn-default btn-lg btn-primary" (click)="setUpPermissions()"> Set up permissions </button>
|
||||
</span>
|
||||
<div class="privilege-checker-wrapper">
|
||||
<uhk-message header="Cannot talk to your UHK"
|
||||
subtitle="Your UHK has been detected, but its permissions are not set up yet, so Agent can't talk to it."></uhk-message>
|
||||
|
||||
<button class="btn btn-default btn-lg btn-primary"
|
||||
(click)="setUpPermissions()"> Set up permissions
|
||||
</button>
|
||||
|
||||
<div class="mt-10">
|
||||
<a class="link-inline"
|
||||
*ngIf="state.showWhatWillThisDo"
|
||||
(click)="whatWillThisDo()">What will this do?
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<p class="privilege-error"
|
||||
#privilegeError
|
||||
*ngIf="state.permissionSetupFailed">
|
||||
Agent wasn't able to set up permissions via PolicyKit. This is most likely because the
|
||||
<code>polkit</code> package is not installed on your system.
|
||||
</p>
|
||||
|
||||
<div *ngIf="state.showWhatWillThisDoContent">
|
||||
Agent uses the following script to set up permissions. You can run it manually as root, then
|
||||
<a class="link-inline"
|
||||
(click)="retry()">retry</a>.
|
||||
<div class="copy-container">
|
||||
<span class="fa fa-2x fa-copy"
|
||||
ngxClipboard
|
||||
[cbContent]="command"
|
||||
title="Copy to clipboard"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"></span>
|
||||
<pre><code>{{ command }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,3 +9,19 @@
|
||||
uhk-message {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.privilege-error {
|
||||
animation: error-fade-in 2s;
|
||||
}
|
||||
|
||||
@keyframes error-fade-in {
|
||||
0% {
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
100% {
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import 'rxjs/add/operator/ignoreElements';
|
||||
import 'rxjs/add/operator/takeWhile';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { AppState } from '../../store/index';
|
||||
import { AppState, getPrivilegePageState } from '../../store';
|
||||
import { SetPrivilegeOnLinuxAction } from '../../store/actions/device';
|
||||
import { LoadAppStartInfoAction, PrivilegeWhatWillThisDoAction } from '../../store/actions/app';
|
||||
import { PrivilagePageSate } from '../../models/privilage-page-sate';
|
||||
|
||||
@Component({
|
||||
selector: 'privilege-checker',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './privilege-checker.component.html',
|
||||
styleUrls: ['./privilege-checker.component.scss']
|
||||
})
|
||||
export class PrivilegeCheckerComponent {
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
export class PrivilegeCheckerComponent implements OnInit, OnDestroy {
|
||||
|
||||
state: PrivilagePageSate;
|
||||
|
||||
command = `cat <<EOF >/etc/udev/rules.d/50-uhk60.rules
|
||||
# Ultimate Hacking Keyboard rules
|
||||
# These are the udev rules for accessing the USB interfaces of the UHK as non-root users.
|
||||
# Copy this file to /etc/udev/rules.d and physically reconnect the UHK afterwards.
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE:="0666"
|
||||
EOF
|
||||
udevadm trigger
|
||||
udevadm settle`;
|
||||
|
||||
private stateSubscription: Subscription;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateSubscription = this.store.select(getPrivilegePageState)
|
||||
.subscribe(state => {
|
||||
this.state = state;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.stateSubscription) {
|
||||
this.stateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
setUpPermissions(): void {
|
||||
this.store.dispatch(new SetPrivilegeOnLinuxAction());
|
||||
}
|
||||
|
||||
whatWillThisDo(): void {
|
||||
this.store.dispatch(new PrivilegeWhatWillThisDoAction());
|
||||
}
|
||||
|
||||
retry(): void {
|
||||
this.store.dispatch(new LoadAppStartInfoAction());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<li class="sidebar__level-0--item">
|
||||
<div class="sidebar__level-0">
|
||||
<i class="uhk-icon uhk-icon-0401-usb-stick rotate-right"></i>
|
||||
<input #deviceName cancelable
|
||||
class="pane-title__name"
|
||||
type="text"
|
||||
(change)="editDeviceName($event.target.value)"
|
||||
(keyup.enter)="deviceName.blur()"
|
||||
(keyup)="calculateHeaderTextWidth($event.target.value)">
|
||||
<auto-grow-input [ngModel]="state.deviceName"
|
||||
[maxParentWidthPercent]="0.65"
|
||||
[css]="'side-menu-pane-title__name'"
|
||||
[disabled]="state.restoreUserConfiguration || state.updatingFirmware"
|
||||
(ngModelChange)="editDeviceName($event)"></auto-grow-input>
|
||||
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'device')"></i>
|
||||
</div>
|
||||
<ul [@toggler]="animation['device']">
|
||||
@@ -17,33 +16,43 @@
|
||||
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'configuration')"></i>
|
||||
</div>
|
||||
<ul [@toggler]="animation['configuration']">
|
||||
<li class="sidebar__level-2--item">
|
||||
<li class="sidebar__level-2--item"
|
||||
*ngIf="!state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/device/mouse-speed']"
|
||||
[class.disabled]="updatingFirmware$ | async">Mouse speed</a>
|
||||
[class.disabled]="state.updatingFirmware">Mouse speed</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item">
|
||||
<li class="sidebar__level-2--item"
|
||||
*ngIf="!state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/device/led-brightness']"
|
||||
[class.disabled]="updatingFirmware$ | async">LED brightness</a>
|
||||
[class.disabled]="state.updatingFirmware">LED brightness</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item">
|
||||
<li class="sidebar__level-2--item"
|
||||
*ngIf="!state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/device/configuration']"
|
||||
[class.disabled]="updatingFirmware$ | async">Configuration</a>
|
||||
[class.disabled]="state.updatingFirmware">Configuration</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item"
|
||||
*ngIf="state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/device/restore-user-configuration']">Fix configuration</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/device/firmware']"
|
||||
[class.disabled]="updatingFirmware$ | async">Firmware</a>
|
||||
[class.disabled]="state.updatingFirmware">Firmware</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="sidebar__level-1--item">
|
||||
<li class="sidebar__level-1--item"
|
||||
*ngIf="!state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-1">
|
||||
<i class="fa fa-keyboard-o"></i> Keymaps
|
||||
<!--a [routerLink]="['/keymap/add']"
|
||||
@@ -55,10 +64,10 @@
|
||||
(click)="toggleHide($event, 'keymap')"></i>
|
||||
</div>
|
||||
<ul [@toggler]="animation['keymap']">
|
||||
<li *ngFor="let keymap of keymaps$ | async" class="sidebar__level-2--item">
|
||||
<li *ngFor="let keymap of state.keymaps" class="sidebar__level-2--item">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/keymap', keymap.abbreviation]"
|
||||
[class.disabled]="updatingFirmware$ | async">{{keymap.name}}</a>
|
||||
[class.disabled]="state.updatingFirmware">{{keymap.name}}</a>
|
||||
<i *ngIf="keymap.isDefault" class="fa fa-star sidebar__fav"
|
||||
title="This is the default keymap which gets activated when powering the keyboard."
|
||||
data-toggle="tooltip" data-placement="bottom"></i>
|
||||
@@ -66,26 +75,27 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="sidebar__level-1--item">
|
||||
<li class="sidebar__level-1--item"
|
||||
*ngIf="!state.restoreUserConfiguration">
|
||||
<div class="sidebar__level-1">
|
||||
<i class="fa fa-play"></i> Macros
|
||||
<a (click)="addMacro()"
|
||||
class="btn btn-default pull-right btn-sm"
|
||||
[class.disabled]="updatingFirmware$ | async">
|
||||
[class.disabled]="state.updatingFirmware">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'macro')"></i>
|
||||
</div>
|
||||
<ul [@toggler]="animation['macro']">
|
||||
<li *ngFor="let macro of macros$ | async" class="sidebar__level-2--item">
|
||||
<li *ngFor="let macro of state.macros" class="sidebar__level-2--item">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/macro', macro.id]"
|
||||
[class.disabled]="updatingFirmware$ | async">{{macro.name}}</a>
|
||||
[class.disabled]="state.updatingFirmware">{{macro.name}}</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="sidebar__level-1--item" *ngIf="showAddonMenu$ | async">
|
||||
<li class="sidebar__level-1--item" *ngIf="state.showAddonMenu">
|
||||
<div class="sidebar__level-1">
|
||||
<i class="fa fa-puzzle-piece"></i> Add-on modules
|
||||
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'addon')"></i>
|
||||
@@ -94,25 +104,25 @@
|
||||
<li class="sidebar__level-2--item" data-name="Key cluster" data-abbrev="">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/add-on', 'Key cluster']"
|
||||
[class.disabled]="updatingFirmware$ | async">Key cluster</a>
|
||||
[class.disabled]="state.updatingFirmware">Key cluster</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item" data-name="Trackball" data-abbrev="">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/add-on', 'Trackball']"
|
||||
[class.disabled]="updatingFirmware$ | async">Trackball</a>
|
||||
[class.disabled]="state.updatingFirmware">Trackball</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item" data-name="Toucpad" data-abbrev="">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/add-on', 'Touchpad']"
|
||||
[class.disabled]="updatingFirmware$ | async">Touchpad</a>
|
||||
[class.disabled]="state.updatingFirmware">Touchpad</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item" data-name="Trackpoint" data-abbrev="">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/add-on', 'Trackpoint']"
|
||||
[class.disabled]="updatingFirmware$ | async">Trackpoint</a>
|
||||
[class.disabled]="state.updatingFirmware">Trackpoint</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -129,13 +139,13 @@
|
||||
<li class="sidebar__level-2--item">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/settings']"
|
||||
[class.disabled]="updatingFirmware$ | async">Settings</a>
|
||||
[class.disabled]="state.updatingFirmware">Settings</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__level-2--item">
|
||||
<div class="sidebar__level-2" [routerLinkActive]="['active']">
|
||||
<a [routerLink]="['/about']"
|
||||
[class.disabled]="updatingFirmware$ | async">About</a>
|
||||
[class.disabled]="state.updatingFirmware">About</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -162,22 +162,3 @@ ul {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pane-title {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&__name {
|
||||
border: none;
|
||||
border-bottom: 2px dotted #999;
|
||||
padding: 0;
|
||||
margin: 0 0.25rem;
|
||||
text-overflow: ellipsis;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { AfterContentInit, Component, ElementRef, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy, OnInit,
|
||||
Renderer2,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { Keymap, Macro } from 'uhk-common';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/let';
|
||||
|
||||
import { AppState, getDeviceName, runningInElectron, showAddonMenu, updatingFirmware } from '../../store';
|
||||
import { AppState, getSideMenuPageState } from '../../store';
|
||||
import { MacroActions } from '../../store/actions';
|
||||
import { getKeymaps, getMacros } from '../../store/reducers/user-configuration';
|
||||
import * as util from '../../util';
|
||||
import { RenameUserConfigurationAction } from '../../store/actions/user-config';
|
||||
import { SideMenuPageState } from '../../models/side-menu-page-state';
|
||||
|
||||
@Component({
|
||||
animations: [
|
||||
@@ -30,24 +35,19 @@ import { RenameUserConfigurationAction } from '../../store/actions/user-config';
|
||||
],
|
||||
selector: 'side-menu',
|
||||
templateUrl: './side-menu.component.html',
|
||||
styleUrls: ['./side-menu.component.scss']
|
||||
styleUrls: ['./side-menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SideMenuComponent implements AfterContentInit, OnDestroy {
|
||||
showAddonMenu$: Observable<boolean>;
|
||||
runInElectron$: Observable<boolean>;
|
||||
updatingFirmware$: Observable<boolean>;
|
||||
|
||||
deviceName$: Observable<string>;
|
||||
deviceNameSubscription: Subscription;
|
||||
keymaps$: Observable<Keymap[]>;
|
||||
macros$: Observable<Macro[]>;
|
||||
export class SideMenuComponent implements OnInit, OnDestroy {
|
||||
state: SideMenuPageState;
|
||||
animation: { [key: string]: 'active' | 'inactive' };
|
||||
deviceNameValue: string;
|
||||
updatingFirmware = false;
|
||||
updatingFirmwareSubscription: Subscription;
|
||||
@ViewChild('deviceName') deviceName: ElementRef;
|
||||
|
||||
constructor(private store: Store<AppState>, private renderer: Renderer2) {
|
||||
private stateSubscription: Subscription;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private renderer: Renderer2,
|
||||
private cdRef: ChangeDetectorRef) {
|
||||
this.animation = {
|
||||
device: 'active',
|
||||
configuration: 'active',
|
||||
@@ -55,34 +55,23 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
|
||||
macro: 'active',
|
||||
addon: 'active'
|
||||
};
|
||||
|
||||
this.keymaps$ = store.let(getKeymaps());
|
||||
this.macros$ = store.let(getMacros());
|
||||
|
||||
this.showAddonMenu$ = this.store.select(showAddonMenu);
|
||||
this.runInElectron$ = this.store.select(runningInElectron);
|
||||
this.deviceName$ = store.select(getDeviceName);
|
||||
this.deviceNameSubscription = this.deviceName$.subscribe(name => {
|
||||
this.deviceNameValue = name;
|
||||
this.setDeviceName();
|
||||
});
|
||||
this.updatingFirmware$ = store.select(updatingFirmware);
|
||||
this.updatingFirmwareSubscription = this.updatingFirmware$.subscribe(updating => {
|
||||
this.updatingFirmware = updating;
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.setDeviceName();
|
||||
ngOnInit(): void {
|
||||
this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => {
|
||||
this.state = data;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.deviceNameSubscription.unsubscribe();
|
||||
this.updatingFirmwareSubscription.unsubscribe();
|
||||
if (this.stateSubscription) {
|
||||
this.stateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
toggleHide(event: Event, type: string) {
|
||||
if (this.updatingFirmware) {
|
||||
if (this.state.updatingFirmware) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,24 +99,6 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
|
||||
}
|
||||
|
||||
editDeviceName(name: string): void {
|
||||
if (!util.isValidName(name) || name.trim() === this.deviceNameValue) {
|
||||
this.setDeviceName();
|
||||
return;
|
||||
}
|
||||
this.store.dispatch(new RenameUserConfigurationAction(name));
|
||||
}
|
||||
|
||||
calculateHeaderTextWidth(text): void {
|
||||
const htmlInput = this.deviceName.nativeElement as HTMLInputElement;
|
||||
const maxWidth = htmlInput.parentElement.offsetWidth * 0.66;
|
||||
const textWidth = util.getContentWidth(window.getComputedStyle(htmlInput), text);
|
||||
this.renderer.setStyle(htmlInput, 'width', Math.min(maxWidth, textWidth) + 'px');
|
||||
}
|
||||
|
||||
private setDeviceName(): void {
|
||||
if (this.deviceName) {
|
||||
this.renderer.setProperty(this.deviceName.nativeElement, 'value', this.deviceNameValue);
|
||||
this.calculateHeaderTextWidth(this.deviceName.nativeElement.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AfterViewInit, Component, EventEmitter, forwardRef, Input, Output, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { NouisliderComponent } from 'ng2-nouislider/src/nouislider';
|
||||
import { NouisliderComponent } from 'ng2-nouislider';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observer } from 'rxjs/Observer';
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
|
||||