Thursday, 28 May, 2015 UTC


Summary

Imagine that you are at the airport, waiting for your flight and you hear a couple of guys sitting behind talking about Nodejs, Angularjs, MongoDB and you being a full stack developer and blogger start eves dropping. And all of sudden one guy mentions.. “Dude, you should totally check out this awesome blog.. It is titled ‘The Jackal of Javascript’ & that guy writes sweet stuff about node webkit, ionic framework and full stack application development with Javascript”. And the other guy says “cool..” And with his airport wifi connection starts browsing the blog on his laptop.. With in a few moments he loses interest because the pages aren’t loading.
What if the owner of the wordpress blog had an offline viewer and the first guy showed all the awesome stuff in the blog to the other guy on his laptop without any internet connection, wouldn’t that be cool?
So that is what we are going to do in this post. Build an offline viewer for a wordpress blog.
Below is a quick video introduction to the application we are going to build. 
 
Sweet right? 
So, let us stop imagining and let us build the offline viewer.
You can find the completed code here.
Architecture
As mentioned in the video, this is a POC for creating an offline viewer for a wordpress blog. We are using Electron (a.k.a Atom shell) to build the offline viewer. We will be using Angularjs in the form of Angular Material project to build the user interface for the application.
As shown above, We will be using the request node module to make HTTP request to the wordpress API to download the blog posts in JSON format and persist it locally using DiskDB.
Once all the posts are downloaded, we will sandbox the content. The word sandbox here refers to containment of user interaction & experience inside the viewer. As in how we are capturing the content and replaying it back to the user. In our case, we are sandboxing the images and links, to control the user experience when the user is online or offline. This is better illustrated in the video above.
The JSON response from the WordPress API would look like : https://public-api.wordpress.com/rest/v1.1/sites/thejackalofjavascript.com/posts?page=1.
If you replace my blog domain name from the above URL with your WordPress blog, you should see the posts from your blog. We will be working with content property of the post object.
Note : We will be using recursions at a lot of places to process collections, asynchronously. I am still not sure if this is an ideal design pattern for an application like this.
Prerequisites
The offline viewer is a bit complicated in terms of number of components used. Before you proceed, I recommend taking a look at the following posts
  • Electron : Quick Start
  • Nodejs : Hello Node
  • Sockets : Websockets & Socket.io
  • Disk DB : Building a Todo App with DiskDB
  • Angularjs : Angularjs hands on tutorial
  • Angular Material : Using AngularJS Material Design
  • WordPress REST API : WordPress REST API Docs
Getting started
We will be using a yeoman generator named generator-electron to scaffold our base project, and then we will add the remaining components as we go along.
Create a new folder named offline_viewer and open terminal/prompt there.
To setup the generator, run the following
npm install yo grunt-cli bower generator-electron
This may take a few minutes to download all the once modules. Once this is done, we will scaffold a new app inside the offline_viewer folder. Run
yo electron
You can fill in the questions after executing the above command as applicable. Once the project is scaffolded and dependencies are installed, we will add a few more modules applicable to the offline viewer. Run,
npm install cheerio diskdb request socket.io --save
  • cheerio is to manage DOM manipulation in Nodejs. This module will help us sandboxing the content.
  • diskdb for data persistence.
  • request module for contacting the wordpress blog and get the posts as well as to sandbox images.
  • socket.io for realtime communication between the locally persisted data and the UI.
For generating installer for mac & some folder maintenance, we will add the below two modules.
npm install trash appdmg --save-dev
The final package.json should look like
{
    "name": "wordpress-offline-viewer",
    "productName": "WordpressOfflineViewer",
    "version": "0.1.0",
    "description": "Electron, WordPress & Angular Material – An Offline Viewer",
    "license": "MIT",
    "main": "index.js",
    "repository": "https://github.com/arvindr21/wordpress-offline-viewer",
    "author": {
        "name": "Arvind Ravulavaru",
        "email": "[email protected]",
        "url": "thejackalofjavascript.com"
    },
    "engines": {
        "node": ">=0.10.0"
    },
    "scripts": {
        "start": "electron .",
        "clean" : "trash --force build",
        "clean-mac": "trash --force build/mac release",
        "clean-linux": "trash --force build/linux",
        "clean-win": "trash --force build/win",
        "build-mac": "npm run clean-mac && electron-packager . 'The Jackal of Javascript' --platform=darwin --arch=x64 --version=0.25.2 --icon ./resources/icon.icns --out ./build/mac --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git",
        "build-linux": "npm run clean-linux && electron-packager . 'The Jackal of Javascript' --platform=linux --arch=x64 --version=0.25.2 --icon ./resources/icon.icns --out ./build/linux --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git",
        "build-win": "npm run clean-win && electron-packager . 'The Jackal of Javascript' --platform=win32 --arch=ia32 --version=0.25.2 --out ./build/win --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git",
        "build" : "npm run clean && npm run build-win && npm run build-linux && npm run build-mac",
        "release-mac": "npm run clean-mac && npm run build-mac && mkdir release && cd release && appdmg ../app-dmg.json 'The Javascript of Javascript.dmg'"
    },
    "keywords": [
        "electron-app",
        "offline",
        "viewer"
    ],
    "devDependencies": {
        "appdmg": "^0.3.1",
        "electron-packager": "^4.1.1",
        "electron-prebuilt": "^0.25.2",
        "trash": "^1.4.1"
    },
    "dependencies": {
        "cheerio": "^0.19.0",
        "diskdb": "^0.1.14",
        "request": "^2.55.0",
        "socket.io": "^1.3.5"
    }
}
Do notice that I have added bunch of scripts and updated the meta information of the project. We will be using NPM itself as a task runner to run, build and release the app.
To make sure everything is working fine, run
npm run start
And you should see
Build the Socket Server
Now, we will create the server interface for our offline viewer, that talks to the WordPress JSON API.
Create a new folder named app at the root of the project. Inside the app folder, create two folders named serverclient. This folders kind of visually demarcate the server code vs. the client code. Here the server being Nodejs & client being Angular application. Inside the Electron shell, any code can be accessed any where, but to keep the code base clean and manageable, we will be maintaining the above folder structure.
Inside the app/server create a file named server.js. This file is responsible for setting up the socket server. Open server.js and update it as below
var getport = require('./getport');
var fetcher = require('./fetcher');
var searcher = require('./searcher');

module.exports = function(callback) {
	// get an unused port!
	getport(function(port) {

		var io = require('socket.io')(port);

		io.sockets.on('connection', function(socket) {

			socket.on('load', function(isOnline) {
				fetcher(isOnline, function(posts) {
					socket.emit('loaded', posts);
				});
			});

			socket.on('search', function(q) {
				searcher(q, function(results) {
					socket.emit('results', results);
				});
			});

		});

		callback(port);
	});
}
Things to notice
Line 1: We require the getport module. This module takes care of looking for an available port on the user’s machine that we can use to start the socket server. We cannot pick 8080 or 3000 or any other static port and assume that the selected port would be available on the client. We will work on this file in a moment.
Line 2 : We require the fetcher module. This module is responsible for fetching the content from WordPress REST API, save the data to DiskDB and send the response back.
Line 3 : We require the searcher module. This module is responsible for searching the locally persisted JSON data, as part of the search feature.
Line 7 : As soon as we get a valid port from getport module, we will start a new socket server.
Line 11 : Once a client connects to the server, we will setup listeners for load event and search event.
Line 13 : This event will be fired when the client wants to get the posts from the server. Once the data arrives, we emits a loaded event with the posts
Line 19 : This event will be fired when the client send a query to be searched. Once the results arrive, we emit the results event with the found posts.
Line 27 : Once the socket server is setup, we will execute the callback and send the used port number back.
Next, create a new file named getport.js inside the app/server folder. This file will have the code to return an unused port. Update getport.js as below
// https://gist.github.com/mikeal/1840641
var net = require('net');
var portrange = 45032

function getPort(cb) {
	var port = portrange
	portrange += 1

	var server = net.createServer();
	server.listen(port, function(err) {
		server.once('close', function() {
			cb(port);
		});
		server.close();
	});

	server.on('error', function(err) {
		getPort(cb);
	});
}

module.exports = getPort;
Things to notice
Line 2 : Require the net module, to start a new server
Line 3 : Starting value of the port, from which we need to start checking for a available port
Line 5 : A recursive function that keeps running till it finds a free port.
Line 10 : We attempt to start a server on the port specified, if we are successful, we call the callback function with the port that worked else, if the server errors out, we call the
getPort()
  again. And this time, we increment the port by one & then try starting server. This goes on till the server creation succeeds.
Next, create a file named fetcher.js inside app/server folder. This file will consist of all the business logic for our application. Update app/server/fetcher.js as below
var request = require('request');
var sandboxer = require('./sandboxer');
var imageSandBoxer = require('./imageSandBoxer');
var db = require('./connection');

var baseUrl = 'https://public-api.wordpress.com/rest/v1.1/sites/thejackalofjavascript.com/posts',
    page = 1,
    perPage = 20,
    length = 0,
    total, inProgress = false;

module.exports = function fetcher(isOnline, callback, skip) {
    var posts = db.posts.find();
    var total = db.meta.findOne({
        'key': 'found'
    });

    if (!total) {
        total = {
            'key': 'found',
            'total': -1
        }
        db.meta.save(total);
    }

    total = total.total;
    length = posts.length;
    if (length > 0 && !skip) {
    	// if all posts are downloaded, we will send them back
    	// with out making a call to the JSON server

    	// Feature : If you want, you can add a property to the
    	// meta collection, named `lastUpdate`
    	// > And if all the posts are downloaded, you can dispatch them
    	// as usual & then check the `lastUpdate` and if it is > 1 day or 1 week,
    	// download all the posts again to check for any updates

        if (length == total) {
        	return sendByParts(posts.splice(0, perPage));
        } else {
            sendByParts(posts.splice(0, perPage));

            if (total > length) {
                page = length / perPage + 1;
                fetcher(isOnline, callback, true);
            }
            return false;
        }

    }
    // clean up old posts
    if (page === 1) {
        db.posts.remove();
        db.loadCollections(['posts']);
    }

    // make request only if we are online!
    if (isOnline) {
        request(baseUrl + '?page=' + page,
            function(error, response, body) {
                if (!error && response.statusCode == 200) {
                    body = JSON.parse(body);
                    posts = body.posts;
                    if (posts.length > 0) {

                        // update the found count
                        db.meta.update({
                            'key': 'found'
                        }, {
                            'total': body.found
                        }, {
                            upsert: true
                        });

                        // sandbox the pages
                        sandboxer(posts, function(posts) {
                            posts = posts.reverse();
                            db.posts.save(posts);
                            posts = posts.reverse();
                            callback(posts);
                            page++;
                            fetcher(isOnline, callback, true);
                        });
                    } else {
                        page = 1;

                        // now that we have all the posts,
                        // we will start sandboxing the images
                        // this is the process of taking a image URL
                        // and converting it to base64
                        // making this app a true offline viewer!

                        imageSandBoxer(isOnline, function() {
                            // ALL Done!
                            //console.log('Sandboxing Images done!!')
                        });

                        // Videos are a bit complex, so left it out :D
                    }
                }
            });
    }

    function sendByParts(_posts) {
        if (_posts) {
            // send only 20 posts per 1 second
            setTimeout(function() {
                callback(_posts);
                return sendByParts(posts.splice(0, perPage));
            }, 1000);
        }
    }
}
Things to notice
* Most of the iterative logic implemented here is recursive.
Line 1 : We include the request module
Line 2 : We include the sandboxer module. This module will be used to sandbox the responses that we get from the WordPress API
Line 3 : We include the imageSandBoxer module. This module consists of logic to sandbox images. As in convert the http URL to a base64 one.
Line 4: We include the connection module. This module consists of the code to initialize the DiskDB and export the db object.
Line 6 : We set a few values to be used while processing.
Line 12 : The fetcher is invoked from the server.js passing in the online/offline status of the viewer and a callback. Since we are using recursions, we pass in a third argument to 
fetcher()
  named skip, which will decide if we need to skip/stop making REST calls to the WordPress REST API.
Line 13, 14 : We query the DiskDB for all the posts and meta data.
Line 18 : If meta data is not present, we create a new entry with the total value set to -1. Total stores the total post that the blog has.
Line 28 : We check if at least one batch of posts are downloaded and we can skip fetching from the REST API. If it is true, we check if all the posts are downloaded. If yes, we call 
sendByParts()
 and send 20 posts at once.
Line 104 : 
sendByParts()
 is a recursive function that send back 20 posts per second
Line 40 : If not all posts are downloaded, we send what we have downloaded so far and then call 
fetcher()
 passing in the online/offline status, the callback and set skip to true. Now, when 
