Skip to content

A guide to building your first menubar application with Electron.

License

Notifications You must be signed in to change notification settings

stevekinney/clipmaster-9000-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clipmaster 9000

This application was built for the Build Cross-Platform Desktop Apps with Electron Course for Frontend Masters.

Getting Started and Acclimated

To get started, clone this repository and install the dependencies using npm install.

We'll be working with four files for the duration of this tutorial:

  • lib/main.js, which will contain code for the main process
  • lib/renderer.js, which will code for the renderer process
  • lib/index.html, which will contain the HTML for the user interface
  • lib/style.css, which will contain the CSS to style the user interface

In a more robust application, you might break stuff into smaller files, but—for the sake of simplicity—we're not going to.

Hello Menubar

In this application, we're going to use Max Ogden's excellent menubar module. This module abstracts some of the OS-specific implementation details of building a application that lives in the menu bar (OS X) or system tray (Windows).

In main.js, we'll get things rolling by including Electron and menubar.

const electron = require('electron');
const Menubar = require('menubar');

The Menubar is a constructor. We'll create an instance to work with.

const menubar = Menubar();

In this case our menubar instance is very simular to app in Fire Sale. We'll wait for the application to be fire a ready event and then we'll log to the console.

menubar.on('ready', () => {
  console.log('Application is ready.');
});

Let's use npm start to verify that it works correctly. The library gives us a pleasant little cat icon as a default.

Cat icon in the menu bar on OS X

We also get a window correctly positioned above or below—depending on your operating system—the icon, which will load a blank page for starters. This is an instance of BrowserWindow as we saw before in Fire Sale.

A correctly placed window appears when we click on the cat

Loading Our HTML File

As we alluded to just a sentence or two ago, Menubar will create a BrowserWindow on our behalf. When it has done so, it will fire a after-create-window event. We can listen for this event and load our HTML page accordingly.

menubar.on('after-create-window', function () {
  menubar.window.loadURL(`file://${__dirname}/index.html`);
});

Implementing The Renderer Functionality

With menubar application up and running, it's time to shift our focus to the implementing the application's primary functionality.

When a user clicks the "Copy from Clipboard" button, we want to read from the clipboard and add that new clipping to the list.

We can make a few assumptions off the bat:

  1. We'll need access to Electron's clipboard module.
  2. We'll want a reference to the "Copy From Clipboard" button.
  3. We'll want a reference to the clippings list in order to add our clippings later on.

Let's implement all three in one swift motion:

const { clipboard } = require('electron');

const $clippingsList = $('.clippings-list');
const $copyFromClipboardButton = $('#copy-from-clipboard');

Building the element that will display our clipping can be tedious. In the interest of time and focus, I've provided a function that will take some text and return a jQuery-wrapped DOM node that's ready to be appended to the clippings list.

const createClippingElement = require('./support/create-clipping-element');

Spoiler alert: we'll eventually want to trigger reading from the clipboard by other means. So, let's keep break this functionality out into it's own function so that we can use it in multiple places.

addClippingToList = () => {
  const text = clipboard.readText();
  const $clipping = createClippingElement(text);
  $clippingsList.append($clipping);
}

Now, when a user clicks the "Copy from Clipboard" button, we'll read from the clipboard and add that clipping to the list.

$copyFromClipboardButton.on('click', addClippingToList);

If all went well, our renderer.js looks something like this:

const $ = require('jquery');
const { clipboard } = require('electron');

const $clippingsList = $('.clippings-list');
const $copyFromClipboardButton = $('#copy-from-clipboard');
const createClippingElement = require('./support/create-clipping-element');

addClippingToList = () => {
  const text = clipboard.readText();
  const $clipping = createClippingElement(text);
  $clippingsList.append($clipping);
}

$copyFromClipboardButton.on('click', addClippingToList);

Let's fire up our application and take it for a spin.

Wiring Up Our Actions

We have three buttons on each clipping element.

  1. "→ Clipboard" will write that clipping back to the clipboard.
  2. "Publish" will send it up to an API that we can share.
  3. "Remove" will remove it from the list.

We'll take advantage of event delegation, in order to avoid memory leaks. Disclaimer, we'll do this in the quickest—not necessarily the best—possible way in order to get back to focusing on Electron concepts.

Let's implement event listeners for all three. We'll use dummy functionality for "copy" and "publish".

$clippingsList.on('click', '.remove-clipping', (event) => {
  $(event.target).parents('.clippings-list-item').remove();
});

$clippingsList.on('click', '.copy-clipping', (event) => {
  const text = $(event.target).parents('.clippings-list-item').find('.clipping-text').text();
  console.log('COPY', text);
});

$clippingsList.on('click', '.publish-clipping', (event) => {
  const text = $(event.target).parents('.clippings-list-item').find('.clipping-text').text();
  console.log('PUBLISH', text);
});

Let's head back over to our application to verify that everything works. You can fire open developer tools using Command-Option-I or Control-Option-I for OS X and Windows respectively. I like to break them out to their own window.

Break out the developer tools

