Thursday, August 15, 2013

The current state of plaforms: research and testing

Candidate #1: Chrome

Once I knew I was going for some type of raw tcp socket support an extensive google search was the next step. The first thing I stumbled upon was Chrome's packaged app Network Communications section. Naturally I dropped the research stage and went straight to hacking.
Packaged apps deliver an experience as capable as a native app, but as safe as a web page. Just like web apps, packaged apps are written in HTML5, JavaScript, and CSS. But packaged apps look and behave like native apps, and they have native-like capabilities that are much more powerful than those available to web apps.

Getting started

I already knew from some work on my chrome plugin that developing extensions for Chrome is as easy as it gets. Throw together a manifest.json that has the correct permissions, and you're allowed to use the API.

{
  "manifest_version": 2,
  "version": "0.1",
  "name": "SchizIRC Chrome",
  "app": {
    "background": {
      "scripts": ["main.js"]
    }
  },
  "permissions": [{
    "socket": [
      "tcp-connect:*:6667",
      "tcp-listen:*:6667"
      ]
    }]
}

Main.js contains nothing fancy, It just listens for the onLaunched event and creates the main window with some predefined bounds.

chrome.app.runtime.onLaunched.addListener(function() {

  chrome.app.window.create('index-chrome.html', {
    bounds: {
      width: 980,
      height: 650
    }
  }, function(createdWindow) {
    appWindow = createdWindow.dom;

  });

});

Loading a chrome app in developer mode is as easy as it gets as well:
  • Visit chrome://extensions in your browser
  • Ensure that the Developer mode checkbox in the top right-hand corner is checked.
  • Click Load unpacked extension… to pop up a file-selection dialog.
  • Find the directory where you cloned SchizIRC into and select it.
  • Press 'Launch'
Note that regular CTRL+R, F5 or even window.location.reload() won't work, when developing a packaged app, but the main app window has a 'reload app' menu item in the contextmenu that does this. You will also be able to use the full Webkit Inspector from this menu.
I threw together a basic layout using Divshot; Some tabs, a title, an input field and a button. This was enough to get me started on where I wanted to go. My app also got a working title: SchizIRC (because why not)


divshot layouting
Divshot

After taking all the assets offline (required for a chrome app, and needed to be done anyway for future packaging) I ended up with this directory structure:

manifest.json
index-chrome.html
main.js
index.html
css\bootstrap-combined.min.css
css\google-bootstrap.css
css\schizirc.css
js\Irc.js
js\controller.js
js\gui.js
js\vendor\mootools-more.js
js\vendor\mootools-core.js
js\vendor\mustache.js
js\vendor\bootstrap.min.js

Time to get classy

As shown above, I threw down some classes: Irc.js, Controller.js (which will server as the main kickstarter for the app, already eyeballing we're going to have several app wrappers / frameworks here that need a common entry point) and Gui.js, which will bind DOM events to my app's events. Code says more than words:
/** 
 * Controller.js : Generic entry point for the app.
 */
Controller = new Class({
 gui: false,
 server: false,
 config: { // some default config options i've prepared, these will be overwritten by the config in the future
  server: 'irc.freenode.net',
  port: '6667',
  defaultChannels: '#schizirc',
  nick: 'SchizoIRC',
  userName: 'SchizoIRC',
  realName: 'SchizoIRC v0.01',
  appVersion: 'SchizoIRC v0.01',
  finger: 'omnomnom',
  password: null, 
  logging: true
 },

 initialize: function() {
  console.log("IRC Client initted." );
  this.irc = new IRC(this.config);
  this.gui = new Gui();
  this.irc.connect();
 }
});

window.onload = function() {
 window.app = new Controller();
};
User interface bindings are handled by the User Interface Class (which needs some refactoring for the future but will do for now)
/** 
 * Gui.js implements DOM bindings to change the user interface
 * Will delegate user interaction to the app components that need it and implement the base tab view
 */
Gui = new Class({
 Implements: [Events], // allow this class to fire and receive events.

 initialize: function() {
  window.addEvent('mousedown:relay(.nav-tabs li)', this.switchTabs); // handle all clicks on tab labels
  window.addEvent('/tab/changed', this.showActiveTab); // a custom event channel fired when a tab has changed
  window.addEvent('/tab/change', this.switchTab); // a custom event channel that changes a tab to $name
  this.showActiveTab('home'); // always show the home tab.
 },

 switchTabs: function() {
  $$(".nav-tabs li").removeClass('active'); // deactivate all tab labels.
  $(this).addClass('active'); // activate the tab label that was clicked
  window.fireEvent('/tab/changed', this.getAttribute('data-tab')); // fire our tab changed event.
 },

 switchTab: function(newTab) {
  console.log("Change tab to: ", newTab, $$("li[data-tab="+newTab+"]")); 
  $$(".nav-tabs li").removeClass('active'); // hide all tab labels.
  $$("li[data-tab="+newTab+"]").addClass('active'); // show our new tab
  window.fireEvent('/tab/changed', newTab); // notify that the active tab has changed.
 },

 showActiveTab: function(name) {
  $$('section[data-tab]').hide(); // hide all tab content panels
  $$('section[data-tab='+name+']').show(); // show the one passed as name
  $$('section[data-tab='+name+'] input[type=text]')[0].focus(); // focus the text field in the tab
 }
});

Now on to the real deal, the IRC class. This will hold the connection object, the info about the server, channels, etc.

IRC = new Class({
 Implements: [Options, Events], 

 options: { // set some default IRC options that will be overwritten with options passed from class Controller
  server: 'irc.freenode.net',
  port: '6667',
  defaultChannels: '#schizirc',
  nick: 'SchizoIRC',
  userName: 'SchizoIRC',
  realName: 'SchizoIRC v0.01',
  appVersion: 'SchizoIRC v0.01',
  finger: 'omnomnom',
   autoJoinOnInvite: true
  },

  connection: false,
  server: false,
  channels: false,

  initialize: function(options) {
   this.setOptions(options, this.options); // merge the passed options with this.options.
  if(!this.connection) {                  // create a new IRC Connection object that gets the current options.
    this.connection = new IRC.Connection(this.options);
   }
  },

  connect: function() {
   this.connection.connect();
  },

  disconnect: function() {
   this.connection.disconnect();
  }

 });

No comments: