[help] how to open SPA(ember app) route in electron

Hi, i am trying to do music player based on electron. When mainwindow loads, system tray injected on taskbar. Then user toggle miniplayer it should redirect to my ember app’s “player” route. Currently i got two separate window and other way i got is resetting mainwindow size but without redirection

Hey @Zorig, one window is probably ideal because with two windows you have two whole separate application instances running with all their own concerns and window behavior and coordinating them is a pain. Our app gives the user the ability to open multiple (theoretically unlimited) windows and it’s kind of a mess. So from what you described I’d recommend the latter scenario where you do a redirect.

Essentially you’ll need to set up an IPC messaging system so you can send messages back and forth from the mainWindow (a BrowserWindow object) to the Ember application. Your ember app will use IPC renderer module and your main thread code will use IPC main module.

First of all if you’re using ember-electron it will do some of the work for you, if not you’ll need to figure out how you want to handle the require collision. Basically I’d recommend using ember-electron for that and other reasons. In your Ember app you’ll need to require the ipcRenderer package from electron (via requireNode, not regular), something like this:

let { ipcRenderer } = requireNode('electron');

In our case we did all the IPC stuff in a service because we use our app both in and out of electron. If your case is simpler, which it sounds like, you may just want to do this in the application route/controller. If you’d like I can share a rough sketch of the service that we use which is mostly courtesy of @bendemboski on slack, but that may be overkill for you. Anyway, next you want to set up a listener to listen for messages on the ipcRenderer module, something like:

ipcRenderer.on("toggle-mini-player", (name, <args>) => {
  // handle the 'toggle-mini-player' event here, with the args you pass from ipcMain, if any
});

Next you’ll write the code to send a message to the ember app. Actually you don’t even need ipc main for this, you just have to say mainWindow.send('toggle-mini-player', <args>) to send a message to that window’s renderer process.

Anyway, sorry if that’s a little convoluted. I found the ipc stuff confusing as first but once you get it set up it’s awesome.

@dknutsen thank you for explanation. Also i really appreciate that rough sketch, i was thinking about doing that with ipcRenderer but making it in service and using private service(-routing) in that case is not option for me, so i did it on application controller but no difference. So maybe that sketch would be a huge help

One alternative to using the private routing service is just setting window.location.hash or window.location.href. That’s actually what we’re doing. It’s not great, kind of a hack, but you could consider that as an alternative to using the private routing service so you can still do some basic redirects from the service.

Anyway, here’s the service we use sans some of the app specific code. We just call it services/electron.js:

/* global requireNode */
import Ember from 'ember';

const {
  Evented,
  Service,
  run
} = Ember;


//
// Much of this courtesy of bendemboski, he may make an addon for this in the future
// so we should keep an eye out
//

/* EXAMPLE USAGE

electron: inject.service('electron'),

didInsertElement() {
  this.get('electron').on('my-ipc-event', this, this.onWhatever);
},

willClearRender() {
  this.get('electron').off('my-ipc-event', this, this.onWhatever);
},

onWhatever(arg1, args) {
  console.log("I'm IPCing!!!")
}

*/


export default Service.extend(Evented, {
  eventListeners: null,

  init() {
    let self = this;
    this._super(...arguments);

    let ipc;
    try {
      ipc = this._ipc();
    } catch (e) {
      // fall through
    }

    this.set('isElectron', Boolean(ipc));
    this.eventListeners = {};

    if(ipc) {
      // this is where we set up some of the Ember-side IPC listeners
      self.on('done-authenticating', (redirectHash) => {
        self.set('isAuthenticating', false);
        window.location.hash = redirectHash;
      });
      self.sendEvent('ember-ready', true);
    }
  },


  /*== public interface ==*/

  // Send an event to the IPC main process
  sendEvent(event, ...args) {
    if(!this.get('isElectron')) {
      return;
    }
    this._ipc().send(event, ...args);
  },

  /*== end public interface ==*/






  /*== event listener methods (Ember.Evented overrides) ==*/

  on(name, target, method) {
    if (this.get('isElectron')) {
      this._ensureIpcListener(name);
    }
    return this._super(...arguments);
  },

  one(name, target, method) {
    if (this.get('isElectron')) {
      this._ensureIpcListener(name);
    }
    return this._super(...arguments);
  },

  off(name, target, method) {
    let ret = this._super(...arguments);

    if (this.get('isElectron')) {
      if (!this.has(name)) {
        // Last listener for this event
        this._ipc().removeListener(name, this.eventListeners[name]);
        delete this.eventListeners[name];
      }
    }
    return ret;
  },

  trigger(name) {
    let ret = this._super(...arguments);
    if (this.get('isElectron')) {
      if (!this.has(name)) {
        // The last listener for this event must have been a .one() listener and
        // just been unregistered.
        this._ipc().removeListener(name, this.eventListeners[name]);
        delete this.eventListeners[name];
      }
    }
    return ret;
  },

  /*== end ember.evented overrides ==*/




  /*== private helpers ==*/

  // Private helper to set up an ipc listener for the given event if we don't already have one.
  _ensureIpcListener(name) {
    if (!this.eventListeners[name]) {
      // First listener for this event, so set up the event listener, but ignore
      // the first event argument and instead include the event name to better
      // match Ember.Evented semantics.
      let handler = run.bind(this, (event, ...args) => this.trigger(name, ...args));
      this.eventListeners[name] = handler;
      this._ipc().on(name, this.eventListeners[name]);
    }
  },

  // Private helper to tear down an ipc listener for the given event
  _removeIpcListener(name) {
    this._ipc().removeListener(name, this.eventListeners[name]);
    delete this.eventListeners[name];
  },

  // Helper to call an EventEmitter method on the ipc object
  _ipcEventReg(methodName, eventName, target, method) {
    let fn;
    if (method) {
      fn = method.bind(target);
    } else {
      fn = target;
    }

    this._ipc()[methodName](eventName, fn);
    return [ target, method, fn ];
  },

  _ipc() {
    let { ipcRenderer } = requireNode('electron');
    return ipcRenderer;
  }

  /*== end private helpers ==*/

});


1 Like

wow, this is much more than sketch, thank you so much