<commit_before>/**
 * StopFlash
 *
 * https://github.com/JWhile/StopFlash
 *
 * content_script.js
 */

var whitelist = []; // :Array<String>
var elementId = 0; // :int

// function isFlash(HTMLElement element):boolean
var isFlash = function(element)
{
    return (element.nodeName === 'OBJECT' || element.nodeName === 'EMBED');
};

// class FlashCollection
function FlashCollection(doc)
{
    this.flashElements = []; // :Array<FlashElement>

    this.port = chrome.runtime.connect({'name': 'stopflashContentScript'});

    var self = this;

    this.port.onMessage.addListener(function(req)
    {
        if(req['stopflashWhitelist'])
        {
            whitelist = req['stopflashWhitelist'];

            // init
            self.add(doc.getElementsByTagName('OBJECT'));
            self.add(doc.getElementsByTagName('EMBED'));

            self.port.postMessage({'stopflashHaveChange': true, 'stopflashIsMainFrame': (window == window.top)});
        }

        if(req['stopflashDataUpdate'])
        {
            self.sendData();
        }

        if(req['stopflashBlock'])
        {
            for(var i = 0, e; i < req['stopflashBlock'].length; ++i)
            {
                e = self.get(req['stopflashBlock'][i]);

                if(e != null)
                {
                    e.block();
                }
            }
        }

        if(req['stopflashUnblock'])
        {
            for(var i = 0, e; i < req['stopflashUnblock'].length; ++i)
            {
                e = self.get(req['stopflashUnblock'][i]);

                if(e != null)
                {
                    e.unblock();
                }
            }
        }
    });

    this.port.postMessage({'stopflashInit': true, 'stopflashIsMainFrame': (window == window.top)});

    var observer = new MutationObserver(function(changes)
    {
        var changed = false;

        for(var i = 0, c; i < changes.length; ++i)
        {
            c = changes[i];

            if(c.addedNodes != null)
            {
                changed = (self.add(c.addedNodes) || false);
            }

            if(c.removedNodes != null)
            {
                changed = (self.remove(c.removedNodes) || false);
            }
        }

        if(changed)
        {
            self.port.postMessage({'stopflashHaveChange': true});
        }
    });

    observer.observe(doc, {childList: true, subtree: true});

    this.sendData();
}
// function sendData():void
FlashCollection.prototype.sendData = function()
{
    var data = [];

    for(var i = 0; i < this.flashElements.length; ++i)
    {
        data.push(this.flashElements[i].getData());
    }

    this.port.postMessage({'stopflashDataUpdate': true, 'stopflashData': data});
};
// function add(NodeList elements):boolean
FlashCollection.prototype.add = function(elements)
{
    var changed = false;

    for(var i = 0, e; i < elements.length; ++i)
    {
        e = elements[i];

        if(isFlash(e))
        {
            var parent = e;

            while(parent = parent.parentNode)
            {
                if(isFlash(parent))
                {
                    e = parent;
                }
            }

            var f = this.get(e);

            if(f == null)
            {
                f = new FlashElement(++elementId, e);

                this.flashElements.push(f);

                if(!f.isWhitelist())
                {
                    f.block();
                }

                changed = true;
            }
            else if(f.blocked)
            {
                f.block();
            }
        }
    }

    return changed;
};
// function remove(NodeList elements):boolean
FlashCollection.prototype.remove = function(elements)
{
    var changed = false;

    for(var i = 0, f; i < elements.length; ++i)
    {
        f = this.get(elements[i]);

        if(f != null && !f.blocked)
        {
            f.remove();

            changed = true;
        }
    }

    return changed;
};
// function get(HTMLElement element)
FlashCollection.prototype.get = function(element)
{
    for(var i = 0; i < this.flashElements.length; ++i)
    {
        if(element == this.flashElements[i].element)
        {
            return this.flashElements[i];
        }
    }

    return null;
};

var styles = [
    'float', 'clear',
    'position', 'top', 'left',
    'width', 'height',
    'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
    'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
    'border-top', 'border-right', 'border-bottom', 'border-left'
]; // :Array<String>

// class FlashElement
function FlashElement(id, element)
{
    this.id = id;

    this.parent = element.parentNode; // :HTMLElement
    this.element = element; // :HTMLElement
    this.nextSibling = element.nextSibling; // :HTMLElement

    this.blocked = false; // :boolean

    var self = this;

    var replacement = new Builder('div')
        .css('display', 'block')
        .css('cursor', 'pointer')
        .css('min-width', '64px')
        .css('min-height', '64px')
        .css('background', '#333 url("'+ chrome.extension.getURL('icons/stopflash_64.png') +'") center center no-repeat')
        .event('click', function()
        {
            self.unblock();
        });

    for(var i = 0; i < styles.length; ++i)
    {
        replacement.css(styles[i], Builder.getStyle(element, styles[i]));
    }

    this.replacement = replacement.node;
}
// function getData():Object
FlashElement.prototype.getData = function()
{
    return {
        'id': this.id,
        'type': this.element.nodeName,
        'url': this.getUrl(),
        'blocked': this.blocked,
        'whitelist': this.isWhitelist()
    };
};
// function isWhitelist():boolean
FlashElement.prototype.isWhitelist = function()
{
    var url = this.getUrl();

    for(var i = 0; i < whitelist.length; ++i)
    {
        if(whitelist[i].indexOf(url) >= 0)
        {
            return true;
        }
    }

    return false;
};
// function getUrl():String
FlashElement.prototype.getUrl = function()
{
    if(this.element.nodeName === 'EMBED')
    {
        return this.element.src;
    }
    else if(this.element.nodeName === 'OBJECT')
    {
        if(this.element.data)
        {
            return this.element.data;
        }
        else
        {
            var child = this.element.firstChild;

            while(child != null)
            {
                if(child.nodeName === 'PARAM' && (child.name === 'src' || child.name === 'movie'))
                {
                    return child.value;
                }

                child = child.nextSibling;
            }
        }
    }

    return null;
};
// function block():void
FlashElement.prototype.block = function()
{
    this.blocked = true;

    this.parent.insertBefore(this.replacement, this.nextSibling);

    this.parent.removeChild(this.element);
};
// function unblock():void
FlashElement.prototype.unblock = function()
{
    this.blocked = false;

    this.parent.insertBefore(this.element, this.nextSibling);

    this.parent.removeChild(this.replacement);
};
// function remove():void
FlashElement.prototype.remove = function()
{
    if(this.replacement.parentNode)
    {
        this.replacement.parentNode.removeChild(this.replacement);
    }

    if(this.element.parentNode)
    {
        this.element.parentNode.removeChild(this.element);
    }
};

// main
var collection = null;

var main = function()
{
    collection = new FlashCollection(document);
};

document.addEventListener('DOMContentLoaded', main, false);
<commit_msg>Add FlashCollection::getById()<commit_after>/**
 * StopFlash
 *
 * https://github.com/JWhile/StopFlash
 *
 * content_script.js
 */

var whitelist = []; // :Array<String>
var elementId = 0; // :int

// function isFlash(HTMLElement element):boolean
var isFlash = function(element)
{
    return (element.nodeName === 'OBJECT' || element.nodeName === 'EMBED');
};

// class FlashCollection
function FlashCollection(doc)
{
    this.flashElements = []; // :Array<FlashElement>

    this.port = chrome.runtime.connect({'name': 'stopflashContentScript'});

    var self = this;

    this.port.onMessage.addListener(function(req)
    {
        if(req['stopflashWhitelist'])
        {
            whitelist = req['stopflashWhitelist'];

            // init
            self.add(doc.getElementsByTagName('OBJECT'));
            self.add(doc.getElementsByTagName('EMBED'));

            self.port.postMessage({'stopflashHaveChange': true, 'stopflashIsMainFrame': (window == window.top)});
        }

        if(req['stopflashDataUpdate'])
        {
            self.sendData();
        }

        if(req['stopflashBlock'])
        {
            for(var i = 0, e; i < req['stopflashBlock'].length; ++i)
            {
                e = self.get(req['stopflashBlock'][i]);

                if(e != null)
                {
                    e.block();
                }
            }
        }

        if(req['stopflashUnblock'])
        {
            for(var i = 0, e; i < req['stopflashUnblock'].length; ++i)
            {
                e = self.get(req['stopflashUnblock'][i]);

                if(e != null)
                {
                    e.unblock();
                }
            }
        }
    });

    this.port.postMessage({'stopflashInit': true, 'stopflashIsMainFrame': (window == window.top)});

    var observer = new MutationObserver(function(changes)
    {
        var changed = false;

        for(var i = 0, c; i < changes.length; ++i)
        {
            c = changes[i];

            if(c.addedNodes != null)
            {
                changed = (self.add(c.addedNodes) || false);
            }

            if(c.removedNodes != null)
            {
                changed = (self.remove(c.removedNodes) || false);
            }
        }

        if(changed)
        {
            self.port.postMessage({'stopflashHaveChange': true});
        }
    });

    observer.observe(doc, {childList: true, subtree: true});

    this.sendData();
}
// function sendData():void
FlashCollection.prototype.sendData = function()
{
    var data = [];

    for(var i = 0; i < this.flashElements.length; ++i)
    {
        data.push(this.flashElements[i].getData());
    }

    this.port.postMessage({'stopflashDataUpdate': true, 'stopflashData': data});
};
// function add(NodeList elements):boolean
FlashCollection.prototype.add = function(elements)
{
    var changed = false;

    for(var i = 0, e; i < elements.length; ++i)
    {
        e = elements[i];

        if(isFlash(e))
        {
            var parent = e;

            while(parent = parent.parentNode)
            {
                if(isFlash(parent))
                {
                    e = parent;
                }
            }

            var f = this.get(e);

            if(f == null)
            {
                f = new FlashElement(++elementId, e);

                this.flashElements.push(f);

                if(!f.isWhitelist())
                {
                    f.block();
                }

                changed = true;
            }
            else if(f.blocked)
            {
                f.block();
            }
        }
    }

    return changed;
};
// function remove(NodeList elements):boolean
FlashCollection.prototype.remove = function(elements)
{
    var changed = false;

    for(var i = 0, f; i < elements.length; ++i)
    {
        f = this.get(elements[i]);

        if(f != null && !f.blocked)
        {
            f.remove();

            changed = true;
        }
    }

    return changed;
};
// function get(HTMLElement element)
FlashCollection.prototype.get = function(element)
{
    for(var i = 0; i < this.flashElements.length; ++i)
    {
        if(element == this.flashElements[i].element)
        {
            return this.flashElements[i];
        }
    }

    return null;
};
// function getById(int id)
FlashCollection.prototype.getById = function(id)
{
    for(var i = 0; < i this.flashElements.length; ++i)
    {
        if(id === this.flashElements[i].id)
        {
            return this.flashElements[i];
        }
    }

    return null;
};

