112 Commits

Author SHA1 Message Date
László Monda
123cab5724 Bump Agent version to 1.2.9 2018-09-13 20:42:07 +02:00
Róbert Kiss
c16365a0e5 fix: Alt+Tab triggers "Remap on all layers" (#773) 2018-09-11 22:29:30 +02:00
Róbert Kiss
a21d278c0c fix: modifier button layout (#772) 2018-09-11 21:56:30 +02:00
Róbert Kiss
0466916be1 fix: key modifier reordering (#771) 2018-09-11 00:29:22 +02:00
László Monda
9a845d8f6a Fix eeprom.js 2018-09-10 19:29:58 +02:00
Róbert Kiss
9ae1673499 feat: secondary role visualisation (#767)
* feat: secondary role visualisation

* fix: recalculate the text position of the secondary role if changes

* fix: recalculate the text position of SvgKeystrokeKeyComponent

* fix: recalculate the text position when changes anything

* fix: two line text key position calculation

* fix: fix space positioning

* fix: visualize second character of complex key

* style: remove extra line
2018-09-08 12:09:16 +02:00
László Monda
2d5a5e7aef Exchange Alt and Super modifiers in the key action popover. 2018-09-03 03:58:03 +02:00
Róbert Kiss
3e4d439852 feat: display OS-specific modifiers (#764)
* chore: git ignore "out-tsc/" folder in uhk-web package

* feat: add OperationSystem calculation to the app reducer

* feat: create os specific key modifier

* feat: Os specific texts

* revert: KeyModifierValues and getKeyModifiers selector

* refactor: remove unnecessary return

* refactor: rename OperationSystem => OperatingSystem
2018-09-03 00:21:55 +02:00
László Monda
aba0b09109 Bump Agent to version 1.2.8 and update changelog. 2018-08-26 23:42:31 +02:00
László Monda
af608ee17d Downgrade to firmware 8.2.5 2018-08-26 23:35:54 +02:00
Róbert Kiss
df817e86d6 fix: use OnPush change detection on the keymap edit and add pages (#760)
* fix: use OnPush change detection on the keymap edit and add pages

* fix: font-size
2018-08-26 22:08:29 +02:00
Róbert Kiss
a7d3b62512 chore: upgrade many dependencies and restructure tsconfigs (#757)
* chore: upgrade many dependencies and restructure tsconfigs

Summary:
- upgrade many dependencies
- remove dev dependencies from test-serializer and uhk-common
- created root tsconfig.json and test-serializer and uhk-common
  tsconfigs extends it
- fixed e2e test

* chore: upgrade more dependencies
2018-08-22 00:34:16 +02:00
Róbert Kiss
b41f14192a chore: upgrade stylelint => 9.5.0 (#756)
it is fix "Unknown CSS word" error in IntelliJ
2018-08-21 20:43:18 +02:00
Róbert Kiss
475ec71983 fix: uncheck 'Remap on all keyboard' and 'Remap on all layer' checkbox by default (#754)
* fix: uncheck 'Remap on all keyboard' and 'Remap on all layer' checkbox by default

* fix: popover checkbox checked state
2018-08-21 20:41:54 +02:00
László Monda
b5cff2fa93 Fix the padding of the secondary role tooltip. 2018-08-14 04:10:30 +02:00
László Monda
80e8c014ec Bind left and right Shift on the Mouse layer of all keymaps in the default configuration. 2018-08-08 17:00:32 +02:00
László Monda
67d42f666c Simplify the output of get-debug-info.js 2018-08-04 02:29:39 +02:00
László Monda
fa32f95438 Add note to the LED brightness page saying that current UHK versions are not backlit. Also include spacing-bootstrap-3 2018-07-30 00:05:04 +02:00
László Monda
06878dd56a Remove the redundant scrollbar from the LED brightness page. 2018-07-29 00:28:34 +02:00
Róbert Kiss
374f6a3e6e chore: downgrade electron builder => 20.14.7 (#741)
* chore: downgrade electron builder => 20.14.7

* chore: downgrade electron builder => 20.8.1
2018-07-26 23:30:20 +02:00
László Monda
0b9c804a3d Bump Agent version to 1.2.7 and update changelog. 2018-07-26 22:20:28 +02:00
László Monda
365a459d61 Merge branch 'master' of github.com:UltimateHackingKeyboard/agent 2018-07-26 22:16:33 +02:00
László Monda
cec891a2c0 Change the shortcut which enables the USB stack test code, so that it can be triggered with the default Mac US keymap. 2018-07-26 22:15:21 +02:00
Róbert Kiss
8eb8aa3032 chore: upgrade electron builder => 20.26.0 (#740) 2018-07-26 22:06:32 +02:00
László Monda
38184e7968 Bump Agent version to 1.2.6 and firmware version to 8.4.0. Update changelog. 2018-07-26 06:04:47 +02:00
Róbert Kiss
f6092ea195 fix: popover components use OnPush change detection (#727)
* fix: popover components use OnPush change detection

* fix: select2 selection bug

* chore: upgrade @ert78gb/ngx-select-ex => 3.7.0
2018-07-26 05:37:58 +02:00
László Monda
ac7d66e338 Add keyboard shortcut for enabling the USB stack test mode of the firmware. Resolves #735. 2018-07-26 05:34:48 +02:00
László Monda
b82a1da92a Remove usbVariables in favor of variableNameToId. 2018-07-25 11:44:57 +02:00
László Monda
b8859f7b64 Make {get,set}-variable.js expect variable name and use async/await. 2018-07-25 02:08:36 +02:00
László Monda
a04fa67446 Comment out the Settings menu until auto update is implemented. 2018-07-24 16:40:34 +02:00
Eric Tang
ac89aff018 Add scripts for getting/setting firmware variables (#734) 2018-07-22 16:07:21 +02:00
Róbert Kiss
e7cf8dc966 fix: no scroll when macro tab selected on popover (#731) 2018-07-17 22:08:45 +02:00
Róbert Kiss
d0102f5bdb feat: add help page (#728)
* feat: add help page

* feat: add help page content
2018-07-16 23:05:41 +02:00
László Monda
eb0daadf98 Overwrite the blhost binary with a statically compiled version that doesn't use special instructions.
See https://github.com/UltimateHackingKeyboard/agent/issues/681#issuecomment-403057795
2018-07-14 14:43:11 +02:00
László Monda
49d6ca173d Remove the blhost-old x86-64 Linux blhost binary. 2018-07-14 14:41:53 +02:00
László Monda
a3eb6a6b7e Fix typo. 2018-07-11 14:48:28 +02:00
László Monda
144ed57b20 Fix epic typo. 2018-07-08 14:44:41 +02:00
Róbert Kiss
6086ddabf0 build: build only AppImage for Linux (#719)
upgrade:
 - electron-builder => 20.15.0
 - electron-log => 2.2.16
 - electron-rebuild => 1.8.1
2018-07-08 14:34:38 +02:00
Róbert Kiss
84f378a276 feat: add save to keyboard and remap shortcut keys (#712)
* feat: add save to keyboard and remap shortcut keys

* feat: Alt and Shift keys set the remapOnAllKeymap and remapOnAllLayer

* fix: control + enter trigger remap keymap
2018-07-08 14:31:48 +02:00
Róbert Kiss
648e8d5f2c feat: keep current selected layer when changing keymap (#714) 2018-07-05 23:58:44 +02:00
Róbert Kiss
15df8d7129 WIP feat: replace ng2-select2 => ngx-select-ex (#706)
* feat: replace ng2-select2 => ngx-select-ex

* feat: style the ngrx-select

* feat: replace secondary role select2

* feat: replace Select2OptionData => SelectOptionData

* feat: replace select2 => ngx-select in macro-tab component

* feat: replace select2 => ngx-select in keymap-tab component

* feat: fix styles

* chore: remove select2 from dependencies

* fix: macro editor overflow

* fix: set the same font size for the toggle button

* fix: overflow

* chore: use @ert78gb/ngx-select-ex version of ngx-select-ex
2018-07-02 23:44:39 +02:00
Sylvain Benner
cfc0af9655 Fix Sleep key for macOS users in Mac keymaps and Fn layers (#707)
Reference: https://support.apple.com/en-us/HT201236
2018-06-29 12:19:59 +02:00
László Monda
f02e3181a6 Improve the phrasing of the firmware update error message. 2018-06-28 18:49:01 +02:00
Róbert Kiss
3d59bcf97e feat: Tweak unsupported Windows firmware update notification (#705)
* feat: Tweak unsupported Windows firmware update notification

* feat: Display firmware update status

* feat: throw error when left half not connected under firmware upgrade
2018-06-28 18:41:16 +02:00
László Monda
5e4fc983fb Rename the "Remap Key" button to "Remay key". 2018-06-27 21:28:16 +02:00
László Monda
32d9635b34 Tone down the color of the separator line. 2018-06-26 06:05:03 +02:00
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
197 changed files with 16474 additions and 6752 deletions

2
.nvmrc
View File

@@ -1 +1 @@
8.9.4
8.11.2

View File

@@ -6,6 +6,99 @@ 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.9] - 2018-09-13
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
- Display OS-specific modifiers.
- Display secondary roles.
- Don't trigger "Remap on all layers" after leaving Agent with Alt+Tab.
## [1.2.8] - 2018-08-26
Firmware: 8.**2.5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
- Uncheck the "Remap on all keymaps" and "Remap on all layers" checkboxes of the key action popover by default.
- Bind left and right Shift on the Mouse layer of all keymaps in the default configuration.
- Make ng2-select2 widgets faster.
- Add note to the LED brightness page saying that current UHK versions are not backlit.
- Fix the padding of the secondary role tooltip.
- Remove the redundant scrollbar from the LED brightness page.
## [1.2.7] - 2018-07-26
Firmware: 8.4.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.4.0)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
- Fix Agent startup exception on Linux by upgrading Electron builder.
- Change the shortcut which enables the USB stack test code, so that it can be triggered with the default Mac US keymap.
## [1.2.6] - 2018-07-26
Firmware: 8.**4.0** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.4.0)] | Device Protocol: 4.**4.0** | User Config: 4.0.1 | Hardware Config: 1.0.0
- Replace the Linux blhost binary with a statically compiled version that doesn't use special instructions and shouldn't segfault.
- Keep the current layer when changing keymaps.
- Fix the sleep key of Mac keymaps.
- Add help page.
- Add "save to keyboard" and "remap key" shortcuts.
- Build only AppImages for Linux.
- Replace ng2-select2 widgets with ngx-select-ex that always shows up in the correct position.
- Improve the phrasing of the firmware update error message.
- Tweak unsupported Windows firmware update notification.
- Hide the Settings menu until auto update is implemented.
- Don't scroll when the macro tab of the key action popover gets selected.
- Add keyboard shortcut for enabling the USB stack test mode of the firmware. `DEVICEPROTOCOL:MINOR`
- Tone down the color of the separator line.
## [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 popover 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

9773
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
"private": true,
"author": "Ultimate Gadget Laboratories",
"main": "electron/dist/electron-main.js",
"version": "1.2.1",
"firmwareVersion": "8.2.2",
"deviceProtocolVersion": "4.3.0",
"version": "1.2.9",
"firmwareVersion": "8.2.5",
"deviceProtocolVersion": "4.4.0",
"userConfigVersion": "4.0.1",
"hardwareConfigVersion": "1.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
@@ -22,9 +22,10 @@
"@types/electron-devtools-installer": "2.0.2",
"@types/electron-settings": "3.0.0",
"@types/fs-extra": "5.0.1",
"@types/jasmine": "2.6.0",
"@types/jasmine": "2.8.8",
"@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",
@@ -37,36 +38,41 @@
"core-js": "2.4.1",
"cross-env": "5.0.5",
"decompress": "4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-tarbz2": "4.1.1",
"devtron": "1.4.0",
"electron": "1.8.4",
"electron": "1.8.7",
"electron-builder": "20.8.1",
"electron-debug": "1.5.0",
"electron-devtools-installer": "2.2.3",
"electron-log": "2.2.14",
"electron-rebuild": "1.7.3",
"electron-log": "2.2.16",
"electron-rebuild": "1.8.2",
"electron-settings": "3.1.4",
"electron-updater": "2.21.4",
"exports-loader": "0.6.3",
"file-loader": "0.10.0",
"fs-extra": "5.0.0",
"gh-pages": "1.1.0",
"jasmine": "2.8.0",
"jasmine-core": "2.8.0",
"jasmine-node": "2.0.1",
"jasmine-ts": "0.2.1",
"jsonfile": "4.0.0",
"lerna": "2.9.0",
"lerna": "3.1.4",
"lodash-es": "4.17.4",
"mkdirp": "0.5.1",
"node-hid": "0.5.7",
"npm-run-all": "4.0.2",
"pre-commit": "1.2.2",
"request": "2.83.0",
"request": "2.88.0",
"rimraf": "2.6.1",
"standard-version": "4.2.0",
"stylelint": "7.13.0",
"svg-sprite": "1.3.7",
"stylelint": "9.5.0",
"svg-sprite": "1.4.0",
"ts-loader": "2.3.1",
"ts-node": "3.0.4",
"ts-node": "7.0.1",
"tslint": "5.9.1",
"typescript": "2.6.2",
"webpack": "3.10.0"
"webpack": "3.12.0"
},
"pre-commit": [
"precommit-msg"
@@ -86,6 +92,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",
"e2e": "lerna run e2e --scope uhk-web",
"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",

View File

@@ -201,11 +201,11 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"gaze": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz",
"integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
"integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
"requires": {
"globule": "0.1.0"
"globule": "1.2.1"
}
},
"get-caller-file": {
@@ -219,41 +219,37 @@
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"glob": {
"version": "3.1.21",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
"integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"graceful-fs": "1.2.3",
"inherits": "1.0.2",
"minimatch": "0.2.14"
},
"dependencies": {
"inherits": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz",
"integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js="
}
"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"
}
},
"globule": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz",
"integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
"integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
"requires": {
"glob": "3.1.21",
"lodash": "1.0.2",
"minimatch": "0.2.14"
"glob": "7.1.2",
"lodash": "4.17.10",
"minimatch": "3.0.4"
}
},
"graceful-fs": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz",
"integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q="
},
"growl": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz",
"integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto="
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
},
"grunt-exec": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/grunt-exec/-/grunt-exec-0.4.7.tgz",
"integrity": "sha1-QAUf+k6wyWV+BTuV6I1ENSocLCU="
},
"has-flag": {
"version": "2.0.0",
@@ -362,31 +358,34 @@
"integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4="
},
"jasmine-growl-reporter": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.2.1.tgz",
"integrity": "sha1-1fCje5L2qD/VxkgrgJSVyQqLVf4=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-1.0.1.tgz",
"integrity": "sha512-dh7VjP3l0OLxL9+sw5vK6RrdH4gdHCNkTnUd9orViHDPr7Fe8LsXY+IObWauS2hX5khMFtjKRZCfTcDHKAjm/A==",
"requires": {
"growl": "1.7.0"
"growl": "1.10.5"
}
},
"jasmine-node": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-2.0.0.tgz",
"integrity": "sha1-gXUacjJfVJdJCxQYGlUIfxsDcf8=",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-2.0.1.tgz",
"integrity": "sha512-1S5Z4Mof5yxwqLIApzyo2pV5WN2kRpTSgICvEo3+rJmKve9P94kolzC9eS0u5cyiT+gxBY2mwOQdxLbkhwKzoA==",
"requires": {
"coffee-script": "1.7.1",
"gaze": "0.5.2",
"jasmine-growl-reporter": "0.2.1",
"minimist": "0.0.8",
"gaze": "1.1.3",
"grunt-exec": "0.4.7",
"jasmine-growl-reporter": "1.0.1",
"jasmine-reporters": "git://github.com/larrymyers/jasmine-reporters.git#2c7242dc11c15c2f156169bc704798568b8cb50d",
"minimist": "0.0.10",
"mkdirp": "0.3.5",
"underscore": "1.6.0",
"walkdir": "0.0.12"
"walkdir": "0.0.12",
"xml2js": "0.4.19"
},
"dependencies": {
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
},
"mkdirp": {
"version": "0.3.5",
@@ -395,6 +394,19 @@
}
}
},
"jasmine-reporters": {
"version": "git://github.com/larrymyers/jasmine-reporters.git#2c7242dc11c15c2f156169bc704798568b8cb50d",
"requires": {
"mkdirp": "0.3.5"
},
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc="
}
}
},
"jasmine-ts": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.2.1.tgz",
@@ -442,14 +454,9 @@
}
},
"lodash": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz",
"integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE="
},
"lru-cache": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
"integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
},
"make-error": {
"version": "1.3.0",
@@ -470,12 +477,11 @@
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
},
"minimatch": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"lru-cache": "2.7.3",
"sigmund": "1.0.1"
"brace-expansion": "1.1.8"
}
},
"minimist": {
@@ -633,6 +639,11 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
@@ -656,11 +667,6 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@@ -854,6 +860,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.7"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",

View File

@@ -14,16 +14,9 @@
"npm": ">=5.1.0 <6.0.0"
},
"dependencies": {
"uhk-common": "1.0.0"
},
"devDependencies": {
"@types/jasmine": "2.6.0",
"@types/node": "8.0.30",
"jasmine": "2.8.0",
"jasmine-core": "2.8.0",
"jasmine-node": "2.0.0",
"jasmine-ts": "0.2.1",
"ts-node": "3.3.0",
"uhk-common": "1.0.0"
},
"scripts": {
"test": "jasmine-ts --config=jasmine.json"

View File

@@ -1,11 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"typeRoots": [
"./node_modules/@types"
]
"experimentalDecorators": true
}
}

