Developing desktop apps with React & Electron

Developing desktop apps with React & Electron

Code walkthrough to a desktop tool, I wrote for my self called Focused Task. It is built with React, Redux, and Electron.

App can be found at 👉 https://github.com/RStankov/FocusedTask
Video can be found at 👉 https://www.youtube.com/watch?v=nGgaak8MEmg

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

June 02, 2020
Tweet

Transcript

  1. Developing desktop apps with React & Electron Radoslav Stankov 04/06/2020

  2. Radoslav Stankov @rstankov blog.rstankov.com
 twitter.com/rstankov
 github.com/rstankov
 speakerdeck.com/rstankov

  3. None
  4. https://speakerdeck.com/rstankov

  5. https://github.com/RStankov/FocusedTask

  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. (main) (render)

  14. (main)

  15. (main) (render) rpc

  16. (main) (render) rpc

  17. const electron = window.require('electron') function closeApp() { electron.remote.getCurrentWindow().close(); } (render)

  18. const electron = window.require('electron') electron.ipcRenderer.send('event', arg1, arg2); (render)

  19. const electron = require('electron'); electron.ipcMain.on('event', function(_e, arg1, arg) { //

    ... }); (main)
  20. None
  21. None
  22. None
  23. None
  24. Focused Task https://github.com/RStankov/FocusedTask

  25. None
  26. None
  27. ! Tech Stack

  28. ! Tech Stack

  29. None
  30. None
  31. 3 package.json! "

  32. None
  33. shell/package.json app/package.json ./package.json distribution (shell) (app)

  34. https://github.com/maxogden/menubar

  35. const { menubar } = require('menubar'); const electron = require('electron');

    const mb = menubar({ index: process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, './build/index.html'), protocol: 'file:', slashes: true, }), icon: path.join(__dirname, 'assets/MenuBarIconTemplate.png'), browserWindow: { width: 500, height: 600, minWidth: 300, maxHeight: 900, minHeight: 600, webPreferences: { nodeIntegration: true, scrollBounce: true, }, }, }); mb.on('after-create-window', () => { // ... });
  36. const { menubar } = require('menubar'); const electron = require('electron');

    const mb = menubar({ index: process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, './build/index.html'), protocol: 'file:', slashes: true, }), icon: path.join(__dirname, 'assets/MenuBarIconTemplate.png'), browserWindow: { width: 500, height: 600, minWidth: 300, maxHeight: 900, minHeight: 600, webPreferences: { nodeIntegration: true, scrollBounce: true, }, }, }); mb.on('after-create-window', () => { // ... });
  37. create-react-app

  38. Development ./bin/react-start ELECTRON_START_URL=http://localhost:3000 ./bin/electron-start

  39. Distribution ./bin/react-build ./bin/electron-package FocusedTask.app

  40. None
  41. Access Electron via React

  42. Access Electron via React

  43. Access Electron via React

  44. Access Electron via React

  45. Open external links

  46. Open external links mb.app.on('web-contents-created', (e, contents) => { const openExternal

    = (e, url) => { e.preventDefault(); electron.shell.openExternal(url); }; contents.on('new-window', openExternal); contents.on('will-navigate', (e, url) => { if (url !== contents.getURL()) { openExternal(e, url); } }); });
  47. Open external links export function openURI(uri: string) { if (!isElectron)

    { return; } if (!isURI(uri)) { return; } if (isFilePathUri(uri)) { electron.remote.shell.openItem(uri); } else { electron.remote.shell.openExternal(uri); } }
  48. Open external links import * as React from 'react'; import

    { openURI } from 'utils/electron'; interface IProps { children: React.ReactNode; href: string; className?: string; } export default function ExternalLink(props: IProps) { return ( <a target="_blank" rel="noopener noreferrer" onClick={e => { e.preventDefault(); openURI(props.href); }} {...props} /> ); }
  49. Resize window to fit content

  50. Resize window to fit content export function resizeBasedOnContent() { if

    (!isElectron) { return; } const bodyStyle = window.getComputedStyle(document.body) as any; const padding = parseInt(bodyStyle['margin-top'], 10) + parseInt(bodyStyle['margin-bottom'], 10) + parseInt(bodyStyle['padding-top'], 10) + parseInt(bodyStyle['padding-bottom'], 10); const observer = new ResizeObserver(() => { const height = Math.min(900, document.body.offsetHeight + padding); const bounds = electron.remote .getCurrentWindow() .webContents.getOwnerBrowserWindow() .getBounds(); if (bounds.height === height) { return; } electron.ipcRenderer.send('resize', bounds.width, height); }); observer.observe(document.body); }
  51. Resize window to fit content electron.ipcMain.on('resize', function(_e, width, height) {

    mb.window.setSize(width, height); });
  52. Store window position

  53. Store window position const settings = require('electron-settings'); const electron =

    require('electron'); const _ = require('lodash'); const BOUNDS_KEY = 'windowBounds'; module.exports = { setWindowBounds(menubar) { menubar.setOption('browserWindow', { ...menubar.getOption('browserWindow'), ...(settings.get(BOUNDS_KEY) || {}), }); }, trackWindowBounds(menubar) { const win = menubar.window; const handler = _.debounce(() => { menubar.setOption('browserWindow', { ...menubar.getOption('browserWindow'), ...win.getBounds(), }); settings.set(BOUNDS_KEY, win.getBounds()); }, 500); win.on('resize', handler); win.on('move', handler); }, };
  54. Store window position mb.on('after-create-window', () => { settings.trackWindowBounds(mb); }); mb.app.on('ready',

    () => { settings.setWindowBounds(mb); });
  55. Global shortcut

  56. None
  57. None
  58. Global shortcut export function getGlobalShortcutKey() { if (!isElectron) { return;

    } return electron.remote.getGlobal('globalShortcutKey'); } export function updateGlobalShortcutKey(key: string) { if (!isElectron) { return; } if (key.length !== 1) { return; } electron.ipcRenderer.send('updateGlobalShortcutKey', key); }
  59. Global shortcut mb.app.on('ready', () => { settings.setGlobalShortcut(mb); }); mb.app.on('will-quit', ()

    => { electron.globalShortcut.unregisterAll(); }); electron.ipcMain.on('updateGlobalShortcutKey', (_e, key) => { settings.updateGlobalShortcutKey(mb, key); });
  60. Global shortcut const settings = require('electron-settings'); const electron = require('electron');

    const _ = require('lodash'); module.exports = { setGlobalShortcut(menubar) { const key = settings.get(SHORTCUT_KEY) || "'"; global.globalShortcutKey = key; electron.globalShortcut.register(`CommandOrControl+${key}`, () => { if (menubar.window && menubar.window.isVisible()) { menubar.hideWindow(); } else { menubar.showWindow(); } }); }, updateGlobalShortcutKey(menubar, key) { settings.set(SHORTCUT_KEY, key); electron.globalShortcut.unregisterAll(); this.setGlobalShortcut(menubar); }, };
  61. Changelog

  62. None
  63. None
  64. Changelog import React from 'react'; import Stack from 'components/Stack'; import

    BackButton from 'components/BackButton'; import Title from 'components/Title'; import ReactMarkdown from 'react-markdown'; import styles from './styles.module.css'; import raw from 'raw.macro'; const markdown = raw('../../../../CHANGELOG.md').replace(/^# .*\n\n/, ''); export default function Shortcuts() { return ( <> <BackButton /> <Stack.Column> <Title emoji="#" title="Changelog" /> <div className={styles.markdown}> <ReactMarkdown source={markdown} escapeHtml={true} /> </div> </Stack.Column> </> ); }
  65. Distribution

  66. const packager = require('electron-packager'); const setLanguages = require('electron-packager-languages'); const createDMG

    = require('electron-installer-dmg'); const version = '0.1.0'; const distPath = './dist'; const name = 'FocusedTask'; packager({ dir: './shell', overwrite: true, out: distPath, afterCopy: [setLanguages(['en', 'en_GB'])], name, productName: 'Focused Task', appBundleId: 'com.rstankov.focused-task', appCopyright: 'Copyright (C) 2020 Radoslav Stankov', appVersion: version, appCategoryType: 'public.app-category.productivity', // osxSign: { // identity: '[?]', // 'hardened-runtime': true, // entitlements: 'entitlements.plist', // 'entitlements-inherit': 'entitlements.plist', // 'signature-flags': 'library', // }, // osxNotarize: { // appleId: '[?]', // appleIdPassword: '[?]', // }, }).then(() => {
  67. None
  68. None
  69. None
  70. ...payment was rejected two times $

  71. %

  72. ...third time's a charm&

  73. Thanks ' https://speakerdeck.com/rstankov https://github.com/RStankov/FocusedTask (

  74. None