var styles = [
    'float', 'clear',
    'position', 'top', 'left',
    'width', 'height',
    'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
    'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
    'border-top', 'border-right', 'border-bottom', 'border-left'
]; // :Array<String>

// class FlashElement
function FlashElement(id, element)
{
    this.id = id;

    this.parent = element.parentNode; // :HTMLElement
    this.element = element; // :HTMLElement
    this.nextSibling = element.nextSibling; // :HTMLElement

    this.blocked = false; // :boolean

    var self = this;

    var replacement = new Builder('div')
        .css('display', 'block')
        .css('cursor', 'pointer')
        .css('min-width', '64px')
        .css('min-height', '64px')
        .css('background', '#333 url("'+ chrome.extension.getURL('icons/stopflash_64.png') +'") center center no-repeat')
        .event('click', function()
        {
            self.unblock();
        });

    for(var i = 0; i < styles.length; ++i)
    {
        replacement.css(styles[i], Builder.getStyle(element, styles[i]));
    }

    this.replacement = replacement.node;
}
// function getData():Object
FlashElement.prototype.getData = function()
{
    return {
        'id': this.id,
        'type': this.element.nodeName,
        'url': this.getUrl(),
        'blocked': this.blocked,
        'whitelist': this.isWhitelist()
    };
};
// function isWhitelist():boolean
FlashElement.prototype.isWhitelist = function()
{
    var url = this.getUrl();

    for(var i = 0; i < whitelist.length; ++i)
    {
        if(whitelist[i].indexOf(url) >= 0)
        {
            return true;
        }
    }

    return false;
};
// function getUrl():String
FlashElement.prototype.getUrl = function()
{
    if(this.element.nodeName === 'EMBED')
    {
        return this.element.src;
    }
    else if(this.element.nodeName === 'OBJECT')
    {
        if(this.element.data)
        {
            return this.element.data;
        }
        else
        {
            var child = this.element.firstChild;

            while(child != null)
            {
                if(child.nodeName === 'PARAM' && (child.name === 'src' || child.name === 'movie'))
                {
                    return child.value;
                }

                child = child.nextSibling;
            }
        }
    }

    return null;
};
// function block():void
FlashElement.prototype.block = function()
{
    this.blocked = true;

    this.parent.insertBefore(this.replacement, this.nextSibling);

    this.parent.removeChild(this.element);
};
// function unblock():void
FlashElement.prototype.unblock = function()
{
    this.blocked = false;

    this.parent.insertBefore(this.element, this.nextSibling);

    this.parent.removeChild(this.replacement);
};
// function remove():void
FlashElement.prototype.remove = function()
{
    if(this.replacement.parentNode)
    {
        this.replacement.parentNode.removeChild(this.replacement);
    }

    if(this.element.parentNode)
    {
        this.element.parentNode.removeChild(this.element);
    }
};

// main
var collection = null;

var main = function()
{
    collection = new FlashCollection(document);
};

document.addEventListener('DOMContentLoaded', main, false);
<|endoftext|>


<commit_before>/**
 * A module that handles read and writing to the disk.
 *
 * @module settings
 * @author Nathan Buchar
 * @copyright 2016-2017 Nathan Buchar <hello@nathanbuchar.com>
 * @license ISC
 */

const assert = require('assert');
const electron = require('electron');
const { EventEmitter } = require('events');
const fs = require('fs-extra');
const path = require('path');

const Helpers = require('./settings-helpers');
const Observer = require('./settings-observer');

/**
 * A reference to the Electron app. If this framework is required within a
 * renderer processes, we need to load the app via `remote`.
 *
 * @type {string}
 */
const app = electron.app || electron.remote.app;

/**
 * The Electron app's user data path.
 *
 * @type {string}
 */
const userDataPath = app.getPath('userData');

/**
 * The name of the settings file.
 *
 * @type {string}
 */
const settingsFileName = 'Settings';

/**
 * The absolute path to th user settings file.
 *
 * @type {string}
 */
const settingsFilePath = path.join(userDataPath, settingsFileName);

/**
 * The electron-settings class.
 *
 * @extends EventEmitter
 * @class
 */
class Settings extends EventEmitter {

  constructor() {
    super();

    /**
     * The absolute path to the settings file on the disk.
     *
     * @type {string}
     * @private
     */
    this._settingsFilePath = settingsFilePath;

    /**
     * The FSWatcher instance. This will watch if the settings file and
     * notify key path observers.
     *
     * @type {FSWatcher}
     * @default null
     * @private
     */
    this._fsWatcher = null;

    /**
     * Called when the settings file is changed or renamed.
     *
     * @type {Object}
     * @private
     */
    this._handleSettingsFileChange = this._onSettingsFileChange.bind(this);
  }

  /**
   * Watches the settings file for changes using the native `FSWatcher`
   * class in case the settings file is changed outside of
   * ElectronSettings' jursidiction.
   *
   * @private
   */
  _watchSettings() {
    if (!this._fsWatcher) {
      try {
        this._fsWatcher = fs.watch(this._settingsFilePath, this._handleSettingsFileChange);
      } catch (err) {
        // File may not exist yet or may not have appropriate permissions.
      }
    }
  }

  /**
   * Unwatches the settings file by closing the FSWatcher and nullifying its
   * references. If the `reset` parameter is true, attempt to watch the
   * settings file again.
   *
   * @param {boolean} [reset=false]
   * @private
   */
  _unwatchSettings(reset = false) {
    if (this._fsWatcher) {
      this._fsWatcher.close();
      this._fsWatcher = null;

      if (reset) {
        this._watchSettings();
      }
    }
  }

  /**
   * Ensures that the settings file exists, then initializes the FSWatcher.
   *
   * @private
   */
  _ensureSettings() {
    try {
      fs.readJsonSync(this._settingsFilePath);
    } catch (err) {
      try {
        fs.writeJsonSync(this._settingsFilePath, {});
      } catch (err) {
        // Cannot read or write file. This could be because the user does not
        // have the necessary permissions. Throw error.
        throw err;
      }
    }

    this._watchSettings();
  }

  /**
   * Writes the settings to the disk.
   *
   * @param {Object} [obj={}]
   * @param {Object} [opts={}]
   * @private
   */
  _writeSettings(obj = {}, opts = {}) {
    this._ensureSettings();

    try {
      fs.outputJsonSync(this._settingsFilePath, obj, {
        spaces: opts.prettify ? 2 : 0
      });
    } catch (err) {
      // Could not write the file. This could be because the user does not
      // have the necessary permissions. Throw error.
      throw err;
    }
  }

  /**
   * Returns the parsed contents of the settings file.
   *
   * @returns {Object}
   * @private
   */
  _readSettings() {
    this._ensureSettings();

    try {
      return fs.readJsonSync(this._settingsFilePath);
    } catch (err) {
      // Could not read the file. This could be because the user does not
      // have the necessary permissions. Throw error.
      throw err;
    }
  }

  /**
   * Called when the settings file has been changed or
   * renamed (moved/deleted).
   *
   * @type {string} eventType
   * @private
   */
  _onSettingsFileChange(eventType) {
    switch (eventType) {
      case Settings.FSWatcherEvents.CHANGE: {
        this._emitChangeEvent();
        break;
      }
      case Settings.FSWatcherEvents.RENAME: {
        this._unwatchSettings(true);
        break;
      }
    }
  }

  /**
   * Broadcasts the internal "change" event.
   *
   * @emits ElectronSettings:change
   * @private
   */
  _emitChangeEvent() {
    this.emit(Settings.Events.CHANGE);
  }

  /**
   * Returns a boolean indicating whether the settings object contains
   * the given key path.
   *
   * @param {string} keyPath
   * @returns {boolean}
   * @private
   */
  _checkKeyPathExists(keyPath) {
    const obj = this._readSettings();
    const exists = Helpers.hasKeyPath(obj, keyPath);

    return exists;
  }

  /**
   * Sets the value at the given key path, or the entire settings object if
   * an empty key path is given.
   *
   * @param {string} keyPath
   * @param {any} value
   * @param {Object} opts
   * @private
   */
  _setValueAtKeyPath(keyPath, value, opts) {
    let obj = value;

    if (keyPath !== '') {
      obj = this._readSettings();

      Helpers.setValueAtKeyPath(obj, keyPath, value);
    }

    this._writeSettings(obj, opts);
  }

  /**
   * Returns the value at the given key path, or sets the value at that key
   * path to the default value, if provided, if the key does not exist. If an
   * empty key path is given, the entire settings object will be returned.
   *
   * @param {string} keyPath
   * @param {any} [defaultValue]
   * @returns {any}
   * @private
   */
  _getValueAtKeyPath(keyPath, defaultValue) {
    const obj = this._readSettings();

    if (keyPath !== '') {
      const exists = Helpers.hasKeyPath(obj, keyPath);
      const value = Helpers.getValueAtKeyPath(obj, keyPath);

      // The key does not exist but a default value does. Set the value at the
      // key path to the default value and then get the new value.
      if (!exists && typeof defaultValue !== 'undefined') {
        this._setValueAtKeyPath(keyPath, defaultValue);

        // Get the new value now that the default has been set.
        return this._getValueAtKeyPath(keyPath);
      }

      return value;
    }

    return obj;
  }

