$30 off During Our Annual Pro Sale. View Details »

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

Radoslav Stankov

June 02, 2020
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

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

    View Slide

  2. Radoslav Stankov
    @rstankov
    blog.rstankov.com

    twitter.com/rstankov

    github.com/rstankov

    speakerdeck.com/rstankov

    View Slide

  3. View Slide

  4. https://speakerdeck.com/rstankov

    View Slide

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

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. (main) (render)

    View Slide

  14. (main)

    View Slide

  15. (main) (render)
    rpc

    View Slide

  16. (main) (render)
    rpc

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. Focused Task
    https://github.com/RStankov/FocusedTask

    View Slide

  25. View Slide

  26. View Slide

  27. ! Tech Stack

    View Slide

  28. ! Tech Stack

    View Slide

  29. View Slide

  30. View Slide

  31. 3 package.json! "

    View Slide

  32. View Slide

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

    View Slide

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

    View Slide

  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', () => {
    // ...
    });

    View Slide

  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', () => {
    // ...
    });

    View Slide

  37. create-react-app

    View Slide

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

    View Slide

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

    View Slide

  40. View Slide

  41. Access Electron via React

    View Slide

  42. Access Electron via React

    View Slide

  43. Access Electron via React

    View Slide

  44. Access Electron via React

    View Slide

  45. Open external links

    View Slide

  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);
    }
    });
    });

    View Slide

  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);
    }
    }

    View Slide

  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 (
    target="_blank"
    rel="noopener noreferrer"
    onClick={e => {
    e.preventDefault();
    openURI(props.href);
    }}
    {...props}
    />
    );
    }

    View Slide

  49. Resize window to fit content

    View Slide

  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);
    }

    View Slide

  51. Resize window to fit content
    electron.ipcMain.on('resize', function(_e, width, height) {
    mb.window.setSize(width, height);
    });

    View Slide

  52. Store window position

    View Slide

  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);
    },
    };

    View Slide

  54. Store window position
    mb.on('after-create-window', () => {
    settings.trackWindowBounds(mb);
    });
    mb.app.on('ready', () => {
    settings.setWindowBounds(mb);
    });

    View Slide

  55. Global shortcut

    View Slide

  56. View Slide

  57. View Slide

  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);
    }

    View Slide

  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);
    });

    View Slide

  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);
    },
    };

    View Slide

  61. Changelog

    View Slide

  62. View Slide

  63. View Slide

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







    >
    );
    }

    View Slide

  65. Distribution

    View Slide

  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(() => {

    View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. ...payment was rejected two times $

    View Slide

  71. %

    View Slide

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

    View Slide

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

    View Slide

  74. View Slide