October 3, 2013

JS Adventure: Creating a Desktop App - Part 2

In the previous post, we talked about how I got the idea of building a desktop application using JavaScript/HTML/CSS as well as the basic requirements for our application. In this post, we are going to setup our development environment and create the basic HTML, CSS and bootstrap script to begin.



The Development Environment

First, fire up your favorite code editor or IDE. I use Sublime Text 2 (highly recommended) as my editor and below you can find the basic structure for this project. As you can see, I saved it as a sublime project. You can just ignore the last 2 files if you are not using sublime. (Oh, I named this application jayhoo! -- after my daughter's nickname). The src folder shall contain our JavaScript (JS) files while the res folder shall contain our css, images and other static resources.



jayhoo.html


<html>
<head>
<title>jayhoo!</title>
<link rel="stylesheet" href="res/styles.css">
</head>
<body>
<div class="container">
    <h1 class="header">My Movie Collections</h1>
    <ul id="movie-list"></ul>
</div>
<script src="src/bootstrap.js"></script>
</body>
</html>

I don't think this requires explanation. It's just a very basic HTML just enough to test our application at the end of this post.


res/styles.css

The CSS file does not contain anything yet. We don't want to trouble ourselves thinking about the UI design at this stage. We can always come back to that later on.

Before anything else, since we are going to be using ActiveX, then we also should ONLY use Internet Explorer to view our product once in while (we can't use other browsers AFAIK). I am using IE10 by the way.


src/bootstrap.js

What the script of this file will do is basically start the application in a "good state". Good State in our case means, all my movie files are displayed on this web page together with their titles, years and poster images (we can't really display the poster for now, that will be on the next post). So, what we will do for this post for now are:
  1. read the INI file of the application (which contains the path of our movie files)
  2. gather the files scanned from the path set in the INI file
  3. display the files in our list (#movie-list, no poster image for now and in the format [folder]/[filename])
window.onload = function() {
    var applicationLocation = !window.external ? '' : 'C:/Users/redcat/Documents/projects/jayhoo/';
    bootstrap(applicationLocation);
};

function bootstrap(applicationLocation) {

    var fileSystem = new WinFileSystem();
    var movieListView = new MovieListView();

    var ini = readIniFile(applicationLocation + 'jayhoo.ini');
    var movies = gatherMovies(ini.path);

    movieListView.display(movies);

    /**
     * Reads the content of a file as an INI file and returns
     * an INI object which is basically a hash.
     * NOTE: This does not support INI grouping.
     * @param  {String} path The path of the file
     * @return {Object}      A hash of key-value pairs
     */
    function readIniFile(path) {

        // read the content as text file
        var text = fileSystem.readTextFile(path);

        // we are actually in windows so we expect the linefeed-newline
        // combination but in order to be sure, we will replace it with just
        // the newline character
        text = text.replace('\r\n', '\n');

        // now we can split it using the newline character
        var lines = text.split('\n'),
            i = 0, length = lines.length,
            config = {},
            line, splitLine, key, value;

        while (i < length) {
            // spaces before and after a line or even on keys and values
            // are allowed but they should be ignored when read
            line = lines[i].trim();

            // The line must also not empty
            // We are allowing INI Comments being started by the '#' character
            // if a line doesn't have an '=' character, then we will ignore it
            if (line &&
                line.indexOf('#') !== 0 &&
                line.indexOf('=') !== -1) {

                // split the line and assign to as key=value
                splitLine = line.split('=');
                key = splitLine[0].trim();
                value = splitLine[1].trim();

                // add it to our INI hash, existing keys will be replaced
                config[key] = value;
            }
            i++;
        }
        return config;
    }

    /**
     * Scans the file system for movie files and creates a
     * movie object out from each file
     * @param  {String} path       The path to scan
     * @return {Array}             An array of movie objects
     */
    function gatherMovies(path) {
        // gather the raw file information
        var files = fileSystem.getAllFiles(path),
            movies = [], file = null,
            i = 0; length = files.length;

        while (i < length) {
            file = files[i];

            // create a movie object that basically contains:
            //      a file information object and a movie
            //      information object
            movies.push({
                file: file,
                info: null
            });

            i++;
        }

        // return the valid movies found
        return movies;
    }
}

We want to start the application only when all other javascript files have been loaded so we use window.onload() to start our application. The applicationLocation is needed to determine file or folder paths relative to our application directory like the INI file. You may ask why not include the code in the bootstrap function (and not in window.onload()). The reason is that the applicationLocation variable is only needed when we test the application in the browser (IE). Once we run the application as HTA, we don't need that variable because HTA's working directory is the location of the of HTA file itself but when we run this application from IE, the working directory is the user's desktop. So, basically it is an unnecessary variable for our final product but necessary for our testing (in the browser) so we don't need to pollute our bootstrap function with that variable. To determine if we are running in HTA or in IE browser, we just checked the window.external object (little bit hacky) because according to MS, window.external is not supported in HTA.

Note: Be sure to have a forward slash (or double backslash if you used backslash) at the end of the path.

We don't have the codes yet to create the fileSystem and movieListView variables so the above code will not work for now unless you create mock objects in place. The reading of the INI file should be relatively easy to follow due to the comments I placed. readIniFile() will return an object which we will use like a hash of key-value pairs that correspond to the key-value pairs of the INI file. Do take note that this function does not support reading INI groups (the one with square brackets - we don't need that, at least for now). gatherMovies() function is also very simple, using our yet-to-be-implemented fileSystem.getAllFiles() and then create a movie object out from those files. Each movie object has only 2 properties (at least for now):
  1. file - which contains file information properties (name, size, extension, created, folder(name) and (full)path) and
  2. info - which contains movie meta information properties (taken from the web like title, year, genres, actors, directors, etc...). We don't have this data yet so we set it to null.
After that we passed our movies to movieListView and have it display those movies. I hope you know the reason why we did not display them in our bootstrap function. Displaying the movies requires access to the UI (HTML) and we don't want mix our UI codes with our business codes so we delegated this to a view object.

src/movie-list-view.js

/**
 * MovieListView constructor function
 */
function MovieListView() {
    // get the container element since we are going to
    // be referring to this element often
    this._containerElement = document.querySelector('#movie-list');
}
MovieListView.prototype = {
    /**
     * display the movies but technically adding
     * html content to the UL#movie-list
     * @param  {Array} movies The array of movie objects
     */
    display: function(movies) {

        // clear any existing movies displayed first
        this.clear();

        // let's use a DocumentFragment since adding
        // directly to the container element is costly
        // Here's a good explanation: http://ejohn.org/blog/dom-documentfragments/
        var documentFragment = document.createDocumentFragment(),
            li, movie, i = 0, length = movies.length;

        while (i < length) {
            movie = movies[i];
            i++;

            li = document.createElement('li');
            // our basic requirements requires folder/filename only (of course, for now)
            li.innerHTML = movie.file.folder + '/' + movie.file.name + '.' + movie.file.extension;

            documentFragment.appendChild(li);
        }

        // then add all the LI elements to the UL at once
        this._containerElement.appendChild(documentFragment);
    },
    /**
     * A helper function used to clear the content of the container UL
     */
    clear: function() {
        this._containerElement.innerHTML = '';
    }
}

This constructor function creates a view object which basically is tightly coupled to our HTML. It also uses DocumentFragment to build the HTML (some good information about DocumentFragment here). Basically, it's a good way to add a lot of HTML elements to the page for performance reason.

src/file-system.js

function WinFileSystem() {
    this._fso = new ActiveXObject('Scripting.FileSystemObject');
}
WinFileSystem.prototype = {
    readTextFile: function(path) {
        var text = null,
            file = null;
        if (this._fso.fileExists(path)) {
            file = this._fso.openTextFile(path, 1);
            text = file.readAll();
            file.close();
        }
        return text;
    },
    getAllFiles: function(path) {
        var files = [];
        // we recursively call this function to get
        // all files on any subfolder level
        this.appendFiles(files, path);
        return files;
    },
    appendFiles: function(files, path) {
        // create a folder object
        // an iterator to the subfolders of the folder object
        // since we want to check those subfolders as well for 
        // any move files, and those subfolders' subfolders and so on
        var folder = this._fso.getFolder(path),
            iterator = new Enumerator(folder.subFolders),
            file;

        while (!iterator.atEnd()) {
            // call again our method but this time
            // using the subfolder path
            this.appendFiles(files, iterator.item().path);
            iterator.moveNext();
        }

        // now let's iterate through all the files
        iterator = new Enumerator(folder.files);
        while (!iterator.atEnd()) {
            file = iterator.item();
            // take note of these properties since this will
            // serve as the interface for our file information object
            files.push({
                path: file.path,
                name: file.name.substr(0, file.name.lastIndexOf('.')),
                extension: file.name.substr(file.name.lastIndexOf('.') + 1).toLowerCase(),
                size: file.size,
                folder: folder.name,
                // the file.dateCreated is not a JS Date object
                // so we will convert it to one
                created: new Date(file.dateCreated)
            });
            iterator.moveNext();
        }
    }
}

The FileSystem (I named it WinFileSystem, since this is Windows and the FileSystem term will act like an imaginary interface should we change the implementation. Okay, we don't have the interface but I think you get the idea) for now has 2 methods that we need: the readTextFile() which basically reads a file as text and the getAllFiles() which calls a recursive method appendFiles() and returns an array of files found from the path parameter. A recursive method (or function) is a method that calls itself. This is especially helpful in this situation since it simplifies coding. We can have loops to re-implement them without recursion but this solution is more elegant. Apart from that, nothing really fancy here. If you want to learn more about ActiveX's FileSystemObject, refer to this really nice documentation.

Now, we need sample data (movie files) and our INI file should contain this:

path = c:\movies

You can use your own path here. I also have below sample files, you can create yours. I am not actually using real movie files. We don't validate if a file is a movie or not so we can use anything (at least for now). Each folder contains file (or files) for that particular movie. Notice that the folder names are formatted by title and year.



Let's run our application (open it in IE) and below is my output. We have successfully achieved our 3 basic requirements for this post. Nothing eye-popping since that can come later.


In our next post, we will tackle on getting the movie information from IMDb or something like that and display our movies with poster images.

Isn't that interesting? Stay Tuned!