  /**
   * Deletes the key and value at the given key path, or clears the entire
   * settings object if an empty key path is given.
   *
   * @param {string} keyPath
   * @param {Object} opts
   * @private
   */
  _deleteValueAtKeyPath(keyPath, opts) {
    if (keyPath === '') {
      this._writeSettings({}, opts);
    } else {
      const obj = this._readSettings();
      const exists = Helpers.hasKeyPath(obj, keyPath);

      if (exists) {
        Helpers.deleteValueAtKeyPath(obj, keyPath);
        this._writeSettings(obj, opts);
      }
    }
  }

  /**
   * Watches the given key path for changes and calls the given handler
   * if the value changes. To unsubscribe from changes, call `dispose()`
   * on the Observer instance that is returned.
   *
   * @param {string} keyPath
   * @param {Function} handler
   * @returns {Observer}
   * @private
   */
  _watchValueAtKeyPath(keyPath, handler) {
    const currentValue = this._getValueAtKeyPath(keyPath);

    return new Observer(this, keyPath, handler, currentValue);
  }

  /**
   * Returns a boolean indicating whether the settings object contains
   * the given key path.
   *
   * @param {string} keyPath
   * @returns {boolean}
   * @public
   */
  has(keyPath) {
    assert.strictEqual(typeof keyPath, 'string', 'Key path parameter must be a string');

    return this._checkKeyPathExists(keyPath);
  }

  /**
   * Sets the value at the given key path.
   *
   * @param {string} keyPath
   * @param {any} value
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  set(keyPath, value, opts = {}) {
    assert.strictEqual(typeof keyPath, 'string', 'Key path parameter must be a string. Did you mean to use `setAll()` instead?');
    assert.strictEqual(typeof opts, 'object', 'Options parameter must be an object');

    this._setValueAtKeyPath(keyPath, value, opts);
  }

  /**
   * Sets all settings.
   *
   * @param {Object} obj
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  setAll(obj, opts = {}) {
    assert.strictEqual(typeof obj, 'object', 'Obj parameter must be an object');
    assert.strictEqual(typeof opts, 'object', 'Options parameter must be an object');

    this._setValueAtKeyPath('', obj, opts);
  }

  /**
   * Returns the value at the given key path, or sets the value at that key
   * path to the default value, if provided, if the key does not exist.
   *
   * @param {string} keyPath
   * @param {any} [defaultValue]
   * @returns {any}
   * @public
   */
  get(keyPath, defaultValue) {
    assert.strictEqual(typeof keyPath, 'string', 'Key path parameter must be a string. Did you mean to use `getAll()` instead?');

    return this._getValueAtKeyPath(keyPath, defaultValue);
  }

  /**
   * Returns all settings.
   *
   * @returns {Object}
   * @public
   */
  getAll() {
    return this._getValueAtKeyPath('');
  }

  /**
   * Deletes the key and value at the given key path.
   *
   * @param {string} keyPath
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  delete(keyPath, opts = {}) {
    assert.strictEqual(typeof keyPath, 'string', 'Key path parameter must be a string. Did you mean to use `deleteAll()` instead?');
    assert.strictEqual(typeof opts, 'object', 'Options parameter must be an object');

    this._deleteValueAtKeyPath(keyPath, opts);
  }

  /**
   * Deletes all settings.
   *
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  deleteAll(opts = {}) {
    assert.strictEqual(typeof opts, 'object', 'Options parameter must be an object');

    this._deleteValueAtKeyPath('', opts);
  }

  /**
   * Watches the given key path for changes and calls the given handler
   * if the value changes. To unsubscribe from changes, call `dispose()`
   * on the Observer instance that is returned.
   *
   * @param {string} keyPath
   * @param {Function} handler
   * @returns {Observer}
   * @public
   */
  watch(keyPath, handler) {
    assert.strictEqual(typeof keyPath, 'string', 'Key path parameter must be a string');
    assert.strictEqual(typeof handler, 'function', 'Handler parameter must be a function');

    return this._watchValueAtKeyPath(keyPath, handler);
  }

  /**
   * Returns the absolute path to where the settings file is or will be stored.
   *
   * @returns {string}
   * @public
   */
  file() {
    return this._getSettingsPath();
  }
}

/**
 * ElectronSettings event names.
 *
 * @enum {string}
 * @readonly
 */
Settings.FSWatcherEvents = {
  CHANGE: 'change',
  RENAME: 'rename'
};

/**
 * ElectronSettings event names.
 *
 * @enum {string}
 * @readonly
 */
Settings.Events = {
  CHANGE: 'change'
};

module.exports = Settings;
<commit_msg>Indicate index of param instead of name in type checks<commit_after>/**
 * A module that handles read and writing to the disk.
 *
 * @module settings
 * @author Nathan Buchar
 * @copyright 2016-2017 Nathan Buchar <hello@nathanbuchar.com>
 * @license ISC
 */

const assert = require('assert');
const electron = require('electron');
const { EventEmitter } = require('events');
const fs = require('fs-extra');
const path = require('path');

const Helpers = require('./settings-helpers');
const Observer = require('./settings-observer');

/**
 * A reference to the Electron app. If this framework is required within a
 * renderer processes, we need to load the app via `remote`.
 *
 * @type {string}
 */
const app = electron.app || electron.remote.app;

/**
 * The Electron app's user data path.
 *
 * @type {string}
 */
const userDataPath = app.getPath('userData');

/**
 * The name of the settings file.
 *
 * @type {string}
 */
const settingsFileName = 'Settings';

/**
 * The absolute path to th user settings file.
 *
 * @type {string}
 */
const settingsFilePath = path.join(userDataPath, settingsFileName);

/**
 * The electron-settings class.
 *
 * @extends EventEmitter
 * @class
 */
class Settings extends EventEmitter {

  constructor() {
    super();

    /**
     * The absolute path to the settings file on the disk.
     *
     * @type {string}
     * @private
     */
    this._settingsFilePath = settingsFilePath;

    /**
     * The FSWatcher instance. This will watch if the settings file and
     * notify key path observers.
     *
     * @type {FSWatcher}
     * @default null
     * @private
     */
    this._fsWatcher = null;

    /**
     * Called when the settings file is changed or renamed.
     *
     * @type {Object}
     * @private
     */
    this._handleSettingsFileChange = this._onSettingsFileChange.bind(this);
  }

  /**
   * Watches the settings file for changes using the native `FSWatcher`
   * class in case the settings file is changed outside of
   * ElectronSettings' jursidiction.
   *
   * @private
   */
  _watchSettings() {
    if (!this._fsWatcher) {
      try {
        this._fsWatcher = fs.watch(this._settingsFilePath, this._handleSettingsFileChange);
      } catch (err) {
        // File may not exist yet or may not have appropriate permissions.
      }
    }
  }

  /**
   * Unwatches the settings file by closing the FSWatcher and nullifying its
   * references. If the `reset` parameter is true, attempt to watch the
   * settings file again.
   *
   * @param {boolean} [reset=false]
   * @private
   */
  _unwatchSettings(reset = false) {
    if (this._fsWatcher) {
      this._fsWatcher.close();
      this._fsWatcher = null;

      if (reset) {
        this._watchSettings();
      }
    }
  }

  /**
   * Ensures that the settings file exists, then initializes the FSWatcher.
   *
   * @private
   */
  _ensureSettings() {
    try {
      fs.readJsonSync(this._settingsFilePath);
    } catch (err) {
      try {
        fs.writeJsonSync(this._settingsFilePath, {});
      } catch (err) {
        // Cannot read or write file. This could be because the user does not
        // have the necessary permissions. Throw error.
        throw err;
      }
    }

    this._watchSettings();
  }

  /**
   * Writes the settings to the disk.
   *
   * @param {Object} [obj={}]
   * @param {Object} [opts={}]
   * @private
   */
  _writeSettings(obj = {}, opts = {}) {
    this._ensureSettings();

    try {
      fs.outputJsonSync(this._settingsFilePath, obj, {
        spaces: opts.prettify ? 2 : 0
      });
    } catch (err) {
      // Could not write the file. This could be because the user does not
      // have the necessary permissions. Throw error.
      throw err;
    }
  }

  /**
   * Returns the parsed contents of the settings file.
   *
   * @returns {Object}
   * @private
   */
  _readSettings() {
    this._ensureSettings();

    try {
      return fs.readJsonSync(this._settingsFilePath);
    } catch (err) {
      // Could not read the file. This could be because the user does not
      // have the necessary permissions. Throw error.
      throw err;
    }
  }

  /**
   * Called when the settings file has been changed or
   * renamed (moved/deleted).
   *
   * @type {string} eventType
   * @private
   */
  _onSettingsFileChange(eventType) {
    switch (eventType) {
      case Settings.FSWatcherEvents.CHANGE: {
        this._emitChangeEvent();
        break;
      }
      case Settings.FSWatcherEvents.RENAME: {
        this._unwatchSettings(true);
        break;
      }
    }
  }

  /**
   * Broadcasts the internal "change" event.
   *
   * @emits ElectronSettings:change
   * @private
   */
  _emitChangeEvent() {
    this.emit(Settings.Events.CHANGE);
  }

  /**
   * Returns a boolean indicating whether the settings object contains
   * the given key path.
   *
   * @param {string} keyPath
   * @returns {boolean}
   * @private
   */
  _checkKeyPathExists(keyPath) {
    const obj = this._readSettings();
    const exists = Helpers.hasKeyPath(obj, keyPath);

    return exists;
  }

  /**
   * Sets the value at the given key path, or the entire settings object if
   * an empty key path is given.
   *
   * @param {string} keyPath
   * @param {any} value
   * @param {Object} opts
   * @private
   */
  _setValueAtKeyPath(keyPath, value, opts) {
    let obj = value;

    if (keyPath !== '') {
      obj = this._readSettings();

      Helpers.setValueAtKeyPath(obj, keyPath, value);
    }

    this._writeSettings(obj, opts);
  }

  /**
   * Returns the value at the given key path, or sets the value at that key
   * path to the default value, if provided, if the key does not exist. If an
   * empty key path is given, the entire settings object will be returned.
   *
   * @param {string} keyPath
   * @param {any} [defaultValue]
   * @returns {any}
   * @private
   */
  _getValueAtKeyPath(keyPath, defaultValue) {
    const obj = this._readSettings();

    if (keyPath !== '') {
      const exists = Helpers.hasKeyPath(obj, keyPath);
      const value = Helpers.getValueAtKeyPath(obj, keyPath);

      // The key does not exist but a default value does. Set the value at the
      // key path to the default value and then get the new value.
      if (!exists && typeof defaultValue !== 'undefined') {
        this._setValueAtKeyPath(keyPath, defaultValue);

        // Get the new value now that the default has been set.
        return this._getValueAtKeyPath(keyPath);
      }

      return value;
    }

    return obj;
  }

