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. 3.
  2. 6.
  3. 7.
  4. 8.
  5. 9.
  6. 10.
  7. 11.
  8. 12.
  9. 14.
  10. 20.
  11. 21.
  12. 22.
  13. 23.
  14. 25.
  15. 26.
  16. 29.
  17. 30.
  18. 32.
  19. 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', () => { // ... });
  20. 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', () => { // ... });
  21. 40.
  22. 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); } }); });
  23. 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); } }
  24. 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} /> ); }
  25. 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); }
  26. 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); }, };
  27. 56.
  28. 57.
  29. 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); }
  30. 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); });
  31. 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); }, };
  32. 61.
  33. 62.
  34. 63.
  35. 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> </> ); }
  36. 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(() => {
  37. 67.
  38. 68.
  39. 69.
  40. 71.

    %

  41. 74.