View File

@@ -1,11 +1,14 @@
{
"requires": true,
"name": "uhk-agent",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/decompress": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.0.tgz",
"integrity": "sha512-f7bLkRy09Z+1kid9ylcWMRlJrEWe4ceqtoGvlFQ/hnxL7WIr+AEy5Bv8SCIRDm8FLqTv4VF+hTUCpSd7rHgtkQ==",
"dev": true,
"requires": {
"@types/node": "8.0.33"
}
@@ -13,12 +16,14 @@
"@types/node": {
"version": "8.0.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.33.tgz",
"integrity": "sha512-vmCdO8Bm1ExT+FWfC9sd9r4jwqM7o97gGy2WBshkkXbf/2nLAJQUrZfIhw27yVOtLUev6kSZc4cav/46KbDd8A=="
"integrity": "sha512-vmCdO8Bm1ExT+FWfC9sd9r4jwqM7o97gGy2WBshkkXbf/2nLAJQUrZfIhw27yVOtLUev6kSZc4cav/46KbDd8A==",
"dev": true
},
"@types/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0="
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=",
"dev": true
},
"ansi-regex": {
"version": "2.1.1",

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,24 @@ 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
});
});
ipcMain.on(IpcEvents.device.enableUsbStackTest, (...args: any[]) => {
this.queueManager.add({
method: this.enableUsbStackTest,
bind: this,
params: args,
asynchronous: true
});
});
logService.debug('[DeviceService] init success');
}
@@ -84,10 +109,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 +132,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;
}
@@ -145,9 +201,42 @@ export class DeviceService {
}
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);
}
public async enableUsbStackTest(event: Electron.Event) {
await this.device.enableUsbStackTest();
}
/**
* HID API not support device attached and detached event.
* This method check the keyboard is attached to the computer or not.
@@ -161,16 +250,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

@@ -19,16 +19,6 @@
"coverage": "nyc jasmine-ts --config=jasmine.json"
},
"license": "GPL-3.0",
"devDependencies": {
"@types/jasmine": "2.6.0",
"@types/node": "8.0.30",
"jasmine": "2.8.0",
"jasmine-core": "2.8.0",
"jasmine-node": "2.0.0",
"jasmine-ts": "0.2.1",
"nyc": "11.2.1",
"ts-node": "3.3.0"
},
"nyc": {
"extension": [
".ts"

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

@@ -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,8 @@ 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';
public static readonly enableUsbStackTest = 'enable-usb-stack-test';
}
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,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
@@ -9,9 +10,6 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2015.iterable",
"dom",

View File

@@ -147,11 +147,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"lodash-es": {
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.10.tgz",
"integrity": "sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg=="
},
"mimic-response": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",

View File

@@ -11,7 +11,6 @@
"@types/node": "8.0.28"
},
"dependencies": {
"lodash-es": "^4.17.10",
"node-hid": "0.5.7",
"uhk-common": "1.0.0"
}

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;
}
@@ -22,7 +23,12 @@ export enum UsbCommand {
GetDebugBuffer = 0x0b,
GetAdcValue = 0x0c,
SetLedPwmBrightness = 0x0d,
GetModuleProperty = 0x0e
GetModuleProperty = 0x0e,
GetSlaveI2cErrors = 0x0f,
SetI2cBaudRate = 0x10,
SwitchKeymap = 0x11,
GetVariable = 0x12,
SetVariable = 0x13
}
export enum EepromOperation {
@@ -85,3 +91,10 @@ export enum KbootCommands {
export enum ModulePropertyId {
protocolVersions = 0
}
export enum UsbVariables {
testSwitches = 0,
testUsbStack = 1,
debounceTimePress = 2,
debounceTimeRelease = 3
}

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,6 +1,5 @@
import { cloneDeep, isEqual } from 'lodash-es';
import { Device, devices, HID } from 'node-hid';
import { CommandLineArgs, LogService } from 'uhk-common';
import { CommandLineArgs, DeviceConnectionState, isEqualArray, LogService } from 'uhk-common';
import {
ConfigBufferId,
@@ -11,9 +10,10 @@ import {
KbootCommands,
ModuleSlotToI2cAddress,
ModuleSlotToId,
UsbCommand
UsbCommand,
UsbVariables
} from './constants';
import { bufferToString, getTransferData, retry, snooze } from './util';
import { bufferToString, getTransferData, isUhkDevice, retry, snooze } from './util';
export const BOOTLOADER_TIMEOUT_MS = 5000;
@@ -25,7 +25,7 @@ export class UhkHidDevice {
* Internal variable that represent the USB UHK device
* @private
*/
private _prevDevices = {};
private _prevDevices = [];
private _device: HID;
private _hasPermission = false;
@@ -50,12 +50,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) {
@@ -69,15 +73,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;
}
/**
@@ -97,6 +110,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);
@@ -120,6 +134,11 @@ export class UhkHidDevice {
await this.waitUntilKeyboardBusy();
}
public async enableUsbStackTest(): Promise<void> {
await this.write(new Buffer([UsbCommand.SetVariable, UsbVariables.testUsbStack, 1]));
await this.waitUntilKeyboardBusy();
}
/**
* Close the communication chanel with UHK Device
*/
@@ -144,6 +163,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}`);
@@ -235,27 +258,24 @@ export class UhkHidDevice {
private connectToDevice(): HID {
try {
const devs = devices();
if (!isEqual(this._prevDevices, devs)) {
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 &&
// hidapi can not read the interface number on Mac, so check the usage page and usage
((x.usagePage === 128 && x.usage === 129) || // Old firmware
(x.usagePage === (0xFF00 | 0x00) && x.usage === 0x01) || // New firmware
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) {

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)}`];
@@ -57,8 +59,9 @@ export class UhkOperations {
const leftModuleBricked = await this.waitForKbootIdle();
if (!leftModuleBricked) {
this.logService.error('[UhkOperations] Couldn\'t connect to the left keyboard half.');
return;
const msg = '[UhkOperations] Couldn\'t connect to the left keyboard half.';
this.logService.error(msg);
throw new Error(msg);
}
await this.device.reenumerate(EnumerationModes.Buspal);

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

@@ -26,7 +26,6 @@
],
"scripts": [
"../node_modules/bootstrap/dist/js/bootstrap.js",
"../node_modules/select2/dist/js/select2.full.js",
"../node_modules/nouislider/distribute/nouislider.js"
],
"environmentSource": "environments/environment.ts",