  /**
   * Deletes the key and value at the given key path, or clears the entire
   * settings object if an empty key path is given.
   *
   * @param {string} keyPath
   * @param {Object} opts
   * @private
   */
  _deleteValueAtKeyPath(keyPath, opts) {
    if (keyPath === '') {
      this._writeSettings({}, opts);
    } else {
      const obj = this._readSettings();
      const exists = Helpers.hasKeyPath(obj, keyPath);

      if (exists) {
        Helpers.deleteValueAtKeyPath(obj, keyPath);
        this._writeSettings(obj, opts);
      }
    }
  }

  /**
   * Watches the given key path for changes and calls the given handler
   * if the value changes. To unsubscribe from changes, call `dispose()`
   * on the Observer instance that is returned.
   *
   * @param {string} keyPath
   * @param {Function} handler
   * @returns {Observer}
   * @private
   */
  _watchValueAtKeyPath(keyPath, handler) {
    const currentValue = this._getValueAtKeyPath(keyPath);

    return new Observer(this, keyPath, handler, currentValue);
  }

  /**
   * Returns a boolean indicating whether the settings object contains
   * the given key path.
   *
   * @param {string} keyPath
   * @returns {boolean}
   * @public
   */
  has(keyPath) {
    assert.strictEqual(typeof keyPath, 'string', 'First parameter must be a string');

    return this._checkKeyPathExists(keyPath);
  }

  /**
   * Sets the value at the given key path.
   *
   * @param {string} keyPath
   * @param {any} value
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  set(keyPath, value, opts = {}) {
    assert.strictEqual(typeof keyPath, 'string', 'First parameter must be a string. Did you mean to use `setAll()` instead?');
    assert.strictEqual(typeof opts, 'object', 'Second parameter must be an object');

    this._setValueAtKeyPath(keyPath, value, opts);
  }

  /**
   * Sets all settings.
   *
   * @param {Object} obj
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  setAll(obj, opts = {}) {
    assert.strictEqual(typeof obj, 'object', 'First parameter must be an object');
    assert.strictEqual(typeof opts, 'object', 'Second parameter must be an object');

    this._setValueAtKeyPath('', obj, opts);
  }

  /**
   * Returns the value at the given key path, or sets the value at that key
   * path to the default value, if provided, if the key does not exist.
   *
   * @param {string} keyPath
   * @param {any} [defaultValue]
   * @returns {any}
   * @public
   */
  get(keyPath, defaultValue) {
    assert.strictEqual(typeof keyPath, 'string', 'First parameter must be a string. Did you mean to use `getAll()` instead?');

    return this._getValueAtKeyPath(keyPath, defaultValue);
  }

  /**
   * Returns all settings.
   *
   * @returns {Object}
   * @public
   */
  getAll() {
    return this._getValueAtKeyPath('');
  }

  /**
   * Deletes the key and value at the given key path.
   *
   * @param {string} keyPath
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  delete(keyPath, opts = {}) {
    assert.strictEqual(typeof keyPath, 'string', 'First parameter must be a string. Did you mean to use `deleteAll()` instead?');
    assert.strictEqual(typeof opts, 'object', 'Second parameter must be an object');

    this._deleteValueAtKeyPath(keyPath, opts);
  }

  /**
   * Deletes all settings.
   *
   * @param {Object} [opts={}]
   * @param {boolean} [opts.prettify=false]
   * @public
   */
  deleteAll(opts = {}) {
    assert.strictEqual(typeof opts, 'object', 'First parameter must be an object');

    this._deleteValueAtKeyPath('', opts);
  }

  /**
   * Watches the given key path for changes and calls the given handler
   * if the value changes. To unsubscribe from changes, call `dispose()`
   * on the Observer instance that is returned.
   *
   * @param {string} keyPath
   * @param {Function} handler
   * @returns {Observer}
   * @public
   */
  watch(keyPath, handler) {
    assert.strictEqual(typeof keyPath, 'string', 'First parameter must be a string');
    assert.strictEqual(typeof handler, 'function', 'Second parameter must be a function');

    return this._watchValueAtKeyPath(keyPath, handler);
  }

  /**
   * Returns the absolute path to where the settings file is or will be stored.
   *
   * @returns {string}
   * @public
   */
  file() {
    return this._getSettingsPath();
  }
}

/**
 * ElectronSettings event names.
 *
 * @enum {string}
 * @readonly
 */
Settings.FSWatcherEvents = {
  CHANGE: 'change',
  RENAME: 'rename'
};

/**
 * ElectronSettings event names.
 *
 * @enum {string}
 * @readonly
 */
Settings.Events = {
  CHANGE: 'change'
};

module.exports = Settings;
<|endoftext|>


<commit_before>/*
 * dependencies: jquery, backbonejs, underscorejs
 */
(function() {
    "use strict";

    var $ = window.jQuery,
        _ = window._;
    $.extend(true, window.sliMpd, {
        modules : {}
    });
    window.sliMpd.modules.MpdPlayer = window.sliMpd.modules.AbstractPlayer.extend({

        mode : 'mpd',
        faviconDoghnutColor : 'rgb(255,156,1)',
        faviconBackgroundColor : '#444',

        timeGridSelectorCanvas : 'timegrid-mpd',
		timeGridSelectorSeekbar : '.mpd-ctrl-seekbar',
		timeGridStrokeColor : '#7B6137',
		timeGridStrokeColor2 : '#FCC772',

		intervalActive : 2000,
		intervalInactive : 5000,
		timeLineLight : null,

        initialize : function(options) {
        	this.$content = $('.player-'+ this.mode, this.$el);

        	this.trackAnimation = { currentPosPerc: 0 };
        	this.timeLineLight = new TimelineLite();

        	this.poll();
            window.sliMpd.modules.AbstractPlayer.prototype.initialize.call(this, options);
        },

        render : function(options) {
            window.sliMpd.modules.AbstractPlayer.prototype.render.call(this, options);
        },

        play : function(item) {
			window.sliMpd.fireRequestAndNotify(item.mpdurl);
			this.redraw();
    		//this.reloadCss(item.hash);
    		window.sliMpd.modules.AbstractPlayer.prototype.play.call(this, item);
        },

        togglePause : function(item) {
			if(this.nowPlayingState == 'play') {
				window.sliMpd.fireRequestAndNotify('/mpdctrl/pause');
				this.nowPlayingState = 'pause';
			} else {
				window.sliMpd.fireRequestAndNotify('/mpdctrl/play');
				this.nowPlayingState = 'play';
			}
			window.sliMpd.modules.AbstractPlayer.prototype.togglePause.call(this, item);
			this.setPlayPauseIcon(item);
			window.sliMpd.drawFavicon();
		},

        seek : function(item) {
			window.sliMpd.fireRequestAndNotify(item.mpdurl);
			window.sliMpd.modules.AbstractPlayer.prototype.seek.call(this, item);
        },

        prev : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.prev.call(this, item);
        },

        next : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	this.refreshInterval();
        	// TODO: do we really need to append '?null' to routename comparison?
        	window.sliMpd.router.refreshIfName('playlist?null');
        	window.sliMpd.modules.AbstractPlayer.prototype.next.call(this, item);
        },

        remove : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.remove.call(this, item);
        },

        toggleRepeat : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleRepeat.call(this, item);
        },

        toggleRandom : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleRandom.call(this, item);
        },

        toggleConsume : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleConsume.call(this, item);
        },

        clearPlaylistNotCurrent : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.clearPlaylistNotCurrent.call(this, item);
        },

        addDirToPlaylist : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.clearPlaylistNotCurrent.call(this, item);
        },

        process : function(item) {
        	window.sliMpd.modules.AbstractPlayer.prototype.process.call(this, item);
        },

        // IMPORTANT TODO: how to avoid growing memory consumption on those frequent poll-requests?
		poll : function (){
			var that = this;
		    $.get('/mpdstatus', function(data) {
		    	data = JSON.parse(data);

		    	that.nowPlayingPercent = data.percent;
        		that.nowPlayingState = data.state;
        		that.nowPlayingDuration = data.duration;
        		that.nowPlayingElapsed = data.elapsed;
		    	that.nowPlayingItem = data.songid;

		    	that.stateRepeat = data.repeat;
		    	that.stateRandom = data.random;
		    	that.stateConsume = data.consume;

		    	// no need to update this stuff in case local player is active...
		    	if(window.sliMpd.currentPlayer.mode === 'mpd') {

			    	['repeat', 'random', 'consume'].forEach(function(prop) {
					    if(data[prop] == '1') {
			    			$('.mpd-status-'+prop, that.$el).addClass('active');
				    	} else {
				    		$('.mpd-status-'+prop, that.$el).removeClass('active');
				    	}
					});

					that.setPlayPauseIcon();

					// TODO: interpolate nowPlayingElapsed independent frpm poll interval
			    	$('.mpd-status-elapsed').text(that.formatTime(that.nowPlayingElapsed));
			    	$('.mpd-status-total').text(that.formatTime(that.nowPlayingDuration));

			    	// animate from 0 to 100, onUpdate -> change Text
					that.timeLineLight = new TimelineLite();
					that.trackAnimation.currentPosPerc = 0;
					that.timeLineLight.to(that.trackAnimation, that.nowPlayingDuration, {
					  	currentPosPerc: 100,
					  	ease: Linear.easeNone,
					  	onUpdate: that.updateSlider,
					  	onUpdateScope: that
					});

			    	if(that.nowPlayingState == 'play') {
			    		that.timelineSetValue(that.nowPlayingPercent);
					} else {
			    		that.timeLineLight.pause();
			    	}
		    	}

		    	// update trackinfo only onTrackChange()
		    	if(that.previousPlayingItem != that.nowPlayingItem) {
		    		that.previousPlayingItem = that.nowPlayingItem
		    		that.redraw('');
		    		that.refreshInterval();
		    		return;
		    	}
		        that.poller = setTimeout(that.poll, ((window.sliMpd.currentPlayer.mode === 'mpd') ? that.intervalActive:that.intervalInactive));
		    });
		},

        onRedrawComplete : function(item) {
        	//this.refreshInterval(); // TODO: why is this doubling our interval?
        	var that = this;
        	$('.mpd-ctrl-seekbar').on('click', function(e){
				// TODO: how to respect parents padding (15px) on absolute positioned div with width 100% ?
				var percent = Math.round((e.pageX - $(this).offset().left) / (($(this).width()+15)/100));
				$('.mpd-status-progressbar', that.$el).css('width', 'calc('+ percent+'% - 15px)');
				that.process({'action': 'seek', 'mpdurl' : '/mpdctrl/seekPercent/' + percent});
				that.timelineSetValue(percent);
			});
			window.sliMpd.notify({'notify':1, 'message': 'MPD playing: ' + $('.player-mpd .now-playing-string').text()});
        	window.sliMpd.modules.AbstractPlayer.prototype.onRedrawComplete.call(this, item);
        },

        // TODO: make markup more generic and move this to AbstractPlayer for usage in both players (local+mpd)
        setPlayPauseIcon : function(item) {
			if(this.nowPlayingState == 'play') {
				$('.mpd-status-playpause', this.$el).addClass('fa-pause');
				$('.mpd-status-playpause', this.$el).removeClass('fa-play');
			} else {
				$('.mpd-status-playpause', this.$el).removeClass('fa-pause');
				$('.mpd-status-playpause', this.$el).addClass('fa-play');
			}
			window.sliMpd.modules.AbstractPlayer.prototype.setPlayPauseIcon.call(this, item);
		},

		formatTime : function(seconds) {
			if(typeof seconds == 'undefined') {
				return '-- : --';
			}
			var seconds 	= Math.round(seconds);
			var hour 		= Math.floor(seconds / 3600);
			var minutes 	= Math.floor(seconds / 60) % 60;
			seconds 		= seconds % 60;

			if (hour > 0)	return hour + ':' + this.zeroPad(minutes, 2) + ':' + this.zeroPad(seconds, 2);
			else			return minutes + ':' + this.zeroPad(seconds, 2);
		},

		zeroPad : function(number, n) {
			var zeroPad = '' + number;
			while(zeroPad.length < n) {
				zeroPad = '0' + zeroPad;
			}
			return zeroPad;
		},

		timelineSetValue : function(value) {
			this.timeLineLight.progress(value/100);
			window.sliMpd.modules.AbstractPlayer.prototype.timelineSetValue.call(this, value);
		},

		updateSlider : function(item) {
			// TODO: how to respect parents padding on absolute positioned div with width 100% ?
			$('.mpd-status-progressbar').css('width', 'calc('+ this.timeLineLight.progress() *100 +'% - 15px)');
			window.sliMpd.modules.AbstractPlayer.prototype.updateSlider.call(this, item);
		}
    });

})();
<commit_msg>activated drawTimeGrid on mpd-player poll-response<commit_after>/*
 * dependencies: jquery, backbonejs, underscorejs
 */
