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 0000000..5696010 Binary files /dev/null and b/assets/dark/icon-active.png differ diff --git a/assets/dark/icon-active@2x.png b/assets/dark/icon-active@2x.png new file mode 100644 index 0000000..22ecfa2 Binary files /dev/null and b/assets/dark/icon-active@2x.png differ diff --git a/assets/dark/icon.png b/assets/dark/icon.png new file mode 100644 index 0000000..9307f9c Binary files /dev/null and b/assets/dark/icon.png differ diff --git a/assets/dark/icon@2x.png b/assets/dark/icon@2x.png new file mode 100644 index 0000000..9a1587d Binary files /dev/null and b/assets/dark/icon@2x.png differ diff --git a/assets/light/icon-active.png b/assets/light/icon-active.png new file mode 100644 index 0000000..d1cb4ee Binary files /dev/null and b/assets/light/icon-active.png differ diff --git a/assets/light/icon-active@2x.png b/assets/light/icon-active@2x.png new file mode 100644 index 0000000..faa44ce Binary files /dev/null and b/assets/light/icon-active@2x.png differ diff --git a/assets/light/icon.png b/assets/light/icon.png new file mode 100644 index 0000000..f50e3bd Binary files /dev/null and b/assets/light/icon.png differ diff --git a/assets/light/icon@2x.png b/assets/light/icon@2x.png new file mode 100644 index 0000000..49d5f12 Binary files /dev/null and b/assets/light/icon@2x.png differ diff --git a/client/constants.js b/client/constants.js new file mode 100644 index 0000000..9272fff --- /dev/null +++ b/client/constants.js @@ -0,0 +1,82 @@ +module.exports.humanizedKeyCodes = new Map([ + [41, '`'], + [2, '1'], + [3, '2'], + [4, '3'], + [5, '4'], + [6, '5'], + [7, '6'], + [8, '7'], + [9, '8'], + [10, '9'], + [11, '0'], + [12, '-'], + [13, '+'], + + [16, 'q'], + [17, 'w'], + [18, 'e'], + [19, 'r'], + [20, 't'], + [21, 'y'], + [22, 'u'], + [23, 'i'], + [24, 'o'], + [25, 'p'], + [26, '['], + [27, ']'], + [43, '\\'], + + [30, 'a'], + [31, 's'], + [32, 'd'], + [33, 'f'], + [34, 'g'], + [35, 'h'], + [36, 'j'], + [37, 'k'], + [38, 'l'], + [39, ';'], + [40, '\''], + + [44, 'z'], + [45, 'x'], + [46, 'c'], + [47, 'v'], + [48, 'b'], + [49, 'n'], + [50, 'm'], + [51, ','], + [52, '.'], + [53, '/'], + + [59, 'f1'], + [60, 'f2'], + [61, 'f3'], + [62, 'f4'], + [63, 'f5'], + [64, 'f6'], + [65, 'f7'], + [66, 'f8'], + [67, 'f9'], + [68, 'f10'], + [87, 'f11'], + [88, 'f12'], + + [1, 'esc'], + [14, '⌫'], // delete + [15, 'tab'], + [29, '⌃'], // ctrl + [56, '⌥'], // alt + [57, '␣'], // space + [42, '⇧'], // shift + [54, 'right⇧'], // rshift + [28, '⏎︎'], // enter + [3675, '⌘'], // cmd + [3676, 'right⌘'], // cmd + [3640, 'right⌥'], // ralt + [57416, '↑'], // up + [57424, '↓'], // down + [57419, '←'], // left + [57421, '→'], // right +]); diff --git a/client/index.css b/client/index.css new file mode 100644 index 0000000..54b894e --- /dev/null +++ b/client/index.css @@ -0,0 +1,47 @@ +body { + margin: 0; + width: 100vw; + height: 100vh; +} + +.keypress { + position: absolute; + left: 0; + bottom: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding: 2vmax 1vmax; + width: 100%; + text-align: center; + box-sizing: border-box; +} + +.keypress__item { + padding: 1vmax; +} + +.keypress__item_type_plus { + padding: 0; +} + +.keypress-item { + padding: 1vmax; + font-family: Arial, -apple-system, sans-serif; + font-weight: 700; + font-size: 4vmax; + line-height: 1; + text-transform: uppercase; + color: #fff; + border-radius: 0.5vmax; + background-color: rgba(0, 0, 0, 0.5); +} + +.keypress-item-plus { + font-family: Arial, -apple-system, sans-serif; + font-weight: 700; + font-size: 4vmax; + line-height: 1; + color: #fff; +} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..5922538 --- /dev/null +++ b/client/index.html @@ -0,0 +1,29 @@ + + + + + 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;