fetcher()
 is invoked from here, the if condition on line 28 will be false. So, it will move on and download the remaining posts. Before we call 
fetcher()
, we will set the page value.
Line 52 : If we are loading the posts from page 1 again, we are removing all the posts and downloading again. This is as part of the POC. Ideally, we need to implement a replace logic while saving existing posts instead of removing the file and adding it again.
Line 58 : We check if the user is online and then only start making calls to the WordPress API.
Line 59 : We create a request to fetch the first page posts.
Line 67 : If it is a success, and we have more than one post in the response, we update the total post count that is sent by the API in our meta collection.
Line 76 : Once the meta collection is updated, we invoke the
sandboxer()
. The
sandboxer()
 takes in a set of posts and sandboxes the URLs and content. And the sandboxed content is sent back.
Line 80 : We save the sandboxed posts to DiskDB and return the same to the UI. After that we increment the page number and call 
fetcher()
. As mentioned most of logic in application runs recursively.
Line 85 : If we are done downloading all the posts, we rest the page number and invoke the 
imageSandBoxer()
, which reads all the saved posts from DiskDB and converts the images with http urls to base 64.
Next, we will implement the sandboxer. Create a file named sandboxer.js inside app/server folder. And update it as below
var cheerio = require('cheerio');
var _sandBoxedPosts = [],
	_posts = [],
	$imgs;

module.exports = function(posts, callback) {
	_sandBoxedPosts = [];
	_posts = posts;
	process(_posts.shift(), function() {
		callback(_sandBoxedPosts);
	});
}

function process(post, cb) {
	if (post) {
		var $ = cheerio.load(post.content);

		// anchor tags
		$('a').each(function(i, e) {

			// sandbox links
			//e.attribs.href = 'openExternal("' + e.attribs.href + '")';
			e.attribs['ng-click'] = 'openExternal(\'' + e.attribs.href + '\')';

			// remove onclick functions
			// I use GA to track clicks
			delete e.attribs.onclick;
			delete e.attribs.target;
			e.attribs.href = 'javascript:';

			var c = e.children;

			if (c.length == 1) {
				// pure anchor tag

			} else {
				// clean up image tags
				if (c.length && c.length > 0 && c[0].name == 'img') {
					var i = c[0];
					i.attribs.src = i.attribs["data-lazy-src"];
					i.attribs.src = i.attribs.src.split('?resize')[0];
					delete i.attribs["data-lazy-src"];
					// remove the parent anchor link for
					// a image tag
					delete e.attribs['ng-click'];
					e.attribs['class'] = 'no-pointer';
				}
			}

		});

		// pre tags
		$('pre').each(function(i, e) {
			e.attribs.hljs = 'true';
			e.attribs['no-escape'] = 'true';
		});

		// iframes
		$('iframe').each(function(i, e) {
			var src = e.attribs.src;
			if (src.indexOf('youtube') > 0) {
				if (src.indexOf('http') != 0) {
					e.attribs.src = 'http:' + e.attribs.src;
				}
				var p = $(e).parents('p')[0];
				p.attribs['ng-class'] = '{hide : !status}';
			}
		});

		$('img').each(function(i, e) {
			e.attribs.src = e.attribs.src.split('?resize')[0];
		})

		post.content = $.html();
		_sandBoxedPosts.push(post);
		return process(_posts.shift(), cb);
	} else {
		cb();
	}
}
Things to notice
* Most of the iterative logic implemented here is recursive.
Line 1 : We include cheerio
Line 2 : We create a few global scoped variables for recursion.
Line 9 : When the sandboxer is called, we reset the global variables and then call the 
process()
 recursively till all the posts in the current batch are done.
Line 15 : Inside the
process()
, we check if a post exists. If it does, we start the sandboxing process, if not we execute the callback on line 78
Line 16 : We will access the content property on post and run the content through 
cheerio.load()
. This provides us with a jQuery like wrapper to work with DOM inside Nodejs. This is quite essential for us to sandbox the content
Line 19 : We sandbox the links. We iterate through each of the links and add a ng-click attribute to it with a custom function. Since I know I am going to use Angularjs, I have attached an ng-click attribute. Apart from that I remove unwanted attributes and reset the href, not to fire links by default.
Line 37 : If the anchor tag has children, I need to process them. In my blog, all the images are wrapped around with an anchor tag because of a plugin I use. I do not want that kind of markup here, where clicking on the image takes the user to the original images. So we clean that up and add custom classes to manage the cursor. All this is sandboxing links.
Line 52 :  For syntax highlighting, I am using an Angular directive named hljs. So, I iterate over all the pre tags in my content and set attribute on them that will help me work with then hljs directive. This is a typical example of integrating a third party angular directive with the sandboxer.
Line 58 : We sandbox all the iframe urls which have youtube as their src. By default all the youtube embed URL are protocol relative. They would look like
//youtube.com?watch=1234
. This will not work properly in the viewer, hence we will convert them to a http URL. Also, I am adding an ng-class attribute on the iFrame, whose src has youtube in it. This is to show or hide the iFrame depending on the network status as demoed in the video.
Line 69 : We sandbox the image tag, replace any additional parameters in the URL. This is specific to my blog. I have a plugin which adds this.
Line 73 : Once the sandboxing is done, we need to update the original HTML with the sandboxed version.
Line 75 : Call 
process()
 on the next post.
To complete the sandboxing, we will be adding a new file named imageSandBoxer.js inside app/server folder. Update imageSandBoxer.js as below
var db = require('./connection.js');
var cheerio = require('cheerio');

// http://stackoverflow.com/a/17133012/1015046
var request = require('request').defaults({
	encoding: null
});

var $imgs, posts, $;

module.exports = function imageSandBoxer(isOnline, callback) {
	if (!isOnline) return;

	posts = db.posts.find();
	processPost(posts.shift(), callback);
}

function processPost(post, callback) {
	if (post) {
		$ = cheerio.load(post.content);
		$imgs = $('img').toArray();
		sandBoxImage($imgs.shift(), post, callback);
	} else {
		callback();
	}
}

function sandBoxImage($img, post, callback) {
	if ($img) {
		request.get($img.attribs.src, function(error, response, body) {
			if (!error && response.statusCode == 200) {
				data = "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64');
				$img.attribs.src = data;
				sandBoxImage($imgs.shift(), post, callback);
			}
		});

	} else {
		post.content = $.html();
		var res = db.posts.update({
			"ID": post.ID
		}, post);
		return processPost(posts.shift(), callback);
	}

}
Things to notice
* Most of the iterative logic implemented here is recursive.
imageSandBoxer()
 will be called only after all the posts are downloaded.
Line 1 : We require the connection module. This module consists of the code to initialize the DiskDB and export the db object.
Line 2 : We require cheerio
Line 5 : We require the request module. Do notice that we are setting encoding to null. This is to make sure we download image response as binary.
Line 12 : If there is no network connection, do not make calls
Line 15 : We
processPost()
 by passing in the post and a callback. This recursively runs till all the posts are done processing.
Line 21 : We get all the images from the post and call the 
sandBoxImage()
 by passing in one image at a time. This recursively runs till all the images are done processing.
Line 29 : If there is a valid image, get the image data. Convert the response to a base 64 format on line 32 and update the src attribute.
Line 39 : Once all images in a given post are done processing, we update the data in DiskDB and go to the next post.
Now, we will create a file named connection.js inside app/server folder. Update it as below
var db = require('diskdb');
db = db.connect(__dirname + '/db', ['posts', 'meta']);

module.exports = db;
For DiskDB to work, create an empty folder named db inside app/serve folder.
To complete the so called server, we will create a file named searcher.js inside app/server folder. Update searcher.js  as below
var db = require('./connection.js');

module.exports = function searcher(q, cb) {

    //NO search API for disk DB :(
    var posts = db.posts.find();
    var results = [];

    for (var i = 0; i < posts.length; i++) {

        var p = posts[i];

        if (p.title.indexOf(q) >= 0 || p.content.indexOf(q) >= 0) {
            results.push(p);
        }

    };

    cb(results);
}
Things to notice
Line 1 : We require the connection to DB
Line 6 : We fetch all the posts
Line 9 :  We run through each post’s title and content and check if the keyword we are searching for exists. if yes, we push it to the results.
Line 19 : Finally we send back the results
This completes our server.
Build the Angularjs Client
To work with client side dependencies, we will use bower package manager. From the root of offline_viewer folder run
bower init
And fill the fields as applicable. This will create a bower.json file at the root of the project. Next, create a file named .bowerrc at the same level as bower.json. Update it as below
{
  "directory": "app/client/lib"
}
This will take care of downloading all the dependencies inside the lib folder.
Next, run
bower install angular-material angular-route roboto-fontface open-sans-fontface angular-highlightjs ng-mfb ionicons jquery --save
If you see a message like Unable to find a suitable version for angular, do as shown below
Quick explanation of the libraries
  • jquery : DOM manipulation.
  • angular-material : Material Design in Angular.js
  • angular-route : The ngRoute module provides routing and deeplinking services and directives for angular apps.
  • angular-highlightjs : AngularJS directive for syntax highlighting with highlight.js
  • ng-mfb : Material design floating menu with action buttons implemented as an Angularjs directive.
  • roboto-fontface : Bower package for the Roboto font-face
  • open-sans-fontface – Bower package for the open-sans font-face
  • ionicons : The premium icon font for Ionic Framework.
The final bower.json would look like
{
  "name": "wordpress-offline-viewer",
  "main": "index.js",
  "version": "0.1.0",
  "authors": [
    "Arvind Ravulavaru <[email protected]>"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "resolutions": {
    "angular": "1.4.0"
  },
  "dependencies": {
    "angular-material": "~0.9.4",
    "angular-route": "~1.4.0",
    "roboto-fontface": "~0.4.2",
    "open-sans-fontface": "~1.4.0",
    "angular-highlightjs": "~0.4.1",
    "ng-mfb": "~0.6.0",
    "ionicons": "~2.0.1",
    "jquery": "~2.1.4"
  }
}
Next, we will update index.html file present at the root of  offline_viewer folder.
<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>The Jackal of Javascript | Offline Viewer</title>
    <link rel="stylesheet" href="app/client/lib/roboto-fontface/css/roboto-fontface.css">
    <link rel="stylesheet" type="text/css" href="app/client/lib/open-sans-fontface/open-sans.css">
    <link rel="stylesheet" type="text/css" href="app/client/lib/angular-material/angular-material.css">
    <link rel="stylesheet" type="text/css" href="app/client/lib/ionicons/css/ionicons.min.css">
    <link rel="stylesheet" type="text/css" href="app/client/css/highlight.css">
    <link rel="stylesheet" type="text/css" href="app/client/lib/ng-mfb/mfb/dist/mfb.css">
    <link rel="stylesheet" href="app/client/css/index.css">
    <script type="text/javascript">
    // start the socket server
    var app = require('./app/server/server');
    app(function(port) {
        window.serverPort = port;
    });
    </script>
</head>

<body layout="column">
    <div ng-include="'app/client/templates/header.html'"></div>
    <ng-view layout="row" flex></ng-view>
    <!-- https://github.com/atom/electron/issues/254 -->
    <script>
    window.$ = window.jQuery = require('./app/client/lib/jquery/dist/jquery.min');
    </script>
    <script type="text/javascript" src="app/client/lib/angular/angular.js"></script>
    <script type="text/javascript" src="app/client/lib/angular-animate/angular-animate.js"></script>
    <script type="text/javascript" src="app/client/lib/angular-aria/angular-aria.js"></script>
    <script type="text/javascript" src="app/client/lib/angular-material/angular-material.js"></script>
    <script type="text/javascript" src="app/client/lib/angular-route/angular-route.js"></script>
    <script type="text/javascript" src="app/client/lib/highlightjs/highlight.pack.js"></script>
    <script type="text/javascript" src="app/client/lib/angular-highlightjs/angular-highlightjs.min.js"></script>
    <script type="text/javascript" src="app/client/lib/ng-mfb/src/mfb-directive.js"></script>
    <script type="text/javascript" src="app/client/js/services.js"></script>
    <script type="text/javascript" src="app/client/js/app.js"></script>
    <script type="text/javascript" src="app/client/js/controllers.js"></script>
    <script type="text/javascript" src="app/client/js/filters.js"></script>
    <script type="text/javascript" src="app/client/js/directives.js"></script>
    <nav mfb-menu ng-controller="SearchCtrl" position="br" effect="zoomin" active-icon="ion-search" resting-icon="ion-search" toggling-method="click" md-ink-ripple ng-click="SearchBox()">
    </nav>
</body>

</html>
Things to notice
Lines 7 to 13 : We require all the CSS files.
Line 16 : We require the server.js file.
Line 17 : We invoke
app()
, which starts the socket server on an available port. We get the used port as the first argument in the callback. We set that value on the window object.
Line 27 : We require jQuery using the module format
Lines 30 – 42 : We require all js files needed. We will create the missing files as we go along.
Line 43 : We set up the fab button. This is the search button on the bottom right hand corner of the viewer.
Note : We have not used ng-app on any DOM element. We are going to bootstrap this Angular app manually.
Next, open index.js and update as below
'use strict';
const app = require('app');
const BrowserWindow = require('browser-window');
const Menu = require('menu');

// report crashes to the Electron project
require('crash-reporter').start();

// prevent window being GC'd
let mainWindow = null;

app.on('ready', function() {
    mainWindow = new BrowserWindow({
        // https://github.com/atom/electron/blob/master/docs/api/browser-window.md
        'min-width': 1000,
        'min-height': 400,
        width: 1200,
        height: 600,
        center: true,
        resizable: true
    });

    mainWindow.loadUrl(`file://${__dirname}/index.html`);

    mainWindow.on('closed', function() {
        // deref the window
        // for multiple windows store them in an array
        mainWindow = null;
    });

    // uncomment the below line to open devetools
    //mainWindow.openDevTools();
});

// does not work when placed on top for some reason.
app.on('window-all-closed', function() {
    app.quit();
});
Do notice line nos. 15, 32 and 36.
If you want you can delete index.css present at the root. We will be creating one more inside the client folder later.
Next, we will add the missing scripts. Create a folder named js inside app/client. Inside the js folder, create a file named app.js. Update app.js as below.
// download socket.io/socket.io.js via JS, as we do not know the port
var socketScript = document.createElement('script');
socketScript.setAttribute('type', 'text/javascript');
socketScript.setAttribute('src', 'http://localhost:' + window.serverPort + '/socket.io/socket.io.js');
document.getElementsByTagName('head').item(0).appendChild(socketScript);

// wait for the socket.io.js to be downloaded
// then  bootstrap Angular Manually
// 			-> We need socket.io right from the word go!

socketScript.onload = function() {
    angular.bootstrap(document, ["OfflineViewer"]);
}

angular.module('OfflineViewer', ['ngMaterial', 'ngRoute', 'hljs', 'socket.io', 'ng-mfb', 'OfflineViewer.controllers', 'OfflineViewer.filters', 'OfflineViewer.directives'])

.config(['$routeProvider', '$mdThemingProvider', '$socketProvider',
        function($routeProvider, $mdThemingProvider, $socketProvider) {

            $socketProvider.setConnectionUrl('http://localhost:' + window.serverPort);

            $mdThemingProvider.theme('default')
                .primaryPalette('red')
                .accentPalette('orange');

            $routeProvider
                .when('/', {
                    templateUrl: 'app/client/templates/App.html',
                    controller: 'AppCtrl'
                })

            .otherwise({
                redirectTo: '/'
            });
        }
    ])
    .run(['$rootScope', '$window', function($rootScope, $window) {

        $rootScope.inProgress = true;

        $rootScope.onLine = $window.navigator.onLine;

        $window.addEventListener('online', function() {
            $rootScope.onLine = true;
            $rootScope.$broadcast('viewer-online');
        });

        $window.addEventListener('offline', function() {
            $rootScope.onLine = false;
            $rootScope.$broadcast('viewer-offline');
        });

    }])
Things to notice
Lines 2 – 5 : We need to manually download the socket script file as we are not of the port that the socket server would start on.
Line 11 : Once the script is loaded, we will manually bootstrap the angular application.
Line 15 : Initialize a new Angular module named OfflineViewer and add all the dependencies.
Line 17 :  We will be configuring the default theme of Angular Material, the connection url for sockets and the routes.
Line 37 : We add listeners to the online & offline event. And when the state changes, we broadcast the appropriate event. So that the viewer can behave accordingly.
Next, create a new file named controller.js inside app/client folder. Update it as below
angular.module('OfflineViewer.controllers', [])


.controller('HeaderCtrl', ['$scope', '$mdSidenav', '$rootScope', function($scope, $mdSidenav, $rootScope) {
	$scope.status = $rootScope.onLine; // True: Online | False: Offline

	$scope.toggleSidenav = function(menuId) {
		$mdSidenav(menuId).toggle();
	};

	$scope.$on('viewer-online', function() {
		$scope.status = true;
		$scope.$apply();
	});

	$scope.$on('viewer-offline', function() {
		$scope.status = false;
		$scope.$apply();
	});
}])

.controller('AppCtrl', ['$scope', '$socket', '$mdDialog', '$rootScope', '$window',
	function($scope, $socket, $mdDialog, $rootScope, $window) {
		// recheck again.. sometimes eventhough the wifi
		// is disconnected, the navigator thinks it is online!

		$scope.status = $rootScope.onLine = $window.navigator.onLine;

		$scope.posts = [];

		if (!$rootScope.onLine) {
			$mdDialog.show(
				$mdDialog.alert()
				.parent(angular.element(document.body))
				.title('You are offline!')
				.content('I have noticed that you are offline, I need internet access for a while to download the posts. If you do not see any posts after sometime, launch the viewer after connecting to the internet. Prior saved posts will be accessible from the menu. ')
				.ariaLabel('You are offline')
				.ok('Got it!')
			);
		}

		$socket.emit('load', $rootScope.onLine);

		$socket.on('loaded', function(posts) {
			$scope.posts = $scope.posts.concat(posts);
			$rootScope.inProgress = false;
		});

		$scope.showPost = function(post) {
			$scope.post = post.content;
		}

		$scope.openExternal = function(url) {
			var confirm = $mdDialog.confirm()
				.parent(angular.element(document.body))
				.title('Open the link?')
				.content('Are you sure you want to open ' + url)
				.ok('Yeah! Sure!')
				.cancel('No Thanks!')

			$mdDialog.show(confirm).then(function() {
				require('shell').openExternal(url);
			}, function() {
				// noop
			});
		}

		$scope.$on('viewer-online', function() {
			$scope.status = true;
			$socket.emit('load', $scope.status);
			$scope.$apply();
		});

		$scope.$on('viewer-offline', function() {
			$scope.status = false;
			$scope.$apply();
		});


		$scope.$on('showSearchPost', function($event, post) {
			$scope.showPost(post);
		});
	}
])

.controller('SearchCtrl', ['$scope', '$socket', '$mdDialog', function($scope, $socket, $mdDialog) {

	$scope.SearchBox = function() {
		$mdDialog.show({
			controller: SearchDialogCtrl,
			templateUrl: 'app/client/templates/search.html',
		});
	}

	function SearchDialogCtrl($scope, $mdDialog, $socket, $rootScope) {

		$scope.results = $rootScope.results;

		$scope.hide = function() {
			$mdDialog.hide();
		};

		$scope.search = function($event) {
			if ($event.which === 13) {
				$scope.searching = true;
				$socket.emit('search', $scope.searchText);
			}
		}

		$scope.showPost = function(post) {
			$scope.hide();
			$rootScope.$broadcast('showSearchPost', post);
		}

		$socket.on('results', function(results) {
			$scope.searching = false;
			$scope.results = results;
			$rootScope.results = results;
		});
	}

}])
Things to notice
Line 1 : We create a 
OfflineViewer.controllers
 module
Line 4 : 
HeaderCtrl
 is the controller for our application header. Here we define the status of the connectivity and are listening to 
viewer-online
 and 
viewer-offline
. And we set the status variable depending on the event fired.
Line 22 : 
AppCtrl
 is the main controller for our application, that notifies the socket server about the status of the viewer and makes requests to fetch the posts,
Line 31 : As soon as the 
AppCtrl
 is initialized, it will check if the viewer has access to the internet. If no, it shows a dialog with the information that the user is offline and it needs some kind of internet access to download the initial set of posts.
Line 42 : We emit the load event to our server, with the status. We will talk about the 
$socket
 a bit letter.
Line 45 : Once the socket server receives the first set of posts, it will fire the the loaded event. And the hook here would be called with the first set of posts. Here we concat the incoming posts with the existing posts and assign it to the scope variable.
Line 49 : When a user clicks on a post links, we update the main viewer with the content of the posts.
Line 53 : This is the method we have added while sandboxing the anchor tags. When the user clicks on a link, this method would be fired and it show a popup dialog, asking the user, if s/he wants to open the link. If yes, we execute line 62.
Line 68 : When ever the user goes online, we emit the load method, indicating our socket server to start fetching the posts if it has not already done so.
Line 74 : We reset the status to false if the user goes offline. This status is used to hide/show youtube videos in the content.
Line 86 : The 
SearchCtrl
 for managing the search feature. When the user clicks on the search icon, on bottom right corner, we show the search dialog.
Line 103 : When the user enters text and clicks on search, we emit the search event with the query text.
Line 115 : Once the results arrive, we update the scope variable with results, which displays the text.
Line 110 : When a user clicks on a post, we broadcast a showSearchPost event, which is listened to on line 80 and shows the post in the main content area.
Quite a lot of important functionality.
Next, we will create a custom directive, that takes up the content of the posts and renders it. Before we render it, we need to compile it so all the attributes we have added while sandboxing will come to life.
Create a new file named directives.js inside app/client/js folder and update it as below
angular.module('OfflineViewer.directives', [])

.directive('blogPost', ['$compile', '$parse', function($compile, $parse) {

	//http://stackoverflow.com/a/21374642/1015046
	var template = "<div>{{post.content}}</div>"
	return {
		restrict: 'E',
		link: function(s, e, a) {
			s.$watch('post', function(val) {
				if (val) {
					e.html($parse(a.post)(s));
					$compile(e.contents())(s);
				}
			});
		}
	};
}])
All we do is parse and compile the content and render it inside a div.
Next, we will add a custom filter, all it does is takes in a content and runs a 
$sce.trustAsHtml()
.
Create a new file named filters.js inside app/client/js folder and update it as below
angular.module('OfflineViewer.filters', [])

.filter('toHTML', function($sce) {
    return function(input) {
        return $sce.trustAsHtml(input);
    }
})
And finally the service component that talks to the socket server. The code written below is taken from the an awesome blog post written by Maciej Sopyło, Tutsplus named : More Responsive Single-Page Applications With AngularJS & Socket.IO: Creating the Library.
Create a new file named services.js inside app/client/js folder and update it as below
// -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
// https://github.com/tiagocparra/angularJS-socketIO//
// -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//

/*
 *http://code.tutsplus.com/tutorials/more-responsive-single-page-applications-with-angularjs-socketio-creating-the-library--cms-21738
 */

var module = angular.module('socket.io', []);

module.provider('$socket', function $socketProvider() {

    var ioUrl = '';
    var ioConfig = {};

    function setOption(name, value, type) {
        if (typeof value != type) {
            throw new TypeError("'" + name + "' must be of type '" + type + "'");
        }
        ioConfig[name] = value;
    }

    this.setResource = function setResource(value) {
        setOption('resource', value, 'string');
    };
    this.setConnectTimeout = function setConnectTimeout(value) {
        setOption('connect timeout', value, 'number');
    };
    this.setTryMultipleTransports = function setTryMultipleTransports(value) {
        setOption('try multiple transports', value, 'boolean');
    };
    this.setReconnect = function setReconnect(value) {
        setOption('reconnect', value, 'boolean');
    };
    this.setReconnectionDelay = function setReconnectionDelay(value) {
        setOption('reconnection delay', value, 'number');
    };
    this.setReconnectionLimit = function setReconnectionLimit(value) {
        setOption('reconnection limit', value, 'number');
    };
    this.setMaxReconnectionAttempts = function setMaxReconnectionAttempts(value) {
        setOption('max reconnection attempts', value, 'number');
    };
    this.setSyncDisconnectOnUnload = function setSyncDisconnectOnUnload(value) {
        setOption('sync disconnect on unload', value, 'boolean');
    };
    this.setAutoConnect = function setAutoConnect(value) {
        setOption('auto connect', value, 'boolean');
    };
    this.setFlashPolicyPort = function setFlashPolicyPort(value) {
        setOption('flash policy port', value, 'number')
    };
    this.setForceNewConnection = function setForceNewConnection(value) {
        setOption('force new connection', value, 'boolean');
    };
    this.setConnectionUrl = function setConnectionUrl(value) {
        if ('string' !== typeof value) {
            throw new TypeError("setConnectionUrl error: value must be of type 'string'");
        }
        ioUrl = value;
    }

    this.$get = function $socketFactory($rootScope) {

        var socket = io(ioUrl, ioConfig);

        return {
            on: function on(event, callback) {
                socket.on(event, function() {
                    //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
                    var args = arguments;
                    // $apply faz com que a callback possa invocar variaveis
                    // em $scope que tenham sido declaradas pela
                    // directiva ng-model ou {{}}
                    $rootScope.$apply(function() {
                        // este callback.apply regista a função callback que
                        // poderá conter referencias à variavel socket
                        callback.apply(socket, args);
                    });
                });
            },
            off: function off(event, callback) {
                if (typeof callback == 'function') {
                    //neste caso o callback nao tem acesso a $scope nem à
                    //scope definida pela variavel socket
                    socket.removeListener(event, callback);
                } else {
                    socket.removeAllListeners(event);
                }
            },
            emit: function emit(event, data, callback) {
                if (typeof callback == 'function') {
                    socket.emit(event, data, function() {
                        var args = arguments;
                        $rootScope.$apply(function() {
                            callback.apply(socket, args);
                        });
                    });
                } else {
                    socket.emit(event, data);
                }
            }
        };
    };
});
You can read more about how the above service is written in the post linked above.
Now, we will create the three templates used in the app. The header, the app and the search.
Create a new folder named templates inside app/client. Create a new file named header.html inside the app/client/templates folder and update it as below
<md-toolbar layout="row" ng-controller="HeaderCtrl" md-scroll-shrink="false">
    <button ng-click="toggleSidenav('left')" class="md-icon-button menuBtn">
        <span class="md-visually-hidden">Menu</span>
    </button>
    <h1 class="md-toolbar-tools" layout-align-gt-sm="center">The Jackal of Javascript</h1>
    <div class="md-padding">
        <md-progress-circular ng-show="inProgress" class="md-accent re-pos-loader" md-mode="indeterminate"></md-progress-circular>
    </div>
    <div class="md-padding">
        <div class="online" ng-show="status">
            <md-tooltip>
                You are online!
            </md-tooltip>
        </div>
        <div class="offline" ng-show="!status">
            <md-tooltip>
                You are offline!
            </md-tooltip>
        </div>
    </div>
</md-toolbar>
Next, comes the app template. Create a file named app.html inside app/client/templates folder. Update it as below
<md-sidenav layout="column" class="md-sidenav-left md-whiteframe-z2" md-component-id="left" md-is-locked-close="$mdMedia('lt-sm')">
	<md-list>
		<md-subheader>All Posts</md-subheader>
		<md-progress-circular ng-show="posts.length === 0" class="md-accent center-block" md-mode="indeterminate"></md-progress-circular>
		<md-list-item ng-click="showPost(post)" ng-repeat="post in posts" class="list">
			<div ng-bind-html="post.title | toHTML" class="post-title"></div>
			<md-divider></md-divider>
		</md-list-item>
	</md-list>
</md-sidenav>
<div layout="column" flex id="content">
	<md-content layout="column" flex class="post">
		<md-whiteframe ng-show="!post" class="md-whiteframe-z2" layout layout-align="center center">
			<div layout="column" layout-align="center center">
				<div class="md-title">Click on the Menu to start reading posts!</div>
				<div class="mg-top-20p md-title" ng-show="posts.length === 0">
					Waiting for First Page Posts to Load <span class="md-subhead" ng-show="onLine">(This may take some time)</span>..
				</div>
			</div>
		</md-whiteframe>
		<!-- <div ng-bind-html="post | post"></div> -->
		<blog-post post="post" ng-show="post" class="md-padding"></blog-post>
	</md-content>
</div>
This file consist of the main application view template.
Finally the search template. Create a file named search.html inside app/client/templates.
<md-dialog aria-label="Search Posts" class="search-dialog">
    <md-dialog-content class="sticky-container" layout="column">
        <md-toolbar layout="column"><span flex="flex"></span>
            <div class="md-toolbar-tools">
                <div layout="row" flex="flex" class="fill-height">
                    <div lass="md-toolbar-item md-breadcrumb">
                        <span>Search Posts</span>
                    </div>
                    <span flex="flex"></span>
                    <div layout="row" class="md-toolbar-item md-tools">
                        <a ng-click="hide()" href="javascript:" class="md-primary"><i class="ion-close"></i></a>
                    </div>
                </div>
            </div>
        </md-toolbar>
        <md-content layout-padding>
            <md-input-container>
                <label>I am looking for..</label>
                <input name="description" ng-model="searchText" ng-keypress="search($event)">
            </md-input-container>
            <md-progress-circular ng-show="searching" class="md-accent center-block" md-mode="indeterminate"></md-progress-circular>
            <md-subheader ng-show="results.length == 0">No results found!!</md-subheader>
            <md-list ng-show="results.length > 0">
                <md-list-item ng-click="showPost(post)" ng-repeat="post in results" class="list">
                    <div ng-bind-html="post.title | toHTML" class="post-title"></div>
                    <md-divider></md-divider>
                </md-list-item>
            </md-list>
        </md-content>
    </md-dialog-content>
    </form>
</md-dialog>
Finally the styles. Create a folder named css. Create a file named index.css inside app/client/css. We will add application specific overrides here. Update index.css as below
body {
    color: #333;
    font-family: 'Open Sans', sans-serif !important;
}

.menuBtn {
    background-color: transparent;
    border: none;
    margin-left: 16px;
    outline: none;
}

md-list .md-button {
    color: inherit;
    text-align: left;
    width: 100%;
}

/* Using Data-URI converted from svg until <md-icon> becomes available
https://github.com/google/material-design-icons
*/
.menuBtn {
    background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGcgaWQ9IkhlYWRlciI+CiAgICA8Zz4KICAgICAgICA8cmVjdCB4PSItNjE4IiB5PSItMjIzMiIgZmlsbD0ibm9uZSIgd2lkdGg9IjE0MDAiIGhlaWdodD0iMzYwMCIvPgogICAgPC9nPgo8L2c+CjxnIGlkPSJMYWJlbCI+CjwvZz4KPGcgaWQ9Ikljb24iPgogICAgPGc+CiAgICAgICAgPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CiAgICAgICAgPHBhdGggZD0iTTMsMThoMTh2LTJIM1YxOHogTTMsMTNoMTh2LTJIM1YxM3ogTTMsNnYyaDE4VjZIM3oiIHN0eWxlPSJmaWxsOiNmM2YzZjM7Ii8+CiAgICA8L2c+CjwvZz4KPGcgaWQ9IkdyaWQiIGRpc3BsYXk9Im5vbmUiPgogICAgPGcgZGlzcGxheT0iaW5saW5lIj4KICAgIDwvZz4KPC9nPgo8L3N2Zz4=) no-repeat center center;
}

.list {
    padding-top: 3px;
    padding-bottom: 3px;
}

img {
    display: block;
    clear: both;
    margin: 13.5px auto;
    box-shadow: 1px 1px 39px 9px rgba(0, 0, 0, 0.5);
    border: 0;
    height: auto;
    max-width: 63%;
}

.crayon-inline {
    border-width: 1px !important;
    border-color: #ccc !important;
    background: #f5f5f5 !important;
    border-style: solid !important;
    padding: 5px;
    border-radius: 4px;
    margin: 0 0 10px;
    color: #000;
    font-family: monospace;
}

blog-post {
    overflow-x: hidden;
    font-size: 18px;
}

pre {
    -webkit-font-smoothing: auto;
    font-size: 14px;
    background: #f5f5f5;
    padding: 10px;
}

a {
    color: #f45145;
    text-decoration: none;
}

.post-title {
    color: #f45145;
    font-size: 18px;
    font-weight: 600;
}

.mfb-component__button--main,
.mfb-component__button--child {
    /*    color: #F44336;
    background-color: white;*/

    color: white;
    background-color: #F44336;
}

.no-pointer {
    cursor: default;
}

md-content {
    overflow-x: hidden;
}

md-whiteframe {
    background: #fff;
    margin: 20px;
    padding: 20px;
}

.center-block {
    margin: 0 auto;
    display: block;
}

.mg-top-20p {
    margin-top: 20px;
}

.search-dialog {
    width: 100%;
    min-height: 333px;
}

.online {
    width: 30px;
    height: 30px;
    background: green;
    border-radius: 50%;
    box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
    right: 0;
    position: absolute;
    margin-right: 30px;
    cursor: pointer;
}

.offline {
    width: 30px;
    height: 30px;
    background: orange;
    border-radius: 50%;
    box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
    right: 0;
    position: absolute;
    margin-right: 30px;
    cursor: pointer;
}

.hide {
    display: none;
}

.re-pos-loader {
    position: absolute;
    margin-right: 72px;
    right: 0px;
    top: 7px;
}
And some styles for code highlighting. Create a file named highlight.css inside app/client/css folder. Update it as below
/*http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/styles/github.min.css*/

.hljs, {
    display: block;
    overflow-x: auto;
    padding: 0.5em;
    color: #333;
    background: #f8f8f8;
    -webkit-text-size-adjust: none;
    -webkit-font-smoothing: auto;
    font-size: 14px;
}

.hljs-comment,
.hljs-template_comment,
.diff .hljs-header,
.hljs-javadoc {
    color: #998;
    font-style: italic
}

.hljs-keyword,
.css .rule .hljs-keyword,
.hljs-winutils,
.javascript .hljs-title,
.nginx .hljs-title,
.hljs-subst,
.hljs-request,
.hljs-status {
    color: #333;
    font-weight: bold
}

