<commit_before>/**
 * Mock session service for sessionService
 */
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var utils = require('../../util/utils');

var EXPORT_INCLUDE_FIELDS = ['id', 'frontendId', 'uid', 'settings'];

/**
 * Service that maintains local sessions and the communiation with frontend
 * server.
 *
 * LocalSessionService would be created in each server process and maintains
 * local sessions for current process and communicates with the relative
 * frontend servers.
 *
 * LocalSessionService instance could be accessed by
 * `app.get('localSessionService')`.
 *
 * @class
 * @constructor
 */
var LocalSessionService = function(app) {
  this.app = app;
};

module.exports = LocalSessionService;

LocalSessionService.prototype.create = function(opts) {
  if(!opts) {
    throw new Error('opts should not be empty.');
  }
  return new LocalSession(opts, this);
};

/**
 * Get local session by frontend server id and session id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   sid        session id
 * @param  {Function} cb         callback function. args: cb(err, localSession)
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.get = function(frontendId, sid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'getLocalSessionBySid';
  var args = [sid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, localSessionCB.bind(null, this, cb));

};

/**
 * Get local session by frontend server id and user id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   uid        user id binded with the session
 * @param  {Function} cb         callback function. args: cb(err, localSession)
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.getByUid = function(frontendId, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'getLocalSessionByUid';
  var args = [uid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, localSessionCB.bind(null, this, cb));
};

/**
 * Kick a session by session id.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.kickBySid = function(frontendId, sid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'kickBySid';
  var args = [sid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Kick a session by user id.
 *
 * @param  {String}          frontendId cooperating frontend server id
 * @param  {Number|String}   uid        user id
 * @param  {Function}        cb         callback function
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.kickByUid = function(frontendId, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'kickByUid';
  var args = [uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Bind the session with the specified user id. It would finally invoke the
 * the sessionService.bind in the cooperating frontend server.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   uid        user id
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.bind = function(frontendId, sid, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'bind';
  var args = [sid, uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push the specified local customized change to the global session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   key        key in session that should be push
 * @param  {Object}   value      value in session, primitive js object
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.push = function(frontendId, sid, key, value, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'push';
  var args = [sid, key, value];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push all the local customized changes to the global session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Object}   settings   key/values in session that should be push
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.pushAll = function(frontendId, sid, settings, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'pushAll';
  var args = [sid, settings];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

var rpcInvoke = function(app, sid, namespace, service, method, args, cb) {
  app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: args}, cb);
};

/**
 * LocalSession is the proxy for global session passed to handlers and it helps to keep
 * the key/value pairs for the server local. Global session locates in frontend server
 * and should not be accessed directly.
 *
 * The mainly operation on local session should be read and any changes happen in local
 * session is local and would be discarded in next request. You have to push the
 * changes to the frontend manually if necessary. Any push would overwrite the last push
 * of the same key silently and the changes would be saw in next request.
 * And you have to make sure the transaction outside if you would push the session
 * concurrently in different processes.
 *
 * See the api below for more details.
 *
 * @class
 * @constructor
 */
var LocalSession = function(opts, service) {
  EventEmitter.call(this);
  for(var f in opts) {
    this[f] = opts[f];
  }
  this.__sessionService__ = service;
};

util.inherits(LocalSession, EventEmitter);

/**
 * Bind current session with the user id. It would push the uid to frontend
 * server and bind  uid to the global session.
 *
 * @param  {Number|String}   uid user id
 * @param  {Function} cb  callback function
 *
 * @memberOf LocalSession
 */
LocalSession.prototype.bind = function(uid, cb) {
  var self = this;
  this.__sessionService__.bind(this, uid, function(err) {
    if(!err) {
      self.uid = uid;
    }
    cb(err);
  });
};

/**
 * Set the key/value into local session.
 *
 * @param {String} key   key
 * @param {Object} value value
 */
LocalSession.prototype.set = function(key, value) {
  this.settings[key] = value;
};

/**
 * Get the value from local session by key.
 *
 * @param  {String} key key
 * @return {Object}     value
 */
LocalSession.prototype.get = function(key) {
  return this.settings[key];
};

/**
 * Push the key/value in local session to the global session.
 *
 * @param  {String}   key key
 * @param  {Function} cb  callback function
 */
LocalSession.prototype.push = function(key, cb) {
  this.__sessionService__.push(this.frontendId, this.id, key, this.get(key), cb);
};

/**
 * Push all the key/values in local session to the global session.
 *
 * @param  {Function} cb callback function
 */
LocalSession.prototype.pushAll = function(cb) {
  this.__sessionService__.pushAll(this.frontendId, this.id, this.settings, cb);
};

/**
 * Export the key/values for serialization.
 *
 * @api private
 */
LocalSession.prototype.export = function() {
  var res = {}, f;
  for(var i=0, l=EXPORT_INCLUDE_FIELDS.length; i<l; i++) {
    f = EXPORT_INCLUDE_FIELDS[i];
    res[f] = this[f];
  }

  return res;
};

var localSessionCB = function(service, cb, err, sinfo) {
  if(err) {
    cb(err);
    return;
  }

  if(!sinfo) {
    cb();
    return;
  }

  cb(null, service.create(sinfo));
};
<commit_msg>fix session.bind bug in backend server<commit_after>/**
 * Mock session service for sessionService
 */
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var utils = require('../../util/utils');

var EXPORT_INCLUDE_FIELDS = ['id', 'frontendId', 'uid', 'settings'];

/**
 * Service that maintains local sessions and the communiation with frontend
 * server.
 *
 * LocalSessionService would be created in each server process and maintains
 * local sessions for current process and communicates with the relative
 * frontend servers.
 *
 * LocalSessionService instance could be accessed by
 * `app.get('localSessionService')`.
 *
 * @class
 * @constructor
 */
var LocalSessionService = function(app) {
  this.app = app;
};

module.exports = LocalSessionService;

LocalSessionService.prototype.create = function(opts) {
  if(!opts) {
    throw new Error('opts should not be empty.');
  }
  return new LocalSession(opts, this);
};

/**
 * Get local session by frontend server id and session id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   sid        session id
 * @param  {Function} cb         callback function. args: cb(err, localSession)
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.get = function(frontendId, sid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'getLocalSessionBySid';
  var args = [sid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, localSessionCB.bind(null, this, cb));

};

/**
 * Get local session by frontend server id and user id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   uid        user id binded with the session
 * @param  {Function} cb         callback function. args: cb(err, localSession)
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.getByUid = function(frontendId, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'getLocalSessionByUid';
  var args = [uid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, localSessionCB.bind(null, this, cb));
};

/**
 * Kick a session by session id.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.kickBySid = function(frontendId, sid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'kickBySid';
  var args = [sid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Kick a session by user id.
 *
 * @param  {String}          frontendId cooperating frontend server id
 * @param  {Number|String}   uid        user id
 * @param  {Function}        cb         callback function
 *
 * @memberOf SessionService
 */