Writing Text to the Clipboard.

In the previous code we just wrote, we were just logging the clipping's contents to the console. Let's write it to the clipboard instead.

$clippingsList.on('click', '.copy-clipping', (event) => {
  const text = $(event.target).parents('.clippings-list-item').find('.clipping-text').text();
  clipboard.writeText(text);
});

Publishing to a Gist

Let's say we have a clipping that is super important. It's so important that we just want to share it with the world. Well, if it's that important than we'll probably want to get that publish button working.

In a normal browser environment, we couldn't just send a AJAX request to some remote server at a different domain. The browser's security features won't allow that. Instead, we'd have to have send the request to our own server (maybe it's written in Node) and have our server send the HTTP request to the remote server. This is where Electron's privileged status as a Node application shines.

We'll bring in the Request library.

const request = require('request');

We're going to be hitting the same endpoint no matter what. So, it makes sense to set some of the details as defaults.

const request = require('request').defaults({
  url: 'https://api.github.com/gists',
  headers: {
    'User-Agent': 'Clipmaster 9000'
  }
});

Now, one of the most dense pieces of code we're going to write today will be the HTTP request. We'll be using Github's Gist API. We'll need to set three important pieces of information:

  1. The URL of the Gist API
  2. A User-Agent (the Gist API requires this)
  3. A body with the text we'd like to use formatted in a particular way

Our data will look as follows:

{
  body: JSON.stringify({
    description: "Created with Clipmaster 9000",
    public: "true",
    files:{
      "clipping.txt": { content }
    }
  })
}

We'll send that information using request.post. Request takes a callback function that it will execute when it hears back from the server. The callback function will be handed three arguments: error, response, and body.

We'll start by using alerts to notify the user of the success or failure of our API request. We'll also write the URL of the new gist to the clipboard if it was successful.

$clippingsList.on('click', '.publish-clipping', () => {
  const content = $(event.target).parents('.clippings-list-item').find('.clipping-text').text();
  request.post({
    body: JSON.stringify({
      description: "Created with Clipmaster 9000",
      public: "true",
      files:{
        "clipping.txt": { content }
      }
    })
  },
  (err, response, body) => {
    if (err) { return alert(JSON.parse(err).message); }

    const gistUrl = JSON.parse(body).html_url;
    alert(gistUrl);
    clipboard.writeText(gistUrl);
  });
});

Using Notifications

A Note About Notifications: Notifications work out of the box on Windows 10 and OS X. In earlier versions of Windows, you'll have to take some additional steps. We're going to move forward assuming you're using either OS X or Windows 10, but you can totally check out this documentation for more details.

Here's a little snipped form the documentation demonstrating how to use notifications.

const myNotification = new Notification('Title', {
  body: 'Lorem Ipsum Dolor Sit Amet'
});

myNotification.onclick = () => console.log('Notification clicked');

Let's replace our alerts with notifications. We'll be modifying the callback in the Request callback from just a few minutes ago:

(err, response, body) => {
  if (err) {
    return new Notification('Error Publishing Your Clipping', {
      body: JSON.parse(err).message
    });
  }

  const gistUrl = JSON.parse(body).html_url;
  const notification = new Notification('Your Clipping Has Been Published', {
    body: `Click to open ${gistUrl} in your browser.`
  });

  notification.onclick = electron.shell.openExternal(gistUrl);

  clipboard.writeText(gistUrl);
})

Adding Global Shortcuts

Electron can register global shortcuts with the operating system. Let's take this for a spin in main.js.

We'll start by creating a reference to Electron globalShortcut module.

const { globalShortcut } = require('electron');

When the ready event is fired, we'll register our shortcut.

menubar.on('ready', function () {
  console.log('Application is ready.');

  const createClipping = globalShortcut.register('CommandOrControl+!', () => {
    console.log('This will eventually trigger creating a new clipping.');
  });

  if (!createClipping) { console.error('Registration failed', 'createClipping'); }
});

In our specific application, all of our clippings are managed by the renderer process. So, when the global shortcut is hit, we'll have to let the renderer process know.

Let's modify the event listener to send a message to the renderer process.

const createClipping = globalShortcut.register('CommandOrControl+!', () => {
  menubar.window.webContents.send('create-new-clipping');
});

In renderer.js, we'll listen for this message. First, we'll require the ipcRenderer module.

const ipc = electron.ipcRenderer;

We'll then listen for an event on the create-new-clipping channel.

ipc.on('create-new-clipping', (event) => {
  addClippingToList();
  new Notification('Clipping Added', {
    body: `${clipboard.readText()}`
  });
});

We won't do this now, because it's more of the same. But could add additional shortcuts to our application as well.

const copyClipping = globalShortcut.register('CmdOrCtrl+Alt+@', () => {
  menubar.window.webContents.send('clipping-to-clipboard');
});

const publishClipping = globalShortcut.register('CmdOrCtrl+Alt+#', () => {
  menubar.window.webContents.send('publish-clipping');
});

About

A guide to building your first menubar application with Electron.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages