Dmitry Petrov Back

Building electron app

All posts from the series can be found here

I used Radiant player for a long time and didn’t have any complaints till the moment most of Google Music interface stopped working well with the app. I checked bugtracker multiple times and no one raised similar issues. Feeling myself weak in Objective C land I didn’t have any desire to go and fix things myself and, as a consequence found myself opening GM app in the browser.

For some reason I really don’t like having apps openned in the background tabs. First of all it makes restarting browser a pain, second multimedia keys are not supported. So, after a while I cloned electron quickstart repository and started launching google music there and few days ago finally found the time to make a complete app - Perlotto. It’s called like this just because this word sounds amazing and I wanted to name something with it.

Overall setup took less time than I expected although I had solve few common issues, and solutions will be bellow.

Packaging

Somce it’s a real app it should be packaged as a real app, I have no desire to run npm start every morning to start my music player. The task is really simple with electron-builder, although it require a bit more effort to get app packaged automatically, since why would anyone want to package app manuall all the time, right?

Building part is explained in electron builder readme.

Here is publishing part (I care only about Mac OS X at the moment). Travis CI can be used to automate packaging and publish attefact on github releases page.

My .travis.yml

osx_image: xcode7.1
os:
- osx
language: node_js
node_js:
- 6.1.0
script: npm run dist
cache:
  directories:
    - node_modules
    - $HOME/.electron
    - "test/fixtures/app-executable-deps/app/node_modules"
deploy:
  provider: releases
  skip_cleanup: true
  file_glob: true
  file: dist/mac/Perlotto-*.dmg
  api_key: $GH_TOKEN
  on:
    tags: true
    repo: can3p/perlotto

Few things there:

I used travis cli tool to generate some parts of the file and stole others from the internet, however it’s absolutely possible to do everything without the tool via web interface.

So, this is how release process looks now:

Closing the window

Default behaviour of electron quick start app is to exit application when user clicks close button. I’m not aware of any serious player that behaves like this hence this misbehaviour has to be fixed.

First thought is of course to prevent this on window close event, but the result is that it’s not possible to close app in any way other process manager.

So, trick there is to raise flag on app ‘before-quit’ event and check it on window ‘close’ event:

let forceQuiteApp;

function createWindow () {
    // Create the browser window.
    mainWindow = new BrowserWindow({width: 800, height: 600})
    mainWindow.maximize();

    // and load the index.html of the app.
    mainWindow.loadURL('file://' + __dirname + '/index.html');

    mainWindow.on('close', function(e){
        if (!forceQuiteApp) {
            e.preventDefault();
            mainWindow.hide();
        }
    });

    })
}

app.on('ready', createWindow)
app.on('before-quit', () => forceQuiteApp = true);

I run maximize method right after the start because I don’t want to worry about saving window dimensions anywhere

Media keys

This one is simple, the only moment is that keys should be bound with globalShortcut object available from electron module on app thread and actual dom manipulation happens in content thread. Here is relevant app code part:

function createWindow () {
    // ... init part ...

    globalShortcut.register('MediaPlayPause', function(){
        mainWindow.webContents.send('play-control', 'play-pause');
    }) || console.log('MediaPlayPause binding failed');

    globalShortcut.register('MediaPreviousTrack', function(){
        mainWindow.webContents.send('play-control', 'rewind');
    }) || console.log('MediaPreviousTrack binding failed');

    globalShortcut.register('MediaNextTrack', function(){
        mainWindow.webContents.send('play-control', 'forward');
    }) || console.log('MediaNextTrack binding failed');
}

We use event api to transfer knowledge from app to thread, here is content part:

var ipc = require('electron').ipcRenderer;

ipc.on('play-control', function(event, command){
  var webView = document.querySelector('webview#gpm-player');
  webView.executeJavaScript("document.querySelector('#player-bar-" + command + "').click()");
});

Since script has to be embedded into content, local file is needed, hence here is index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Perlotto</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
    </style>
  </head>
  <body style="overflow: hidden">
    <webview id="gpm-player" src="https://play.google.com/music/" style="height:100%;width:100%;position:absolute;"></webview>
    <script src="./content.js"></script>
  </body>
</html>

Margins had to be reset, otherwise webview is rendered with some margins.

Easy.

Conclusion

It’s funny, but this post contains 99% of project’s code. I treat project feature complete since I don’t care about notifications, tray icons and any other garbage that is useful only for distractions.

Electron ecosystem is surprisingly feature complete and stable for me, thanks guys!