From d9cea5712bb9da0156c9043e68ffd2006deede69 Mon Sep 17 00:00:00 2001 From: "i.mirdzhamolov" Date: Sat, 25 Apr 2020 16:56:45 +0300 Subject: [PATCH] feat: create first version of keypress-shower --- .gitignore | 5 +- assets/dark/icon-active.png | Bin 0 -> 526 bytes assets/dark/icon-active@2x.png | Bin 0 -> 1291 bytes assets/dark/icon.png | Bin 0 -> 440 bytes assets/dark/icon@2x.png | Bin 0 -> 1021 bytes assets/light/icon-active.png | Bin 0 -> 344 bytes assets/light/icon-active@2x.png | Bin 0 -> 802 bytes assets/light/icon.png | Bin 0 -> 297 bytes assets/light/icon@2x.png | Bin 0 -> 648 bytes client/constants.js | 82 ++++++++++++++ client/index.css | 47 ++++++++ client/index.html | 29 +++++ client/index.js | 37 +++++++ index.html | 15 --- main.js | 46 +++++--- server/AppController.js | 173 ++++++++++++++++++++++++++++++ server/BrowserWindowController.js | 91 ++++++++++++++++ 17 files changed, 494 insertions(+), 31 deletions(-) create mode 100644 assets/dark/icon-active.png create mode 100644 assets/dark/icon-active@2x.png create mode 100644 assets/dark/icon.png create mode 100644 assets/dark/icon@2x.png create mode 100644 assets/light/icon-active.png create mode 100644 assets/light/icon-active@2x.png create mode 100644 assets/light/icon.png create mode 100644 assets/light/icon@2x.png create mode 100644 client/constants.js create mode 100644 client/index.css create mode 100644 client/index.html create mode 100644 client/index.js delete mode 100644 index.html create mode 100644 server/AppController.js create mode 100644 server/BrowserWindowController.js diff --git a/.gitignore b/.gitignore index 30bc162..4c32ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/node_modules \ No newline at end of file +# Specific system files should be ignored by user in global `.gitignore`. +# More info here https://gist.github.com/subfuzion/db7f57fff2fb6998a16c + +/node_modules diff --git a/assets/dark/icon-active.png b/assets/dark/icon-active.png new file mode 100644 index 0000000000000000000000000000000000000000..56960108e7c19b041602ddcc7d66e8529271d174 GIT binary patch literal 526 zcmV+p0`dKcP)j|7h>fIc!PKXiQ4IfFQoK^C9*UPlcz;o{07^F>-ny8Y;F)SUOmi1TY+k$9RLV7 zlPXN2St5$bwMKjDw$45AOSmoHt2X(p@ABT^hdkCnCO18WNB!E_Wk(=J2;dRDy@IH9 zsFmu;%yelxblel)1Jp?pEn-Tp3uCTwZHRUXN_B~TOmbe>FX@y`u+@x!&##YnGn8^d zMvnMRzo`J|^P2x7c56PH=aAWs4TR8RN{V<1D3G4)wre{ptuz5z$T!4Z2)w5+Yy3|E zR!@ju*Yt^QEnt@*M4TfM6P7@JBGHNdT!53whbFQV5DBv(BMvYyX2u=Hu7dfXxmw=V z#i2l`@QZUYnTB~Q)BO*)&sDiRAmj=b^zQ@%BzmKUvaCCA%lSB$kV!dKrP4&FX>WH} z_9{^0sgfRPsjhkdJxtWw(&&IpOMV_E@ECAAOt6PQ!{QhdSmDX9yc-Mg4r=_tKI4kx Q5C8xG07*qoM6N<$g2BH7BWosz1rjm45dx>PLx_oczJSaZ#QB0GJ9c)yAjythk}rt;1+kL_v!ltd zm|YxJATp>C2qC+xigUW{-~>C}ndC}scUO1aSDm`|loTbUT8pZ|2X@s7(U4MxUWl5+ zFyG-m!TrE{ALFra|J0pi&`Zkbm4$|%MKu$+4JpMc3=EtUU6#r^DgP zx6p}9RCT3tjHOwgEUTJ)wNPK#5z_ZA&rWwU&noUnNEr$5dsKp}iz^XccEsHzG(oV> zo8NQYU1MEq@-hSA1awvbC~*!HIOM(b2hz(6aSwr#MJavr^6VtHg)IwUo&%K21ZB1a zI`A#LJ)9W7^3M9h%b!kr;vTZ_`PY$Ji*tP^l2)n}!}q?AcTeKfP5^eT8Xj zi*nLN#M|>;vZf#lKfrcdX=_(8*1Z}1ET!3RjtMG{uilKuQ2jdBzMUh?f<#rNZGccU zprI$qNjJ523x*bnZ0-nLgLI_VIi{@L9y?PgEa}(+4F!ON!uhzIeGzM8Hc{)FItU^} z5i^n?uo1_EF-`28ei=pVCjfY8xVNn_&BP+2odP_s8H|D6Y z!%@)8gmX2;qEfonqb>kE$8-e7YlwP@mr<&!y!gf$gF+^1WmJuAR+YHBRJr!n`@{)< z;hdOu{?1KHQ5m;lnU4uLo2D2P{?q zA^E`umS7$KYa9=N1vuBzkAg*%YJxB-(nSIzlo~JtKYC{iCuBkprs+Y(cJ!!+MnxRd zMq2l>F=_WudY8(mB6~uqkCkG|0H89H2tuQE3q~)D4fbW;9m#%<@-e-GC0Q+{%A@`` z(O2-k!$?ZtV0-KY4YaaO`JaS0#nGWLJ8phglnhyDvqi$-1zAy=m)s*P`i@Ygit9{4ujr+Ew9fk{ z^_2}o7X5a1GQ0JYMrWk7zOp7BSg122rQvl_s;3|R`DKtidirr+DfQj^AAKVK{`Kcq z;sJz_BZuEwmA*{k@83+nn|(0J4-{-Vy)wtEc`|jnTAk*b(=xLe5tP8hGC=qTX*B8B zO+H^_m~-@FcP>dSp=%!FDn6W!k5^gU2jv2j=_~At^e+=nPQcm8`Mn`{g^t*kS)Or< z?jq{lBC1y!-Wi=He?+yzq@{VsbELG->ROg#{0sd|YjheTrr-bo002ovPDHLkV1iF+ BUY-B| literal 0 HcmV?d00001 diff --git a/assets/dark/icon.png b/assets/dark/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9307f9c390a2765bad3f697adc39e93ad1986d06 GIT binary patch literal 440 zcmV;p0Z0CcP)N66gcA}gd3O>1fPK91mp(s4Y-15LXpw~6qKe1Mzv#U(R#)i zPi$~O%PVZH?Duw8ddt}f+{^2L0R*fCMV`m%ZJ*Wmc(ssTAf17^L&WU!Wf!*zjK+S* zIge%Dd-?9#jCiA#j z6BpPY@h?CE#w6r03b_gu-(T7LA6f^fALx`o>4R89UoXC% zcbn3Q1;9r1dRv|6(dn=>6s++?Pd{i7w`JAG57ygYbjXpZ--n5Nl&~5m-pF8w#W5yq iO>fWZj z6Hyes@6EI(8chQj7baSF#AqFM#t0q1fbs<`Ur^k+D|`X4b4U3C+ApAmh3u^0f*pe^ zjm3=77}L&s&vU0Osn|M&*6}25XF6}*z3;yB?mbt~h)AW~RN|(}X`-wU!ZAc?fxr0~ zu6*||T@x7Wq%Df`d@T{A*6S^n_?11df)% z5hiqy$Cg#Qz2+Yucvo_OS8U(XC9JN1L7Rb;8$E-OPCIZ~>jK`*!=Xy^Xz%5v9N<-3 zNxdtHs%Rai5lib;TxM0P^&%=7otGXybEY#3Co`G(6`ByHFc;5x8*~44Ag`PU zPCuS6(1gKr^4zJ}r|JIx?fx4MyCAdKJlZdJ$fvdp7|(1ESq|oR8K0uj(Fj_tr8CTl zNTjS}au-`O*V|Uv8#R+zSSMj>&BOhT(6gl8At78crpA-tQ^m(8BPw(`ICdhj#TQ>X zb60JpOTC97345ed5=3cqhysU<5vDARIHfUwN$v|_3eCg4jx8Kn5c342oMTdkTVN$j z2~W3s#?QS|X1n=*zeZzV=FxMPge>&#IrLcEW+ItnlCMbih&>Est4uM0a|$x*BHNah zQaSury}0^+366q5R%P2Dq0C~ZY7{d#rQ-->CBpBJ4vR><4xpZZ;Ev?89oTm44sbEU zcUp2%xoWN-2VI*fN&gwV1wJ1KiWyy-K5v1Sz+OMcnlSa1AdBs zL_tcI#so*V|DIRRLRX3QTIdXa!4nU1X}c>FItrG#Aw&_{(7$r3mNE-t9NF{tx%9Kx zJ?j>~b4i9MtU@Tfg~3x+MS*24V_SAWmvxen?ZZ%@>eT|Pz0B8BnS~-&=6*ihdoXY? z%ji&)W)|`^Vep)xD0O}prB?sV&ySveZTfCqO8NT6oqOWPrw<1-0r1KZ!&gcMOMc_c zasRWyjY+4W*kvA!(q0WiUw4)3*PHzqSd5sI;KUqA_?2ZeG+gPTo{uuj?^w6=iC}4o zxrQmO=;pNBUSVJ!5vf4Q)Fn``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{##EM$B+ufrNNE7O$Guvyv!vma~#|gSj`id zZ#tMY^2#`x38)LGZ}16tf6`Iva!}{|n0-I*-T!|$e6@b@InjbvuISZU<(kYrOdNQp zOiXz=#Xz$eiYp@&+W*L`dj)t7p}d#7PJceL|6*COMvtwz%4s^|E3s-1hXZ`pg* z;_A63UxNPn?Jqg9``Y8{kxNc7d}$DqOICBa%4#TKJ=T*DtWo! nx9YD0Sa%pLx_I(L-2-OEMc>5#uq`zPdYr-2)z4*}Q$iB}^nivs literal 0 HcmV?d00001 diff --git a/assets/light/icon-active@2x.png b/assets/light/icon-active@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..faa44cebbf43dc4ac49a8faf798c72713d31d718 GIT binary patch literal 802 zcmV+-1Ks?IP)#sF&_N8eU#gVSgv(l+M^xJC56RRb%1pR4l)cSjM~g`It1CZV_T zz)yFweoHN5c`Su*bQ!LNx!4W9(EFY6Cio)l)r<~D1jy(@u+IJADu5P%9{fA^Qk@9wv>e|$X^N2a5{a(Fd1!hcw%xiHi3Qr~ms#%OB|-&f>!HLC!O))Un`Uo32*MptA*3Evoj zw8Pe!kzcM5;)LInSX%d5=PZWQXF8znfU`>Zrq6)RX8;JO*uLYH!aMTw+!V3Vy0yiS z`mC2g!lnF8|0-Vjb*4&O;sf$fncK}_@;Ndua4mWj`S!xW1le g``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{yt9^$B+ufx4{Q_4=eDrX#F>G+T|Jcf-NV& zOk?%|Hra(am&%$halY8ORY{vkQ1gDou7sJ^((H?`9`e!@p5}Q~H*4+utFj!t4;XI? z2p?za+hO{VS0eOy$Ke>|=00<#4{YxaEWV;^@ZIZHGDnWSIB;+RGtegtp00i_>zopr0HoS$4FCWD literal 0 HcmV?d00001 diff --git a/assets/light/icon@2x.png b/assets/light/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..49d5f120d0ccd4125cedf531ff8dc42ddf252f00 GIT binary patch literal 648 zcmV;30(bq1P)?T88#GK%IzhVuYyh1AIst4DHlR!pm;gEf=>&ucR6QamCI^S}A-ubH!8n$l z9a)w=as)H`^7s_K2}5BZ^tC$`Cc?dNEBx_1FQqmhjsxLbco4?Ii7-?_xj5lK`&OJG z!dR*cH44W^;mmgED0-%%=TsGH0D~7*9N)0BR9k`Tpil=)s3qv9fz9yVi)Q8{Y8kM3 zcO~qZtC(10fVGdUwkvkVskIXRHRlMp$ZGG@z)F8F)Op0!5nFa)Cl5@|%vK(F>0S13 zqW`fxmcl=3h8N*T>>6K~{Xy6_zDQ>^qstWm2GkIo1im2vxqU<3V-4RBfaI{J?y-Sy2ne1eAc1cP@LM}ND?ojogYc<Vao&9pun~Sx_dx#HUZh34_ra++Q2TyQw*#_LYZZ_K zhQejO{Z-MCFDqRxpOAIHStZ0)+f-cv+Md;c(xm$p$KAW*)kex}5C#>ysQx36|l z^qizUDJwOxV3%Y&ZjP9Xl57kgH|1K(Go+RQo9t4a^JB{LF}sxI)&R2-5p(_P6&JUw zn=`+97-@Bcpg9Tg4X0IvsN*SR#U}#2U%TSs;(7(3L`Q9p + + + + Keypress Shower + + + + + + +
+ + + + + + + + + diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..6e0804f --- /dev/null +++ b/client/index.js @@ -0,0 +1,37 @@ +const { ipcRenderer } = require('electron'); +const { humanizedKeyCodes } = require('./constants.js'); + +ipcRenderer.on('keydown', (event, message) => { + computeKeyPressed(message.type, message.keycode); +}); + +ipcRenderer.on('keyup', (event, message) => { + computeKeyPressed(message.type, message.keycode); +}); + +const pressedKeys = new Map(); + +function computeKeyPressed(type, keyCode) { + switch (type) { + case 'keydown': + pressedKeys.set(keyCode, humanizedKeyCodes.get(keyCode)); + break; + case 'keyup': + pressedKeys.delete(keyCode); + break; + } + + renderKeysPressed(); +} + +const elemRoot = document.getElementById('root'); + +function renderKeysPressed() { + const result = []; + + pressedKeys.forEach((value) => { + result.push(`
${value}
`); + }); + + elemRoot.innerHTML = result.join(`
+
`); +} diff --git a/index.html b/index.html deleted file mode 100644 index ec20cca..0000000 --- a/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Привет мир! - - - - -

Привет мир!

- Мы используем node , - Chrome , - и Electron . - - diff --git a/main.js b/main.js index d624143..86f65e6 100644 --- a/main.js +++ b/main.js @@ -1,17 +1,33 @@ -const { app, BrowserWindow } = require('electron') - -function createWindow () { - // Создаем окно браузера. - let win = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - nodeIntegration: true - } - }) - - // и загрузить index.html приложения. - win.loadFile('index.html') +/* + * TODO + * - Hide App in Mission Control + */ + +// libs +const { app } = require('electron'); + +// modules +const { AppController } = require('./server/AppController.js'); +const { BrowserWindowController } = require('./server/BrowserWindowController.js'); + +let appController = null; + +function handleAppReady() { + appController = new AppController(new BrowserWindowController('./client/index.html')); + + appController.onQuit(() => { + appController.stop(); + app.exit(0); + }); + + appController.start(); +} + +function handleAppWindowAllClosed(event) { + // Hook for prevent app quit. + event.preventDefault(); } -app.whenReady().then(createWindow) \ No newline at end of file +// app.dock.hide(); +app.once('ready', handleAppReady); +app.on('window-all-closed', handleAppWindowAllClosed); diff --git a/server/AppController.js b/server/AppController.js new file mode 100644 index 0000000..27bc62c --- /dev/null +++ b/server/AppController.js @@ -0,0 +1,173 @@ +const { Tray, Menu, nativeImage, NativeImage, MenuItem, nativeTheme } = require('electron'); +const path = require('path'); + +/** + * @namespace AppController + * @typedef {('dark' | 'light')} Theme + */ +class AppController { + /** @return {Theme} */ + static getCurrentThemeName() { + return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + } + + /** + * @param {Theme} theme + * @return {Theme} + */ + static invertThemeByName(theme) { + switch (theme) { + case 'dark': + return 'light'; + case 'light': + return 'dark'; + } + } + + /** @param {(BrowserWindowController)} browserWindowController */ + constructor(browserWindowController) { + this.isActive = false; + this.theme = AppController.getCurrentThemeName(); + this.tray = new Tray(nativeImage.createEmpty()); + + this.setTrayImage(); + this.setTrayTooltip(); + + this.handleActivateMenuItemClick = this.handleActivateMenuItemClick.bind(this); + this.handleQuitClick = this.handleQuitClick.bind(this); + this.handleThemeChange = this.handleThemeChange.bind(this); + + this.tray.setContextMenu(Menu.buildFromTemplate([ + { + type: 'checkbox', + label: 'Activate', + click: this.handleActivateMenuItemClick, + checked: this.isActive, + }, + { + label: 'Quit', + click: this.handleQuitClick, + }, + ])); + + this.browserWindowController = browserWindowController; + } + + start() { + this.bindEventListeners(); + } + + stop() { + this.unbindEventListeners(); + + if (!this.tray.isDestroyed()) { + this.tray.destroy(); + } + + this.browserWindowController.destroy(); + } + + /** + * @param {function} callback + * @return {function} + */ + onQuit(callback) { + this.quitHandler = callback; + + return () => { + this.quitHandler = () => {}; + }; + } + + /** + * @type {boolean} + * @private + */ + isActive; + /** + * @type {Theme} + * @private + */ + theme; + /** + * @type {(Electron.Tray)} + * @private + */ + tray; + /** + * @type {(BrowserWindowController)} + * @private + */ + browserWindowController; + /** + * @type {function} + * @private + */ + quitHandler = () => {}; + + /** @private */ + bindEventListeners() { + nativeTheme.on('updated', this.handleThemeChange); + } + + /** @private */ + unbindEventListeners() { + nativeTheme.off('updated', this.handleThemeChange); + } + + /** @private */ + setTrayImage() { + this.tray.setImage(this.getImagePath(AppController.invertThemeByName(this.theme))); + this.tray.setPressedImage(this.getImagePath(this.theme)); + } + + /** @private */ + setTrayTooltip() { + if (this.isActive) { + this.tray.setToolTip('Keypress shower is turned on'); + } else { + this.tray.setToolTip('Keypress shower is turned off'); + } + } + + /** + * @param {Theme} theme + * @return string + * @private + */ + getImagePath(theme) { + const pathToImage = this.isActive ? `assets/${theme}/icon-active.png` : `assets/${theme}/icon.png`; + + return path.join(process.cwd(), pathToImage); + } + + /** + * @param {Electron.MenuItem} menuItem + * @private + */ + handleActivateMenuItemClick(menuItem) { + this.isActive = menuItem.checked; + + this.setTrayImage(); + this.setTrayTooltip(); + + if (this.isActive) { + this.browserWindowController.create(); + } else { + this.browserWindowController.destroy(); + } + } + + /** @private */ + handleQuitClick() { + this.quitHandler(); + } + + /** @private */ + handleThemeChange() { + this.theme = AppController.getCurrentThemeName(); + this.setTrayImage(); + } +} + +module.exports.AppController = AppController; diff --git a/server/BrowserWindowController.js b/server/BrowserWindowController.js new file mode 100644 index 0000000..db158f1 --- /dev/null +++ b/server/BrowserWindowController.js @@ -0,0 +1,91 @@ +const { screen, BrowserWindow } = require('electron'); +const ioHook = require('iohook'); + +/** + * @namespace BrowserWindowController + */ +class BrowserWindowController { + constructor(viewUrl) { + this.viewUrl = viewUrl; + this.handleKeyEvent = this.handleKeyEvent.bind(this); + } + + create() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + + this.instance = new BrowserWindow({ + width, + height, + frame: false, + transparent: true, + // backgroundColor: 'rgba(0, 0, 0, 0)', + // skipTaskbar: true, + hasShadow: false, + webPreferences: { + nodeIntegration: true + } + }); + + // 'screen-saver' move our window to the higher level. + this.instance.setAlwaysOnTop(true, 'screen-saver'); + + this.instance.setIgnoreMouseEvents(true); + + this.instance.loadFile(this.viewUrl); + + this.bindKeyEventsListeners(); + } + + destroy() { + if (this.instance) { + if (!this.instance.isDestroyed()) { + this.instance.destroy(); + } + + this.instance = null; + } + + this.unbindKeyEventsListeners(); + } + + /** + * @type {(Electron.BrowserWindow | null)} + * @private + */ + instance = null; + + /** + * @type {string} + * @private + */ + viewUrl = ''; + + /** @private */ + bindKeyEventsListeners() { + ioHook.start(); + ioHook.on('keydown', this.handleKeyEvent); + ioHook.on('keyup', this.handleKeyEvent); + } + + /** @private */ + unbindKeyEventsListeners() { + ioHook.stop(); + ioHook.off('keydown', this.handleKeyEvent); + ioHook.off('keyup', this.handleKeyEvent); + } + + /** @private */ + handleKeyEvent(event) { + if (!this.instance || this.instance.isDestroyed()) { + return; + } + + try { + this.instance.webContents.send(event.type, event); + } catch (error) { + console.error(error); + } + } +} + +module.exports.BrowserWindowController = BrowserWindowController;