1
packages/uhk-web/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
out-tsc/

View File

@@ -7,8 +7,8 @@ describe('web App', () => {
page = new WebPage();
});
it('should display welcome message', () => {
it('should display default device name', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
expect(page.getDeviceName()).toEqual('My UHK');
});
});

View File

@@ -5,7 +5,8 @@ export class WebPage {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
getDeviceName() {
return element(by.css('body > main-app > side-menu > ul > li:nth-child(1) > div > auto-grow-input > input'))
.getAttribute('value');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,10 +36,9 @@
"@types/electron-devtools-installer": "2.0.2",
"@types/electron-settings": "3.0.0",
"@types/file-saver": "0.0.1",
"@types/jasmine": "2.5.53",
"@types/jasminewd2": "2.0.2",
"@types/jasmine": "2.8.8",
"@types/jasminewd2": "2.0.3",
"@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",
@@ -51,27 +50,25 @@
"dragula": "3.7.2",
"font-awesome": "4.7.0",
"html-webpack-plugin": "^2.29.0",
"jasmine-core": "2.6.2",
"jasmine-spec-reporter": "4.1.0",
"jasmine-core": "3.2.1",
"jasmine-spec-reporter": "4.2.1",
"jquery": "3.2.1",
"jsonfile": "3.0.1",
"karma": "1.7.0",
"karma-chrome-launcher": "2.1.1",
"karma-coverage-istanbul-reporter": "1.2.1",
"karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2",
"lodash-es": "4.17.4",
"karma": "3.0.0",
"karma-chrome-launcher": "2.2.0",
"karma-coverage-istanbul-reporter": "2.0.1",
"karma-jasmine": "1.1.2",
"karma-jasmine-html-reporter": "1.3.1",
"ng2-dragula": "1.5.0",
"ng2-nouislider": "^1.7.7",
"ng2-select2": "1.0.0-beta.10",
"ngx-clipboard": "10.0.0",
"@ert78gb/ngx-select-ex": "3.7.2",
"ngrx-store-freeze": "0.1.9",
"nouislider": "^11.1.0",
"postcss-url": "^7.1.2",
"protractor": "5.1.2",
"protractor": "5.4.0",
"reselect": "3.0.1",
"rxjs": "5.5.8",
"select2": "4.0.3",
"typescript": "2.6.2",
"uhk-common": "1.0.0",
"xml-loader": "1.2.1",
@@ -81,6 +78,7 @@
},
"dependencies": {
"classlist.js": "1.1.20150312",
"file-saver": "1.3.3"
"file-saver": "1.3.3",
"spacing-bootstrap-3": "^1.0.0"
}
}

