82 Commits

Author SHA1 Message Date
László Monda
3978011d2e Bump Agent version to 1.2.5 and update changelog. 2018-06-26 03:13:49 +02:00
László Monda
cd1952a7df Restore blhost as blhost.old from 0f24427628 2018-06-26 02:21:33 +02:00
László Monda
4251477451 Comment out the export keymap icon because there isn't a way to import keymap yet, so it's useless. 2018-06-26 01:53:04 +02:00
Róbert Kiss
873f1de1ef fix: cancelling the key action popover holds its state (#699) 2018-06-25 00:27:14 +02:00
Róbert Kiss
150f993e5f feat: change side menu Agent icon (#698) 2018-06-25 00:05:01 +02:00
Róbert Kiss
06e76e5e0f fix: only flash the remapped key (#697) 2018-06-24 23:39:32 +02:00
Róbert Kiss
a208a264c7 fix: only animate keyboard separator when splitting (#696) 2018-06-24 23:06:33 +02:00
Róbert Kiss
114014fa13 feat: Show not supported OS on firmware page when relevant (#695) 2018-06-24 22:16:00 +02:00
Róbert Kiss
94cfd9d2e9 fix: SwitchKeymapAction not allow to refer to itself (#694) 2018-06-24 20:32:32 +02:00
Róbert Kiss
0aa9c73b4b feat: log firmware version before upgrading firmware (#693) 2018-06-24 19:56:11 +02:00
Róbert Kiss
5234f85dbe fix: close device when any error occurred in the communication (#692) 2018-06-24 18:59:13 +02:00
László Monda
bd8a2f704f Bump version to 1.2.4 and update changelog. 2018-06-21 18:23:49 +02:00
László Monda
439886d69f Replace Linux x86-64 blhost with a statically linked version that should run fine on every Linux distro. 2018-06-21 18:10:55 +02:00
László Monda
b2a37795e3 Add agent-logo-with-text.svg 2018-06-19 23:54:03 +02:00
László Monda
440db56080 Bump Agent version to 1.2.3 and update changelog. 2018-06-19 03:07:26 +02:00
László Monda
337e6e6bb6 Add example to the secondary role tooltip. 2018-06-19 02:44:44 +02:00
László Monda
a1aeda3d35 Further tweak the scancode tooltip. 2018-06-19 01:26:20 +02:00
László Monda
c6a83f8c9b Merge branch 'feat-tooltip-width' 2018-06-19 00:04:14 +02:00
László Monda
0f24427628 Replace the Linux x64 blhost binary with @iprok's version that seems to be more stable. 2018-06-18 23:53:51 +02:00
Róbert Kiss
f52dc36a6a feat: allow wider tooltip width than parent container (#682) 2018-06-18 23:37:15 +02:00
Róbert Kiss
63a936968d feat: allow wider tooltip width than parent container 2018-06-18 23:31:34 +02:00
László Monda
cabfde7963 Tweak the text of the scancode tooltip. 2018-06-18 23:26:06 +02:00
László Monda
79628c2351 Tweak the text of the key remap tooltip. 2018-06-18 22:49:48 +02:00
Róbert Kiss
762fa6f8bf fix: remap SwitchLayerAction 2018-06-18 19:50:34 +02:00
László Monda
a258c097a9 Downgrade to firmware 8.2.5 because it's the latest recommended version that doesn't contain annoying bugs. 2018-06-18 19:14:38 +02:00
László Monda
41faa98fcd Update the firmware update instructions and URL. 2018-06-16 18:22:01 +02:00
Róbert Kiss
c4d7318686 chore: make firmware update log shorter (#675)
* chore: add lodash to the roor package.json

* chore: make firmware update log shorter
2018-06-13 10:07:40 +02:00
Róbert Kiss
9ef11eaa34 feat: add keyboard separator line (#673)
* fix: blhost wrapper return with Promise.reject if error code !== 0

* feat: add keyboard separator line
2018-06-13 00:39:14 +02:00
Róbert Kiss
f34cb2df56 feat: remap key on all keymaps / layers (#668)
* add: popover checkboxs

* feat: add KeyActionRemap

* fix: template driven form checkbox name

* fix: delete key action only if it SwitchLayerAction

* feat: use remap on all keymaps/layers checkbox values in SAVE_KEY action

* feat: set default value to the remapOnAllKeymap and remapOnAllLayer checkbox

* fix: layer mapping
2018-06-10 21:50:49 +02:00
Róbert Kiss
83b9f0d1e9 fix: blhost wrapper return with Promise.reject if error code !== 0 (#669) 2018-06-07 23:38:27 +02:00
Róbert Kiss
7d81cf0c6a style: fix tslint error in switch-layer-action.ts (#666) 2018-06-07 22:54:20 +02:00
tenteen
82b76a9455 Fix left half timeout during fw update (#567) (#626)
The snooze call is skipped in the try block when the command throws an
exception.  As a result, the command tries maxTry times as fast as
possible.

The fix is to move the snooze call outside of the try block.

PS: #GotMyUHK
2018-06-07 22:50:18 +02:00
Róbert Kiss
4ae577f936 feat: make double tap to hold layer optional per key (#662)
* feat: make double tap to hold layer optional per key

* test: fix test serializer

* fix: remove "application start" text

* Add double-tap.svg

* Add closing dot at the end of the sentence.

* fead: add double-tap icon

* Bundle firmware version 8.3.0

* feat: 'layer-double-tap' feature flag

* feat: convert SwitchLayerMode to string enum
2018-06-07 22:11:41 +02:00
László Monda
81a83994ab Make the Export device configuration button show up as a primary button. 2018-05-28 15:58:17 +02:00
László Monda
1d3a3c7f5f Fix changelog. 2018-05-27 20:04:48 +02:00
László Monda
8bb645125d Update changelog and re-release v1.2.2 2018-05-27 02:53:10 +02:00
László Monda
9471b31a5d Instruct the user to power cycle the keyboard when encountered with an invalid hardware configuration. 2018-05-27 02:43:12 +02:00
László Monda
ffa52757c9 Upgrade to firmware 8.2.5 2018-05-27 02:31:22 +02:00
Róbert Kiss
ee53a0df9b feat: set disable style of device name in side menu (#656)
* feat: visualise disabled state of the 'Device name' input control

* fix: original value handling
2018-05-27 02:30:41 +02:00
Róbert Kiss
8e20c85e07 feat: add file upload component (#655)
The component allow upload the same file multiple times
2018-05-26 22:46:41 +02:00
László Monda
65ea786358 Don't include os.platform into the firmware update log because os.type makes it practically redundant. 2018-05-26 17:13:10 +02:00
László Monda
1035837b3b Fix lint error. 2018-05-22 15:29:48 +02:00
Róbert Kiss
18fc2e6b3f fix: permission detection when device not plugged (#653) 2018-05-22 15:11:43 +02:00
László Monda
fc728697d7 Bump version to 1.2.2 and update changelog and package.json 2018-05-22 02:19:48 +02:00
László Monda
cdf3caee9e Display device list at the beginning of the firmware update process. 2018-05-22 01:53:14 +02:00
László Monda
0a4d3a002e Include operating system type to the firmware update log. Add a new tip to the firmware page. 2018-05-22 01:16:02 +02:00
László Monda
d11c532ea4 Add a lot of useful instructions to the firmware page. 2018-05-22 00:38:27 +02:00
László Monda
1ff51697b1 Upgrade from firmware 8.2.2 to 8.2.4 2018-05-21 14:34:42 +02:00
Róbert Kiss
ab8ae31324 fix: permission detection on linux (#651) 2018-05-21 11:46:19 +02:00
László Monda
daa0e723b1 Display more detailed "invalid hardware configuration" errors. 2018-05-21 11:03:05 +02:00
Róbert Kiss
609aba856a fix: permission detection on win and mac (#650) 2018-05-21 11:00:57 +02:00
Róbert Kiss
a6678bd537 feat: update firmware version after update (#649)
* feat: add clipboard copy icon to the x-term-component

* feat: start device poll after firmware upgrade

* feat: remove the OK button from the firmware upgrade page

* feat: read the firmware after firmware upgrade

* fix: scrolling of the x-term-component

* feat: refresh the firmware version after recovery device

* fix: remove the scrollbar styling

* fix: stay on device firmware upgrade screen
2018-05-21 10:57:34 +02:00
László Monda
6c4f580fc2 Check if the keyboard is in factory reset mode and if so, display a relevant instruction. 2018-05-20 01:40:45 +02:00
László Monda
ea41661c65 Add erase-user-config.js 2018-05-19 21:53:41 +02:00
László Monda
c553c7b63b Rename erase-hca.js to erase-hardware-config.js 2018-05-19 21:33:12 +02:00
László Monda
e5988aa800 Rename write-hca.js to write-hardware-config.js 2018-05-19 21:32:15 +02:00
László Monda
ae319c607f Rename write-userconfig.js to write-user-config.js 2018-05-19 21:14:48 +02:00
László Monda
5d23ad1c9e Fix write-hca.js and write-user.js, and remove write-config.js. Fixes #627. 2018-05-19 20:59:11 +02:00
László Monda
55eef50da7 Add erase-hca.js 2018-05-19 20:05:41 +02:00
Róbert Kiss
653465f0e0 feat: device recovery mode (#642)
* add new page and ipc processing

* refactor: remove unused references from uhk.js

* feat: add device recovery route

* refactor: device permission

* feat: write firmware update log to the screen

* fix: xterm height

* feat: add reload button to the recovery page

* refactor: deviceConnectionState.hasPermission in appStartInfo

* refactor: use correct imports

* refactor: move .ok-button css class to the main style.scss

* feat: add bootload active route guard

* style: move RecoveryDeviceAction into new line

* feat: delete reload button

* feat: start device polling after device recovery
2018-05-19 17:22:46 +02:00
László Monda
2cf8044987 Check the signature of the hardware configuration area instead of uniqueId. 2018-05-19 14:07:18 +02:00
László Monda
3c056a7255 Display "Invalid hardware configuration" when the hardware configuration area is uninitialized instead of a general error message. Improves #623. 2018-05-19 13:38:16 +02:00
Róbert Kiss
091796d13c feat: not allow non ascii character in macro action text (#645) 2018-05-17 23:56:36 +02:00
László Monda
eb97dd844f Make the bootloader timeout of the reenumerate script specifiable. 2018-05-16 23:19:36 +02:00
Róbert Kiss
17693ec8fe build: upgrade node.js => 8.11.2 (#641) 2018-05-16 22:08:11 +02:00
Róbert Kiss
7c7ce8f50f feat: only send auto update notification when user indicated the update (#640)
* feat: only send auto update notification when user indicated the update

* fix: add more logging to the auto update process
2018-05-16 22:05:13 +02:00
László Monda
e294727ac5 Bump Agent version to 1.2.1 and update the changelog. 2018-05-12 11:26:41 +02:00
László Monda
f29d64c803 Rename kbootKommandName to kbootCommandName. 2018-05-09 02:16:38 +02:00
László Monda
0385b0ce29 Simply write that the list of available devices is unchanged instead of always listing the devices. 2018-05-09 01:32:55 +02:00
László Monda
b526274cd7 Upgrade to firmware 8.2.2 2018-05-09 00:38:48 +02:00
László Monda
88c16af4a9 Merge branch 'master' of github.com:UltimateHackingKeyboard/agent 2018-05-08 17:17:36 +02:00
László Monda
05ac9a6832 Clarify the content of the tooltop regarding non-US characters. 2018-05-08 17:17:04 +02:00
Kristian Sloth Lauszus
04aa5236c2 The usage page and usage number was changed in 6f0b1adc14 (#630) 2018-05-06 21:38:40 +02:00
László Monda
ec98e4e1c6 Rephrase and add explanations about the macro engine not being ready yet. 2018-05-06 03:24:54 +02:00
László Monda
bb9ece494c Update README.md 2018-05-05 21:55:52 +02:00
László Monda
217e6776ac Add changelog entry that I forgot to add. 2018-05-03 01:38:40 +02:00
László Monda
2286218980 Make long media key macro actions work. (#621)
* Make long media key macro actions work.

* fix: macro key action mapping
2018-05-03 00:28:35 +02:00
Róbert Kiss
3d9c83f9f4 build: check node version before build (#625)
* build: check node version before build

* use package.json engine section
2018-05-01 22:50:28 +02:00
Róbert Kiss
14ed163238 chore: npm run clean delete root node_modules and dist folders (#624) 2018-05-01 22:46:23 +02:00
László Monda
c815de0718 Edit phrasing: "... layer by *tapping* this key" 2018-05-01 03:20:44 +02:00
László Monda
6a46556d9e Make get-right-firmware-version.js display not only firmware version but also device protocol version, module protocol version, user config version and hardware config version. 2018-05-01 02:31:17 +02:00
krokofant
f8f820529f Resolve #553 (#614) 2018-04-29 21:32:33 +02:00
120 changed files with 2806 additions and 580 deletions

2
.nvmrc
View File

@@ -1 +1 @@
8.9.4
8.11.2

View File

@@ -6,11 +6,70 @@ 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.5] - 2018-06-26
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
- When remapping a switch keymap action on all keymaps, don't set it on its own keymap.
- Make the key action popver always contain the action of the current key, even after cancelled.
- Include the firmware version to be updated to the firmware update log.
- Update the Agent icon of the side menu and the about page.
- When remapping a key, only flash the affected key instead of all keys.
- Fade in/out the keyboard separator line only when splitting the keyboard.
- Only show the unsupported OS message of the firmware page on relevant Windows versions.
- Close and reopen USB device when an error occurs.
- Temporarily remove the export keymap feature because it's useless until import is implemented.
## [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

View File

@@ -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

191
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "uhk-agent",
"version": "1.1.4",
"version": "1.2.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -96,6 +96,21 @@
"@types/node": "8.0.53"
}
},
"@types/lodash": {
"version": "4.14.109",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz",
"integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==",
"dev": true
},
"@types/lodash-es": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.0.tgz",
"integrity": "sha512-h8lkWQSgT4qjs9PcIhcL2nWubZeXRVzjZxYlRFmcX9BW1PIk5qRc0djtRWZqtM+GDDFhwBt0ztRu72D/YxIcEw==",
"dev": true,
"requires": {
"@types/lodash": "4.14.109"
}
},
"@types/node": {
"version": "8.0.53",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz",
@@ -916,6 +931,12 @@
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
"dev": true
},
"base64url": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz",
"integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=",
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
@@ -1494,6 +1515,58 @@
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
"dev": true
},
"check-node-version": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-3.2.0.tgz",
"integrity": "sha512-mJu4dADRf+NUeOyGgFTXaLtjyyffD3Eej2RA9IEk1CdHmoVurErLD++e/Ps6uKfsB273ky+0Z9NlOiuplxuNdw==",
"dev": true,
"requires": {
"chalk": "2.4.1",
"map-values": "1.0.1",
"minimist": "1.2.0",
"object-filter": "1.0.2",
"object.assign": "4.1.0",
"run-parallel": "1.1.9",
"semver": "5.4.1"
},
"dependencies": {
"ansi-styles": {
"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==",
"dev": true,
"requires": {
"color-convert": "1.9.1"
}
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
"ansi-styles": "3.2.1",
"escape-string-regexp": "1.0.5",
"supports-color": "5.4.0"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"supports-color": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"dev": true,
"requires": {
"has-flag": "3.0.0"
}
}
}
},
"chokidar": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
@@ -6384,6 +6457,72 @@
"assert-plus": "1.0.0"
}
},
"gh-pages": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-1.1.0.tgz",
"integrity": "sha512-ZpDkeOVmIrN5mz+sBWDz5zmTqcbNJzI/updCwEv/7rrSdpTNlj1B5GhBqG7f4Q8p5sJOdnBV0SIqxJrxtZQ9FA==",
"dev": true,
"requires": {
"async": "2.6.0",
"base64url": "2.0.0",
"commander": "2.11.0",
"fs-extra": "4.0.3",
"globby": "6.1.0",
"graceful-fs": "4.1.11",
"rimraf": "2.6.2"
},
"dependencies": {
"async": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
"dev": true,
"requires": {
"lodash": "4.17.4"
}
},
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"fs-extra": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
"integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
"dev": true,
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"requires": {
"glob": "7.1.2"
}
}
}
},
"git-raw-commits": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.2.0.tgz",
@@ -6692,6 +6831,12 @@
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
"dev": true
},
"has-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
"dev": true
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -8223,6 +8368,12 @@
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true
},
"lodash-es": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=",
"dev": true
},
"lodash._arraymap": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._arraymap/-/lodash._arraymap-3.0.0.tgz",
@@ -8462,6 +8613,12 @@
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
},
"map-values": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz",
"integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=",
"dev": true
},
"map-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
@@ -9197,6 +9354,12 @@
}
}
},
"object-filter": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz",
"integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=",
"dev": true
},
"object-keys": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz",
@@ -9220,6 +9383,26 @@
}
}
},
"object.assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": {
"define-properties": "1.1.2",
"function-bind": "1.1.1",
"has-symbols": "1.0.0",
"object-keys": "1.0.11"
},
"dependencies": {
"object-keys": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
"integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
"dev": true
}
}
},
"object.omit": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
@@ -10567,6 +10750,12 @@
"is-promise": "2.1.0"
}
},
"run-parallel": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
"integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
"dev": true
},
"rx-lite": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",

View File

@@ -3,10 +3,10 @@
"private": true,
"author": "Ultimate Gadget Laboratories",
"main": "electron/dist/electron-main.js",
"version": "1.2.0",
"firmwareVersion": "8.2.0",
"deviceProtocolVersion": "4.3.0",
"userConfigVersion": "4.0.0",
"version": "1.2.5",
"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": {
@@ -23,16 +23,18 @@
"@types/electron-settings": "3.0.0",
"@types/fs-extra": "5.0.1",
"@types/jasmine": "2.6.0",
"@types/jsonfile": "4.0.1",
"@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",
@@ -49,8 +51,10 @@
"exports-loader": "0.6.3",
"file-loader": "0.10.0",
"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",
@@ -84,6 +88,7 @@
"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",
@@ -99,7 +104,9 @@
"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": {}
}

View File

@@ -21,7 +21,9 @@ import * as isDev from 'electron-is-dev';
const optionDefinitions = [
{name: 'addons', type: Boolean},
{name: 'spe', type: Boolean} // simulate privilege escalation error
{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: CommandLineArgs = commandLineArgs(optionDefinitions);
@@ -104,7 +106,7 @@ function createWindow() {
uhkHidDeviceService = new UhkHidDevice(logger, options);
uhkBlhost = new UhkBlhost(logger, packagesDir);
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
sudoService = new SudoService(logger, options);

View File

@@ -3,5 +3,6 @@ import { SynchrounousResult } from 'tmp';
export interface TmpFirmware {
rightFirmwarePath: string;
leftFirmwarePath: string;
packageJsonPath: string;
tmpDirectory: SynchrounousResult;
}

View File

@@ -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;
}

View File

@@ -1,5 +1,6 @@
import { BrowserWindow, ipcMain, shell } from 'electron';
import { ipcMain, shell } from 'electron';
import { UhkHidDevice } from 'uhk-usb';
import * as os from 'os';
import { AppStartInfo, IpcEvents, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
@@ -22,13 +23,17 @@ 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,
platform: process.platform as string,
osVersion: os.release()
};
this.logService.info('[AppService] getAppStartInfo response:', response);
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);

View File

@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import {
ConfigurationReply,
DeviceConnectionState,
FirmwareUpgradeIpcResponse,
getHardwareConfigFromDeviceResponse,
HardwareModules,
IpcEvents,
@@ -10,10 +11,11 @@ import {
mapObjectToUserConfigBinaryBuffer,
SaveUserConfigurationData
} from 'uhk-common';
import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
import { deviceConnectionStateComparer, snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { emptyDir } from 'fs-extra';
import * as path from 'path';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/startWith';
@@ -21,10 +23,14 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
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';
import {
backupUserConfiguration,
getBackupUserConfigurationContent,
getPackageJsonFromPathAsync,
saveTmpFirmware
} from '../util';
/**
* IpcMain pair of the UHK Communication
@@ -39,7 +45,8 @@ export class DeviceService {
constructor(private logService: LogService,
private win: Electron.BrowserWindow,
private device: UhkHidDevice,
private operations: UhkOperations) {
private operations: UhkOperations,
private rootDir: string) {
this.pollUhkDevice();
ipcMain.on(IpcEvents.device.saveUserConfiguration, (...args: any[]) => {
@@ -71,6 +78,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');
}
@@ -84,10 +100,7 @@ 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;
@@ -110,33 +123,67 @@ 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 {
const hardwareModules = await this.getHardwareModules(false);
this.logService.debug('Device right firmware version:', hardwareModules.rightModuleInfo.firmwareVersion);
this.logService.debug('Device left firmware version:', hardwareModules.leftModuleInfo.firmwareVersion);
this.device.resetDeviceCache();
this.stopPollTimer();
if (args && args.length > 0) {
firmwarePathData = await saveTmpFirmware(args[0]);
const packageJson = await getPackageJsonFromPathAsync(firmwarePathData.packageJsonPath);
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath);
await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath);
}
else {
const packageJsonPath = path.join(this.rootDir, 'packages/firmware/package.json');
const packageJson = await getPackageJsonFromPathAsync(packageJsonPath);
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
await this.operations.updateRightFirmware();
await this.operations.updateLeftModule();
}
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;
}
@@ -144,6 +191,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);
}
@@ -161,16 +237,11 @@ 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();
}

View File

@@ -0,0 +1,13 @@
import * as fs from 'fs';
export const getPackageJsonFromPathAsync = async (filePath: string): Promise<any> => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, {encoding: 'utf-8'}, (err, data) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(data));
});
});
};

View File

@@ -0,0 +1,3 @@
export * from './backup-user-confoguration';
export * from './get-package-json-from-path-async';
export * from './save-extract-firmware';

View File

@@ -16,8 +16,8 @@ export async function saveTmpFirmware(data: string): Promise<TmpFirmware> {
return {
tmpDirectory,
rightFirmwarePath: path.join(tmpDirectory.name, 'devices/uhk60-right/firmware.hex'),
leftFirmwarePath: path.join(tmpDirectory.name, 'modules/uhk60-left.bin')
leftFirmwarePath: path.join(tmpDirectory.name, 'modules/uhk60-left.bin'),
packageJsonPath: path.join(tmpDirectory.name, 'package.json')
};
}

View File

@@ -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 {

View File

@@ -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);
});
});
});

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,4 +1,4 @@
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';
@@ -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 {

View File

@@ -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">');
});
});

View File

@@ -4,4 +4,7 @@ export interface AppStartInfo {
commandLineArgs: CommandLineArgs;
deviceConnected: boolean;
hasPermission: boolean;
bootloaderActive: boolean;
platform: string;
osVersion: string;
}

View File

@@ -7,4 +7,9 @@ export interface CommandLineArgs {
* 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;
}

View File

@@ -1,4 +1,5 @@
export interface DeviceConnectionState {
connected: boolean;
hasPermission: boolean;
bootloaderActive: boolean;
}

View File

@@ -1,4 +1,10 @@
import { HardwareModules } from './hardware-modules';
export class IpcResponse {
success: boolean;
error?: { message: string };
}
export class FirmwareUpgradeIpcResponse extends IpcResponse {
modules?: HardwareModules;
}

View File

@@ -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';
}

View File

@@ -5,10 +5,15 @@ export const getHardwareConfigFromDeviceResponse = (json: string): HardwareConfi
const hardwareConfig = new HardwareConfiguration();
hardwareConfig.fromBinary(UhkBuffer.fromArray(data));
if (hardwareConfig.uniqueId > 0) {
return hardwareConfig;
if (hardwareConfig.signature === 'FTY') {
throw Error('The device is in factory reset mode. Power-cycle the device to use it with Agent!');
}
return null;
if (hardwareConfig.signature !== 'UHK') {
throw Error('Please power cycle your keyboard (Invalid hardware configuration: Invalid signature)');
}
return hardwareConfig;
};
export const getUserConfigFromDeviceResponse = (json: string): UserConfiguration => {

View File

@@ -2,6 +2,7 @@ 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 {

View File

@@ -29,6 +29,7 @@ export 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 {

View 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;
};

View File

@@ -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;
}

View File

@@ -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}`));
}

View File

@@ -1,5 +1,5 @@
import { Device, devices, HID } from 'node-hid';
import { CommandLineArgs, 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,6 +24,7 @@ export class UhkHidDevice {
* Internal variable that represent the USB UHK device
* @private
*/
private _prevDevices = [];
private _device: HID;
private _hasPermission = false;
@@ -48,12 +49,16 @@ export class UhkHidDevice {
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) {
@@ -67,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;
}
/**
@@ -95,6 +109,7 @@ export class UhkHidDevice {
device.read((err: any, receivedData: Array<number>) => {
if (err) {
this.logService.error('[UhkHidDevice] Transfer error: ', err);
this.close();
return reject(err);
}
const logString = bufferToString(receivedData);
@@ -142,6 +157,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}`);
@@ -197,7 +216,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]);
@@ -233,19 +252,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) {
@@ -256,7 +280,7 @@ export class UhkHidDevice {
}
}
function kbootKommandName(module: ModuleSlotToI2cAddress): string {
function kbootCommandName(module: ModuleSlotToI2cAddress): string {
switch (module) {
case ModuleSlotToI2cAddress.leftHalf:
return 'leftHalf';

View File

@@ -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)}`];

View File

@@ -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);
};

View File

@@ -1260,19 +1260,6 @@
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.2.9.tgz",
"integrity": "sha512-AmYGadmTv+Xh6re2CH5ruyvV3znvtJbhxyT00JQAGFP2U+xgqhf+C2xfjdP/GgK5d9YmSif/UYs2ssMl4gW6fw=="
},
"@types/lodash": {
"version": "4.14.106",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.106.tgz",
"integrity": "sha512-tOSvCVrvSqFZ4A/qrqqm6p37GZoawsZtoR0SJhlF7EonNZUgrn8FfT+RNQ11h+NUpMt6QVe36033f3qEKBwfWA=="
},
"@types/lodash-es": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.0.tgz",
"integrity": "sha512-h8lkWQSgT4qjs9PcIhcL2nWubZeXRVzjZxYlRFmcX9BW1PIk5qRc0djtRWZqtM+GDDFhwBt0ztRu72D/YxIcEw==",
"requires": {
"@types/lodash": "4.14.106"
}
},
"@types/node": {
"version": "9.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.2.tgz",
@@ -6075,11 +6062,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="
},
"lodash-es": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
},
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",

View File

@@ -39,7 +39,6 @@
"@types/jasmine": "2.5.53",
"@types/jasminewd2": "2.0.2",
"@types/jquery": "3.2.9",
"@types/lodash-es": "4.17.0",
"@types/usb": "1.1.3",
"angular-confirmation-popover": "3.2.0",
"angular-notifier": "2.0.0",
@@ -60,7 +59,6 @@
"karma-coverage-istanbul-reporter": "1.2.1",
"karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2",
"lodash-es": "4.17.4",
"ng2-dragula": "1.5.0",
"ng2-nouislider": "^1.7.7",
"ng2-select2": "1.0.0-beta.10",

View File

@@ -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,

View File

@@ -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)">

View File

@@ -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');
}
}

View File

@@ -0,0 +1 @@
export * from './auto-grow-input.component';

View File

@@ -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"

View File

@@ -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));
}
}

View File

@@ -5,6 +5,7 @@ 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 = [
{
@@ -34,6 +35,10 @@ export const deviceRoutes: Routes = [
{
path: 'restore-user-configuration',
component: RestoreConfigurationComponent
},
{
path: 'recovery-mode',
component: RecoveryModeComponent
}
]
}

View File

@@ -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 *ngIf="showUnsupportedOsToFirmwareUpgrade$ | async">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>

View File

@@ -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;
}

View File

@@ -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, HardwareModules, VersionInformation } from 'uhk-common';
import { OpenUrlInNewWindowAction } from '../../../store/actions/app';
import {
AppState,
firmwareOkButtonDisabled,
flashFirmwareButtonDisbabled,
getAgentVersionInfo,
getHardwareModules,
showUnsupportedOsToFirmwareUpgrade,
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,22 @@ 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;
showUnsupportedOsToFirmwareUpgrade$: Observable<boolean>;
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;
});
this.showUnsupportedOsToFirmwareUpgrade$ = store.select(showUnsupportedOsToFirmwareUpgrade);
}
ngOnDestroy(): void {
this.xtermLogSubscription.unsubscribe();
this.hardwareModulesSubscription.unsubscribe();
}
@@ -60,22 +51,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));
}
}

View File

@@ -3,4 +3,5 @@ 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';

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
width: 100%;
p {
margin: 1.5rem 0;
}
}

View File

@@ -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());
}
}

View File

@@ -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>

View File

@@ -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]);
}
}

View File

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

View File

@@ -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>

View File

@@ -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() {

View File

@@ -37,12 +37,12 @@
data-placement="bottom"
(click)="duplicateKeymap()"
></i>
<i class="fa fa-download keymap__download pull-right"
<!--i class="fa fa-download keymap__download pull-right"
title="Download keymap"
[html]="true"
data-toggle="tooltip"
data-placement="bottom"
(click)="onDownloadIconClick()"></i>
(click)="onDownloadIconClick()"></i-->
</h1>
</div>
</uhk-header>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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 = () => {

View File

@@ -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"

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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,
@@ -82,9 +83,10 @@ export class PopoverComponent implements OnChanges {
@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);

View File

@@ -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>

View File

@@ -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]];
}

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -2,13 +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"
[readonly]="state.restoreUserConfiguration"
(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']">

View File

@@ -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;
}
}
}

View File

@@ -1,5 +1,4 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@@ -19,7 +18,6 @@ import 'rxjs/add/operator/let';
import { AppState, getSideMenuPageState } from '../../store';
import { MacroActions } from '../../store/actions';
import * as util from '../../util';
import { RenameUserConfigurationAction } from '../../store/actions/user-config';
import { SideMenuPageState } from '../../models/side-menu-page-state';
@@ -40,7 +38,7 @@ import { SideMenuPageState } from '../../models/side-menu-page-state';
styleUrls: ['./side-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
export class SideMenuComponent implements OnInit, OnDestroy {
state: SideMenuPageState;
animation: { [key: string]: 'active' | 'inactive' };
@ViewChild('deviceName') deviceName: ElementRef;
@@ -62,15 +60,10 @@ export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
ngOnInit(): void {
this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => {
this.state = data;
this.setDeviceName();
this.cdRef.markForCheck();
});
}
ngAfterContentInit(): void {
this.setDeviceName();
}
ngOnDestroy(): void {
if (this.stateSubscription) {
this.stateSubscription.unsubscribe();
@@ -106,24 +99,6 @@ export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
}
editDeviceName(name: string): void {
if (!util.isValidName(name) || name.trim() === this.state.deviceName) {
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.state.deviceName);
this.calculateHeaderTextWidth(this.deviceName.nativeElement.value);
}
}
}

View File

@@ -1,5 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" [attr.viewBox]="viewBox" height="100%" width="100%">
<svg:g svg-module *ngFor="let module of modules; let i = index"
<svg xmlns="http://www.w3.org/2000/svg"
[attr.viewBox]="viewBox"
height="100%"
width="100%">
<svg:g svg-module
*ngFor="let module of modules; let i = index"
[coverages]="module.coverages"
[keyboardKeys]="module.keyboardKeys"
[keybindAnimationEnabled]="keybindAnimationEnabled"
@@ -11,8 +15,11 @@
[selected]="selectedKey?.moduleId === i"
(keyClick)="onKeyClick(i, $event.index, $event.keyTarget)"
(keyHover)="onKeyHover($event.index, $event.event, $event.over, i)"
(capture)="onCapture(i, $event.index, $event.captured)"
/>
(capture)="onCapture(i, $event.index, $event.captured)" />
<svg:path [@fadeSeparator]="separatorAnimation"
[attr.d]="separator.d"
[attr.style]="separatorStyle" />
</svg>
<editable-text *ngIf="showDescription"
[ngModel]="description"

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,10 +1,12 @@
import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges, ChangeDetectionStrategy } from '@angular/core';
import { animate, state, trigger, style, transition } from '@angular/animations';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { Module } from 'uhk-common';
import { SvgModule } from '../module';
import { SvgModuleProviderService } from '../../../services/svg-module-provider.service';
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
import { SvgSeparator } from '../separator';
@Component({
selector: 'svg-keyboard',
@@ -20,6 +22,16 @@ import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
transform: 'translate(3%, 15%) rotate(-4deg) scale(0.92, 0.92)'
})),
transition('* <=> *', animate(500))
]),
trigger('fadeSeparator', [
state('visible', style({
opacity: 1
})),
state('invisible', style({
opacity: 0
})),
transition('visible => invisible', animate(500)),
transition('invisible => visible', animate(1500))
])
]
})
@@ -41,8 +53,12 @@ export class SvgKeyboardComponent implements OnInit {
modules: SvgModule[];
viewBox: string;
moduleAnimationStates: string[];
separator: SvgSeparator;
separatorStyle: SafeStyle;
separatorAnimation = 'visible';
constructor(private svgModuleProvider: SvgModuleProviderService) {
constructor(private svgModuleProvider: SvgModuleProviderService,
private sanitizer: DomSanitizer) {
this.modules = [];
this.viewBox = '-520 582 1100 470';
this.halvesSplit = false;
@@ -91,12 +107,16 @@ export class SvgKeyboardComponent implements OnInit {
private updateModuleAnimationStates() {
if (this.halvesSplit) {
this.moduleAnimationStates = ['rotateRight', 'rotateLeft'];
this.separatorAnimation = 'invisible';
} else {
this.moduleAnimationStates = [];
this.separatorAnimation = 'visible';
}
}
private setModules() {
this.modules = this.svgModuleProvider.getSvgModules(this.keyboardLayout);
this.separator = this.svgModuleProvider.getSvgSeparator();
this.separatorStyle = this.sanitizer.bypassSecurityTrustStyle(this.separator.style);
}
}

View File

@@ -17,13 +17,14 @@ import {
MouseAction,
PlayMacroAction,
SwitchKeymapAction,
SwitchLayerAction
SwitchLayerAction,
SwitchLayerMode
} from 'uhk-common';
import { CaptureService } from '../../../../services/capture.service';
import { MapperService } from '../../../../services/mapper.service';
import { AppState } from '../../../../store/index';
import { AppState } from '../../../../store';
import { getMacros } from '../../../../store/reducers/user-configuration';
enum LabelTypes {
@@ -288,12 +289,18 @@ export class SvgKeyboardKeyComponent implements OnInit, OnChanges, OnDestroy {
break;
}
if (keyAction.isLayerToggleable) {
if (keyAction.switchLayerMode === SwitchLayerMode.toggle) {
this.labelType = LabelTypes.TextIcon;
this.labelSource = {
text: newLabelSource,
icon: this.mapper.getIcon('toggle')
};
} else if (keyAction.switchLayerMode === SwitchLayerMode.holdAndDoubleTapToggle) {
this.labelType = LabelTypes.TextIcon;
this.labelSource = {
text: newLabelSource,
icon: this.mapper.getIcon('double-tap')
};
} else {
this.labelType = LabelTypes.OneLineText;
this.labelSource = newLabelSource;

View File

@@ -0,0 +1,5 @@
import { SvgSeparator } from './svg-separator.model';
export const convertXmlToSvgSeparator = (obj: { path: any[], $: Object }): SvgSeparator => {
return obj.path[0].$;
};

View File

@@ -0,0 +1,2 @@
export * from './svg-separator.model';
export * from './convert-xml-to-svg-separator';

View File

@@ -0,0 +1,4 @@
export interface SvgSeparator {
style: string;
d: string;
}

View File

@@ -13,8 +13,18 @@
(capture)="onCapture($event.moduleId, $event.keyId, $event.captured)"
(descriptionChanged)="onDescriptionChanged($event)"
></keyboard-slider>
<popover tabindex="0" [visible]="popoverShown" [keyPosition]="keyPosition" [wrapPosition]="wrapPosition" [defaultKeyAction]="popoverInitKeyAction"
[currentKeymap]="keymap" [currentLayer]="currentLayer" (cancel)="hidePopover()" (remap)="onRemap($event)"></popover>
<popover tabindex="0"
[visible]="popoverShown"
[keyPosition]="keyPosition"
[wrapPosition]="wrapPosition"
[defaultKeyAction]="popoverInitKeyAction"
[currentKeymap]="keymap"
[currentLayer]="currentLayer"
[allowLayerDoubleTap]="allowLayerDoubleTap"
(cancel)="hidePopover()"
(remap)="onRemap($event)"></popover>
<div class="tooltip bottom"
[class.in]="tooltipData.show"
[style.top.px]="tooltipData.posTop"

View File

@@ -32,7 +32,8 @@ import {
PlayMacroAction,
SecondaryRoleAction,
SwitchKeymapAction,
SwitchLayerAction
SwitchLayerAction,
SwitchLayerMode
} from 'uhk-common';
import { MapperService } from '../../../services/mapper.service';
@@ -41,6 +42,7 @@ import { KeymapActions } from '../../../store/actions';
import { PopoverComponent } from '../../popover';
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription';
import { KeyActionRemap } from '../../../models/key-action-remap';
interface NameValuePair {
name: string;
@@ -59,6 +61,8 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
@Input() tooltipEnabled: boolean = false;
@Input() halvesSplit: boolean;
@Input() keyboardLayout: KeyboardLayout.ANSI;
@Input() allowLayerDoubleTap: boolean;
@Output() descriptionChanged = new EventEmitter<ChangeKeymapDescription>();
@ViewChild(PopoverComponent, { read: ElementRef }) popover: ElementRef;
@@ -178,11 +182,15 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
this.currentLayer,
moduleId,
keyId,
keystrokeAction)
{
remapOnAllKeymap: false,
remapOnAllLayer: false,
action: keystrokeAction
})
);
}
onRemap(keyAction: KeyAction): void {
onRemap(keyAction: KeyActionRemap): void {
this.store.dispatch(
KeymapActions.saveKey(
this.keymap,
@@ -231,6 +239,7 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
hidePopover(): void {
this.popoverShown = false;
this.selectedKey = undefined;
this.popoverInitKeyAction = null;
}
selectLayer(index: number): void {
@@ -350,7 +359,7 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
},
{
name: 'Toogle',
value: switchLayerAction.isLayerToggleable ? 'On' : 'Off'
value: switchLayerAction.switchLayerMode === SwitchLayerMode.toggle ? 'On' : 'Off'
}
];
return Observable.of(content);

View File

@@ -1,5 +1,17 @@
<div class="wrapper">
<div class="x-term-container">
<div class="x-term-wrapper" #scrollMe>
<ul class="list-unstyled">
<li *ngFor="let log of logs" [ngClass]="log.cssClass"><span>{{ log.message }}</span></li>
</ul>
</div>
<div class="copy-container-wrapper">
<div class="copy-container">
<span class="fa fa-2x fa-copy"
ngxClipboard
[cbContent]="getClipboardContent()"
title="Copy to clipboard"
data-toggle="tooltip"
data-placement="top"></span>
</div>
</div>
</div>

View File

@@ -1,9 +1,36 @@
$scrollbar-color: #ffffff;
$scrollbar-radius: 6px;
:host {
background-color: yellow;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
height: 100%;
}
.wrapper {
.x-term-container {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
position: relative;
}
.x-term-wrapper {
background-color: black;
overflow: auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.copy-container-wrapper {
position: absolute;
top: 2px;
right: 14px;
}
.xterm-standard {

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { XtermLog } from '../../models/xterm-log';
@Component({
@@ -7,6 +7,21 @@ import { XtermLog } from '../../models/xterm-log';
styleUrls: ['./xterm.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class XtermComponent {
export class XtermComponent implements OnChanges {
@Input() logs: Array<XtermLog> = [];
@ViewChild('scrollMe') divElement: ElementRef;
ngOnChanges(changes: SimpleChanges): void {
if (changes.logs && this.divElement && this.divElement.nativeElement) {
setTimeout(() => {
this.divElement.nativeElement.scrollTop = this.divElement.nativeElement.scrollHeight;
});
}
}
getClipboardContent(): string {
return this.logs.reduce((value, line) => value + line.message + '\n', '');
}
}

View File

@@ -9,15 +9,10 @@ export class TooltipDirective implements AfterContentInit, OnChanges {
@HostBinding('attr.data-placement') placement: string;
@Input('title') title: string;
@Input('html') html: boolean;
@Input() maxWidth: number;
private customTooltipTemplate = `
<div class="tooltip">
<div class="tooltip-arrow"></div>
<div class="tooltip-inner"></div>
</div>
`;
constructor(private elementRef: ElementRef, private sanitizer: DomSanitizer) { }
constructor(private elementRef: ElementRef, private sanitizer: DomSanitizer) {
}
ngAfterContentInit() {
this.init();
@@ -33,7 +28,7 @@ export class TooltipDirective implements AfterContentInit, OnChanges {
(<any>jQuery(this.elementRef.nativeElement)).tooltip({
placement: this.placement,
html: this.html,
template: this.customTooltipTemplate,
template: this.getCustomTemplate(),
title: this.title
});
}
@@ -42,7 +37,7 @@ export class TooltipDirective implements AfterContentInit, OnChanges {
(<any>jQuery(this.elementRef.nativeElement)).tooltip({
placement: this.placement,
html: this.html,
template: this.customTooltipTemplate,
template: this.getCustomTemplate(),
title: this.title
});
@@ -50,4 +45,19 @@ export class TooltipDirective implements AfterContentInit, OnChanges {
.attr('title', this.title))
.tooltip('fixTitle');
}
private getCustomTemplate(): string {
let tooltipStyle = '';
let innerStyle = '';
if (this.maxWidth) {
tooltipStyle = `style="width: ${this.maxWidth}px;"`;
innerStyle = `style="max-width: ${this.maxWidth}px;"`;
}
return `<div class="tooltip" ${tooltipStyle}>
<div class="tooltip-arrow"></div>
<div class="tooltip-inner" ${innerStyle}></div>
</div>`;
}
}

View File

@@ -0,0 +1,6 @@
import { HardwareModules } from 'uhk-common';
export interface FirmwareUpgradeError {
error: any;
modules?: HardwareModules;
}

View File

@@ -0,0 +1,7 @@
import { KeyAction } from 'uhk-common';
export interface KeyActionRemap {
remapOnAllKeymap: boolean;
remapOnAllLayer: boolean;
action: KeyAction;
}

View File

@@ -46,6 +46,10 @@ export class DeviceRendererService {
this.ipcRenderer.send(IpcEvents.device.startConnectionPoller);
}
recoveryDevice(): void {
this.ipcRenderer.send(IpcEvents.device.recoveryDevice);
}
private registerEvents(): void {
this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: DeviceConnectionState) => {
this.dispachStoreAction(new ConnectionStateChangedAction(arg));

View File

@@ -262,6 +262,7 @@ export class MapperService {
private initNameToFileNames(): void {
this.nameToFileName = new Map<string, string>();
this.nameToFileName.set('toggle', 'icon-kbd__fn--toggle');
this.nameToFileName.set('double-tap', 'icon-kbd__fn--double-tap');
this.nameToFileName.set('switch-keymap', 'icon-kbd__mod--switch-keymap');
this.nameToFileName.set('macro', 'icon-icon__macro');
this.nameToFileName.set('shift', 'icon-kbd__default--modifier-shift');

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { SvgModule } from '../components/svg/module';
import { KeyboardLayout } from '../keyboard/keyboard-layout.enum';
import { convertXmlToSvgSeparator, SvgSeparator } from '../components/svg/separator';
@Injectable()
export class SvgModuleProviderService {
@@ -9,11 +10,20 @@ export class SvgModuleProviderService {
private ansiLeft: SvgModule;
private isoLeft: SvgModule;
private right: SvgModule;
private separator: SvgSeparator;
getSvgModules(layout = KeyboardLayout.ANSI): SvgModule[] {
return [this.getRightModule(), this.getLeftModule(layout)];
}
getSvgSeparator(): SvgSeparator {
if (!this.separator) {
this.separator = convertXmlToSvgSeparator(require('xml-loader!../../devices/uhk60-right/separator.xml').svg);
}
return this.separator;
}
private getLeftModule(layout = KeyboardLayout.ANSI): SvgModule {
if (layout === KeyboardLayout.ISO) {
if (!this.isoLeft) {

View File

@@ -0,0 +1,24 @@
import { CanActivate, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import { AppState, bootloaderActive } from '../store';
@Injectable()
export class UhkDeviceBootloaderNotActiveGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(): Observable<boolean> {
return this.store.select(bootloaderActive)
.do(active => {
if (!active) {
this.router.navigate(['/']);
}
});
}
}

View File

@@ -179,12 +179,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -275,7 +275,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -356,7 +356,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -366,7 +366,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -507,7 +507,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -654,7 +654,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -732,7 +732,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -819,7 +819,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -941,7 +941,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -1156,12 +1156,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -1252,7 +1252,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -1333,7 +1333,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -1343,7 +1343,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -1484,7 +1484,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -1641,7 +1641,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -1719,7 +1719,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -1806,7 +1806,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -1928,7 +1928,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -2143,12 +2143,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -2239,7 +2239,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -2320,7 +2320,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -2330,7 +2330,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -2471,7 +2471,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -2618,7 +2618,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -2696,7 +2696,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -2783,7 +2783,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -2902,7 +2902,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -3117,12 +3117,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -3213,7 +3213,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -3294,7 +3294,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -3304,7 +3304,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -3445,7 +3445,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -3602,7 +3602,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -3680,7 +3680,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -3767,7 +3767,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -3886,7 +3886,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -4101,12 +4101,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -4197,7 +4197,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -4278,7 +4278,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -4288,7 +4288,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -4429,7 +4429,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -4576,7 +4576,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -4654,7 +4654,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -4741,7 +4741,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -4854,7 +4854,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -5069,12 +5069,12 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -5165,7 +5165,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -5246,7 +5246,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -5256,7 +5256,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -5397,7 +5397,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{
@@ -5554,7 +5554,7 @@
{
"keyActionType": "switchLayer",
"layer": "mod",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null
]
@@ -5635,7 +5635,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
{
"keyActionType": "keystroke",
@@ -5722,7 +5722,7 @@
{
"keyActionType": "switchLayer",
"layer": "fn",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
null,
@@ -5835,7 +5835,7 @@
{
"keyActionType": "switchLayer",
"layer": "mouse",
"toggle": false
"switchLayerMode": "holdAndDoubleTapToggle"
},
null,
{

View File

@@ -17,7 +17,8 @@ import {
DeviceFirmwareComponent,
MouseSpeedComponent,
LEDBrightnessComponent,
RestoreConfigurationComponent
RestoreConfigurationComponent,
RecoveryModeComponent
} from './components/device';
import { KeymapAddComponent, KeymapEditComponent, KeymapHeaderComponent } from './components/keymap';
import { LayersComponent } from './components/layers';
@@ -105,6 +106,9 @@ import { XtermComponent } from './components/xterm/xterm.component';
import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapper.component';
import { EditableTextComponent } from './components/editable-text/editable-text.component';
import { Autofocus } from './directives/autofocus/autofocus.directive';
import { UhkDeviceBootloaderNotActiveGuard } from './services/uhk-device-bootloader-not-active.guard';
import { FileUploadComponent } from './components/file-upload';
import { AutoGrowInputComponent } from './components/auto-grow-input';
@NgModule({
declarations: [
@@ -176,7 +180,10 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
SliderWrapperComponent,
EditableTextComponent,
Autofocus,
RestoreConfigurationComponent
RestoreConfigurationComponent,
RecoveryModeComponent,
FileUploadComponent,
AutoGrowInputComponent
],
imports: [
CommonModule,
@@ -211,7 +218,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
UhkDeviceInitializedGuard,
UhkDeviceUninitializedGuard,
UhkDeviceLoadingGuard,
UhkDeviceLoadedGuard
UhkDeviceLoadedGuard,
UhkDeviceBootloaderNotActiveGuard
],
exports: [
UhkMessageComponent,

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store';
import { AppStartInfo, CommandLineArgs, HardwareConfiguration, Notification, type } from 'uhk-common';
import { AppStartInfo, HardwareConfiguration, Notification, type } from 'uhk-common';
import { ElectronLogEntry } from '../../models/xterm-log';
const PREFIX = '[app] ';
@@ -10,7 +10,7 @@ export const ActionTypes = {
APP_BOOTSRAPPED: type(PREFIX + 'bootstrapped'),
APP_STARTED: type(PREFIX + 'started'),
APP_SHOW_NOTIFICATION: type(PREFIX + 'show notification'),
APPLY_COMMAND_LINE_ARGS: type(PREFIX + 'apply command line args'),
APPLY_APP_START_INFO: type(PREFIX + 'apply command line args'),
APP_PROCESS_START_INFO: type(PREFIX + 'process start info'),
UNDO_LAST: type(PREFIX + 'undo last action'),
UNDO_LAST_SUCCESS: type(PREFIX + 'undo last action success'),
@@ -38,10 +38,10 @@ export class ShowNotificationAction implements Action {
}
}
export class ApplyCommandLineArgsAction implements Action {
type = ActionTypes.APPLY_COMMAND_LINE_ARGS;
export class ApplyAppStartInfoAction implements Action {
type = ActionTypes.APPLY_APP_START_INFO;
constructor(public payload: CommandLineArgs) {
constructor(public payload: AppStartInfo) {
}
}
@@ -107,7 +107,7 @@ export type Actions
= AppStartedAction
| AppBootsrappedAction
| ShowNotificationAction
| ApplyCommandLineArgsAction
| ApplyAppStartInfoAction
| ProcessAppStartInfoAction
| UndoLastAction
| UndoLastSuccessAction

View File

@@ -1,5 +1,6 @@
import { Action } from '@ngrx/store';
import { DeviceConnectionState, HardwareModules, IpcResponse, type } from 'uhk-common';
import { DeviceConnectionState, FirmwareUpgradeIpcResponse, HardwareModules, IpcResponse, type } from 'uhk-common';
import { FirmwareUpgradeError } from '../../models/firmware-upgrade-error';
const PREFIX = '[device] ';
@@ -26,7 +27,8 @@ export const ActionTypes = {
MODULES_INFO_LOADED: type(PREFIX + 'module info loaded'),
HAS_BACKUP_USER_CONFIGURATION: type(PREFIX + 'Store backup user configuration'),
RESTORE_CONFIGURATION_FROM_BACKUP: type(PREFIX + 'Restore configuration from backup'),
RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS: type(PREFIX + 'Restore configuration from backup success')
RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS: type(PREFIX + 'Restore configuration from backup success'),
RECOVERY_DEVICE: type(PREFIX + 'Recovery device')
};
export class SetPrivilegeOnLinuxAction implements Action {
@@ -95,25 +97,23 @@ export class UpdateFirmwareWithAction implements Action {
export class UpdateFirmwareReplyAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_REPLY;
constructor(public payload: IpcResponse) {
constructor(public payload: FirmwareUpgradeIpcResponse) {
}
}
export class UpdateFirmwareSuccessAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_SUCCESS;
constructor(public payload: HardwareModules) {
}
}
export class UpdateFirmwareFailedAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_FAILED;
constructor(public payload: any) {
constructor(public payload: FirmwareUpgradeError) {
}
}
export class UpdateFirmwareOkButtonAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_OK_BUTTON;
}
export class ResetMouseSpeedSettingsAction implements Action {
type = ActionTypes.RESET_MOUSE_SPEED_SETTINGS;
}
@@ -140,6 +140,10 @@ export class RestoreUserConfigurationFromBackupSuccessAction implements Action {
type = ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS;
}
export class RecoveryDeviceAction implements Action {
type = ActionTypes.RECOVERY_DEVICE;
}
export type Actions
= SetPrivilegeOnLinuxAction
| SetPrivilegeOnLinuxReplyAction
@@ -157,9 +161,9 @@ export type Actions
| UpdateFirmwareReplyAction
| UpdateFirmwareSuccessAction
| UpdateFirmwareFailedAction
| UpdateFirmwareOkButtonAction
| HardwareModulesLoadedAction
| RestoreUserConfigurationFromBackupAction
| HasBackupUserConfigurationAction
| RestoreUserConfigurationFromBackupSuccessAction
| RecoveryDeviceAction
;

View File

@@ -1,7 +1,8 @@
import { Action } from '@ngrx/store';
import { KeyAction, Keymap, Macro } from 'uhk-common';
import { Keymap, Macro } from 'uhk-common';
import { UndoUserConfigData } from '../../models/undo-user-config-data';
import { ChangeKeymapDescription } from '../../models/ChangeKeymapDescription';
import { KeyActionRemap } from '../../models/key-action-remap';
export type KeymapAction =
KeymapActions.AddKeymapAction |
@@ -60,7 +61,7 @@ export namespace KeymapActions {
layer: number;
module: number;
key: number;
keyAction: KeyAction;
keyAction: KeyActionRemap;
}
};
@@ -172,7 +173,11 @@ export namespace KeymapActions {
};
}
export function saveKey(keymap: Keymap, layer: number, module: number, key: number, keyAction: KeyAction): SaveKeyAction {
export function saveKey(keymap: Keymap,
layer: number,
module: number,
key: number,
keyAction: KeyActionRemap): SaveKeyAction {
return {
type: KeymapActions.SAVE_KEY,
payload: {

View File

@@ -13,7 +13,7 @@ import 'rxjs/add/operator/catch';
import { AppStartInfo, LogService, Notification, NotificationType } from 'uhk-common';
import {
ActionTypes,
ApplyCommandLineArgsAction,
ApplyAppStartInfoAction,
AppStartedAction,
DismissUndoNotificationAction,
OpenUrlInNewWindowAction,
@@ -65,10 +65,11 @@ export class ApplicationEffects {
.mergeMap((appInfo: AppStartInfo) => {
this.logService.debug('[AppEffect][processStartInfo] payload:', appInfo);
return [
new ApplyCommandLineArgsAction(appInfo.commandLineArgs),
new ApplyAppStartInfoAction(appInfo),
new ConnectionStateChangedAction({
connected: appInfo.deviceConnected,
hasPermission: appInfo.hasPermission
hasPermission: appInfo.hasPermission,
bootloaderActive: appInfo.bootloaderActive
})
];
});

View File

@@ -14,7 +14,7 @@ import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/operator/switchMap';
import {
DeviceConnectionState,
FirmwareUpgradeIpcResponse,
HardwareConfiguration,
IpcResponse,
NotificationType,
@@ -24,6 +24,7 @@ import {
ActionTypes,
ConnectionStateChangedAction,
HideSaveToKeyboardButton,
RecoveryDeviceAction,
ResetUserConfigurationAction,
RestoreUserConfigurationFromBackupSuccessAction,
SaveConfigurationAction,
@@ -33,14 +34,13 @@ import {
SetPrivilegeOnLinuxReplyAction,
UpdateFirmwareAction,
UpdateFirmwareFailedAction,
UpdateFirmwareOkButtonAction,
UpdateFirmwareReplyAction,
UpdateFirmwareSuccessAction,
UpdateFirmwareWithAction
} from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service';
import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
import { AppState } from '../index';
import { AppState, getRouterState } from '../index';
import {
ActionTypes as UserConfigActions,
ApplyUserConfigurationFromFileAction,
@@ -55,11 +55,20 @@ export class DeviceEffects {
@Effect()
deviceConnectionStateChange$: Observable<Action> = this.actions$
.ofType<ConnectionStateChangedAction>(ActionTypes.CONNECTION_STATE_CHANGED)
.map(action => action.payload)
.do((state: DeviceConnectionState) => {
.withLatestFrom(this.store.select(getRouterState))
.do(([action, route]) => {
const state = action.payload;
if (route.state && route.state.url.startsWith('/device/firmware')) {
return;
}
if (!state.hasPermission) {
this.router.navigate(['/privilege']);
}
else if (state.bootloaderActive) {
this.router.navigate(['/recovery-device']);
}
else if (state.connected) {
this.router.navigate(['/']);
}
@@ -67,7 +76,9 @@ export class DeviceEffects {
this.router.navigate(['/detection']);
}
})
.switchMap((state: DeviceConnectionState) => {
.switchMap(([action, route]) => {
const state = action.payload;
if (state.connected && state.hasPermission) {
return Observable.of(new LoadConfigFromDeviceAction());
}
@@ -90,7 +101,8 @@ export class DeviceEffects {
if (response.success) {
return new ConnectionStateChangedAction({
connected: true,
hasPermission: true
hasPermission: true,
bootloaderActive: false
});
}
@@ -198,22 +210,26 @@ export class DeviceEffects {
@Effect() updateFirmwareReply$ = this.actions$
.ofType<UpdateFirmwareReplyAction>(ActionTypes.UPDATE_FIRMWARE_REPLY)
.map(action => action.payload)
.switchMap((response: IpcResponse) => {
.switchMap((response: FirmwareUpgradeIpcResponse)
: Observable<UpdateFirmwareSuccessAction | UpdateFirmwareFailedAction> => {
if (response.success) {
return Observable.of(new UpdateFirmwareSuccessAction());
return Observable.of(new UpdateFirmwareSuccessAction(response.modules));
}
return Observable.of(new UpdateFirmwareFailedAction(response.error));
return Observable.of(new UpdateFirmwareFailedAction({
error: response.error,
modules: response.modules
}));
});
@Effect({dispatch: false}) updateFirmwareOkButton$ = this.actions$
.ofType<UpdateFirmwareOkButtonAction>(ActionTypes.UPDATE_FIRMWARE_OK_BUTTON)
.do(() => this.deviceRendererService.startConnectionPoller());
@Effect() restoreUserConfiguration$ = this.actions$
.ofType<ResetUserConfigurationAction>(ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP)
.map(() => new SaveConfigurationAction());
@Effect({dispatch: false}) recoveryDevice$ = this.actions$
.ofType<RecoveryDeviceAction>(ActionTypes.RECOVERY_DEVICE)
.do(() => this.deviceRendererService.recoveryDevice());
constructor(private actions$: Actions,
private router: Router,
private deviceRendererService: DeviceRendererService,

View File

@@ -36,7 +36,7 @@ import {
import { DataStorageRepositoryService } from '../../services/datastorage-repository.service';
import { DefaultUserConfigurationService } from '../../services/default-user-configuration.service';
import { AppState, getPrevUserConfiguration, getUserConfiguration } from '../index';
import { AppState, getPrevUserConfiguration, getRouterState, getUserConfiguration } from '../index';
import { KeymapAction, KeymapActions, MacroAction, MacroActions } from '../actions';
import {
DismissUndoNotificationAction,
@@ -118,8 +118,10 @@ export class UserConfigEffects {
@Effect() loadConfigFromDeviceReply$ = this.actions$
.ofType<LoadConfigFromDeviceReplyAction>(ActionTypes.LOAD_CONFIG_FROM_DEVICE_REPLY)
.map(action => action.payload)
.mergeMap((data: ConfigurationReply): any => {
.withLatestFrom(this.store.select(getRouterState))
.mergeMap(([action, route]): any => {
const data: ConfigurationReply = action.payload;
if (!data.success) {
return [new ShowNotificationAction({
type: NotificationType.Error,
@@ -128,12 +130,16 @@ export class UserConfigEffects {
}
const result = [];
let newPageDestination = ['/'];
let newPageDestination: Array<string>;
try {
const userConfig = getUserConfigFromDeviceResponse(data.userConfiguration);
result.push(new LoadUserConfigSuccessAction(userConfig));
if (route.state && !route.state.url.startsWith('/device/firmware')) {
newPageDestination = ['/'];
}
} catch (err) {
this.logService.error('Eeprom user-config parse error:', err);
const userConfig = new UserConfiguration().fromJsonObject(data.backupConfiguration);
@@ -158,7 +164,9 @@ export class UserConfigEffects {
result.push(new HardwareModulesLoadedAction(data.modules));
if (newPageDestination) {
this.router.navigate(newPageDestination);
}
return result;
});

View File

@@ -1,6 +1,6 @@
import { createSelector } from 'reselect';
import { MetaReducer } from '@ngrx/store';
import { RouterReducerState } from '@ngrx/router-store';
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { RouterReducerState, routerReducer } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
import { Keymap, UserConfiguration } from 'uhk-common';
@@ -14,15 +14,6 @@ import { initProgressButtonState } from './reducers/progress-button-state';
import { environment } from '../../environments/environment';
import { RouterStateUrl } from './router-util';
export const reducers = {
userConfiguration: fromUserConfig.reducer,
presetKeymaps: fromPreset.reducer,
autoUpdateSettings: autoUpdateSettings.reducer,
app: fromApp.reducer,
appUpdate: fromAppUpdate.reducer,
device: fromDevice.reducer
};
// State interface for the application
export interface AppState {
userConfiguration: UserConfiguration;
@@ -34,6 +25,16 @@ export interface AppState {
device: fromDevice.State;
}
export const reducers: ActionReducerMap<AppState> = {
userConfiguration: fromUserConfig.reducer,
presetKeymaps: fromPreset.reducer,
autoUpdateSettings: autoUpdateSettings.reducer,
app: fromApp.reducer,
router: routerReducer,
appUpdate: fromAppUpdate.reducer,
device: fromDevice.reducer
};
export const metaReducers: MetaReducer<AppState>[] = environment.production
? []
: [storeFreeze];
@@ -43,6 +44,7 @@ export const getUserConfiguration = (state: AppState) => state.userConfiguration
export const appState = (state: AppState) => state.app;
export const showAddonMenu = createSelector(appState, fromApp.showAddonMenu);
export const allowLayerDoubleTap = createSelector(appState, fromApp.allowLayerDoubleTap);
export const getUndoableNotification = createSelector(appState, fromApp.getUndoableNotification);
export const getPrevUserConfiguration = createSelector(appState, fromApp.getPrevUserConfiguration);
export const runningInElectron = createSelector(appState, fromApp.runningInElectron);
@@ -50,6 +52,7 @@ export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLay
export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded);
export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVersionInfo);
export const getPrivilegePageState = createSelector(appState, fromApp.getPrivilagePageState);
export const runningOnNotSupportedWindows = createSelector(appState, fromApp.runningOnNotSupportedWindows);
export const appUpdateState = (state: AppState) => state.appUpdate;
export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable);
@@ -73,12 +76,13 @@ export const saveToKeyboardState = createSelector(runningInElectron, saveToKeybo
});
export const updatingFirmware = createSelector(deviceState, fromDevice.updatingFirmware);
export const xtermLog = createSelector(deviceState, fromDevice.xtermLog);
export const firmwareOkButtonDisabled = createSelector(deviceState, fromDevice.firmwareOkButtonDisabled);
// tslint:disable-next-line: max-line-length
export const flashFirmwareButtonDisbabled = createSelector(runningInElectron, deviceState, (electron, state: fromDevice.State) => !electron || state.updatingFirmware);
export const getHardwareModules = createSelector(deviceState, fromDevice.getHardwareModules);
export const getBackupUserConfigurationState = createSelector(deviceState, fromDevice.getBackupUserConfigurationState);
export const getRestoreUserConfiguration = createSelector(deviceState, fromDevice.getHasBackupUserConfiguration);
export const bootloaderActive = createSelector(deviceState, fromDevice.bootloaderActive);
export const firmwareUpgradeFailed = createSelector(deviceState, fromDevice.firmwareUpgradeFailed);
export const getSideMenuPageState = createSelector(
showAddonMenu,
@@ -102,3 +106,11 @@ export const getSideMenuPageState = createSelector(
};
}
);
export const getRouterState = (state: AppState) => state.router;
export const showUnsupportedOsToFirmwareUpgrade = createSelector(
runningOnNotSupportedWindows,
firmwareUpgradeFailed,
(isUnsupportedOs,
hasFirmwareUpgradeFailed) => isUnsupportedOs && hasFirmwareUpgradeFailed);

View File

@@ -1,6 +1,8 @@
import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { Action } from '@ngrx/store';
import {
AppStartInfo,
CommandLineArgs,
HardwareConfiguration,
Notification,
NotificationType,
@@ -18,7 +20,7 @@ import { PrivilagePageSate } from '../../models/privilage-page-sate';
export interface State {
started: boolean;
showAddonMenu: boolean;
commandLineArgs: CommandLineArgs;
undoableNotification?: Notification;
navigationCountAfterNotification: number;
prevUserConfig?: UserConfiguration;
@@ -28,11 +30,13 @@ export interface State {
agentVersionInfo?: VersionInformation;
privilegeWhatWillThisDoClicked: boolean;
permissionError?: any;
platform?: string;
osVersion?: string;
}
export const initialState: State = {
started: false,
showAddonMenu: false,
commandLineArgs: {},
navigationCountAfterNotification: 0,
runningInElectron: runInElectron(),
configLoading: true,
@@ -49,10 +53,14 @@ export function reducer(state = initialState, action: Action & { payload: any })
};
}
case ActionTypes.APPLY_COMMAND_LINE_ARGS: {
case ActionTypes.APPLY_APP_START_INFO: {
const payload = action.payload as AppStartInfo;
return {
...state,
showAddonMenu: action.payload.addons
commandLineArgs: payload.commandLineArgs,
platform: payload.platform,
osVersion: payload.osVersion
};
}
@@ -148,7 +156,8 @@ export function reducer(state = initialState, action: Action & { payload: any })
}
}
export const showAddonMenu = (state: State) => state.showAddonMenu;
export const showAddonMenu = (state: State) => state.commandLineArgs.addons;
export const allowLayerDoubleTap = (state: State) => state.commandLineArgs.layerDoubleTap;
export const getUndoableNotification = (state: State) => state.undoableNotification;
export const getPrevUserConfiguration = (state: State) => state.prevUserConfig;
export const runningInElectron = (state: State) => state.runningInElectron;
@@ -170,3 +179,15 @@ export const getPrivilagePageState = (state: State): PrivilagePageSate => {
showWhatWillThisDoContent: state.privilegeWhatWillThisDoClicked || permissionSetupFailed
};
};
export const runningOnNotSupportedWindows = (state: State): boolean => {
if (!state.osVersion || state.platform !== 'win32') {
return false;
}
const version = state.osVersion.split('.');
const osMajor = +version[0];
const osMinor = +version[1];
return osMajor < 6 || (osMajor === 6 && osMinor < 2);
};

View File

@@ -7,7 +7,8 @@ import {
HardwareModulesLoadedAction,
SaveConfigurationAction,
HasBackupUserConfigurationAction,
UpdateFirmwareFailedAction
UpdateFirmwareFailedAction,
UpdateFirmwareSuccessAction
} from '../actions/device';
import { ActionTypes as AppActions, ElectronMainLogReceivedAction } from '../actions/app';
import { initProgressButtonState, ProgressButtonState } from './progress-button-state';
@@ -17,9 +18,12 @@ import { RestoreConfigurationState } from '../../models/restore-configuration-st
export interface State {
connected: boolean;
hasPermission: boolean;
bootloaderActive: boolean;
saveToKeyboard: ProgressButtonState;
savingToKeyboard: boolean;
updatingFirmware: boolean;
firmwareUpdateFinished: boolean;
firmwareUpdateFailed?: boolean;
modules: HardwareModules;
log: Array<XtermLog>;
restoringUserConfiguration: boolean;
@@ -29,7 +33,9 @@ export interface State {
export const initialState: State = {
connected: true,
hasPermission: true,
bootloaderActive: false,
saveToKeyboard: initProgressButtonState,
savingToKeyboard: false,
updatingFirmware: false,
firmwareUpdateFinished: false,
modules: {
@@ -46,14 +52,15 @@ export const initialState: State = {
hasBackupUserConfiguration: false
};
export function reducer(state = initialState, action: Action) {
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
case ActionTypes.CONNECTION_STATE_CHANGED: {
const data = (<ConnectionStateChangedAction>action).payload;
return {
...state,
connected: data.connected,
hasPermission: data.hasPermission
hasPermission: data.hasPermission,
bootloaderActive: data.bootloaderActive
};
}
@@ -129,12 +136,15 @@ export function reducer(state = initialState, action: Action) {
return {
...state,
updatingFirmware: false,
firmwareUpdateFinished: true
firmwareUpdateFinished: true,
firmwareUpdateFailed: false,
modules: (action as UpdateFirmwareSuccessAction).payload
};
case ActionTypes.UPDATE_FIRMWARE_FAILED: {
const data = (action as UpdateFirmwareFailedAction).payload;
const logEntry = {
message: (action as UpdateFirmwareFailedAction).payload.message,
message: data.error.message,
cssClass: XtermCssClass.error
};
@@ -142,6 +152,8 @@ export function reducer(state = initialState, action: Action) {
...state,
updatingFirmware: false,
firmwareUpdateFinished: true,
firmwareUpdateFailed: true,
modules: data.modules,
log: [...state.log, logEntry]
};
}
@@ -193,6 +205,13 @@ export function reducer(state = initialState, action: Action) {
hasBackupUserConfiguration: false
};
case ActionTypes.RECOVERY_DEVICE: {
return {
...state,
updatingFirmware: true,
log: [{message: '', cssClass: XtermCssClass.standard}]
};
}
default:
return state;
}
@@ -203,7 +222,6 @@ export const isDeviceConnected = (state: State) => state.connected || state.upda
export const hasDevicePermission = (state: State) => state.hasPermission;
export const getSaveToKeyboardState = (state: State) => state.saveToKeyboard;
export const xtermLog = (state: State) => state.log;
export const firmwareOkButtonDisabled = (state: State) => !state.firmwareUpdateFinished;
export const getHardwareModules = (state: State) => state.modules;
export const getHasBackupUserConfiguration = (state: State) => state.hasBackupUserConfiguration;
export const getBackupUserConfigurationState = (state: State): RestoreConfigurationState => {
@@ -212,3 +230,5 @@ export const getBackupUserConfigurationState = (state: State): RestoreConfigurat
hasBackupUserConfiguration: state.hasBackupUserConfiguration
};
};
export const bootloaderActive = (state: State) => state.bootloaderActive;
export const firmwareUpgradeFailed = (state: State) => state.firmwareUpdateFailed;

View File

@@ -3,7 +3,7 @@ import { Action } from '@ngrx/store';
export interface ProgressButtonState {
showButton: boolean;
text: string;
showProgress: boolean;
showProgress?: boolean;
action?: Action;
}

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