(function() {
    "use strict";

    var $ = window.jQuery,
        _ = window._;
    $.extend(true, window.sliMpd, {
        modules : {}
    });
    window.sliMpd.modules.MpdPlayer = window.sliMpd.modules.AbstractPlayer.extend({

        mode : 'mpd',
        faviconDoghnutColor : 'rgb(255,156,1)',
        faviconBackgroundColor : '#444',

        timeGridSelectorCanvas : 'timegrid-mpd',
		timeGridSelectorSeekbar : '.mpd-ctrl-seekbar',
		timeGridStrokeColor : '#7B6137',
		timeGridStrokeColor2 : '#FCC772',

		intervalActive : 2000,
		intervalInactive : 5000,
		timeLineLight : null,

        initialize : function(options) {
        	this.$content = $('.player-'+ this.mode, this.$el);

        	this.trackAnimation = { currentPosPerc: 0 };
        	this.timeLineLight = new TimelineLite();

        	this.poll();
            window.sliMpd.modules.AbstractPlayer.prototype.initialize.call(this, options);
        },

        render : function(options) {
            window.sliMpd.modules.AbstractPlayer.prototype.render.call(this, options);
        },

        play : function(item) {
			window.sliMpd.fireRequestAndNotify(item.mpdurl);
			this.redraw();
    		//this.reloadCss(item.hash);
    		window.sliMpd.modules.AbstractPlayer.prototype.play.call(this, item);
        },

        togglePause : function(item) {
			if(this.nowPlayingState == 'play') {
				window.sliMpd.fireRequestAndNotify('/mpdctrl/pause');
				this.nowPlayingState = 'pause';
			} else {
				window.sliMpd.fireRequestAndNotify('/mpdctrl/play');
				this.nowPlayingState = 'play';
			}
			window.sliMpd.modules.AbstractPlayer.prototype.togglePause.call(this, item);
			this.setPlayPauseIcon(item);
			window.sliMpd.drawFavicon();
		},

        seek : function(item) {
			window.sliMpd.fireRequestAndNotify(item.mpdurl);
			window.sliMpd.modules.AbstractPlayer.prototype.seek.call(this, item);
        },

        prev : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.prev.call(this, item);
        },

        next : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	this.refreshInterval();
        	// TODO: do we really need to append '?null' to routename comparison?
        	window.sliMpd.router.refreshIfName('playlist?null');
        	window.sliMpd.modules.AbstractPlayer.prototype.next.call(this, item);
        },

        remove : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.remove.call(this, item);
        },

        toggleRepeat : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleRepeat.call(this, item);
        },

        toggleRandom : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleRandom.call(this, item);
        },

        toggleConsume : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	//this.refreshInterval();
        	window.sliMpd.modules.AbstractPlayer.prototype.toggleConsume.call(this, item);
        },

        clearPlaylistNotCurrent : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.clearPlaylistNotCurrent.call(this, item);
        },

        addDirToPlaylist : function(item) {
        	window.sliMpd.fireRequestAndNotify(item.mpdurl);
        	// TODO: check current route and refresh in case we are on current mpd-playlist
        	window.sliMpd.modules.AbstractPlayer.prototype.clearPlaylistNotCurrent.call(this, item);
        },

        process : function(item) {
        	window.sliMpd.modules.AbstractPlayer.prototype.process.call(this, item);
        },

        // IMPORTANT TODO: how to avoid growing memory consumption on those frequent poll-requests?
		poll : function (){
			var that = this;
		    $.get('/mpdstatus', function(data) {
		    	data = JSON.parse(data);

		    	that.nowPlayingPercent = data.percent;
        		that.nowPlayingState = data.state;
        		that.nowPlayingDuration = data.duration;
        		that.nowPlayingElapsed = data.elapsed;
		    	that.nowPlayingItem = data.songid;

		    	that.stateRepeat = data.repeat;
		    	that.stateRandom = data.random;
		    	that.stateConsume = data.consume;

		    	// no need to update this stuff in case local player is active...
		    	if(window.sliMpd.currentPlayer.mode === 'mpd') {

			    	['repeat', 'random', 'consume'].forEach(function(prop) {
					    if(data[prop] == '1') {
			    			$('.mpd-status-'+prop, that.$el).addClass('active');
				    	} else {
				    		$('.mpd-status-'+prop, that.$el).removeClass('active');
				    	}
					});

					that.setPlayPauseIcon();

					// TODO: interpolate nowPlayingElapsed independent frpm poll interval
			    	$('.mpd-status-elapsed').text(that.formatTime(that.nowPlayingElapsed));
			    	$('.mpd-status-total').text(that.formatTime(that.nowPlayingDuration));

			    	// animate from 0 to 100, onUpdate -> change Text
					that.timeLineLight = new TimelineLite();
					that.trackAnimation.currentPosPerc = 0;
					that.timeLineLight.to(that.trackAnimation, that.nowPlayingDuration, {
					  	currentPosPerc: 100,
					  	ease: Linear.easeNone,
					  	onUpdate: that.updateSlider,
					  	onUpdateScope: that
					});

			    	if(that.nowPlayingState == 'play') {
			    		that.timelineSetValue(that.nowPlayingPercent);
					} else {
			    		that.timeLineLight.pause();
			    	}
			    	that.drawTimeGrid();
		    	}

		    	// update trackinfo only onTrackChange()
		    	if(that.previousPlayingItem != that.nowPlayingItem) {
		    		that.previousPlayingItem = that.nowPlayingItem
		    		that.redraw('');
		    		that.refreshInterval();
		    		return;
		    	}
		        that.poller = setTimeout(that.poll, ((window.sliMpd.currentPlayer.mode === 'mpd') ? that.intervalActive:that.intervalInactive));
		    });
		},

        onRedrawComplete : function(item) {
        	//this.refreshInterval(); // TODO: why is this doubling our interval?
        	var that = this;
        	$('.mpd-ctrl-seekbar').on('click', function(e){
				// TODO: how to respect parents padding (15px) on absolute positioned div with width 100% ?
				var percent = Math.round((e.pageX - $(this).offset().left) / (($(this).width()+15)/100));
				$('.mpd-status-progressbar', that.$el).css('width', 'calc('+ percent+'% - 15px)');
				that.process({'action': 'seek', 'mpdurl' : '/mpdctrl/seekPercent/' + percent});
				that.timelineSetValue(percent);
			});
			window.sliMpd.notify({'notify':1, 'message': 'MPD playing: ' + $('.player-mpd .now-playing-string').text()});
        	window.sliMpd.modules.AbstractPlayer.prototype.onRedrawComplete.call(this, item);
        },

        // TODO: make markup more generic and move this to AbstractPlayer for usage in both players (local+mpd)
        setPlayPauseIcon : function(item) {
			if(this.nowPlayingState == 'play') {
				$('.mpd-status-playpause', this.$el).addClass('fa-pause');
				$('.mpd-status-playpause', this.$el).removeClass('fa-play');
			} else {
				$('.mpd-status-playpause', this.$el).removeClass('fa-pause');
				$('.mpd-status-playpause', this.$el).addClass('fa-play');
			}
			window.sliMpd.modules.AbstractPlayer.prototype.setPlayPauseIcon.call(this, item);
		},

		formatTime : function(seconds) {
			if(typeof seconds == 'undefined') {
				return '-- : --';
			}
			var seconds 	= Math.round(seconds);
			var hour 		= Math.floor(seconds / 3600);
			var minutes 	= Math.floor(seconds / 60) % 60;
			seconds 		= seconds % 60;

			if (hour > 0)	return hour + ':' + this.zeroPad(minutes, 2) + ':' + this.zeroPad(seconds, 2);
			else			return minutes + ':' + this.zeroPad(seconds, 2);
		},

		zeroPad : function(number, n) {
			var zeroPad = '' + number;
			while(zeroPad.length < n) {
				zeroPad = '0' + zeroPad;
			}
			return zeroPad;
		},

		timelineSetValue : function(value) {
			this.timeLineLight.progress(value/100);
			window.sliMpd.modules.AbstractPlayer.prototype.timelineSetValue.call(this, value);
		},

		updateSlider : function(item) {
			// TODO: how to respect parents padding on absolute positioned div with width 100% ?
			$('.mpd-status-progressbar').css('width', 'calc('+ this.timeLineLight.progress() *100 +'% - 15px)');
			window.sliMpd.modules.AbstractPlayer.prototype.updateSlider.call(this, item);
		}
    });

})();
<|endoftext|>