View File

@@ -12,7 +12,7 @@ exports.config = {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
baseUrl: 'http://localhost:8080/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,

View File

@@ -9,7 +9,7 @@
</div>
<notifier-container></notifier-container>
<progress-button class="save-to-keyboard-button"
*ngIf="(saveToKeyboardState$ | async).showButton"
*ngIf="saveToKeyboardState.showButton"
[@showSaveToKeyboardButton]
[state]="saveToKeyboardState$ | async"
[state]="saveToKeyboardState"
(clicked)="clickedOnProgressButton($event)"></progress-button>

View File

@@ -1,11 +1,13 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { Component, HostListener, ViewEncapsulation, OnDestroy } from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Action, Store } from '@ngrx/store';
import 'rxjs/add/operator/last';
import { DoNotUpdateAppAction, UpdateAppAction } from './store/actions/app-update.action';
import { EnableUsbStackTestAction } from './store/actions/device';
import {
AppState,
getShowAppUpdateAvailable,
@@ -34,17 +36,44 @@ import { ProgressButtonState } from './store/reducers/progress-button-state';
])
]
})
export class MainAppComponent {
export class MainAppComponent implements OnDestroy {
showUpdateAvailable$: Observable<boolean>;
deviceConfigurationLoaded$: Observable<boolean>;
runningInElectron$: Observable<boolean>;
saveToKeyboardState$: Observable<ProgressButtonState>;
saveToKeyboardState: ProgressButtonState;
private saveToKeyboardStateSubscription: Subscription;
constructor(private store: Store<AppState>) {
this.showUpdateAvailable$ = store.select(getShowAppUpdateAvailable);
this.deviceConfigurationLoaded$ = store.select(deviceConfigurationLoaded);
this.runningInElectron$ = store.select(runningInElectron);
this.saveToKeyboardState$ = store.select(saveToKeyboardState);
this.saveToKeyboardStateSubscription = store.select(saveToKeyboardState)
.subscribe(data => this.saveToKeyboardState = data);
}
ngOnDestroy(): void {
this.saveToKeyboardStateSubscription.unsubscribe();
}
@HostListener('document:keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (this.saveToKeyboardState.showButton &&
event.ctrlKey &&
event.key === 's' &&
!event.defaultPrevented) {
this.clickedOnProgressButton(this.saveToKeyboardState.action);
event.preventDefault();
}
if (event.shiftKey &&
event.ctrlKey &&
event.metaKey &&
event.key === '|' &&
!event.defaultPrevented) {
this.enableUsbStackTest();
event.preventDefault();
}
}
updateApp() {
@@ -58,4 +87,8 @@ export class MainAppComponent {
clickedOnProgressButton(action: Action) {
return this.store.dispatch(action);
}
enableUsbStackTest() {
this.store.dispatch(new EnableUsbStackTestAction());
}
}

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

@@ -5,6 +5,6 @@
</h1>
<div class="col-xs-12">
<div class="agent-version">Agent version: <span class="text-bold">{{ version }}</span></div>
<div><a class="link-github" (click)="openAgentGitHubPage($event)">Agent on GitHub</a></div>
<div><a class="link-github" [href]="agentGithubUrl" externalUrl>Agent on GitHub</a></div>
</div>
</div>

View File

@@ -1,10 +1,7 @@
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Constants } from 'uhk-common';
import { AppState } from '../../../store';
import { getVersions } from '../../../util';
import { OpenUrlInNewWindowAction } from '../../../store/actions/app';
@Component({
selector: 'about-page',
@@ -16,12 +13,5 @@ import { OpenUrlInNewWindowAction } from '../../../store/actions/app';
})
export class AboutComponent {
version: string = getVersions().version;
constructor(private store: Store<AppState>) {
}
openAgentGitHubPage(event) {
event.preventDefault();
this.store.dispatch(new OpenUrlInNewWindowAction(Constants.AGENT_GITHUB_URL));
}
agentGithubUrl = Constants.AGENT_GITHUB_URL;
}

View File

@@ -2,12 +2,17 @@ import { Routes } from '@angular/router';
import { SettingsComponent } from './settings/settings.component';
import { AboutComponent } from './about/about.component';
import { HelpPageComponent } from './help-page/help-page.component';
export const agentRoutes: Routes = [
{
path: 'settings',
component: SettingsComponent
},
{
path: 'help',
component: HelpPageComponent
},
{
path: 'about',
component: AboutComponent

View File

@@ -0,0 +1,28 @@
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-question-circle"></i>
<span class="macro__name pane-title__name">Help</span>
</h1>
</div>
<div class="row">
<div class="col-xs-12">
Frequently asked questions
<ul>
<li><a href="https://ultimatehackingkeyboard.com/blog/2018/06/23/how-can-i-type-accented-characters-with-my-uhk" externalUrl>How can I type accented characters with my UHK?</a></li>
</ul>
</div>
</div>
<div class="row">
<div class="col-xs-12">
Keyboard shortcuts
<ul>
<li><kbd>CTRL</kbd> + <kbd>Enter</kbd> = Remap key</li>
<li><kbd>CTRL</kbd> + <kbd>S</kbd> = Save to keyboard</li>
<li>Right click on a key = Capture key</li>
<li>Hold Shift while clicking on a key = Remap on all keymaps</li>
<li>Hold Alt while clicking on a key = Remap on all layers</li>
</ul>
</div>
</div>

View File

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

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'help-page',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './help-page.component.html',
styleUrls: ['./help-page.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class HelpPageComponent {
}

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,42 +12,40 @@
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="runningOnNotSupportedWindows$ | 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>
<p *ngIf="firmwareUpgradeAllowed$ | async">
<button class="btn btn-primary"
[disabled]="flashFirmwareButtonDisbabled$ | async"
(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 *ngIf="firmwareUpgradeFailed$ | async"
class="alert alert-danger"
role="alert">
<p>Firmware update failed. Disconnect every USB device from your computer (including USB hubs, KVM switches, USB dongles, and everything else), then connect only your UHK and retry.</p>
<p>If you've 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>
</div>
<div class="flex-grow" #scrollMe>
<div *ngIf="firmwareUpgradeSuccess$ | async"
class="alert alert-success"
role="alert">
<p>Firmware update succeeded.</p>
</div>
</div>
<div class="flex-grow" *ngIf="firmwareUpgradeAllowed$ | async">
<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,24 @@
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,
firmwareUpgradeAllowed,
firmwareUpgradeFailed,
firmwareUpgradeSuccess,
flashFirmwareButtonDisbabled,
getAgentVersionInfo,
getHardwareModules,
runningOnNotSupportedWindows,
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 +31,28 @@ 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;
runningOnNotSupportedWindows$: Observable<boolean>;
firmwareUpgradeAllowed$: Observable<boolean>;
firmwareUpgradeFailed$: Observable<boolean>;
firmwareUpgradeSuccess$: 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.runningOnNotSupportedWindows$ = store.select(runningOnNotSupportedWindows);
this.firmwareUpgradeAllowed$ = store.select(firmwareUpgradeAllowed);
this.firmwareUpgradeFailed$ = store.select(firmwareUpgradeFailed);
this.firmwareUpgradeSuccess$ = store.select(firmwareUpgradeSuccess);
}
ngOnDestroy(): void {
this.xtermLogSubscription.unsubscribe();
this.hardwareModulesSubscription.unsubscribe();
}
@@ -60,22 +60,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

@@ -29,7 +29,7 @@
<div class="row led-setting">
<div class="col-xs-12 col-md-6">
<slider-wrapper
label="Key backlight brightness"
label="Key backlight brightness <span class='text-muted pl-1'>Please note that current UHK versions are not backlit.</span>"
[min]="0"
[max]="255"
[step]="1"

View File

@@ -1,5 +1,4 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
width: 100%;

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

@@ -9,6 +9,7 @@
[keyboardLayout]="keyboardLayout"
[description]="description"
[showDescription]="true"
oncontextmenu="return false;"
(keyClick)="keyClick.emit($event)"
(keyHover)="keyHover.emit($event)"
(capture)="capture.emit($event)"

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 853 B

View File

@@ -3,6 +3,11 @@ import { animate, keyframes, state, style, transition, trigger } from '@angular/
import { Layer } from 'uhk-common';
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
import {
SvgKeyboardCaptureEvent,
SvgKeyboardKeyClickEvent,
SvgKeyHoverEvent
} from '../../../models/svg-key-events';
type AnimationKeyboard =
'init' |
@@ -82,9 +87,9 @@ export class KeyboardSliderComponent implements OnChanges {
@Input() selectedKey: { layerId: number, moduleId: number, keyId: number };
@Input() keyboardLayout = KeyboardLayout.ANSI;
@Input() description: string;
@Output() keyClick = new EventEmitter();
@Output() keyHover = new EventEmitter();
@Output() capture = new EventEmitter();
@Output() keyClick = new EventEmitter<SvgKeyboardKeyClickEvent>();
@Output() keyHover = new EventEmitter<SvgKeyHoverEvent>();
@Output() capture = new EventEmitter<SvgKeyboardCaptureEvent>();
@Output() descriptionChanged = new EventEmitter<string>();
layerAnimationState: AnimationKeyboard[];

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Keymap } from 'uhk-common';
@@ -14,6 +14,7 @@ import { KeymapActions } from '../../../store/actions';
selector: 'keymap-add',
templateUrl: './keymap-add.component.html',
styleUrls: ['./keymap-add.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'container-fluid'
}

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 { ChangeDetectionStrategy, 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';
@@ -25,19 +24,19 @@ import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription
selector: 'keymap-edit',
templateUrl: './keymap-edit.component.html',
styleUrls: ['./keymap-edit.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'container-fluid'
}
})
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 +51,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

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

@@ -8,8 +8,10 @@
<icon *ngIf="deletable" name="trash" (click)="deleteAction()"></icon>
</div>
<div class="list-group-item macro-action-editor__container"
[@toggler]="((editable && editing) || newItem) ? 'active' : 'inactive'">
[@toggler]="((editable && editing) || newItem) ? 'active' : 'inactive'"
[style.overflow]="overflow">
<macro-action-editor
*ngIf="editable || newItem"
[macroAction]="macroAction"
(cancel)="cancelEdit()"
(save)="saveEditedAction($event)">

View File

@@ -1,7 +1,7 @@
@import '../../../../styles/variables';
:host {
overflow: hidden;
overflow: visible;
display: block;
&.macro-item:first-of-type {

View File

@@ -45,6 +45,7 @@ export class MacroItemComponent implements OnInit, OnChanges {
iconName: string;
editing: boolean;
newItem: boolean = false;
overflow = 'hidden';
constructor(private mapper: MapperService) { }
@@ -53,6 +54,7 @@ export class MacroItemComponent implements OnInit, OnChanges {
if (!this.macroAction) {
this.editing = true;
this.newItem = true;
this.overflow = 'visible';
}
}
@@ -65,6 +67,7 @@ export class MacroItemComponent implements OnInit, OnChanges {
saveEditedAction(editedAction: MacroAction): void {
this.macroAction = editedAction;
this.editing = false;
this.overflow = 'hidden';
this.updateView();
this.save.emit(editedAction);
}
@@ -77,10 +80,12 @@ export class MacroItemComponent implements OnInit, OnChanges {
this.editing = true;
this.edit.emit();
this.setOverflow('visible');
}
cancelEdit(): void {
this.editing = false;
this.overflow = 'hidden';
this.cancel.emit();
}
@@ -202,4 +207,12 @@ export class MacroItemComponent implements OnInit, OnChanges {
});
this.title += selectedButtonLabels.join(', ');
}
private setOverflow(value: string): void {
// tslint:disable: align
setTimeout(() => {
this.overflow = value;
}, 600);
// tslint:enable: align
}
}

View File

@@ -47,7 +47,7 @@
</ul>
</div>
<div [ngSwitch]="activeTab">
<keypress-tab #tab *ngSwitchCase="tabName.Keypress" class="popover-content"
<keypress-tab #tab *ngSwitchCase="tabName.Keypress" class="popover-content pr-10"
[defaultKeyAction]="defaultKeyAction"
[secondaryRoleEnabled]="true"
(validAction)="keyActionValid=$event"
@@ -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)]="remapInfo.remapOnAllKeymap"> Remap on all keymaps
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
name="remapOnAllLayer"
[(ngModel)]="remapInfo.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 {
@@ -100,6 +99,10 @@
padding: 10px 24px;
}
.pr-10 {
padding-right: 10px;
}
.popover-overlay {
position: fixed;
width: 100%;
@@ -117,19 +120,10 @@
}
}
.select2-item {
position: relative;
font-size: 1.5rem;
.popover-action-form {
margin-top: 4px;
&.keymap-name--wrapper {
padding-left: 50px;
}
.layout-segment-code {
height: 2rem;
position: absolute;
left: 0;
top: 50%;
margin-top: -1rem;
label {
margin-right: 5px;
}
}

View File

@@ -1,4 +1,5 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
@@ -27,10 +28,12 @@ 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';
import { RemapInfo } from '../../models/remap-info';
enum TabName {
Keypress,
@@ -45,6 +48,7 @@ enum TabName {
selector: 'popover',
templateUrl: './popover.component.html',
styleUrls: ['./popover.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('popover', [
state('closed', style({
@@ -82,9 +86,11 @@ export class PopoverComponent implements OnChanges {
@Input() keyPosition: any;
@Input() wrapPosition: any;
@Input() visible: boolean;
@Input() allowLayerDoubleTap: boolean;
@Input() remapInfo: RemapInfo;
@Output() cancel = new EventEmitter<any>();
@Output() remap = new EventEmitter<KeyAction>();
@Output() remap = new EventEmitter<KeyActionRemap>();
@ViewChild('tab') selectedTab: Tab;
@ViewChild('popover') popoverHost: ElementRef;
@@ -155,8 +161,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.remapInfo.remapOnAllKeymap,
remapOnAllLayer: this.remapInfo.remapOnAllLayer,
action: this.selectedTab.toKeyAction()
});
} catch (e) {
// TODO: show error dialog
console.error(e);
@@ -169,6 +178,14 @@ export class PopoverComponent implements OnChanges {
this.cancel.emit();
}
@HostListener('document:keydown.control.enter', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (this.visible) {
this.onRemapKey();
event.preventDefault();
}
}
selectTab(tab: TabName): void {
this.activeTab = tab;
}

View File

@@ -4,12 +4,23 @@
<ng-template [ngIf]="keymapOptions.length > 0">
<div>
<b>Switch to keymap:</b>
<select2
[data]="keymapOptions"
[value]="selectedKeymap?.abbreviation || -1"
(valueChanged)="onChange($event)"
[width]="'100%'"
></select2>
<ngx-select [items]="keymapOptions"
[ngModel]="selectedKeymap?.abbreviation || -1"
[autoActiveOnMouseEnter]="false"
size="small"
optionValueField="id"
optionTextField="text"
(select)="onChange($event)">
<ng-template ngx-select-option let-option>
<span [ngClass]="{'indent-dropdown-item':option.data.id !== '-1'}">
<span>{{ option.text }}</span>
<span class="scancode--searchterm">
{{ option.data.additional?.explanation}}
</span>
</span>
</ng-template>
</ngx-select>
</div>
<div>
<div class="empty" *ngIf="!selectedKeymap?.abbreviation">

View File

@@ -16,7 +16,7 @@
margin-right: 7px;
}
select2 {
ngx-select {
flex: 1;
}
}

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Select2OptionData } from 'ng2-select2/ng2-select2';
import { Keymap, KeyAction, SwitchKeymapAction } from 'uhk-common';
import { Tab } from '../tab';
import { SelectOptionData } from '../../../../models/select-option-data';
@Component({
selector: 'keymap-tab',
@@ -14,7 +14,7 @@ export class KeymapTabComponent extends Tab implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() keymaps: Keymap[];
keymapOptions: Array<Select2OptionData>;
keymapOptions: Array<SelectOptionData>;
selectedKeymap: Keymap;
constructor() {
@@ -25,7 +25,7 @@ export class KeymapTabComponent extends Tab implements OnChanges {
ngOnChanges(changes: SimpleChanges) {
if (changes.keymaps) {
this.keymapOptions = this.keymaps
.map((keymap: Keymap): Select2OptionData => {
.map((keymap: Keymap): SelectOptionData => {
return {
id: keymap.abbreviation,
text: keymap.name
@@ -40,12 +40,11 @@ export class KeymapTabComponent extends Tab implements OnChanges {
this.validAction.emit(true);
}
// TODO: change to the correct type when the wrapper has added it.
onChange(event: any) {
if (event.value === '-1') {
onChange(event: string) {
if (event === '-1') {
this.selectedKeymap = undefined;
} else {
this.selectedKeymap = this.keymaps.find((keymap: Keymap) => keymap.abbreviation === event.value);
this.selectedKeymap = this.keymaps.find((keymap: Keymap) => keymap.abbreviation === event);
}
}

View File

@@ -1,15 +1,33 @@
<div class="scancode-options">
<b class="setting-label">Scancode:</b>
<select2
[data]="scanCodeGroups"
[value]="selectedScancodeOption.id"
(valueChanged)="onScancodeChange($event)"
[width]="200"
[options]="options"
></select2>
<div class="scancode-container">
<ngx-select [items]="scanCodeGroups"
[ngModel]="selectedScancodeOption?.id"
[autoActiveOnMouseEnter]="false"
size="small"
optionValueField="id"
optionTextField="text"
optGroupLabelField="text"
optGroupOptionsField="children"
(select)="onScancodeChange($event)">
<ng-template ngx-select-option let-option>
<span [ngClass]="{'indent-dropdown-item':option.data.id !== '0'}">
<span>{{ option.text }}</span>
<span class="scancode--searchterm">
{{ option.data.additional?.explanation}}
</span>
</span>
</ng-template>
</ngx-select>
</div>
<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. For example, on US keyboards next to Tab there is the Q key, but it's й on Russian keyboards, so in this case choose Q instead of й in Agent."
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>
@@ -18,35 +36,59 @@
<div class="btn-toolbar modifiers">
<div class="btn-group btn-group-sm modifiers__left">
<button type="button" class="btn btn-default"
*ngFor="let modifier of leftModifiers; let index = index"
[class.btn-primary]="leftModifierSelects[index]"
(click)="toggleModifier(false, index)"
*ngFor="let modifier of leftModifiers; trackBy:modifiersTrackBy"
[class.btn-primary]="modifier.checked"
(click)="toggleModifier(modifier)"
>
{{modifier}}
{{ modifier.text }}
</button>
</div>
<div class="btn-group btn-group-sm modifiers__right">
<button type="button" class="btn btn-default"
*ngFor="let modifier of rightModifiers; let index = index"
[class.btn-primary]="rightModifierSelects[index]"
(click)="toggleModifier(true, index)"
*ngFor="let modifier of rightModifiers; trackBy:modifiersTrackBy"
[class.btn-primary]="modifier.checked"
(click)="toggleModifier(modifier)"
>
{{modifier}}
{{ modifier.text }}
</button>
</div>
</div>
</div>
<div class="long-press-container" *ngIf="secondaryRoleEnabled">
<b class="setting-label">Secondary role:</b>
<select2 #secondaryRoleSelect
[data]="secondaryRoleGroups"
[value]="selectedSecondaryRoleIndex.toString()"
(valueChanged)="onSecondaryRoleChange($event)"
[width]="140"
></select2>
<div class="secondary-role-groups-container">
<ngx-select [items]="secondaryRoleGroups"
[ngModel]="selectedSecondaryRoleIndex.toString()"
[autoActiveOnMouseEnter]="false"
size="small"
optionValueField="id"
optionTextField="text"
optGroupLabelField="text"
optGroupOptionsField="children"
(select)="onSecondaryRoleChange($event)">
<ng-template ngx-select-option let-option>
<span [ngClass]="{'indent-dropdown-item':option.data.id !== '-1'}">
<span>{{ option.text }}</span>
<span class="scancode--searchterm">
{{ option.data.additional?.explanation}}
</span>
</span>
</ng-template>
</ngx-select>
</div>
<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'>The secondary role can be any layer or modifier.</p>"
data-placement="bottom"></icon>
</div>

View File

@@ -79,4 +79,14 @@
display: block;
}
}
.scancode-container {
display: inline-block;
width: 200px;
}
.secondary-role-groups-container {
display: inline-block;
width: 140px;
}
}

View File

@@ -1,12 +1,15 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Select2OptionData, Select2TemplateFunction } from 'ng2-select2';
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { KeyAction, KeystrokeAction, KeystrokeType, SCANCODES, SECONDARY_ROLES } from 'uhk-common';
import { Tab } from '../tab';
import { MapperService } from '../../../../services/mapper.service';
import { SelectOptionData } from '../../../../models/select-option-data';
import { KeyModifierModel } from '../../../../models/key-modifier-model';
import { mapLeftRigthModifierToKeyActionModifier } from '../../../../util';
@Component({
selector: 'keypress-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './keypress-tab.component.html',
styleUrls: ['./keypress-tab.component.scss']
})
@@ -14,45 +17,28 @@ export class KeypressTabComponent extends Tab implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() secondaryRoleEnabled: boolean;
leftModifiers: string[];
rightModifiers: string[];
leftModifiers: KeyModifierModel[];
rightModifiers: KeyModifierModel[];
leftModifierSelects: boolean[];
rightModifierSelects: boolean[];
scanCodeGroups: Array<SelectOptionData>;
secondaryRoleGroups: Array<SelectOptionData>;
scanCodeGroups: Array<Select2OptionData>;
secondaryRoleGroups: Array<Select2OptionData>;
options: Select2Options;
selectedScancodeOption: Select2OptionData;
selectedScancodeOption: SelectOptionData;
selectedSecondaryRoleIndex: number;
constructor(private mapper: MapperService) {
super();
this.leftModifiers = ['LShift', 'LCtrl', 'LSuper', 'LAlt'];
this.rightModifiers = ['RShift', 'RCtrl', 'RSuper', 'RAlt'];
this.leftModifiers = mapper.getLeftKeyModifiers();
this.rightModifiers = mapper.getRightKeyModifiers();
this.scanCodeGroups = [{
id: '0',
text: 'None'
}];
this.scanCodeGroups = this.scanCodeGroups.concat(SCANCODES);
this.secondaryRoleGroups = SECONDARY_ROLES;
this.leftModifierSelects = Array(this.leftModifiers.length).fill(false);
this.rightModifierSelects = Array(this.rightModifiers.length).fill(false);
this.selectedScancodeOption = this.scanCodeGroups[0];
this.selectedSecondaryRoleIndex = -1;
this.options = {
templateResult: this.scanCodeTemplateResult,
matcher: (term: string, text: string, data: Select2OptionData) => {
let found = text.toUpperCase().indexOf(term.toUpperCase()) > -1;
if (!found && data.additional && data.additional.explanation) {
found = data.additional.explanation.toUpperCase().indexOf(term.toUpperCase()) > -1;
}
return found;
}
};
}
ngOnChanges() {
@@ -68,15 +54,15 @@ export class KeypressTabComponent extends Tab implements OnChanges {
return (keystrokeAction) ? (keystrokeAction.scancode > 0 || keystrokeAction.modifierMask > 0) : false;
}
onKeysCapture(event: { code: number, left: boolean[], right: boolean[] }) {
onKeysCapture(event: { code: number, left: KeyModifierModel[], right: KeyModifierModel[] }) {
if (event.code) {
this.selectedScancodeOption = this.findScancodeOptionByScancode(event.code, KeystrokeType.basic);
} else {
this.selectedScancodeOption = this.scanCodeGroups[0];
}
this.leftModifierSelects = event.left;
this.rightModifierSelects = event.right;
this.leftModifiers = event.left;
this.rightModifiers = event.right;
this.validAction.emit(this.keyActionValid());
}
@@ -88,16 +74,12 @@ export class KeypressTabComponent extends Tab implements OnChanges {
// Restore selectedScancodeOption
this.selectedScancodeOption = this.findScancodeOptionByScancode(keystrokeAction.scancode || 0, keystrokeAction.type);
const leftModifiersLength: number = this.leftModifiers.length;
// Restore modifiers
for (let i = 0; i < leftModifiersLength; ++i) {
this.leftModifierSelects[this.mapper.modifierMapper(i)] = ((keystrokeAction.modifierMask >> i) & 1) === 1;
for (const modifier of this.leftModifiers) {
modifier.checked = (keystrokeAction.modifierMask & modifier.value) > 0;
}
for (let i = leftModifiersLength; i < leftModifiersLength + this.rightModifierSelects.length; ++i) {
const index: number = this.mapper.modifierMapper(i) - leftModifiersLength;
this.rightModifierSelects[index] = ((keystrokeAction.modifierMask >> i) & 1) === 1;
for (const modifier of this.rightModifiers) {
modifier.checked = (keystrokeAction.modifierMask & modifier.value) > 0;
}
// Restore secondaryRoleAction
@@ -119,11 +101,7 @@ export class KeypressTabComponent extends Tab implements OnChanges {
} else {
keystrokeAction.type = KeystrokeType[scTypePair[1]];
}
keystrokeAction.modifierMask = 0;
const modifiers = this.leftModifierSelects.concat(this.rightModifierSelects).map(x => x ? 1 : 0);
for (let i = 0; i < modifiers.length; ++i) {
keystrokeAction.modifierMask |= modifiers[i] << this.mapper.modifierMapper(i);
}
keystrokeAction.modifierMask = mapLeftRigthModifierToKeyActionModifier(this.leftModifiers, this.rightModifiers);
keystrokeAction.secondaryRoleAction = this.selectedSecondaryRoleIndex === -1
? undefined
@@ -134,50 +112,30 @@ export class KeypressTabComponent extends Tab implements OnChanges {
}
}
scanCodeTemplateResult: Select2TemplateFunction = (state: Select2OptionData): JQuery | string => {
if (!state.id) {
return state.text;
}
if (state.additional && state.additional.explanation) {
return jQuery(
'<span class="select2-item">'
+ '<span>' + state.text + '</span>'
+ '<span class="scancode--searchterm"> '
+ state.additional.explanation
+ '</span>' +
'</span>'
);
} else {
return jQuery('<span class="select2-item">' + state.text + '</span>');
}
}
toggleModifier(right: boolean, index: number) {
const modifierSelects: boolean[] = right ? this.rightModifierSelects : this.leftModifierSelects;
modifierSelects[index] = !modifierSelects[index];
toggleModifier(modifier: KeyModifierModel): void {
modifier.checked = !modifier.checked;
this.validAction.emit(this.keyActionValid());
}
onSecondaryRoleChange(event: { value: string }) {
this.selectedSecondaryRoleIndex = +event.value;
onSecondaryRoleChange(id: string) {
this.selectedSecondaryRoleIndex = +id;
}
onScancodeChange(event: { value: string }) {
const id: string = event.value;
// ng2-select2 should provide the selectedOption in an upcoming release
// TODO: change this when it has become available
onScancodeChange(id: string) {
this.selectedScancodeOption = this.findScancodeOptionById(id);
this.validAction.emit(this.keyActionValid());
}
private findScancodeOptionBy(predicate: (option: Select2OptionData) => boolean): Select2OptionData {
let selectedOption: Select2OptionData;
modifiersTrackBy(index: number, modifier: KeyModifierModel): string {
return `${modifier.value}${modifier.checked}`;
}
const scanCodeGroups: Select2OptionData[] = [...this.scanCodeGroups];
private findScancodeOptionBy(predicate: (option: SelectOptionData) => boolean): SelectOptionData {
let selectedOption: SelectOptionData;
const scanCodeGroups: SelectOptionData[] = [...this.scanCodeGroups];
while (scanCodeGroups.length > 0) {
const scanCodeGroup = scanCodeGroups.shift();
if (predicate(scanCodeGroup)) {
@@ -192,14 +150,14 @@ export class KeypressTabComponent extends Tab implements OnChanges {
return selectedOption;
}
private findScancodeOptionById(id: string): Select2OptionData {
private findScancodeOptionById(id: string): SelectOptionData {
return this.findScancodeOptionBy(option => option.id === id);
}
private findScancodeOptionByScancode(scancode: number, type: KeystrokeType): Select2OptionData {
private findScancodeOptionByScancode(scancode: number, type: KeystrokeType): SelectOptionData {
const typeToFind: string =
(type === KeystrokeType.shortMedia || type === KeystrokeType.longMedia) ? 'media' : KeystrokeType[type];
return this.findScancodeOptionBy((option: Select2OptionData) => {
return this.findScancodeOptionBy((option: SelectOptionData) => {
const additional = option.additional;
if (additional && additional.scancode === scancode && additional.type === typeToFind) {
return true;
@@ -211,7 +169,11 @@ export class KeypressTabComponent extends Tab implements OnChanges {
});
}
private toScancodeTypePair(option: Select2OptionData): [number, string] {
private toScancodeTypePair(option: SelectOptionData): [number, string] {
if (!option) {
return [0, 'basic'];
}
let scanCode: number;
let type: string;
if (option.additional) {

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 tapping 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,26 +1,30 @@
import { Component, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core';
import { KeyAction, LayerName, SwitchLayerAction } from 'uhk-common';
import { ChangeDetectionStrategy, Component, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core';
import { KeyAction, LayerName, SwitchLayerAction, SwitchLayerMode } from 'uhk-common';
import { Tab } from '../tab';
export type toggleType = 'active' | 'toggle';
@Component({
selector: 'layer-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './layer-tab.component.html',
styleUrls: ['./layer-tab.component.scss']
})
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 +44,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 +76,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 +116,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

@@ -5,7 +5,23 @@
<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>
<ngx-select [items]="macroOptions"
[ngModel]="macroOptions[selectedMacroIndex]?.id"
[autoActiveOnMouseEnter]="false"
size="small"
optionValueField="id"
optionTextField="text"
(select)="onChange($event)">
<ng-template ngx-select-option let-option>
<span [ngClass]="{'indent-dropdown-item':option.data.id !== '-1'}">
<span>{{ option.text }}</span>
<span class="scancode--searchterm">
{{ option.data.additional?.explanation}}
</span>
</span>
</ng-template>
</ngx-select>
</div>
<div class="macro-action-container">
<div class="list-group">

View File

@@ -16,7 +16,7 @@
margin-right: 7px;
}
select2 {
ngx-select {
flex: 1;
}
}

View File

@@ -1,16 +1,17 @@
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { Select2OptionData } from 'ng2-select2/ng2-select2';
import { KeyAction, Macro, PlayMacroAction } from 'uhk-common';
import { Tab } from '../tab';
import { AppState } from '../../../../store/index';
import { AppState } from '../../../../store';
import { getMacros } from '../../../../store/reducers/user-configuration';
import { SelectOptionData } from '../../../../models/select-option-data';
@Component({
selector: 'macro-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './macro-tab.component.html',
styleUrls: ['./macro-tab.component.scss']
})
@@ -18,7 +19,7 @@ export class MacroTabComponent extends Tab implements OnInit, OnChanges, OnDestr
@Input() defaultKeyAction: KeyAction;
macros: Macro[];
macroOptions: Array<Select2OptionData>;
macroOptions: Array<SelectOptionData>;
selectedMacroIndex: number;
private subscription: Subscription;
@@ -31,7 +32,7 @@ export class MacroTabComponent extends Tab implements OnInit, OnChanges, OnDestr
}
ngOnInit() {
this.macroOptions = this.macros.map(function (macro: Macro, index: number): Select2OptionData {
this.macroOptions = this.macros.map(function (macro: Macro, index: number): SelectOptionData {
return {
id: index.toString(),
text: macro.name
@@ -44,9 +45,8 @@ export class MacroTabComponent extends Tab implements OnInit, OnChanges, OnDestr
this.validAction.emit(true);
}
// TODO: change to the correct type when the wrapper has added it.
onChange(event: any) {
this.selectedMacroIndex = +event.value;
onChange(id: string) {
this.selectedMacroIndex = +id;
}
keyActionValid(): boolean {

View File

@@ -1,10 +1,11 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { KeyAction, MouseAction, MouseActionParam } from 'uhk-common';
import { Tab } from '../tab';
@Component({
selector: 'mouse-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './mouse-tab.component.html',
styleUrls: ['./mouse-tab.component.scss']
})

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Tab } from '../tab';
@Component({
selector: 'none-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './none-tab.component.html',
styleUrls: ['./none-tab.component.scss']
})

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