LocalSessionService.prototype.kickByUid = function(frontendId, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'kickByUid';
  var args = [uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Bind the session with the specified user id. It would finally invoke the
 * the sessionService.bind in the cooperating frontend server.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   uid        user id
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.bind = function(frontendId, sid, uid, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'bind';
  var args = [sid, uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push the specified local customized change to the global session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   key        key in session that should be push
 * @param  {Object}   value      value in session, primitive js object
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.push = function(frontendId, sid, key, value, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'push';
  var args = [sid, key, value];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push all the local customized changes to the global session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Object}   settings   key/values in session that should be push
 * @param  {Function} cb         callback function
 *
 * @memberOf SessionService
 * @api private
 */
LocalSessionService.prototype.pushAll = function(frontendId, sid, settings, cb) {
  var namespace = 'sys';
  var service = 'sessionRemote';
  var method = 'pushAll';
  var args = [sid, settings];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

var rpcInvoke = function(app, sid, namespace, service, method, args, cb) {
  app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: args}, cb);
};

/**
 * LocalSession is the proxy for global session passed to handlers and it helps to keep
 * the key/value pairs for the server local. Global session locates in frontend server
 * and should not be accessed directly.
 *
 * The mainly operation on local session should be read and any changes happen in local
 * session is local and would be discarded in next request. You have to push the
 * changes to the frontend manually if necessary. Any push would overwrite the last push
 * of the same key silently and the changes would be saw in next request.
 * And you have to make sure the transaction outside if you would push the session
 * concurrently in different processes.
 *
 * See the api below for more details.
 *
 * @class
 * @constructor
 */
var LocalSession = function(opts, service) {
  EventEmitter.call(this);
  for(var f in opts) {
    this[f] = opts[f];
  }
  this.__sessionService__ = service;
};

util.inherits(LocalSession, EventEmitter);

/**
 * Bind current session with the user id. It would push the uid to frontend
 * server and bind  uid to the global session.
 *
 * @param  {Number|String}   uid user id
 * @param  {Function} cb  callback function
 *
 * @memberOf LocalSession
 */
LocalSession.prototype.bind = function(uid, cb) {
  var self = this;
  this.__sessionService__.bind(this.frontendId, this.id, uid, function(err) {
    if(!err) {
      self.uid = uid;
    }
    cb(err);
  });
};

/**
 * Set the key/value into local session.
 *
 * @param {String} key   key
 * @param {Object} value value
 */
LocalSession.prototype.set = function(key, value) {
  this.settings[key] = value;
};

/**
 * Get the value from local session by key.
 *
 * @param  {String} key key
 * @return {Object}     value
 */
LocalSession.prototype.get = function(key) {
  return this.settings[key];
};

/**
 * Push the key/value in local session to the global session.
 *
 * @param  {String}   key key
 * @param  {Function} cb  callback function
 */
LocalSession.prototype.push = function(key, cb) {
  this.__sessionService__.push(this.frontendId, this.id, key, this.get(key), cb);
};

/**
 * Push all the key/values in local session to the global session.
 *
 * @param  {Function} cb callback function
 */
LocalSession.prototype.pushAll = function(cb) {
  this.__sessionService__.pushAll(this.frontendId, this.id, this.settings, cb);
};

/**
 * Export the key/values for serialization.
 *
 * @api private
 */
LocalSession.prototype.export = function() {
  var res = {}, f;
  for(var i=0, l=EXPORT_INCLUDE_FIELDS.length; i<l; i++) {
    f = EXPORT_INCLUDE_FIELDS[i];
    res[f] = this[f];
  }

  return res;
};

var localSessionCB = function(service, cb, err, sinfo) {
  if(err) {
    cb(err);
    return;
  }

  if(!sinfo) {
    cb();
    return;
  }

  cb(null, service.create(sinfo));
};
<|endoftext|>


<commit_before>'use strict';
var _ = require('lodash')
  , path = require('path')
  , pkg = require(path.join(__dirname, '../package.json'))
  , recursiveReaddir = require('recursive-readdir')
  , updateNotifier = require('update-notifier')
  , utils = require('../utils')
  , yeoman = require('yeoman-generator')
  , Generator;

Generator = module.exports = yeoman.generators.Base.extend({
  constructor: function () {
    yeoman.generators.Base.apply(this, arguments);
    this.name = arguments['0'][0];
  }
});

Generator.prototype.askForModuleName = function askForModuleName(params) {
  var done = this.async()
    , notifier;

  // check for updates async
  notifier = updateNotifier({
    packageName: pkg.name,
    packageVersion: pkg.version
  });

  // notifiy user about update, if there is an update
  notifier.notify();

  this.prompt([
    {
      type: 'list',
      name: 'module',
      message: 'Which module is this for?',
      default: this.config.get('lastUsedModule'),
      when: function () {
        return !(this.options && this.options.module);
      }.bind(this),
      choices: function () {
        var moduleDone = this.async();
        recursiveReaddir(utils.getAppDir(), function (err, files) {
          if (err) {
            throw err;
          }
          // remove non-script files
          // will remove folders such as fonts, images, styles
          // not 100% full proof, as a user could create a directory with a script file that's
          // not part of a module
          files = files.filter(function (file) {
            return file.indexOf('.coffee') >= 0 || file.indexOf('.js') >= 0 ||
              file.indexOf('.ts') >= 0 || file.indexOf('.es6') >= 0;
          });
          // only get the directories
          files = files.map(function (file) {
            return path.dirname(file);
          });
          // remove duplicates
          files = _.uniq(files);
          // remove components and types folder
          files = files.filter(function (file) {
            var ignoreDirectories = ['components', 'constants', 'controllers', 'directives', 'factories',
              'filters', 'services', 'providers', 'values', 'views']
              , i;
            for (i = 0; i < ignoreDirectories.length; i++) {
              if (file.indexOf(ignoreDirectories[i]) > -1) {
                return false;
              }
            }
            return true;
          });
          // display full name as name, but remove app/ from value
          files = files.map(function (file) {
            return {
              name: file,
              value: file.replace(utils.getAppDir() + '\\', '').replace(utils.getAppDir() + '/', '')
            };
          });
          moduleDone(files);
        });
      }
    },
    {
      name: 'url',
      message: 'What\'s the URL for this route?',
      default: function () {
        // if child state return child portion as url
        if (this.name.indexOf('.') > -1) {
          return '/' + utils.hyphenName(this.name.split('.')[1]);
        }
        return '/' + utils.hyphenName(this.name);
      }.bind(this),
      when: function () {
        return params && params.url && !this.config.get('ngRoute') && !(this.options && this.options.url);
      }.bind(this)
    },
    {
      name: 'templateUrl',
      message: 'What\'s the templateURL for this route?',
      default: function (answers) {
        var module = answers.module || this.options.module;

        return utils.normalizeModulePath(module) + '/' +
          ((this.options.structure === 'module-type' ||
            this.config.get('structure') === 'module-type') ? 'views/' : '') +
          utils.hyphenName(this.name.replace('.', '-')) + '.tpl.html';
      }.bind(this),
      when: function () {
        return params && params.templateUrl && !(this.options && this.options['template-url']);
      }.bind(this)
    }
  ], function (props) {
    this.module = props.module || this.options.module;
    this.url = props.url || this.options.url || this.name;
    this.templateUrl = props.templateUrl || this.options['template-url'];

    // if moduleName ends with a slash remove it
    if (this.module.charAt(this.module.length - 1) === '/' || this.module.charAt(this.module.length - 1) === '\\') {
      this.module = this.module.slice(0, this.module.length - 1);
    }

    // save this module to suggest later
    this.config.set('lastUsedModule', this.module);

    // prepend slash if missing
    if (this.url && (this.url.charAt(0) !== '/' && this.url.charAt(0) !== '\\')) {
      this.url = '/' + this.url;
    }

    // convert backslashes to forwardslashes for Windows
    if (this.templateUrl) {
      this.templateUrl = this.templateUrl.replace('\\', '/');
    }

    // append .tpl.html if not existing
    if (!(/[.]tpl[.]html$/).test(this.templateUrl)) {
      this.templateUrl = this.templateUrl + '.tpl.html';
    }

    done();
  }.bind(this));
};

Generator.prototype.getConfig = function getConfig() {
  var config = {
    name: this.name,
    markup: this.options.markup || this.config.get('markup'),
    appScript: this.options['app-script'] || this.config.get('appScript'),

    structure: this.options.structure || this.config.get('structure'),
    controllerAs: this.options['controller-as'] !== undefined && this.options['controller-as'] !== null ?
      this.options['controller-as'] : this.config.get('controllerAs'),

    skipController: this.options['skip-controller'] !== undefined && this.options['skip-controller'] !== null ?
      this.options['skip-controller'] : this.config.get('skipController'),

    testScript: this.options['test-script'] || this.config.get('testScript'),
    testFramework: this.config.get('testFramework'),
    e2eTestFramework: this.config.get('e2eTestFramework'),
    style: this.options.style || this.config.get('style'),
    ngRoute: this.options['ng-route'] !== undefined && this.options['ng-route'] !== null ?
      this.options['ng-route'] : this.config.get('ngRoute'),

    appName: utils.getAppName(this.config.path),
    appDir: utils.getAppDir(this.config.path),
    testDir: utils.getUnitTestDir(this.config.path),
    ctrlName: utils.ctrlName(this.name),
    humanName: utils.humanName(this.name),
    hyphenName: utils.hyphenName(this.name),
    lowerCamel: utils.lowerCamel(this.name),
    upperCamel: utils.upperCamel(this.name)
  }
  , modules;

  // only run for Angular components
  if (this.module) {
    modules = utils.extractModuleNames(this.module);
    config.modulePath = utils.normalizeModulePath(this.module);
    config.moduleName = utils.lowerCamel(modules[0]);
    config.parentModuleName = utils.lowerCamel(modules[1]);

    // create reference path to typings/tsd.d.ts from generated file
    if (config.appScript === 'ts') {
      // relative path between module path
      config.referencePath = path.relative(config.modulePath, path.dirname(this.config.path));
      // replace Windows path separators with Unix
      config.referencePath = config.referencePath.replace('\\', '/');
      config.referencePath = '../' + config.referencePath + '/typings/tsd.d.ts';
      // if module/type structure we need to go up one more folder
      if (config.structure === 'module-type' && !this.isModule) {
        config.referencePath = '../' + config.referencePath;
      }
    }
  }

  return config;
};

Generator.prototype.copyFile = function copyFile(type, component, dest, context) {
  var pluralComponent = component === 'factory' ? 'factories' : component + 's'
    , fileName, src;

  if (typeof dest === 'object') {
    context = dest;
    dest = null;
  }
  context = context || this.getConfig();
  if (!dest) {
    // test or app directory?
    dest = (type === 'unit') ? context.testDir : context.appDir;
    // append module path and type (if using module-type)
    dest = path.join(dest, context.modulePath,
      (component !== 'module' && context.structure === 'module-type') ? pluralComponent : '');

    // create file name
    fileName = context.hyphenName + '-' + component;
    if (type === 'markup') {
      fileName += '.tpl.' + context.markup;
    }
    if (type === 'src') {
      fileName += '.' + context.appScript;
    }
    if (type === 'unit') {
      fileName += '_test.' + context.testScript;
    }
    // style types ALWAYS send a dest

    // append file name to dest
    dest = path.join(dest, fileName);
  }

  if (type === 'markup') {
    src = '_' + component + '.' + context.markup;
  }
  if (type === 'src') {
    src = '_' + component + '.' + context.appScript;
  }
  if (type === 'style') {
    src = component + '.' + context.style;
  }
  if (type === 'unit') {
    src = '_spec.' + context.testScript;
  }

  this.copySimpleFile(src, dest, context);
};

Generator.prototype.copySimpleFile = function copySimpleFile(src, dest, context) {
  context = context || this.context;
  // remove underscore from templated file names
  dest = dest || src.replace(/_/g, '');

  this.fs.copyTpl(
    this.templatePath(src),
    this.destinationPath(dest),
    context
  );
};

Generator.prototype.copyMarkupFile = function copyMarkupFile(component, dest, context) {
  return this.copyFile('markup', component, dest, context);
};

Generator.prototype.copySrcFile = function copySrcFile(component, dest, context) {
  return this.copyFile('src', component, dest, context);
};

Generator.prototype.copyStyleFile = function copyStyleFile(component, dest, context) {
  return this.copyFile('style', component, dest, context);
};

Generator.prototype.copyUnitTest = function copyUnitTest(component, dest, context) {
  return this.copyFile('unit', component, dest, context);
};

Generator.prototype.copyE2e = function copyE2e(context) {
  var testScript = context.testScript === 'ts' ? 'js' : context.testScript
    , e2eFile = path.join('e2e', context.hyphenName, context.hyphenName);
  this.copySimpleFile('page.po.' + testScript, e2eFile + '.po.' + testScript, context);
  this.copySimpleFile('page_test.' + testScript, e2eFile + '_test.' + testScript, context);
};

Generator.extend = require('class-extend').extend;
<commit_msg>fix(genBase) remove decorators from module listing<commit_after>'use strict';
var _ = require('lodash')
  , path = require('path')
  , pkg = require(path.join(__dirname, '../package.json'))
  , recursiveReaddir = require('recursive-readdir')
  , updateNotifier = require('update-notifier')
  , utils = require('../utils')
  , yeoman = require('yeoman-generator')
  , Generator;

Generator = module.exports = yeoman.generators.Base.extend({
  constructor: function () {
    yeoman.generators.Base.apply(this, arguments);
    this.name = arguments['0'][0];
  }
});

Generator.prototype.askForModuleName = function askForModuleName(params) {
  var done = this.async()
    , notifier;

  // check for updates async
  notifier = updateNotifier({
    packageName: pkg.name,
    packageVersion: pkg.version
  });

  // notifiy user about update, if there is an update
  notifier.notify();

  this.prompt([
    {
      type: 'list',
      name: 'module',
      message: 'Which module is this for?',
      default: this.config.get('lastUsedModule'),
      when: function () {
        return !(this.options && this.options.module);
      }.bind(this),
      choices: function () {
        var moduleDone = this.async();
        recursiveReaddir(utils.getAppDir(), function (err, files) {
          if (err) {
            throw err;
          }
          // remove non-script files
          // will remove folders such as fonts, images, styles
          // not 100% full proof, as a user could create a directory with a script file that's
          // not part of a module
          files = files.filter(function (file) {
            return file.indexOf('.coffee') >= 0 || file.indexOf('.js') >= 0 ||
              file.indexOf('.ts') >= 0 || file.indexOf('.es6') >= 0;
          });
          // only get the directories
          files = files.map(function (file) {
            return path.dirname(file);
          });
          // remove duplicates
          files = _.uniq(files);
          // remove components and types folder
          files = files.filter(function (file) {
            var ignoreDirectories = ['components', 'constants', 'controllers', 'decorators', 'directives', 'factories',
              'filters', 'services', 'providers', 'values', 'views']
              , i;
            for (i = 0; i < ignoreDirectories.length; i++) {
              if (file.indexOf(ignoreDirectories[i]) > -1) {
                return false;
              }
            }
            return true;
          });
          // display full name as name, but remove app/ from value
          files = files.map(function (file) {
            return {
              name: file,
              value: file.replace(utils.getAppDir() + '\\', '').replace(utils.getAppDir() + '/', '')
            };
          });
          moduleDone(files);
        });
      }
    },
    {
      name: 'url',
      message: 'What\'s the URL for this route?',
      default: function () {
        // if child state return child portion as url
        if (this.name.indexOf('.') > -1) {
          return '/' + utils.hyphenName(this.name.split('.')[1]);
        }
        return '/' + utils.hyphenName(this.name);
      }.bind(this),
      when: function () {
        return params && params.url && !this.config.get('ngRoute') && !(this.options && this.options.url);
      }.bind(this)
    },
    {
      name: 'templateUrl',
      message: 'What\'s the templateURL for this route?',
      default: function (answers) {
        var module = answers.module || this.options.module;

        return utils.normalizeModulePath(module) + '/' +
          ((this.options.structure === 'module-type' ||
            this.config.get('structure') === 'module-type') ? 'views/' : '') +
          utils.hyphenName(this.name.replace('.', '-')) + '.tpl.html';
      }.bind(this),
      when: function () {
        return params && params.templateUrl && !(this.options && this.options['template-url']);
      }.bind(this)
    }
  ], function (props) {
    this.module = props.module || this.options.module;
    this.url = props.url || this.options.url || this.name;
    this.templateUrl = props.templateUrl || this.options['template-url'];

    // if moduleName ends with a slash remove it
    if (this.module.charAt(this.module.length - 1) === '/' || this.module.charAt(this.module.length - 1) === '\\') {
      this.module = this.module.slice(0, this.module.length - 1);
    }

    // save this module to suggest later
    this.config.set('lastUsedModule', this.module);

    // prepend slash if missing
    if (this.url && (this.url.charAt(0) !== '/' && this.url.charAt(0) !== '\\')) {
      this.url = '/' + this.url;
    }

    // convert backslashes to forwardslashes for Windows
    if (this.templateUrl) {
      this.templateUrl = this.templateUrl.replace('\\', '/');
    }

    // append .tpl.html if not existing
    if (!(/[.]tpl[.]html$/).test(this.templateUrl)) {
      this.templateUrl = this.templateUrl + '.tpl.html';
    }

    done();
  }.bind(this));
};

Generator.prototype.getConfig = function getConfig() {
  var config = {
    name: this.name,
    markup: this.options.markup || this.config.get('markup'),
    appScript: this.options['app-script'] || this.config.get('appScript'),

    structure: this.options.structure || this.config.get('structure'),
    controllerAs: this.options['controller-as'] !== undefined && this.options['controller-as'] !== null ?
      this.options['controller-as'] : this.config.get('controllerAs'),

    skipController: this.options['skip-controller'] !== undefined && this.options['skip-controller'] !== null ?
      this.options['skip-controller'] : this.config.get('skipController'),

    testScript: this.options['test-script'] || this.config.get('testScript'),
    testFramework: this.config.get('testFramework'),
    e2eTestFramework: this.config.get('e2eTestFramework'),
    style: this.options.style || this.config.get('style'),
    ngRoute: this.options['ng-route'] !== undefined && this.options['ng-route'] !== null ?
      this.options['ng-route'] : this.config.get('ngRoute'),

    appName: utils.getAppName(this.config.path),
    appDir: utils.getAppDir(this.config.path),
    testDir: utils.getUnitTestDir(this.config.path),
    ctrlName: utils.ctrlName(this.name),
    humanName: utils.humanName(this.name),
    hyphenName: utils.hyphenName(this.name),
    lowerCamel: utils.lowerCamel(this.name),
    upperCamel: utils.upperCamel(this.name)
  }
  , modules;

  // only run for Angular components
  if (this.module) {
    modules = utils.extractModuleNames(this.module);
    config.modulePath = utils.normalizeModulePath(this.module);
    config.moduleName = utils.lowerCamel(modules[0]);
    config.parentModuleName = utils.lowerCamel(modules[1]);

    // create reference path to typings/tsd.d.ts from generated file
    if (config.appScript === 'ts') {
      // relative path between module path
      config.referencePath = path.relative(config.modulePath, path.dirname(this.config.path));
      // replace Windows path separators with Unix
      config.referencePath = config.referencePath.replace('\\', '/');
      config.referencePath = '../' + config.referencePath + '/typings/tsd.d.ts';
      // if module/type structure we need to go up one more folder
      if (config.structure === 'module-type' && !this.isModule) {
        config.referencePath = '../' + config.referencePath;
      }
    }
  }

  return config;
};

Generator.prototype.copyFile = function copyFile(type, component, dest, context) {
  var pluralComponent = component === 'factory' ? 'factories' : component + 's'
    , fileName, src;

  if (typeof dest === 'object') {
    context = dest;
    dest = null;
  }
  context = context || this.getConfig();
  if (!dest) {
    // test or app directory?
    dest = (type === 'unit') ? context.testDir : context.appDir;
    // append module path and type (if using module-type)
    dest = path.join(dest, context.modulePath,
      (component !== 'module' && context.structure === 'module-type') ? pluralComponent : '');

    // create file name
    fileName = context.hyphenName + '-' + component;
    if (type === 'markup') {
      fileName += '.tpl.' + context.markup;
    }
    if (type === 'src') {
      fileName += '.' + context.appScript;
    }
    if (type === 'unit') {
      fileName += '_test.' + context.testScript;
    }
    // style types ALWAYS send a dest

    // append file name to dest
    dest = path.join(dest, fileName);
  }

  if (type === 'markup') {
    src = '_' + component + '.' + context.markup;
  }
  if (type === 'src') {
    src = '_' + component + '.' + context.appScript;
  }
  if (type === 'style') {
    src = component + '.' + context.style;
  }
  if (type === 'unit') {
    src = '_spec.' + context.testScript;
  }

  this.copySimpleFile(src, dest, context);
};

Generator.prototype.copySimpleFile = function copySimpleFile(src, dest, context) {
  context = context || this.context;
  // remove underscore from templated file names
  dest = dest || src.replace(/_/g, '');

  this.fs.copyTpl(
    this.templatePath(src),
    this.destinationPath(dest),
    context
  );
};

Generator.prototype.copyMarkupFile = function copyMarkupFile(component, dest, context) {
  return this.copyFile('markup', component, dest, context);
};

Generator.prototype.copySrcFile = function copySrcFile(component, dest, context) {
  return this.copyFile('src', component, dest, context);
};

Generator.prototype.copyStyleFile = function copyStyleFile(component, dest, context) {
  return this.copyFile('style', component, dest, context);
};

Generator.prototype.copyUnitTest = function copyUnitTest(component, dest, context) {
  return this.copyFile('unit', component, dest, context);
};

Generator.prototype.copyE2e = function copyE2e(context) {
  var testScript = context.testScript === 'ts' ? 'js' : context.testScript
    , e2eFile = path.join('e2e', context.hyphenName, context.hyphenName);
  this.copySimpleFile('page.po.' + testScript, e2eFile + '.po.' + testScript, context);
  this.copySimpleFile('page_test.' + testScript, e2eFile + '_test.' + testScript, context);
};

Generator.extend = require('class-extend').extend;
<|endoftext|>


<commit_before>define([
    "controller/channel",
    "model/model",
    "view/view",
    "queue/queue",
    "events/events",
    "events/states",
    "utils/strings",
    "utils/event",
    "utils/logger",
], function(Channel, Model, View, Queue, Events, States, Strings, Event, Logger) {
    /**
     * gPlayer main controller
     *
     * Responsible for the general flow and creation of the view and models
     * as well as all the events related to the actual wiring of the player logic and channel management.
     */
    var Controller = function(config) {
        this.init(config);
    };

    Controller.prototype = {
        /**
         * Referances
         */
        _view: null,
        _model: null,
        _queue: null,
        _notifications: null,

        _channels: [],
        _state: States.IDLE,

        audioCtx: null,
        mute: false,
        volume: 1,

        /**
         * Constructor function for the main controller
         *
         * Registering important instances to hold here
         *
         * @param {Object} config
         * @param {Object}
         * @return this for chaining
         */
        init: function(config) {
            Logger.debug('CONTROLLER::INIT FIRED');

            this._model = new Model(config);
            this._notifications = new Event();
            this._view = new View(this, this._notifications);
            this._queue = new Queue(this._notifications);

            this.subscribe();

            return this;
        },

        /**
         * Subscribe to interesting actions during creation
         * Called during construction
         */
        subscribe: function() {
            var self = this;

            this.getNotifications().on(Events.STATE_CHANGED, function(state) {
                self._state = state;
            });

            this.getNotifications().on(Events.PLAY, function(item) {
                console.log(['CONTROLLER::ITEM ARRIVED', item]);
                self.channelAdd(item)
                    .play();

                self.changeState(States.PLAYING);
            });

            this.getNotifications().on(Events.PAUSE, function() {
                self.changeState(States.PAUSED);
                self.pause();
            });

            this.getNotifications().on(Events.QUEUE_EMPTY, function() {
                self.changeState(States.IDLE);
                self.pause();
            });

            // translate native event for resize, for internal event
            $(window).bind('resize', function() {
                self.getNotifications().fire(Events.RESIZE);
            });

            this.getNotifications().on(Events.VOLUME_SET, function(volume) {
                self.setVolume(volume);
            });

            this.getNotifications().on(Events.JUMP_TO_SECOND, function(second) {
                self.getActiveChannel().play(second);
            });
        },

        /**
         * Bootstraps the main controller, setups objects and beings the animation loop.
         *
         * @param {HTMLElement} container
         */
        bootstrap: function(container) {
            this._model.setup(container);
            this._view.setup();
            this.loop();
        },

        /**
         * Getter for the Queue object, set on constructor
         *
         * @return {Object} Queue instance
         */
        getQueue: function() {
            return this._queue;
        },

        /**
         * Getter for the Model object, set on constructor
         *
         * @return {Object} Model instance
         */
        getModel: function() {
            return this._model;
        },

        /**
         * Getter for the Notifications object, set on constructor
         *
         * @return {Object} Notifications instance
         */
        getNotifications: function() {
            return this._notifications;
        },

        /**
         * Changes the state of the player by boardcasting the proper event on change.
         * This is the only place modifies the state of the player
         *
         * @param {String} newState (ENUM from States object)
         */
        changeState: function(newState) {
            // verify state is diffrent to avoid setting the same state
            if (newState === this._state) return;

            this.getNotifications().fire(Events.STATE_CHANGED, newState);
        },

        /**
         * Playing? Returns by checking each indevidual channel to prevent errors.
         * This is a protective check, rather than player state based.
         *
         * Can be used to check the real state of the player regardless the state, might be helpful for debuging issues
         *
         * @return {Bool}
         */
        isPlaying: function() {
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i]) && true === this._channels[i].isPlaying())
                    return true;
            }

            return false;
        },

        /**
         * Adds new channel, called when song is changed or started.
         * Stacks the instances of the channels in an array.
         * Each channel is indevidual and can be played more than 1 at once. The current only
         * state more than once channel is playing is during crossfading but more features may come, like mixin etc.
         *
         * @param {Object} item
         * @return {Object} channel instance
         */
        channelAdd: function (item) {
            var channelNew = new Channel({
                parent: this,
                item: item
            });

            this._channels.push(channelNew);

            return channelNew;
        },

        /**
         * Getter for the current active audio channel.
         * Fetched from the array of all channels. Channel 0 is the active..
         * Channel 1 is usually created only while crossfading and even then.. 0 is the active during the current usecases.
         *
         * Logic also can be changed later depending on features
         *
         * @return {Object} active channel instance
         */
        getActiveChannel: function() {
            return this._channels[0];
        },

        /**
         * Getter for the active Audio element instance
         */
        getActiveAudio: function() {
            return this.getActiveChannel().audioEl;
        },

        /**
         * onEnd is fired by one of the channels when it reaches the end of the track
         * or crossfade onBeforeEnd time triggered.
         *
         * @todo Wire by 2 events, for crossfade and regular ITEM_END. Drop the direct function call from channel to _onEnd
         * @param {Bool} crossfade
         */
        _onEnd: function(crossfade) {
            if (true === crossfade) {
                setTimeout($.proxy(function() {
                    this.activeChannelCleanup();
                }, this), this._model.crossfade.onBeforeEnd * 1000);
            } else {
                this.activeChannelCleanup();
            }

            this.getNotifications().fire(Events.PLAY_NEXT);
            //api.onEnd();
        },

        /**
         * Is player muted, or volume = 0
         *
         * @return {Bool}
         */
        isMute: function() {
            return this.mute;
        },

        /**
         * Is crossfade feature enabled?
         *
         * @return {Bool}
         */
        isCrossfade: function() {
            return this._model.crossfade.enabled;
        },

        /**
         * Getter for the volume
         * Volume ranges from 0 to 1
         *
         * @return {Number} volume
         */
        getVolume: function() {
            return this.volume;
        },

        /**
         * Sets the volume and updates all channels
         *
         * @param {Number} volume - ranges from 0 to 1 only.
         */
        setVolume: function(volume) {
            this.volume = (1 <= volume) ? 1 : volume; // can not be greater than 1
            this.mute = (0 >= volume) ? true : false;

            //updates the channel with volume value.
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i]))
                    this._channels[i].setVolume();
            }

            /*
            var interactiveObj = $('.groovy-interactive');
            interactiveObj.find('.groovy-scrubber').find('img').eq(0).css({
                transform: 'scaleY('+arg+')'
            });

            if(arg == 0){
                cthis.find('.scrub-bg-img-reflect').fadeOut('slow');
            } else {
                cthis.find('.scrub-bg-img-reflect').fadeIn('slow');
            }
            last_vol = arg;
            */
        },

        /**
         * Loop for tracking the audio and moving progress of the UI.
         * @todo Split into smaller chunks
         * @todo Move from here, not really related.
         */
        loop: function() {
            // verify everything needed for the loop exists. Run loop only when playing.
            if ('undefined' === typeof(this.getActiveChannel()) || !this.isPlaying()) {
                return this._runLoop();
            }

            var scrubberWidth = this._view.getPlayerView().components.interactive.getWidth(),
                currentTime = $('.groovy-current-time'),
                duration = $('.groovy-duration'),
                scrubberOffset = 0,
                scrubberOffsetPercent = 0,
                scrubberOffsetPixels = 0,
                timeTotal = this.getActiveChannel().getDuration(),
                timeCurr = this.getActiveChannel().getCurrentTime(),
                timeOffset = 3;

            /*
            // might not work.. audiobuffer is per channel.. odd location for referances, check!.
            if (this.audioBuffer && 'placeholder' !== this.audioBuffer) {
                timeTotal = this.audioBuffer.duration;
                timeCurr = this.audioCtx.currentTime;
                //console.log(this.audioBuffer.currentTime);
            }*/

            scrubberOffset = ((timeCurr - this._model.style.interactiveSelectOffset) / timeTotal);
            scrubberOffsetPercent = scrubberOffset * 100;

            // console.log(_scrubbar.children('.scrub-prog'), scrubberOffsetPercent, timeTotal, '-timecurr ', timeCurr, scrubberWidth);
            if (null == this.audioBuffer) {
                $('body').find('.groovy-spectrum-progress').css({
                    width: scrubberOffsetPercent + '%'
                });

                $('body').find('.groovy-scrubber-progress').css({
                    width: scrubberOffsetPercent + '%'
                });

                $('body').find('.groovy-play-bar').css({
                    width: scrubberOffsetPercent + '%'
                });
            }

            if (true === this._model.spectrum.enabled)
                this.drawSpectrum();

            if (true === this._model.timerDynamic) {
                scrubberOffsetPixels = scrubberOffset * scrubberWidth;

                currentTime.css({
                    left: scrubberOffsetPercent + '%'
                });

                if (scrubberOffsetPixels > scrubberWidth - 30) {
                    currentTime.css({
                        opacity: 1 - (((scrubberOffsetPixels - (scrubberWidth - 30)) / 30))
                    });
                } else {
                    if (1 !== Number(currentTime.css('opacity'))) {
                        currentTime.css({
                            opacity: 1
                        });
                    }
                };
            };

            //console.info(_currTime, timeCurr, this.formatTime(timeCurr))
            currentTime.text(new Strings(timeCurr).formatTime());
            duration.text(new Strings(timeTotal).formatTime());

            // notify channel
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i])) {
                    this._channels[i].loop();
                } else {
                    console.log(['DEBUG', this._channels, i]);
                }
            }

            this._runLoop();
        },

        /**
         * Animation tick, keep track of the main loop via requestAnimFrame (optimial)
         */
        _runLoop: function() {
            window.requestAnimFrame($.proxy(function() {
                this.loop();
            }, this));
        },

        /**
         * Cleans up the current channel when track ends, or paused.
         * Shifts the array, removing the current active and progressing the crossfaded track to the 0 position in the array
         */
        activeChannelCleanup: function() {
            if ('undefined' !== typeof(this.getActiveChannel())) {
                this.getActiveChannel().destory();
                console.log(this._channels);
                this._channels.shift();
            }
        },

        /**
         * Pauses the current track by destroying the channel.
         *
         * @todo Fix the pause, it's acting like STOP rather than pause - doesnt keep the position in the track
         */
        pause: function() {
            this.activeChannelCleanup();

            return this;
        },

        /**
         * Lazy getter for AudioContext
         * Keeps track of 1 audio context and used by all the channels.
         *
         * @return {Object} AudioContext singleton
         */
        getCtx: function() {
            if (null === this.audioCtx) {
                // normalize AudioContext, if not supported binds webkit
                window.AudioContext = window.AudioContext || window.webkitAudioContext;

                this.audioCtx = new window.AudioContext;
            }

            return this.audioCtx;
        },

        /**
         * Spectrum rendering
         * Called from the animation loop and draws nice spektrum of the audio.
         *
         * @see http://jsfiddle.net/delz/yy01kxen/ - Based on a jsFiddle I wrote
         * @todo Move from here, not the best place to be here.. a lot of view related stuff
        **/
        drawSpectrum: function() {
            var frequencyByteData = this.getActiveChannel().frequencyByteData,
                canvas = this._view.getPlayerView().getCanvasReferances();

            var width = canvas.el.active.background.width,
                height = canvas.el.active.background.height;

            var BAR_WIDTH = 3,
                SPACER_WIDTH = 1;

            /* test.. disabled.
            var gradient = canvas.context.active.background.createLinearGradient(0, 0, width, height);
            gradient.addColorStop(1,'#000000');
            gradient.addColorStop(0.75,'#ff0000');
            gradient.addColorStop(0.25,'#ffff00');
            gradient.addColorStop(0,'#ffffff');
            */

            //canvas.el.active.background.width = width;
            canvas.context.active.background.clearRect(0, 0, width, height);
            canvas.context.active.background.fillStyle = '#'+ this._model.spectrum.color.bg;

            canvas.context.active.progress.clearRect(0, 0, width, height);
            canvas.context.active.progress.fillStyle = '#'+ this._model.spectrum.color.progress;

            for (var i = 0, magnitude = 0, len = frequencyByteData.length; i < len; i++) {
                magnitude = Math.ceil(frequencyByteData[i] * height / 256);
                if (2 > magnitude) magnitude = 2;

                canvas.context.active.background.fillRect(i * (SPACER_WIDTH + BAR_WIDTH), height, BAR_WIDTH, -magnitude);
                canvas.context.active.progress.fillRect(i * (SPACER_WIDTH + BAR_WIDTH), height, BAR_WIDTH, -magnitude);

                //canvas.context.active.background.fillRect(i/256 * width, height, width/len, -magnitude);
                //canvas.context.active.progress.fillRect(i/256 * width, height, width/len, -magnitude);
            }

            if (true === this._model.reflection) {
                canvas.context.reflect.background.clearRect(0, 0, width, height);
                canvas.context.reflect.background.drawImage(canvas.el.active.background, 0, 0);

                canvas.context.reflect.progress.clearRect(0, 0, width, height);
                canvas.context.reflect.progress.drawImage(canvas.el.active.progress, 0, 0);
            }
        },

        /**
         * On started playing event is fired from the channel, when the item is actually began playing.
         *
         * @todo Figure out what to do with this, maybe consider removing or changing
         *       the state of the player.. event is also an option
         */
        _onStartedPlaying: function() {
            console.log('CONTROLLER::_onStartedPlaying fired');
        }
    };

    /**
     * Normalizes the request animation frame method, depending on env. Based on webAPI
     * Optimized and replaces the timer loop, should be used for all the loops
     *
     * @todo move from here, to more global point, not directly related to this Controller.
     * @return {Function} ticker
     */
    window.requestAnimFrame = (function() {
        return window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function(callback, el) {
                window.setTimeout(callback, 1000 / 60);
            };
    })();

    return Controller;
});<commit_msg>pause added before play to stop other channels<commit_after>define([
    "controller/channel",
    "model/model",
    "view/view",
    "queue/queue",
    "events/events",
    "events/states",
    "utils/strings",
    "utils/event",
    "utils/logger",
], function(Channel, Model, View, Queue, Events, States, Strings, Event, Logger) {
    /**
     * gPlayer main controller
     *
     * Responsible for the general flow and creation of the view and models
     * as well as all the events related to the actual wiring of the player logic and channel management.
     */
    var Controller = function(config) {
        this.init(config);
    };

    Controller.prototype = {
        /**
         * Referances
         */
        _view: null,
        _model: null,
        _queue: null,
        _notifications: null,

        _channels: [],
        _state: States.IDLE,

        audioCtx: null,
        mute: false,
        volume: 1,

        /**
         * Constructor function for the main controller
         *
         * Registering important instances to hold here
         *
         * @param {Object} config
         * @param {Object}
         * @return this for chaining
         */
        init: function(config) {
            Logger.debug('CONTROLLER::INIT FIRED');

            this._model = new Model(config);
            this._notifications = new Event();
            this._view = new View(this, this._notifications);
            this._queue = new Queue(this._notifications);

            this.subscribe();

            return this;
        },

        /**
         * Subscribe to interesting actions during creation
         * Called during construction
         */
        subscribe: function() {
            var self = this;

            this.getNotifications().on(Events.STATE_CHANGED, function(state) {
                self._state = state;
            });

            this.getNotifications().on(Events.PLAY, function(item) {
                console.log(['CONTROLLER::ITEM ARRIVED', item]);
                self.pause();

                self.channelAdd(item)
                    .play();

                self.changeState(States.PLAYING);
            });

            this.getNotifications().on(Events.PAUSE, function() {
                self.changeState(States.PAUSED);
                self.pause();
            });

            this.getNotifications().on(Events.QUEUE_EMPTY, function() {
                self.changeState(States.IDLE);
                self.pause();
            });

            // translate native event for resize, for internal event
            $(window).bind('resize', function() {
                self.getNotifications().fire(Events.RESIZE);
            });

            this.getNotifications().on(Events.VOLUME_SET, function(volume) {
                self.setVolume(volume);
            });

            this.getNotifications().on(Events.JUMP_TO_SECOND, function(second) {
                self.getActiveChannel().play(second);
            });
        },

        /**
         * Bootstraps the main controller, setups objects and beings the animation loop.
         *
         * @param {HTMLElement} container
         */
        bootstrap: function(container) {
            this._model.setup(container);
            this._view.setup();
            this.loop();
        },

        /**
         * Getter for the Queue object, set on constructor
         *
         * @return {Object} Queue instance
         */
        getQueue: function() {
            return this._queue;
        },

        /**
         * Getter for the Model object, set on constructor
         *
         * @return {Object} Model instance
         */
        getModel: function() {
            return this._model;
        },

        /**
         * Getter for the Notifications object, set on constructor
         *
         * @return {Object} Notifications instance
         */
        getNotifications: function() {
            return this._notifications;
        },

        /**
         * Changes the state of the player by boardcasting the proper event on change.
         * This is the only place modifies the state of the player
         *
         * @param {String} newState (ENUM from States object)
         */
        changeState: function(newState) {
            // verify state is diffrent to avoid setting the same state
            if (newState === this._state) return;

            this.getNotifications().fire(Events.STATE_CHANGED, newState);
        },

        /**
         * Playing? Returns by checking each indevidual channel to prevent errors.
         * This is a protective check, rather than player state based.
         *
         * Can be used to check the real state of the player regardless the state, might be helpful for debuging issues
         *
         * @return {Bool}
         */
        isPlaying: function() {
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i]) && true === this._channels[i].isPlaying())
                    return true;
            }

            return false;
        },

        /**
         * Adds new channel, called when song is changed or started.
         * Stacks the instances of the channels in an array.
         * Each channel is indevidual and can be played more than 1 at once. The current only
         * state more than once channel is playing is during crossfading but more features may come, like mixin etc.
         *
         * @param {Object} item
         * @return {Object} channel instance
         */
        channelAdd: function (item) {
            var channelNew = new Channel({
                parent: this,
                item: item
            });

            this._channels.push(channelNew);

            return channelNew;
        },

        /**
         * Getter for the current active audio channel.
         * Fetched from the array of all channels. Channel 0 is the active..
         * Channel 1 is usually created only while crossfading and even then.. 0 is the active during the current usecases.
         *
         * Logic also can be changed later depending on features
         *
         * @return {Object} active channel instance
         */
        getActiveChannel: function() {
            return this._channels[0];
        },

        /**
         * Getter for the active Audio element instance
         */
        getActiveAudio: function() {
            return this.getActiveChannel().audioEl;
        },

        /**
         * onEnd is fired by one of the channels when it reaches the end of the track
         * or crossfade onBeforeEnd time triggered.
         *
         * @todo Wire by 2 events, for crossfade and regular ITEM_END. Drop the direct function call from channel to _onEnd
         * @param {Bool} crossfade
         */
        _onEnd: function(crossfade) {
            if (true === crossfade) {
                setTimeout($.proxy(function() {
                    this.activeChannelCleanup();
                }, this), this._model.crossfade.onBeforeEnd * 1000);
            } else {
                this.activeChannelCleanup();
            }

            this.getNotifications().fire(Events.PLAY_NEXT);
            //api.onEnd();
        },

        /**
         * Is player muted, or volume = 0
         *
         * @return {Bool}
         */
        isMute: function() {
            return this.mute;
        },

        /**
         * Is crossfade feature enabled?
         *
         * @return {Bool}
         */
        isCrossfade: function() {
            return this._model.crossfade.enabled;
        },

        /**
         * Getter for the volume
         * Volume ranges from 0 to 1
         *
         * @return {Number} volume
         */
        getVolume: function() {
            return this.volume;
        },

        /**
         * Sets the volume and updates all channels
         *
         * @param {Number} volume - ranges from 0 to 1 only.
         */
        setVolume: function(volume) {
            this.volume = (1 <= volume) ? 1 : volume; // can not be greater than 1
            this.mute = (0 >= volume) ? true : false;

            //updates the channel with volume value.
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i]))
                    this._channels[i].setVolume();
            }

            /*
            var interactiveObj = $('.groovy-interactive');
            interactiveObj.find('.groovy-scrubber').find('img').eq(0).css({
                transform: 'scaleY('+arg+')'
            });

            if(arg == 0){
                cthis.find('.scrub-bg-img-reflect').fadeOut('slow');
            } else {
                cthis.find('.scrub-bg-img-reflect').fadeIn('slow');
            }
            last_vol = arg;
            */
        },

        /**
         * Loop for tracking the audio and moving progress of the UI.
         * @todo Split into smaller chunks
         * @todo Move from here, not really related.
         */
        loop: function() {
            // verify everything needed for the loop exists. Run loop only when playing.
            if ('undefined' === typeof(this.getActiveChannel()) || !this.isPlaying()) {
                return this._runLoop();
            }

            var scrubberWidth = this._view.getPlayerView().components.interactive.getWidth(),
                currentTime = $('.groovy-current-time'),
                duration = $('.groovy-duration'),
                scrubberOffset = 0,
                scrubberOffsetPercent = 0,
                scrubberOffsetPixels = 0,
                timeTotal = this.getActiveChannel().getDuration(),
                timeCurr = this.getActiveChannel().getCurrentTime(),
                timeOffset = 3;

            /*
            // might not work.. audiobuffer is per channel.. odd location for referances, check!.
            if (this.audioBuffer && 'placeholder' !== this.audioBuffer) {
                timeTotal = this.audioBuffer.duration;
                timeCurr = this.audioCtx.currentTime;
                //console.log(this.audioBuffer.currentTime);
            }*/

            scrubberOffset = ((timeCurr - this._model.style.interactiveSelectOffset) / timeTotal);
            scrubberOffsetPercent = scrubberOffset * 100;

            // console.log(_scrubbar.children('.scrub-prog'), scrubberOffsetPercent, timeTotal, '-timecurr ', timeCurr, scrubberWidth);
            if (null == this.audioBuffer) {
                $('body').find('.groovy-spectrum-progress').css({
                    width: scrubberOffsetPercent + '%'
                });

                $('body').find('.groovy-scrubber-progress').css({
                    width: scrubberOffsetPercent + '%'
                });

                $('body').find('.groovy-play-bar').css({
                    width: scrubberOffsetPercent + '%'
                });
            }

            if (true === this._model.spectrum.enabled)
                this.drawSpectrum();

            if (true === this._model.timerDynamic) {
                scrubberOffsetPixels = scrubberOffset * scrubberWidth;

                currentTime.css({
                    left: scrubberOffsetPercent + '%'
                });

                if (scrubberOffsetPixels > scrubberWidth - 30) {
                    currentTime.css({
                        opacity: 1 - (((scrubberOffsetPixels - (scrubberWidth - 30)) / 30))
                    });
                } else {
                    if (1 !== Number(currentTime.css('opacity'))) {
                        currentTime.css({
                            opacity: 1
                        });
                    }
                };
            };

            //console.info(_currTime, timeCurr, this.formatTime(timeCurr))
            currentTime.text(new Strings(timeCurr).formatTime());
            duration.text(new Strings(timeTotal).formatTime());

            // notify channel
            for (var i = 0, len = this._channels.length; i < len; i++) {
                if ('undefined' !== typeof(this._channels[i])) {
                    this._channels[i].loop();
                } else {
                    console.log(['DEBUG', this._channels, i]);
                }
            }

            this._runLoop();
        },

        /**
         * Animation tick, keep track of the main loop via requestAnimFrame (optimial)
         */
        _runLoop: function() {
            window.requestAnimFrame($.proxy(function() {
                this.loop();
            }, this));
        },

        /**
         * Cleans up the current channel when track ends, or paused.
         * Shifts the array, removing the current active and progressing the crossfaded track to the 0 position in the array
         */
        activeChannelCleanup: function() {
            if ('undefined' !== typeof(this.getActiveChannel())) {
                this.getActiveChannel().destory();
                console.log(this._channels);
                this._channels.shift();
            }
        },

        /**
         * Pauses the current track by destroying the channel.
         *
         * @todo Fix the pause, it's acting like STOP rather than pause - doesnt keep the position in the track
         */
        pause: function() {
            this.activeChannelCleanup();

            return this;
        },

        /**
         * Lazy getter for AudioContext
         * Keeps track of 1 audio context and used by all the channels.
         *
         * @return {Object} AudioContext singleton
         */
        getCtx: function() {
            if (null === this.audioCtx) {
                // normalize AudioContext, if not supported binds webkit
                window.AudioContext = window.AudioContext || window.webkitAudioContext;

                this.audioCtx = new window.AudioContext;
            }

            return this.audioCtx;
        },

        /**
         * Spectrum rendering
         * Called from the animation loop and draws nice spektrum of the audio.
         *
         * @see http://jsfiddle.net/delz/yy01kxen/ - Based on a jsFiddle I wrote
         * @todo Move from here, not the best place to be here.. a lot of view related stuff
        **/
        drawSpectrum: function() {
            var frequencyByteData = this.getActiveChannel().frequencyByteData,
                canvas = this._view.getPlayerView().getCanvasReferances();

            var width = canvas.el.active.background.width,
                height = canvas.el.active.background.height;

            var BAR_WIDTH = 3,
                SPACER_WIDTH = 1;

            /* test.. disabled.
            var gradient = canvas.context.active.background.createLinearGradient(0, 0, width, height);
            gradient.addColorStop(1,'#000000');
            gradient.addColorStop(0.75,'#ff0000');
            gradient.addColorStop(0.25,'#ffff00');
            gradient.addColorStop(0,'#ffffff');
            */

            //canvas.el.active.background.width = width;
            canvas.context.active.background.clearRect(0, 0, width, height);
            canvas.context.active.background.fillStyle = '#'+ this._model.spectrum.color.bg;

            canvas.context.active.progress.clearRect(0, 0, width, height);
            canvas.context.active.progress.fillStyle = '#'+ this._model.spectrum.color.progress;

            for (var i = 0, magnitude = 0, len = frequencyByteData.length; i < len; i++) {
                magnitude = Math.ceil(frequencyByteData[i] * height / 256);
                if (2 > magnitude) magnitude = 2;

                canvas.context.active.background.fillRect(i * (SPACER_WIDTH + BAR_WIDTH), height, BAR_WIDTH, -magnitude);
                canvas.context.active.progress.fillRect(i * (SPACER_WIDTH + BAR_WIDTH), height, BAR_WIDTH, -magnitude);

                //canvas.context.active.background.fillRect(i/256 * width, height, width/len, -magnitude);
                //canvas.context.active.progress.fillRect(i/256 * width, height, width/len, -magnitude);
            }

            if (true === this._model.reflection) {
                canvas.context.reflect.background.clearRect(0, 0, width, height);
                canvas.context.reflect.background.drawImage(canvas.el.active.background, 0, 0);

                canvas.context.reflect.progress.clearRect(0, 0, width, height);
                canvas.context.reflect.progress.drawImage(canvas.el.active.progress, 0, 0);
            }
        },

        /**
         * On started playing event is fired from the channel, when the item is actually began playing.
         *
         * @todo Figure out what to do with this, maybe consider removing or changing
         *       the state of the player.. event is also an option
         */
        _onStartedPlaying: function() {
            console.log('CONTROLLER::_onStartedPlaying fired');
        }
    };

    /**
     * Normalizes the request animation frame method, depending on env. Based on webAPI
     * Optimized and replaces the timer loop, should be used for all the loops
     *
     * @todo move from here, to more global point, not directly related to this Controller.
     * @return {Function} ticker
     */
    window.requestAnimFrame = (function() {
        return window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function(callback, el) {
                window.setTimeout(callback, 1000 / 60);
            };
    })();

    return Controller;
});<|endoftext|>