.hljs-number,
.hljs-hexcolor,
.ruby .hljs-constant {
    color: #008080
}

.hljs-string,
.hljs-tag .hljs-value,
.hljs-phpdoc,
.hljs-dartdoc,
.tex .hljs-formula {
    color: #d14
}

.hljs-title,
.hljs-id,
.scss .hljs-preprocessor {
    color: #900;
    font-weight: bold
}

.javascript .hljs-title,
.hljs-list .hljs-keyword,
.hljs-subst {
    font-weight: normal
}

.hljs-class .hljs-title,
.hljs-type,
.vhdl .hljs-literal,
.tex .hljs-command {
    color: #458;
    font-weight: bold
}

.hljs-tag,
.hljs-tag .hljs-title,
.hljs-rules .hljs-property,
.django .hljs-tag .hljs-keyword {
    color: #000080;
    font-weight: normal
}

.hljs-attribute,
.hljs-variable,
.lisp .hljs-body {
    color: #008080
}

.hljs-regexp {
    color: #009926
}

.hljs-symbol,
.ruby .hljs-symbol .hljs-string,
.lisp .hljs-keyword,
.clojure .hljs-keyword,
.scheme .hljs-keyword,
.tex .hljs-special,
.hljs-prompt {
    color: #990073
}

.hljs-built_in {
    color: #0086b3
}

.hljs-preprocessor,
.hljs-pragma,
.hljs-pi,
.hljs-doctype,
.hljs-shebang,
.hljs-cdata {
    color: #999;
    font-weight: bold
}

.hljs-deletion {
    background: #fdd
}

.hljs-addition {
    background: #dfd
}

.diff .hljs-change {
    background: #0086b3
}

.hljs-chunk {
    color: #aaa
}
That is it!! We are all done!!
Test the Offline Viewer
To test the viewer run,
npm run start
And this should launch the viewer and start downloading all the posts. If you want to open devtools, open index.js and uncomment the below line
// uncomment the below line to open devetools
mainWindow.openDevTools();
And run
npm run start
And you should see the above the view.
You can browse through the app, search, click on links and test it out.
If you inspect the image tag after a while, you should see that the src is replaced with base 64 format of the image.
Build the Offline Viewer
To build and distribute the application for a given OS, we will be running the scripts which we have written in the package.json.
npm run build-mac
or
npm run build-linux
or
npm run build-win
Or if you want to build for all platforms at once run
npm run build
Do not forget to comment the
mainWindow.openDevTools();
 line in index.js. Else your packaged app will have the devtools enabled
Release the Offline Viewer
As of now, I have integrated appdmg module, to generate DMG files for mac. To implement this, create a new file named app-dmg.json at the root of the project. Update app-dmg.json as below
{
    "title": "The Jackal of Javascript",
    "icon": "./resources/icon.icns",
    "background": "./resources/BG.tiff",
    "icon-size": 80,
    "contents": [
        {
            "x": 448,
            "y": 344,
            "type": "link",
            "path": "/Applications"
        }, {
            "x": 192,
            "y": 334,
            "type": "file",
            "path": "./build/mac/The Jackal of Javascript.app"
        }
    ]
}
And for the resources, you can copy all the required files from this folder.
And finally run
npm run release-mac
And it should generate the installer.
So that is it! A Proof of Concept on how to replay the content whether the user is online or offline.

Thanks for reading! Do comment.
@arvindr21
The post Electron, WordPress & Angular Material – An Offline Viewer appeared first on The Jackal of Javascript.