<commit_before>'use strict';

(function() {

  var addBrowserContainer = function(Y) {
    var docBody = Y.one(document.body);
    Y.Node.create('
' +
        '
').appendTo(docBody);
  };

  describe('browser fullscreen view', function() {
    var browser, FullScreen, view, views, Y;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-views',
          'juju-browser',
          'subapp-browser-fullscreen', function(Y) {
            browser = Y.namespace('juju.browser');
            views = Y.namespace('juju.browser.views');
            FullScreen = views.FullScreen;
            done();
          });
    });

    beforeEach(function() {
      addBrowserContainer(Y);
      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };

    });

    afterEach(function() {
      view.destroy();
      Y.one('#subapp-browser').remove(true);
      delete window.juju_config;
    });

    it('knows that it is fullscreen', function() {
      view = new FullScreen();
      view.isFullscreen().should.equal(true);
    });

    // Ensure the search results are rendered inside the container.
    it('must correctly render the initial browser ui', function() {
      var container = Y.one('#subapp-browser');

      view = new FullScreen();
      view.render(container);

      // And the hide button is rendered to the container node.
      assert.isTrue(Y.Lang.isObject(container.one('#bws-fullscreen')));
      // Also verify that the search widget has rendered into the view code.
      assert.isTrue(Y.Lang.isObject(container.one('input')));
    });

  });
})();

(function() {
  var addBrowserContainer = function(Y) {
    var docBody = Y.one(document.body);
    Y.Node.create('
' +
        '
').appendTo(docBody);
  };

  describe('browser sidebar view', function() {
    var Y, browser, view, views, Sidebar;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-browser',
          'juju-views',
          'node-event-simulate',
          'subapp-browser-sidebar',
          function(Y) {
            browser = Y.namespace('juju.browser');
            views = Y.namespace('juju.browser.views');
            Sidebar = views.Sidebar;
            done();
          });
    });

    beforeEach(function() {
      addBrowserContainer(Y);
      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };
    });

    afterEach(function() {
      view.destroy();
      Y.one('#subapp-browser').remove(true);
      delete window.juju_config;
    });

    it('knows that it is not fullscreen', function() {
      view = new Sidebar();
      view.isFullscreen().should.equal(false);
    });

    it('must correctly render the initial browser ui', function() {
      var container = Y.one('#subapp-browser');
      view = new Sidebar();

      // mock out the data source on the view so that it won't actually make a
      // request.
      var emptyData = {
        responseText: Y.JSON.stringify({
          result: {
            'new': [],
            slider: []
          }
        })
      };

      view.get('store').set(
          'datasource',
          new Y.DataSource.Local({source: emptyData}));
      view.render(container);

      // And the hide button is rendered to the container node.
      assert.isTrue(Y.Lang.isObject(container.one('#bws-sidebar')));
      // Also verify that the search widget has rendered into the view code.
      assert.isTrue(Y.Lang.isObject(container.one('input')));
    });

  });
})();

(function() {
  describe('browser app', function() {
    var Y, app, browser;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'app-subapp-extension',
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            browser = Y.namespace('juju.subapps');
            done();
          });
    });

    afterEach(function() {
      if (app) {
        app.destroy();
      }
    });

    it('verify that route callables exist', function() {
      app = new browser.Browser();
      Y.each(app.get('routes'), function(route) {
        assert.isTrue(typeof app[route.callback] === 'function');
      });
    });

  });

  describe.only('browser subapp display tree', function() {
    var Y, browser, hits, ns, resetHits;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'app-subapp-extension',
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            browser = Y.namespace('juju.subapps');

            resetHits = function() {
              hits = {
                fullscreen: false,
                sidebar: false,
                renderCharmDetails: false,
                renderEditorial: false,
                renderSearchResults: false
              };
            };
            done();
          });
    });

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            ns = Y.namespace('juju.subapps');
            done();
          });
    });

    beforeEach(function() {
      var docBody = Y.one(document.body);
      Y.Node.create('
' +
          '
').appendTo(docBody);

      // Track which render functions are hit.
      resetHits();

      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };

      browser = new ns.Browser();
      // Block out each render target so we only track it was hit.
      browser.renderCharmDetails = function() {
        hits.renderCharmDetails = true;
      };
      browser.renderEditorial = function() {
        hits.renderEditorial = true;
      };
      browser.renderSearchResults = function() {
        hits.renderSearchResults = true;
      };
      // showView needs to be hacked because it does the rendering of
      // fullscreen/sidebar.
      browser.showView = function(view) {
        hits[view] = true;
      };
    });

    afterEach(function() {
      browser.destroy();
      Y.one('#subapp-browser').remove(true);
    });

    it('bws-sidebar dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-sidebar-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderEditorial: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-sidebar-search-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/search/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderSearchResults: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-fullscreen dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-search-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/search/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar to sidebar-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Now route through to the charmid from here and we should not hit the
      // editorial content again.
      resetHits();
      req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };

      var expected = Y.merge(hits, {
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar-details to sidebar dispatchse correctly', function() {
      var req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };

      var expected = Y.merge(hits, {});
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen to fullscreen-details dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Now route through to the charmid from here and we should not hit the
      // editorial content again.
      resetHits();
      req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };

      var expected = Y.merge(hits, {
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-details to fullscreen renders editorial', function() {
      var req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };

      var expected = Y.merge(hits, {
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar to fullscreen dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar',
        params: {
          viewmode: 'sidebar'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };

      var expected = Y.merge(hits, {
        fullscreen: true,
        renderEditorial: true
      });
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('changing the query string dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/search/',
        params: {
          viewmode: 'fullscreen'
        },
        query: {
          text: 'test'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/fullscreen/search/',
        params: {
          viewmode: 'fullscreen'
          id: 'charm/id'
        },
        query: {
          text: 'test2'
        }
      };

      var expected = Y.merge(hits, {
        fullscreen: true,
        renderSearchResults: true
      });
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

  });
})();

<commit_msg>Fix the tests for the cases around search dispatch<commit_after>'use strict';

(function() {

  var addBrowserContainer = function(Y) {
    var docBody = Y.one(document.body);
    Y.Node.create('
' +
        '
').appendTo(docBody);
  };

  describe('browser fullscreen view', function() {
    var browser, FullScreen, view, views, Y;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-views',
          'juju-browser',
          'subapp-browser-fullscreen', function(Y) {
            browser = Y.namespace('juju.browser');
            views = Y.namespace('juju.browser.views');
            FullScreen = views.FullScreen;
            done();
          });
    });

    beforeEach(function() {
      addBrowserContainer(Y);
      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };

    });

    afterEach(function() {
      view.destroy();
      Y.one('#subapp-browser').remove(true);
      delete window.juju_config;
    });

    it('knows that it is fullscreen', function() {
      view = new FullScreen();
      view.isFullscreen().should.equal(true);
    });

    // Ensure the search results are rendered inside the container.
    it('must correctly render the initial browser ui', function() {
      var container = Y.one('#subapp-browser');

      view = new FullScreen();
      view.render(container);

      // And the hide button is rendered to the container node.
      assert.isTrue(Y.Lang.isObject(container.one('#bws-fullscreen')));
      // Also verify that the search widget has rendered into the view code.
      assert.isTrue(Y.Lang.isObject(container.one('input')));
    });

  });
})();

(function() {
  var addBrowserContainer = function(Y) {
    var docBody = Y.one(document.body);
    Y.Node.create('
' +
        '
').appendTo(docBody);
  };

  describe('browser sidebar view', function() {
    var Y, browser, view, views, Sidebar;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-browser',
          'juju-views',
          'node-event-simulate',
          'subapp-browser-sidebar',
          function(Y) {
            browser = Y.namespace('juju.browser');
            views = Y.namespace('juju.browser.views');
            Sidebar = views.Sidebar;
            done();
          });
    });

    beforeEach(function() {
      addBrowserContainer(Y);
      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };
    });

    afterEach(function() {
      view.destroy();
      Y.one('#subapp-browser').remove(true);
      delete window.juju_config;
    });

    it('knows that it is not fullscreen', function() {
      view = new Sidebar();
      view.isFullscreen().should.equal(false);
    });

    it('must correctly render the initial browser ui', function() {
      var container = Y.one('#subapp-browser');
      view = new Sidebar();

      // mock out the data source on the view so that it won't actually make a
      // request.
      var emptyData = {
        responseText: Y.JSON.stringify({
          result: {
            'new': [],
            slider: []
          }
        })
      };

      view.get('store').set(
          'datasource',
          new Y.DataSource.Local({source: emptyData}));
      view.render(container);

      // And the hide button is rendered to the container node.
      assert.isTrue(Y.Lang.isObject(container.one('#bws-sidebar')));
      // Also verify that the search widget has rendered into the view code.
      assert.isTrue(Y.Lang.isObject(container.one('input')));
    });

  });
})();

(function() {
  describe('browser app', function() {
    var Y, app, browser;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'app-subapp-extension',
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            browser = Y.namespace('juju.subapps');
            done();
          });
    });

    afterEach(function() {
      if (app) {
        app.destroy();
      }
    });

    it('verify that route callables exist', function() {
      app = new browser.Browser();
      Y.each(app.get('routes'), function(route) {
        assert.isTrue(typeof app[route.callback] === 'function');
      });
    });

  });

  describe('browser subapp display tree', function() {
    var Y, browser, hits, ns, resetHits;

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'app-subapp-extension',
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            browser = Y.namespace('juju.subapps');

            resetHits = function() {
              hits = {
                fullscreen: false,
                sidebar: false,
                renderCharmDetails: false,
                renderEditorial: false,
                renderSearchResults: false
              };
            };
            done();
          });
    });

    before(function(done) {
      Y = YUI(GlobalConfig).use(
          'juju-views',
          'juju-browser',
          'subapp-browser', function(Y) {
            ns = Y.namespace('juju.subapps');
            done();
          });
    });

    beforeEach(function() {
      var docBody = Y.one(document.body);
      Y.Node.create('
' +
          '
').appendTo(docBody);

      // Track which render functions are hit.
      resetHits();

      // Mock out a dummy location for the Store used in view instances.
      window.juju_config = {
        charmworldURL: 'http://localhost'
      };

      browser = new ns.Browser();
      // Block out each render target so we only track it was hit.
      browser.renderCharmDetails = function() {
        hits.renderCharmDetails = true;
      };
      browser.renderEditorial = function() {
        hits.renderEditorial = true;
      };
      browser.renderSearchResults = function() {
        hits.renderSearchResults = true;
      };
      // showView needs to be hacked because it does the rendering of
      // fullscreen/sidebar.
      browser.showView = function(view) {
        hits[view] = true;
      };
    });

    afterEach(function() {
      browser.destroy();
      Y.one('#subapp-browser').remove(true);
    });

    it('bws-sidebar dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-sidebar-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderEditorial: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-sidebar-search-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/search/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        sidebar: true,
        renderSearchResults: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('bws-fullscreen dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-search-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/search/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      var expected = Y.merge(hits, {
        fullscreen: true,
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar to sidebar-charmid dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Now route through to the charmid from here and we should not hit the
      // editorial content again.
      resetHits();
      req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };

      var expected = Y.merge(hits, {
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar-details to sidebar dispatchse correctly', function() {
      var req = {
        path: '/bws/sidebar/precise/apache2-2',
        params: {
          viewmode: 'sidebar',
          id: 'precise/apache2-2'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/sidebar/',
        params: {
          viewmode: 'sidebar'
        }
      };

      var expected = Y.merge(hits, {});
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen to fullscreen-details dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Now route through to the charmid from here and we should not hit the
      // editorial content again.
      resetHits();
      req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };

      var expected = Y.merge(hits, {
        renderCharmDetails: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('fullscreen-details to fullscreen renders editorial', function() {
      var req = {
        path: '/bws/fullscreen/precise/apache2-2',
        params: {
          viewmode: 'fullscreen',
          id: 'precise/apache2-2'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };

      var expected = Y.merge(hits, {
        renderEditorial: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('sidebar to fullscreen dispatches correctly', function() {
      var req = {
        path: '/bws/sidebar',
        params: {
          viewmode: 'sidebar'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req = {
        path: '/bws/fullscreen/',
        params: {
          viewmode: 'fullscreen'
        }
      };

      var expected = Y.merge(hits, {
        fullscreen: true,
        renderEditorial: true
      });
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('changing the query string dispatches correctly', function() {
      var req = {
        path: '/bws/fullscreen/search/',
        params: {
          viewmode: 'fullscreen'
        },
        query: {
          text: 'test'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();
      req.query.text = 'test2';

      var expected = Y.merge(hits, {
        renderSearchResults: true
      });
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('no change to query string does not redraw', function() {
      var req = {
        path: '/bws/fullscreen/search/',
        params: {
          viewmode: 'fullscreen'
        },
        query: {
          text: 'test'
        }
      };
      browser.routeView(req, undefined, function() {});

      // Reset the hits and we should not redraw anything to update the view.
      resetHits();

      var expected = Y.merge(hits);
      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

    it('no querystring still searched', function() {
      var req = {
        path: '/bws/fullscreen/search/',
        params: {
          viewmode: 'fullscreen'
        }
      };

      var expected = Y.merge(hits, {
        fullscreen: true,
        renderSearchResults: true
      });

      browser.routeView(req, undefined, function() {});
      assert.deepEqual(hits, expected);
    });

  });
})();

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Scale',
  extendsModel: 'foam.graphics.Circle',
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Snake',
  extendsModel: 'foam.graphics.CView',
  requires: [
    'foam.demos.supersnake.Scale',
    'foam.demos.supersnake.Laser'
  ],
  imports: [
    'game',
    'timer',
    'R'
  ],

  properties: [
//    { name: 'scales', factory: function() { return []; } },
    { name: 'sx', defaultValue: 240 },
    { name: 'sy', defaultValue: 240 },
    { name: 'vx', defaultValue: 1 },
    { name: 'vy', defaultValue: 0 },
    { name: 'length', defaultValue: 5 }
  ],
  methods: [
    function init() {
      this.SUPER();

      this.timer.i$.addListener(this.tick);
    },
    function up()    { this.vy = -1; this.vx =  0; },
    function down()  { this.vy =  1; this.vx =  0; },
    function left()  { this.vy =  0; this.vx = -1; },
    function right() { this.vy =  0; this.vx =  1; },
    function fire () {
      this.Laser.create({x: this.sx, y: this.sy, vx: this.vx, vy: this.vy});
    },
    function intersects(o) {
      if ( ! this.children.length ) return false;
      return this.children[this.children.length-1].intersects(o);
    }
  ],
  listeners: [
    {
      name: 'tick',
      isMerged: 150,
      code: function() {
        this.sx += this.vx * 2 * this.R;
        this.sy += this.vy * 2 * this.R;
        if ( this.children.length )
          this.children[this.children.length-1].color = 'green';
        this.addChild(this.Scale.create({x: this.sx, y: this.sy, radius: this.R, color: 'red'}));
        if ( this.children.length > this.length )
          this.children.shift();
      }
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Food',
  extendsModel: 'foam.graphics.Circle',
  properties: [
    { name: 'color', defaultValue: 'darkblue' }
  ],
  methods: [
    function init() {
      this.SUPER();

      Movement.animate(15000, function() {
        this.r = 0;
      }.bind(this))();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Mushroom',
  extendsModel: 'foam.graphics.Circle',
  requires: [ 'foam.graphics.Rectangle' ],
  imports: [ 'R as r' ],
  properties: [
    { name: 'color',    defaultValue: 'red' },
    { name: 'endAngle', defaultValue: Math.PI },
    'stem'
  ],
  methods: [
    function init() {
      this.SUPER();

      this.addChild(this.stem = this.Rectangle.create({
        x: -7.5,
        y: -0.5,
        width: 15,
        height: 20,
        background: 'gray'
      }));
    },
    function explode() {
      this.stem.background = 'red';
      Movement.animate(200, function() {
        this.scaleX = this.scaleY = 30;
        this.alpha = 0;
        this.a = Math.PI * 1.5;
        this.stem.alpha = 0;
      }.bind(this)/*, function() { this.table.removeChild(o2); }.bind(this)*/)();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Laser',
  extendsModel: 'foam.graphics.Circle',
  imports: [ 'game' ],
  properties: [
    { name: 'color',  defaultValue: 'yellow' },
    { name: 'r',      defaultValue: 12 },
    'vx', 'vy'
  ],
  methods: [
    function init() {
      this.SUPER();

      this.game.addChild(this);
      Movement.animate(5000, function() {
        this.x += 2000 * this.vx;
        this.y += 2000 * this.vy;
      }.bind(this))();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Game',
  extendsModel: 'foam.ui.View',

  requires: [
    'foam.util.Timer',
    'foam.demos.supersnake.Laser',
    'foam.demos.supersnake.Snake',
    'foam.demos.supersnake.Mushroom',
    'foam.demos.supersnake.Food',
    'foam.graphics.CView',
    'foam.graphics.Rectangle',
    'foam.physics.Collider'
  ],

  exports: [
    'R',
    'timer',
    'as game'
  ],

  constants: { R: 20 },

  properties: [
    {
      name: 'timer',
      factory: function() { return this.Timer.create(); }
    },
    {
      name: 'width',
      defaultValue: 1600
    },
    {
      name: 'height',
      defaultValue: 800
    },
    {
      name: 'snake',
      factory: function() { return this.Snake.create(); }
    },
    {
      name: 'table',
      factory: function() {
        return this.CView.create({background: 'lightblue', width: window.innerWidth, height: window.innerHeight}).addChildren(
          this.snake);
      }
    },
    {
      name: 'collider',
      factory: function() { return this.Collider.create(); }
    }
  ],

  listeners: [
    {
      name: 'tick',
      code: function(_, _, _, t) {
        if ( t % 100 == 0 ) this.addFood();
        if ( Math.random() < 0.02 ) this.addMushroom();
      }
    }
  ],

  actions: [
    {
      name: 'up',
      keyboardShortcuts: [ 38 /* up arrow */, 'w' ],
      action: function() { this.snake.up(); }
    },
    {
      name: 'down',
      keyboardShortcuts: [ 40 /* down arrow */, 's' ],
      action: function() { this.snake.down(); }
    },
    {
      name: 'left',
      keyboardShortcuts: [ 37 /* left arrow */, 'a' ],
      action: function() { this.snake.left(); }
    },
    {
      name: 'right',
      keyboardShortcuts: [ 39 /* right arrow */, 'd' ],
      action: function() { this.snake.right(); }
    },
    {
      name: 'fire',
      keyboardShortcuts: [ ' ', 'x' ],
      action: function() { this.snake.fire(); }
    }
  ],

  templates: [
    function toHTML() {/*

%%table

    */}
  ],

  methods: [
    function init() {
      this.SUPER();

//      this.addChild(this.collider);

      this.timer.i$.addListener(this.tick);
      this.timer.start();

      // Setup Physics
      this.collider.add(this.snake);
      this.collider.collide = function(o1, o2) {
        if ( this.Laser.isInstance(o2) || this.Snake.isInstance(o2) ) {
          var tmp = o1;
          o1 = o2;
          o2 = tmp;
        }
        if ( this.Laser.isInstance(o1) ) {
          if ( this.Mushroom.isInstance(o2) ) {
            o2.explode();
          } else if ( this.Food.isInstance(o2) ) {
          }
        }
        if ( this.Snake.isInstance(o1) && this.Mushroom.isInstance(o2) ) {
          if ( o2.scaleX == 1 )
            this.gameOver();
          else
            this.removeChild(o2);
        }
        if ( this.Snake.isInstance(o1) && this.Food.isInstance(o2) ) {
          this.removeChild(o2);
          this.snake.length++;
        }
//        console.log('BANG', o1, o2);
      }.bind(this);
      this.collider.start();

      window.game = this;
    },

    function gameOver() {
      this.timer.stop();
//      this.collider.stop(); // TODO: add stop() method to collider
      this.table.background='orange';
    },

    function addChild(c) {
      this.table.addChild(c);
      if ( c.intersects ) this.collider.add(c);
    },

    function removeChild(c) {
      this.table.removeChild(c);
      if ( c.intersects ) this.collider.remove(c);
    },

    function addFood() {
      var R = this.R;
      var f = this.Food.create({
        x: Math.round(1+Math.random()*(this.table.width -4*R)/R)*2*R,
        y: Math.round(1+Math.random()*(this.table.height-4*R)/R)*2*R,
      });
      this.addChild(f);
    },

    function addMushroom() {
      var R = this.R;
      var m = this.Mushroom.create({
        x: Math.round(1+Math.random()*(this.table.width -4*R)/R)*2*R,
        y: Math.round(1+Math.random()*(this.table.height-4*R)/R)*2*R,
        scaleX: 0.1,
        scaleY: 0.1});

      Movement.animate(7000, function() {
        m.scaleX = m.scaleY = 1;
      })();

      this.addChild(m);
    }
  ]
});
<commit_msg>Added Robot to SuperSnake.<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Scale',
  extendsModel: 'foam.graphics.Circle',
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Snake',
  extendsModel: 'foam.graphics.CView',
  requires: [
    'foam.demos.supersnake.Scale',
    'foam.demos.supersnake.Laser'
  ],
  imports: [
    'game',
    'timer',
    'R'
  ],

  properties: [
//    { name: 'scales', factory: function() { return []; } },
    { name: 'sx', defaultValue: 240 },
    { name: 'sy', defaultValue: 240 },
    { name: 'vx', defaultValue: 1 },
    { name: 'vy', defaultValue: 0 },
    { name: 'length', defaultValue: 5 }
  ],
  methods: [
    function init() {
      this.SUPER();

      this.timer.i$.addListener(this.tick);
    },
    function up()    { this.vy = -1; this.vx =  0; },
    function down()  { this.vy =  1; this.vx =  0; },
    function left()  { this.vy =  0; this.vx = -1; },
    function right() { this.vy =  0; this.vx =  1; },
    function fire () {
      this.Laser.create({x: this.sx, y: this.sy, vx: this.vx, vy: this.vy});
    },
    function intersects(o) {
      if ( ! this.children.length ) return false;
      return this.children[this.children.length-1].intersects(o);
    }
  ],
  listeners: [
    {
      name: 'tick',
      isMerged: 150,
      code: function() {
        this.sx += this.vx * 2 * this.R;
        this.sy += this.vy * 2 * this.R;
        if ( this.children.length )
          this.children[this.children.length-1].color = 'green';
        this.addChild(this.Scale.create({x: this.sx, y: this.sy, radius: this.R, color: 'red'}));
        if ( this.children.length > this.length )
          this.children.shift();
      }
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Food',
  extendsModel: 'foam.graphics.Circle',
  properties: [
    { name: 'color', defaultValue: 'darkblue' }
  ],
  methods: [
    function init() {
      this.SUPER();

      Movement.animate(15000, function() {
        this.r = 0;
      }.bind(this))();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Mushroom',
  extendsModel: 'foam.graphics.Circle',
  requires: [ 'foam.graphics.Rectangle' ],
  imports: [ 'R as r' ],
  properties: [
    { name: 'color',    defaultValue: 'red' },
    { name: 'endAngle', defaultValue: Math.PI },
    'stem'
  ],
  methods: [
    function init() {
      this.SUPER();

      this.addChild(this.stem = this.Rectangle.create({
        x: -7.5,
        y: -0.5,
        width: 15,
        height: 20,
        background: 'gray'
      }));
    },
    function explode() {
      this.stem.background = 'red';
      Movement.animate(200, function() {
        this.scaleX = this.scaleY = 30;
        this.alpha = 0;
        this.a = Math.PI * 1.5;
        this.stem.alpha = 0;
      }.bind(this)/*, function() { this.table.removeChild(o2); }.bind(this)*/)();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Laser',
  extendsModel: 'foam.graphics.Circle',
  imports: [ 'game' ],
  properties: [
    { name: 'color',  defaultValue: 'yellow' },
    { name: 'r',      defaultValue: 12 },
    'vx', 'vy'
  ],
  methods: [
    function init() {
      this.SUPER();

      this.game.addChild(this);
      Movement.animate(5000, function() {
        this.x += 2000 * this.vx;
        this.y += 2000 * this.vy;
      }.bind(this))();
    }
  ]
});

CLASS({
  package: 'foam.demos.supersnake',
  name: 'Game',
  extendsModel: 'foam.ui.View',

  requires: [
    'foam.util.Timer',
    'foam.demos.supersnake.Robot',
    'foam.demos.supersnake.Laser',
    'foam.demos.supersnake.Snake',
    'foam.demos.supersnake.Mushroom',
    'foam.demos.supersnake.Food',
    'foam.graphics.CView',
    'foam.graphics.Rectangle',
    'foam.physics.Collider'
  ],

  exports: [
    'R',
    'timer',
    'as game'
  ],

  constants: { R: 20 },

  properties: [
    {
      name: 'timer',
      factory: function() { return this.Timer.create(); }
    },
    {
      name: 'width',
      defaultValue: 1600
    },
    {
      name: 'height',
      defaultValue: 800
    },
    {
      name: 'snake',
      factory: function() { return this.Snake.create(); }
    },
    {
      name: 'table',
      factory: function() {
        return this.CView.create({background: 'lightblue', width: window.innerWidth, height: window.innerHeight}).addChildren(
          this.snake);
      }
    },
    {
      name: 'collider',
      factory: function() { return this.Collider.create(); }
    }
  ],

  listeners: [
    {
      name: 'tick',
      code: function(_, _, _, t) {
        if ( t % 100 == 0 ) this.addFood();
        if ( Math.random() < 0.02 ) this.addMushroom();
      }
    }
  ],

  actions: [
    {
      name: 'up',
      keyboardShortcuts: [ 38 /* up arrow */, 'w' ],
      action: function() { this.snake.up(); }
    },
    {
      name: 'down',
      keyboardShortcuts: [ 40 /* down arrow */, 's' ],
      action: function() { this.snake.down(); }
    },
    {
      name: 'left',
      keyboardShortcuts: [ 37 /* left arrow */, 'a' ],
      action: function() { this.snake.left(); }
    },
    {
      name: 'right',
      keyboardShortcuts: [ 39 /* right arrow */, 'd' ],
      action: function() { this.snake.right(); }
    },
    {
      name: 'fire',
      keyboardShortcuts: [ ' ', 'x' ],
      action: function() { this.snake.fire(); }
    }
  ],

  templates: [
    function toHTML() {/*

%%table

    */}
  ],

  methods: [
    function init() {
      this.SUPER();

//      this.addChild(this.collider);

      this.timer.i$.addListener(this.tick);
      this.timer.start();

      this.table.addChild(this.Robot.create({x:200, y:200}));

      // Setup Physics
      this.collider.add(this.snake);
      this.collider.collide = function(o1, o2) {
        if ( this.Laser.isInstance(o2) || this.Snake.isInstance(o2) ) {
          var tmp = o1;
          o1 = o2;
          o2 = tmp;
        }
        if ( this.Laser.isInstance(o1) ) {
          if ( this.Mushroom.isInstance(o2) ) {
            o2.explode();
          } else if ( this.Food.isInstance(o2) ) {
          }
        }
        if ( this.Snake.isInstance(o1) && this.Mushroom.isInstance(o2) ) {
          if ( o2.scaleX == 1 )
            this.gameOver();
          else
            this.removeChild(o2);
        }
        if ( this.Snake.isInstance(o1) && this.Food.isInstance(o2) ) {
          this.removeChild(o2);
          this.snake.length++;
        }
//        console.log('BANG', o1, o2);
      }.bind(this);
      this.collider.start();

      window.game = this;
    },

    function gameOver() {
      this.timer.stop();
//      this.collider.stop(); // TODO: add stop() method to collider
      this.table.background='orange';
    },

    function addChild(c) {
      this.table.addChild(c);
      if ( c.intersects ) this.collider.add(c);
    },

    function removeChild(c) {
      this.table.removeChild(c);
      if ( c.intersects ) this.collider.remove(c);
    },

    function addFood() {
      var R = this.R;
      var f = this.Food.create({
        x: Math.round(1+Math.random()*(this.table.width -4*R)/R)*2*R,
        y: Math.round(1+Math.random()*(this.table.height-4*R)/R)*2*R,
      });
      this.addChild(f);
    },

    function addMushroom() {
      var R = this.R;
      var m = this.Mushroom.create({
        x: Math.round(1+Math.random()*(this.table.width -4*R)/R)*2*R,
        y: Math.round(1+Math.random()*(this.table.height-4*R)/R)*2*R,
        scaleX: 0.1,
        scaleY: 0.1});

      Movement.animate(7000, function() {
        m.scaleX = m.scaleY = 1;
      })();

      this.addChild(m);
    }
  ]
});
<|endoftext|>