<commit_before>'use strict'

let AgentApi = require("resource-management-framework")
	.AgentApi;

class Agent {
	constructor() {
		this.emitter = message_bus;
	}
	init(config) {
		this.iris = new AgentApi();
		this.iris.initContent();
	}
	launch() {
		return this.actionCacheActiveAgents()
			.then(res => true);
	}

	//API
	actionCacheActiveAgents() {
		return this.iris.cacheActiveAgents();
	}

	actionChangeState({
		user_id,
		workstation = [],
		state
	}) {
		return this.iris.getEntryTypeless(user_id)
			.then((res) => {
				let agents = _(res)
					.values()
					.compact()
					.map(a => {
						a.state = state;
						return a;
					})
					.value();
				return this.iris.setEntryTypeless(agents);
			})
			.then((res) => {
				return this.actionCacheActiveAgents();
			})
			.then((res) => {
				// console.log("USER CHSTATE", user_id, res);
				global.logger && logger.info("Agent %s changes state to %s", user_id, state);
				return {
					success: true
				};
			});
	}

	actionLogin({
		user_id,
		user_type,
		workstation
	}) {
		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				if (user_type !== "SystemEntity") {
					let ws = workstations[workstation];
					this.emitter.command('digital-display.emit.command', {
						org_addr: ws.org_addr,
						org_merged: ws.org_merged,
						workstation,
						command: 'refresh'
					});
				}
				return this.actionChangeState({
					user_id,
					state: 'active'
				});
			});

	}

	actionLogout({
		user_id,
		workstation
	}) {
		return this.actionChangeState({
			user_id,
			state: 'inactive'
		});
	}

	actionLogoutAll({
		organization,
		agent_type = 'Employee'
	}) {
		return this.actionActiveAgents({
				organization,
				agent_type,
				state: ['active', 'paused']
			})
			.then((user_id) => {
				return this.actionLogout({
					user_id
				});
			});
	}

	actionPause({
		user_id,
		user_type,
		workstation = []
	}) {
		let response;
		let curr_ws;
		if (user_type != "Employee")
			return {
				success: false,
				reason: "Cannot pause a System Entity."
			}

		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				curr_ws = workstations;
				return Promise.all(_.map(workstations, (ws) => {
					if (user_type !== "SystemEntity") {

						this.emitter.command('digital-display.emit.command', {
							org_addr: ws.org_addr,
							org_merged: ws.org_merged,
							workstation,
							command: 'refresh'
						});
					}
					return this.emitter.addTask('ticket', {
						_action: 'ticket',
						query: {
							state: ['called'],
							operator: user_id,
							destination: workstation,
							org_destination: ws.org_merged.id,
							dedicated_date: moment.tz(ws.org_merged.org_timezone)
						}
					});
				}));
			})
			.then((res) => {
				if (!_.isEmpty(_.flatMap(res, _.values)))
					return Promise.reject(new Error(`User cannot pause or logout with called tickets.`));
				return this.actionChangeState({
					user_id,
					workstation,
					state: 'paused'
				});
			})
			.then((res) => {
				response = res;
				return Promise.map(_.castArray(workstation), (ws) => {
					return this.emitter.addTask('workstation', {
						_action: 'change-state',
						workstation,
						state: 'paused'
					});
				});
			})
			.then((res) => {
				return Promise.map(_.castArray(workstation), (ws) => {
					return this.emitter.addTask('queue', {
						_action: "ticket-close-current",
						user_id,
						workstation: ws
					});
				});
			})
			.then((res) => {
				// console.log("PAUSE RESPONSE", response);
				_.map(curr_ws, ({
					ws,
					org_addr,
					org_chain,
					org_merged
				}) => {
					this.emitter.command('queue.emit.head', {
						user_id,
						org_addr,
						org_merged
					});
				});
				return response;
			})
			.catch(err => {
				console.log("PAUSE ERR", err.stack);
				global.logger && logger.info(
					err, {
						module: 'agent',
						method: 'pause'
					});
				return {
					success: false,
					reason: err.message
				};
			});
	}

	actionResume({
		user_id,
		user_type,
		workstation
	}) {
		return Promise.map(_.castArray(workstation), ws => {
				this.emitter.addTask('workstation', {
					_action: 'occupy',
					workstation: ws,
					user_type,
					user_id
				});
			})
			.then(res => {
				return {
					success: true
				}
			});
	}

	actionInfo({
		user_id,
		user_type,
		parent,
		satellite_type
	}) {
		return Promise.props({
				permissions: this.iris.getAgentPermissions(),
				agent: user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id)
			})
			.then(({
				agent,
				permissions
			}) => {
				// console.log("ENTITY", agent, permissions, user_id);
				let entity = agent[user_id];
				entity.permissions = _.mapValues(permissions, (perm, key) => {
					return _.merge(perm.params, _.get(entity, `permissions.${key}`, {}));
				});
				return Promise.props({
					entity,
					ws_available: this.emitter.addTask('workstation', {
						_action: 'workstation',
						workstation: entity.available_workstation,
						parent,
						satellite_type
					})
				});
			});
	}

	actionActiveAgents({
		agent_type,
		state = 'active'
	}) {
		return this.iris.getActiveAgents()
			.then((res) => {
				// console.log("AGENTS", _.flattenDeep(_.values(_.pick(res[agent_type], _.castArray(state)))));
				return _(res[agent_type])
					.pick(state)
					.values()
					.flattenDeep()
					.value();
			});
	}

	actionResourceKeys({
		role,
		organization
	}) {
		return this.iris.getAgentKeys(organization, role)
			.then(keys => {
				return Promise.props({
					all: keys,
					active: this.emitter.addTask('agent', {
							_action: 'active-agents',
							agent_type: 'Employee',
							state: 'active'
						})
						.then(active_keys => _.intersection(keys, active_keys))
				});
			});
	}

	actionLeave({
		user_id,
		user_type,
		workstation = []
	}) {
		console.log("LEAVE", user_id, workstation);
		let response;
		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				return Promise.all(_.map(workstations, (ws) => {
					if (user_type !== "SystemEntity") {
						this.emitter.command('digital-display.emit.command', {
							org_addr: ws.org_addr,
							org_merged: ws.org_merged,
							workstation,
							command: 'clear'
						});
					}
					return this.emitter.addTask('ticket', {
						_action: 'ticket',
						query: {
							state: ['called'],
							destination: workstation,
							operator: user_id,
							org_destination: ws.org_merged.id,
							dedicated_date: moment.tz(ws.org_merged.org_timezone)
						}
					});
				}));
			})
			.then((res) => {
				if (!_.isEmpty(_.flatMap(res, _.values)))
					return Promise.reject(new Error(`User cannot pause or logout with called tickets.`));
				return this.emitter.addTask('workstation', {
					_action: 'leave',
					user_id,
					workstation
				});
			})
			.then((res) => {
				response = res;
				if (!res.success)
					return Promise.reject(new Error(`User ${user_id} failed to leave workstations ${workstation}`));
				return Promise.map(_.castArray(workstation), (ws) => {
					return this.emitter.addTask('queue', {
						_action: "ticket-close-current",
						user_id,
						workstation: ws
					});
				});
			})
			.then((res) => {
				return response;
			})
			.catch(err => {
				console.log("LEAVE ERR", err.message);
				global.logger && logger.info(
					err, {
						module: 'agent',
						method: 'leave'
					});
				return {
					success: false,
					reason: err.message
				};
			});
	}

	actionDefaultWorkstations({
		user_id,
		user_type
	}) {
		return Promise.resolve(true)
			.then(() => {
				return user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id);
			})
			.then((entity) => {
				let default_ws = entity[user_id].default_workstation;
				if (!default_ws)
					return Promise.reject(new Error("No default workstation for this entity."));
				return this.emitter.addTask('workstation', {
					_action: 'by-id',
					workstation: default_ws
				});
			});
	}

	actionAvailableWorkstations({
		user_id,
		user_type
	}) {
		return Promise.resolve(true)
			.then(() => {
				return user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id);
			})
			.then((entity) => {
				let default_ws = entity[user_id].available_workstation;
				if (!default_ws)
					return Promise.reject(new Error("No available workstation for this entity."));
				return this.emitter.addTask('workstation', {
					_action: 'by-id',
					workstation: default_ws
				});
			});
	}

	actionById({
		agent_id
	}) {
		return this.iris.getEntryTypeless(agent_id)
			.then((res) => _.isArray(agent_id) ? res : res[agent_id]);
	}
}
module.exports = Agent;<commit_msg>closecurr passed to ws<commit_after>'use strict'

let AgentApi = require("resource-management-framework")
	.AgentApi;

class Agent {
	constructor() {
		this.emitter = message_bus;
	}
	init(config) {
		this.iris = new AgentApi();
		this.iris.initContent();
	}
	launch() {
		return this.actionCacheActiveAgents()
			.then(res => true);
	}

	//API
	actionCacheActiveAgents() {
		return this.iris.cacheActiveAgents();
	}

	actionChangeState({
		user_id,
		workstation = [],
		state
	}) {
		return this.iris.getEntryTypeless(user_id)
			.then((res) => {
				let agents = _(res)
					.values()
					.compact()
					.map(a => {
						a.state = state;
						return a;
					})
					.value();
				return this.iris.setEntryTypeless(agents);
			})
			.then((res) => {
				return this.actionCacheActiveAgents();
			})
			.then((res) => {
				// console.log("USER CHSTATE", user_id, res);
				global.logger && logger.info("Agent %s changes state to %s", user_id, state);
				return {
					success: true
				};
			});
	}

	actionLogin({
		user_id,
		user_type,
		workstation
	}) {
		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				if (user_type !== "SystemEntity") {
					let ws = workstations[workstation];
					this.emitter.command('digital-display.emit.command', {
						org_addr: ws.org_addr,
						org_merged: ws.org_merged,
						workstation,
						command: 'refresh'
					});
				}
				return this.actionChangeState({
					user_id,
					state: 'active'
				});
			});

	}

	actionLogout({
		user_id,
		workstation
	}) {
		return this.actionChangeState({
			user_id,
			state: 'inactive'
		});
	}

	actionLogoutAll({
		organization,
		agent_type = 'Employee'
	}) {
		return this.actionActiveAgents({
				organization,
				agent_type,
				state: ['active', 'paused']
			})
			.then((user_id) => {
				return this.actionLogout({
					user_id
				});
			});
	}

	actionPause({
		user_id,
		user_type,
		workstation = []
	}) {
		let response;
		let curr_ws;
		if (user_type != "Employee")
			return {
				success: false,
				reason: "Cannot pause a System Entity."
			}

		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				curr_ws = workstations;
				return Promise.map(_.values(workstations), (ws) => {
					if (user_type !== "SystemEntity") {
						this.emitter.command('digital-display.emit.command', {
							org_addr: ws.org_addr,
							org_merged: ws.org_merged,
							workstation,
							command: 'refresh'
						});
					}

					return this.actionCheckAssigned({
						state: ['called', 'postponed'],
						destination: workstation,
						operator: user_id,
						org_destination: ws.org_merged.id,
						dedicated_date: moment.tz(ws.org_merged.org_timezone)
					});
				});
			})
			.then((res) => {
				if (!_.every(res, t => _.get(t, 'called.count', 0) == 0))
					return Promise.reject(new Error(`User cannot pause or logout with called tickets.`));
				return this.actionChangeState({
					user_id,
					workstation,
					state: 'paused'
				});
			})
			.then((res) => {
				response = res;
				return Promise.map(_.castArray(workstation), (ws) => {
					return this.emitter.addTask('workstation', {
						_action: 'change-state',
						workstation: workstation,
						state: 'paused'
					});
				});
			})
			.then((res) => {
				// console.log("PAUSE RESPONSE", response);
				_.map(curr_ws, ({
					ws,
					org_addr,
					org_chain,
					org_merged
				}) => {
					this.emitter.command('queue.emit.head', {
						user_id,
						org_addr,
						org_merged
					});
				});
				return response;
			})
			.catch(err => {
				console.log("PAUSE ERR", err.stack);
				global.logger && logger.info(
					err, {
						module: 'agent',
						method: 'pause'
					});
				return {
					success: false,
					reason: err.message
				};
			});
	}

	actionResume({
		user_id,
		user_type,
		workstation
	}) {
		return Promise.map(_.castArray(workstation), ws => {
				this.emitter.addTask('workstation', {
					_action: 'occupy',
					workstation: ws,
					user_type,
					user_id
				});
			})
			.then(res => {
				return {
					success: true
				}
			});
	}

	actionInfo({
		user_id,
		user_type,
		parent,
		satellite_type
	}) {
		return Promise.props({
				permissions: this.iris.getAgentPermissions(),
				agent: user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id)
			})
			.then(({
				agent,
				permissions
			}) => {
				// console.log("ENTITY", agent, permissions, user_id);
				let entity = agent[user_id];
				entity.permissions = _.mapValues(permissions, (perm, key) => {
					return _.merge(perm.params, _.get(entity, `permissions.${key}`, {}));
				});
				return Promise.props({
					entity,
					ws_available: this.emitter.addTask('workstation', {
						_action: 'workstation',
						workstation: entity.available_workstation,
						parent,
						satellite_type
					})
				});
			});
	}

	actionActiveAgents({
		agent_type,
		state = 'active'
	}) {
		return this.iris.getActiveAgents()
			.then((res) => {
				// console.log("AGENTS", _.flattenDeep(_.values(_.pick(res[agent_type], _.castArray(state)))));
				return _(res[agent_type])
					.pick(state)
					.values()
					.flattenDeep()
					.value();
			});
	}

	actionResourceKeys({
		role,
		organization
	}) {
		return this.iris.getAgentKeys(organization, role)
			.then(keys => {
				return Promise.props({
					all: keys,
					active: this.emitter.addTask('agent', {
							_action: 'active-agents',
							agent_type: 'Employee',
							state: 'active'
						})
						.then(active_keys => _.intersection(keys, active_keys))
				});
			});
	}

	actionCheckAssigned({
		operator,
		destination,
		org_destination,
		dedicated_date,
		state = ['called', 'postponed']
	}) {
		return this.emitter.addTask('ticket', {
				_action: 'ticket',
				query: {
					state: state,
					destination: destination,
					operator: operator,
					org_destination: org_destination,
					dedicated_date: dedicated_date
				}
			})
			.then((res) => {
				let data = _(res)
					.groupBy('state')
					.mapValues(ticks => {
						return {
							tickets: ticks,
							count: _.size(ticks)
						};
					})
					.value();
				console.log("PASSED", data);
				return data;
			});
	}

	actionLeave({
		user_id,
		user_type,
		workstation = []
	}) {
		console.log("LEAVE", user_id, workstation);
		let response;
		return this.emitter.addTask('workstation', {
				_action: 'workstation-organization-data',
				workstation
			})
			.then((workstations) => {
				return Promise.map(_.values(workstations), (ws) => {
					if (user_type !== "SystemEntity") {
						this.emitter.command('digital-display.emit.command', {
							org_addr: ws.org_addr,
							org_merged: ws.org_merged,
							workstation,
							command: 'clear'
						});
					}
					return this.actionCheckAssigned({
						state: ['called', 'postponed'],
						destination: workstation,
						operator: user_id,
						org_destination: ws.org_merged.id,
						dedicated_date: moment.tz(ws.org_merged.org_timezone)
					});
				});
			})
			.then((res) => {
				if (!_.every(res, t => _.get(t, 'called.count', 0) == 0))
					return Promise.reject(new Error(`User cannot pause or logout with called tickets.`));
				return this.emitter.addTask('workstation', {
					_action: 'leave',
					user_id,
					workstation
				});
			})
			.catch(err => {
				console.log("LEAVE ERR", err.message);
				global.logger && logger.info(
					err, {
						module: 'agent',
						method: 'leave'
					});
				return {
					success: false,
					reason: err.message
				};
			});
	}

	actionDefaultWorkstations({
		user_id,
		user_type
	}) {
		return Promise.resolve(true)
			.then(() => {
				return user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id);
			})
			.then((entity) => {
				let default_ws = entity[user_id].default_workstation;
				if (!default_ws)
					return Promise.reject(new Error("No default workstation for this entity."));
				return this.emitter.addTask('workstation', {
					_action: 'by-id',
					workstation: default_ws
				});
			});
	}

	actionAvailableWorkstations({
		user_id,
		user_type
	}) {
		return Promise.resolve(true)
			.then(() => {
				return user_type ? this.iris.getEntry(user_type, {
					keys: user_id
				}) : this.iris.getEntryTypeless(user_id);
			})
			.then((entity) => {
				let default_ws = entity[user_id].available_workstation;
				if (!default_ws)
					return Promise.reject(new Error("No available workstation for this entity."));
				return this.emitter.addTask('workstation', {
					_action: 'by-id',
					workstation: default_ws
				});
			});
	}

	actionById({
		agent_id
	}) {
		return this.iris.getEntryTypeless(agent_id)
			.then((res) => _.isArray(agent_id) ? res : res[agent_id]);
	}
}
module.exports = Agent;<|endoftext|>


<commit_before>/// <reference path="../libraries/reui/reui.js"/>
/// <reference path="../libraries/ext/ext-core-debug.js"/>
/// <reference path="../libraries/sdata/sdata-client-dependencies-debug.js"/>
/// <reference path="../libraries/sdata/sdata-client-debug.js"/>
/// <reference path="Format.js"/>
/// <reference path="Utility.js"/>

Ext.namespace('Sage.Platform.Mobile');

// todo: where to place these?
Ext.USE_NATIVE_JSON = true;

// see: http://www.sencha.com/forum/showthread.php?44422-OPEN-518-3.x-2.x-Bug-in-radiogroup-when-using-brackets-in-name
Ext.DomQuery.matchers[2] = {
    re: /^(?:([\[\{])(?:@)?([\w-]+)\s?(?:(=|.=)\s?(["']?)(.*?)\4)?[\]\}])/,
    select: 'n = byAttribute(n, "{2}", "{5}", "{3}", "{1}");'
};

Sage.Platform.Mobile.Application = Ext.extend(Ext.util.Observable, {
    defaultServerName: window.location.hostname,
    defaultPort: window.location.port && window.location.port != 80 ? window.location.port : false,
    defaultProtocol: /https/i.test(window.location.protocol) ? 'https' : false,
    constructor: function() {
        /// <field name="initialized" type="Boolean">True if the application has been initialized; False otherwise.</field>
        /// <field name="context" type="Object">A general store for global context data.</field>
        /// <field name="views" elementType="Sage.Platform.Mobile.View">A list of registered views.</field>
        /// <field name="viewsById" type="Object">A map for looking up a view by its ID.</field>

        Sage.Platform.Mobile.Application.superclass.constructor.call(this);

        this.initialized = false;
        this.enableCaching = false;
        this.context = {view: []};
        this.views = {};
        this.services = {};
        this.defaultService = false;
        this.bars = {};
        this.addEvents(
            'resize',
            'registered',
            'beforeviewtransitionaway',
            'beforeviewtransitionto',
            'viewtransitionaway',
            'viewtransitionto'
        );
    },
    setup: function() {
        ///
        ///     Sets up the handling for transition events from iUI.
        ///
        Ext.EventManager.on(window, 'resize', function() {
            this.fireEvent('resize');
        }, this, {buffer: 250});
        Ext.getBody().on('beforetransition', function(evt, el, o) {
            var view = this.getView(el);
            if (view)
            {
                if (evt.browserEvent.out)
                    this.beforeViewTransitionAway(view);
                else
                    this.beforeViewTransitionTo(view);
            }
        }, this);
        Ext.getBody().on('aftertransition', function(evt, el, o) {
            var view = this.getView(el);
            if (view)
            {
                if (evt.browserEvent.out)
                    this.viewTransitionAway(view);
                else
                    this.viewTransitionTo(view);
            }
        }, this);

        if (this.enableCaching)
        {
            if (this.isOnline())
                this.clearSDataRequestCache();
        }
    },
    isOnline: function() {
        return window.navigator.onLine;
    },
    clearSDataRequestCache: function() {
        var check = function(k) {
            return /^sdata\.cache/i.test(k);
        };

        /* todo: find a better way to detect */
        for (var i = window.localStorage.length - 1; i >= 0 ; i--)
        {
            var key = window.localStorage.key(i);
            if (check(key))
                window.localStorage.removeItem(key);
        }
    },
    createCacheKey: function(request) {
        return 'sdata.cache[' + request.build() + ']';
    },
    loadSDataRequest: function(request, o) {
        /// <param name="request" type="Sage.SData.Client.SDataBaseRequest" />
        if (this.isOnline()) return;

        var key = this.createCacheKey(request);
        var feed = window.localStorage.getItem(key);
        if (feed)
        {
            o.result = Ext.decode(feed);
        }
    },
    cacheSDataRequest: function(request, o, feed) {
        /* todo: decide how to handle PUT/POST/DELETE */
        if (/get/i.test(o.method) && typeof feed === 'object')
        {
            var key = this.createCacheKey(request);

            window.localStorage.removeItem(key);
            window.localStorage.setItem(key, Ext.encode(feed));
        }
    },
    init: function() {
        ///
        ///     Initializes this application as well as the toolbar and all currently registered views.
        ///
        this.setup();

        for (var n in this.bars)
            this.bars[n].init();

        for (var n in this.views)
            this.views[n].init();

        this.initialized = true;
    },
    registerService: function(name, s, o) {
        var o = o || {};

        if (s instanceof Sage.SData.Client.SDataService)
            var service = s;
        else
            var service = new Sage.SData.Client.SDataService(s);

        this.services[name] = service;

        if (this.enableCaching && o.offline)
        {
            service.on('beforerequest', this.loadSDataRequest, this);
            service.on('requestcomplete', this.cacheSDataRequest, this);
        }

        if (o.isDefault || !this.defaultService)
        {
            this.defaultService = service;
        }

        return this;
    },
    hasService: function(name) {
        return (typeof this.services[name] !== 'undefined');
    },
    registerView: function(view) {
        ///
        ///     Registers a view with the application.  If the application has already been
        ///     initialized, the view is immediately initialized as well.
        ///
        /// <param name="view" type="Sage.Platform.Mobile.View">The view to be registered.</param>
        this.views[view.id] = view;

        if (this.initialized) view.init();

        this.fireEvent('registered', view);

        return this;
    },
    registerToolbar: function(name, tbar)
    {
        if (typeof name === 'object')
        {
            tbar = name;
            name = tbar.name;
        }

        this.bars[name] = tbar;

        if (this.initialized) tbar.init();

        return this;
    },
    getViews: function() {
        /// <returns elementType="Sage.Platform.Mobile.View">An array containing the currently registered views.</returns>
        var r = [];
        for (var n in this.views) r.push(this.views[n]);
        return r;
    },
    getActiveView: function() {
        /// <returns type="Sage.Platform.Mobile.View">The currently active view.</returns>
        var el = ReUI.getCurrentPage() || ReUI.getCurrentDialog();
        if (el)
            return this.getView(el);

        return null;
    },
    getView: function(key) {
        /// <returns type="Sage.Platform.Mobile.View">The requested view.</returns>
        /// <param name="key" type="String">
        ///     1: id - The id of the view to get.
        ///     2: element - The main element of the view to get.
        /// <param>
        if (key)
        {
            if (typeof key === 'string')
                return this.views[key];

            if (typeof key === 'object' && typeof key.id === 'string')
                return this.views[key.id];
        }
        return null;
    },
    getService: function(name) {
        /// <returns type="Sage.SData.Client.SDataService">The application's SData service instance.</returns>
        if (typeof name === 'string' && this.services[name])
            return this.services[name];

        return this.defaultService;
    },
    setTitle: function(title) {
        /// Sets the applications current title.
        /// <param name="title" type="String">The new title.
        for (var n in this.bars)
            if (this.bars[n].setTitle)
                this.bars[n].setTitle(title);
    },
    beforeViewTransitionAway: function(view) {
        this.fireEvent('beforeviewtransitionaway', view);

        view.beforeTransitionAway();
    },
    beforeViewTransitionTo: function(view) {
        this.fireEvent('beforeviewtransitionto', view);

        for (var n in this.bars)
            this.bars[n].clear();

        view.beforeTransitionTo();
    },
    viewTransitionAway: function(view) {
        this.fireEvent('viewtransitionaway', view);

        view.transitionAway();
    },
    viewTransitionTo: function(view) {
        this.fireEvent('viewtransitionto', view);

        /*
        // todo: should be here instead?
        for (var n in this.bars)
                this.bars[n].clear();
        */

        var tools = view.options ? (view.options.tools || view.tools) : (view.tools);

        if (tools)
        {
            for (var n in tools)
                if (this.bars[n])
                    this.bars[n].display(tools[n]);
        }

        if (this.context.view.unshift(view.getContext()) > 10)
            this.context.view.pop();

        view.transitionTo();
    }
});

Ext.onReady(function(){
    var isApple = /(iphone|ipad|ipod)/i.test(navigator.userAgent),
        isMobile = (typeof window.orientation !== 'undefined'),
        onlyHorizontalSwipe = true,
        root = Ext.get(document.documentElement),
        minSwipeLength = 100.0,
        maxSwipeTime = 0.5,
        minLongClickTime = 1.0,
        longPressTime = 1.5,
        maxLongClickLength = 5.0,
        startAt,
        startTime,
        originalTarget,
        preventClick = false,
        longPressTimer = false;

    var touchClick = function(evt) {
        ReUI.DomHelper.unbind(evt.target || evt.srcElement, 'click', touchClick, true);

        if (evt.preventBubble) evt.preventBubble();
        if (evt.preventDefault) evt.preventDefault();
        if (evt.stopPropagation) evt.stopPropagation();
        if (evt.stopImmediatePropagation) evt.stopImmediatePropagation();

        return false;
    };
    var longPress = function(evt, el) {
      if (longPressTimer) clearTimeout(longPressTimer);

      ReUI.DomHelper.dispatch(el, 'longpress');
      return false;
    };
    var touchMove = function(evt, el, o) {
        /* for general swipe, we do not need mouse move */
    };
    var touchStart = function(evt, el, o) {
        preventClick = false;
        originalTarget = el;
        startAt = evt.getXY();
        startTime = (new Date()).getTime();
        longPressTimer = setTimeout(function(){ longPress(evt, el); }, (longPressTime * 1000));
    };
    var touchEnd = function(evt, el, o) {
        if (longPressTimer) clearTimeout(longPressTimer);

        var endAt = evt.getXY(),
            endTime = (new Date()).getTime();

        var duration = (endTime - startTime) / 1000.0,
            direction = {x: endAt[0] - startAt[0], y: endAt[1] - startAt[1]},
            length = Math.sqrt(direction.x * direction.x + direction.y * direction.y),
            normalized = {x: direction.x / length, y: direction.y / length},
            dotProd = normalized.x * 0.0 + normalized.y * 1.0;

        if (duration <= maxSwipeTime && length >= minSwipeLength)
        {
            var swipe;
            if (!onlyHorizontalSwipe)
            {
                if (dotProd >= 0.71)
                    swipe = 'down';
                else if (dotProd <= -0.71)
                    swipe = 'up';
                else if (normalized.x < 0.0)
                    swipe = 'left';
                else
                    swipe = 'right';
            }
            else
            {
                if (dotProd < 0.71 && dotProd > -0.71)
                {
                    if (normalized.x < 0.0)
                        swipe = 'left';
                    else
                        swipe = 'right';
                }
            }

            if (swipe)
            {
                if (originalTarget === el)
                {
                    ReUI.DomHelper.bind(el, 'click', touchClick, true);
                }

                ReUI.DomHelper.dispatch(el, 'swipe', {direction: swipe});
            }
        }
        else if (duration >= minLongClickTime && length <= maxLongClickLength)
        {
            if (originalTarget === el)
            {
                ReUI.DomHelper.bind(el, 'click', touchClick, true);
                ReUI.DomHelper.dispatch(el, 'clicklong');
            }
        }
    };

    if (!isMobile)
    {
        root.on('mousedown', touchStart);
        root.on('mouseup', touchEnd);
    }
    else
    {
        root.on('touchstart', touchStart);
        root.on('touchend', touchEnd);
    }
});
<commit_msg>[#5480158] Long press issue fixed in iOS.<commit_after>/// <reference path="../libraries/reui/reui.js"/>
/// <reference path="../libraries/ext/ext-core-debug.js"/>
/// <reference path="../libraries/sdata/sdata-client-dependencies-debug.js"/>
/// <reference path="../libraries/sdata/sdata-client-debug.js"/>
/// <reference path="Format.js"/>
/// <reference path="Utility.js"/>

Ext.namespace('Sage.Platform.Mobile');

// todo: where to place these?
Ext.USE_NATIVE_JSON = true;

// see: http://www.sencha.com/forum/showthread.php?44422-OPEN-518-3.x-2.x-Bug-in-radiogroup-when-using-brackets-in-name
Ext.DomQuery.matchers[2] = {
    re: /^(?:([\[\{])(?:@)?([\w-]+)\s?(?:(=|.=)\s?(["']?)(.*?)\4)?[\]\}])/,
    select: 'n = byAttribute(n, "{2}", "{5}", "{3}", "{1}");'
};

Sage.Platform.Mobile.Application = Ext.extend(Ext.util.Observable, {
    defaultServerName: window.location.hostname,
    defaultPort: window.location.port && window.location.port != 80 ? window.location.port : false,
    defaultProtocol: /https/i.test(window.location.protocol) ? 'https' : false,
    constructor: function() {
        /// <field name="initialized" type="Boolean">True if the application has been initialized; False otherwise.</field>
        /// <field name="context" type="Object">A general store for global context data.</field>
        /// <field name="views" elementType="Sage.Platform.Mobile.View">A list of registered views.</field>
        /// <field name="viewsById" type="Object">A map for looking up a view by its ID.</field>

        Sage.Platform.Mobile.Application.superclass.constructor.call(this);

        this.initialized = false;
        this.enableCaching = false;
        this.context = {view: []};
        this.views = {};
        this.services = {};
        this.defaultService = false;
        this.bars = {};
        this.addEvents(
            'resize',
            'registered',
            'beforeviewtransitionaway',
            'beforeviewtransitionto',
            'viewtransitionaway',
            'viewtransitionto'
        );
    },
    setup: function() {
        ///
        ///     Sets up the handling for transition events from iUI.
        ///
        Ext.EventManager.on(window, 'resize', function() {
            this.fireEvent('resize');
        }, this, {buffer: 250});
        Ext.getBody().on('beforetransition', function(evt, el, o) {
            var view = this.getView(el);
            if (view)
            {
                if (evt.browserEvent.out)
                    this.beforeViewTransitionAway(view);
                else
                    this.beforeViewTransitionTo(view);
            }
        }, this);
        Ext.getBody().on('aftertransition', function(evt, el, o) {
            var view = this.getView(el);
            if (view)
            {
                if (evt.browserEvent.out)
                    this.viewTransitionAway(view);
                else
                    this.viewTransitionTo(view);
            }
        }, this);

        if (this.enableCaching)
        {
            if (this.isOnline())
                this.clearSDataRequestCache();
        }
    },
    isOnline: function() {
        return window.navigator.onLine;
    },
    clearSDataRequestCache: function() {
        var check = function(k) {
            return /^sdata\.cache/i.test(k);
        };

        /* todo: find a better way to detect */
        for (var i = window.localStorage.length - 1; i >= 0 ; i--)
        {
            var key = window.localStorage.key(i);
            if (check(key))
                window.localStorage.removeItem(key);
        }
    },
    createCacheKey: function(request) {
        return 'sdata.cache[' + request.build() + ']';
    },
    loadSDataRequest: function(request, o) {
        /// <param name="request" type="Sage.SData.Client.SDataBaseRequest" />
        if (this.isOnline()) return;

        var key = this.createCacheKey(request);
        var feed = window.localStorage.getItem(key);
        if (feed)
        {
            o.result = Ext.decode(feed);
        }
    },
    cacheSDataRequest: function(request, o, feed) {
        /* todo: decide how to handle PUT/POST/DELETE */
        if (/get/i.test(o.method) && typeof feed === 'object')
        {
            var key = this.createCacheKey(request);

            window.localStorage.removeItem(key);
            window.localStorage.setItem(key, Ext.encode(feed));
        }
    },
    init: function() {
        ///
        ///     Initializes this application as well as the toolbar and all currently registered views.
        ///
        this.setup();

        for (var n in this.bars)
            this.bars[n].init();

        for (var n in this.views)
            this.views[n].init();

        this.initialized = true;
    },
    registerService: function(name, s, o) {
        var o = o || {};

        if (s instanceof Sage.SData.Client.SDataService)
            var service = s;
        else
            var service = new Sage.SData.Client.SDataService(s);

        this.services[name] = service;

        if (this.enableCaching && o.offline)
        {
            service.on('beforerequest', this.loadSDataRequest, this);
            service.on('requestcomplete', this.cacheSDataRequest, this);
        }

        if (o.isDefault || !this.defaultService)
        {
            this.defaultService = service;
        }

        return this;
    },
    hasService: function(name) {
        return (typeof this.services[name] !== 'undefined');
    },
    registerView: function(view) {
        ///
        ///     Registers a view with the application.  If the application has already been
        ///     initialized, the view is immediately initialized as well.
        ///
        /// <param name="view" type="Sage.Platform.Mobile.View">The view to be registered.</param>
        this.views[view.id] = view;

        if (this.initialized) view.init();

        this.fireEvent('registered', view);

        return this;
    },
    registerToolbar: function(name, tbar)
    {
        if (typeof name === 'object')
        {
            tbar = name;
            name = tbar.name;
        }

        this.bars[name] = tbar;

        if (this.initialized) tbar.init();

        return this;
    },
    getViews: function() {
        /// <returns elementType="Sage.Platform.Mobile.View">An array containing the currently registered views.</returns>
        var r = [];
        for (var n in this.views) r.push(this.views[n]);
        return r;
    },
    getActiveView: function() {
        /// <returns type="Sage.Platform.Mobile.View">The currently active view.</returns>
        var el = ReUI.getCurrentPage() || ReUI.getCurrentDialog();
        if (el)
            return this.getView(el);

        return null;
    },
    getView: function(key) {
        /// <returns type="Sage.Platform.Mobile.View">The requested view.</returns>
        /// <param name="key" type="String">
        ///     1: id - The id of the view to get.
        ///     2: element - The main element of the view to get.
        /// <param>
        if (key)
        {
            if (typeof key === 'string')
                return this.views[key];

            if (typeof key === 'object' && typeof key.id === 'string')
                return this.views[key.id];
        }
        return null;
    },
    getService: function(name) {
        /// <returns type="Sage.SData.Client.SDataService">The application's SData service instance.</returns>
        if (typeof name === 'string' && this.services[name])
            return this.services[name];

        return this.defaultService;
    },
    setTitle: function(title) {
        /// Sets the applications current title.
        /// <param name="title" type="String">The new title.
        for (var n in this.bars)
            if (this.bars[n].setTitle)
                this.bars[n].setTitle(title);
    },
    beforeViewTransitionAway: function(view) {
        this.fireEvent('beforeviewtransitionaway', view);

        view.beforeTransitionAway();
    },
    beforeViewTransitionTo: function(view) {
        this.fireEvent('beforeviewtransitionto', view);

        for (var n in this.bars)
            this.bars[n].clear();

        view.beforeTransitionTo();
    },
    viewTransitionAway: function(view) {
        this.fireEvent('viewtransitionaway', view);

        view.transitionAway();
    },
    viewTransitionTo: function(view) {
        this.fireEvent('viewtransitionto', view);

        /*
        // todo: should be here instead?
        for (var n in this.bars)
                this.bars[n].clear();
        */

        var tools = view.options ? (view.options.tools || view.tools) : (view.tools);

        if (tools)
        {
            for (var n in tools)
                if (this.bars[n])
                    this.bars[n].display(tools[n]);
        }

        if (this.context.view.unshift(view.getContext()) > 10)
            this.context.view.pop();

        view.transitionTo();
    }
});

Ext.onReady(function(){
    var isApple = /(iphone|ipad|ipod)/i.test(navigator.userAgent),
        isMobile = (typeof window.orientation !== 'undefined'),
        onlyHorizontalSwipe = true,
        root = Ext.get(document.documentElement),
        minSwipeLength = 100.0,
        maxSwipeTime = 0.5,
        minLongClickTime = 1.0,
        longPressTime = 1.5,
        maxLongClickLength = 5.0,
        startAt,
        startTime,
        touchStartAtX,
        touchStartAtY,
        originalTarget,
        preventClick = false,
        longPressTimer = false;

    var touchClick = function(evt) {
        ReUI.DomHelper.unbind(evt.target || evt.srcElement, 'click', touchClick, true);

        if (evt.preventBubble) evt.preventBubble();
        if (evt.preventDefault) evt.preventDefault();
        if (evt.stopPropagation) evt.stopPropagation();
        if (evt.stopImmediatePropagation) evt.stopImmediatePropagation();

        return false;
    };
    var longPress = function(evt, el) {
      //remove mouse out listener
      if (!isMobile)
          Ext.get(el).un('mouseout', dragOut);
      else
          Ext.get(el).un('touchmove', dragOut);

      if (longPressTimer)
      {
          clearTimeout(longPressTimer);
          longPressTimer = false;
      }

      ReUI.DomHelper.dispatch(el, 'longpress');
      return false;
    };
    var touchMove = function(evt, el, o) {
        /* for general swipe, we do not need mouse move */
        var touch = evt.browserEvent.touches[0];
        var direction = {x: touch.pageX - touchStartAtX, y: touch.pageY - touchStartAtY},
            length = Math.sqrt(direction.x * direction.x + direction.y * direction.y);

        if (length > 10 && longPressTimer)
        {
            clearTimeout(longPressTimer);
            longPressTimer = false;
        }
    };
    var dragOut = function(evt, el, o) {
      if (longPressTimer)
      {
          clearTimeout(longPressTimer);
          longPressTimer = false;
      }
    };
    var touchStart = function(evt, el, o) {
        preventClick = false;
        originalTarget = el;
        startAt = evt.getXY();
        touchStartAtX = evt.browserEvent.touches[0].pageX;
        touchStartAtY = evt.browserEvent.touches[0].pageY;
        startTime = (new Date()).getTime();

        longPressTimer = setTimeout(function() {
            longPress(evt, el);
        }, (longPressTime * 1000));

        if (!isMobile)
            Ext.get(el).on('mouseout', dragOut);
        else
            Ext.get(el).on('touchmove', touchMove);
    };
    var touchEnd = function(evt, el, o) {
        if (longPressTimer)
        {
            clearTimeout(longPressTimer);
            longPressTimer = false;
        }
        if (!isMobile)
            Ext.get(el).un('mouseout', dragOut);
        else
            Ext.get(el).un('touchmove', dragOut);

        var endAt = evt.getXY(),
            endTime = (new Date()).getTime();

        var duration = (endTime - startTime) / 1000.0,
            direction = {x: endAt[0] - startAt[0], y: endAt[1] - startAt[1]},
            length = Math.sqrt(direction.x * direction.x + direction.y * direction.y),
            normalized = {x: direction.x / length, y: direction.y / length},
            dotProd = normalized.x * 0.0 + normalized.y * 1.0;

        if (duration <= maxSwipeTime && length >= minSwipeLength)
        {
            var swipe;
            if (!onlyHorizontalSwipe)
            {
                if (dotProd >= 0.71)
                    swipe = 'down';
                else if (dotProd <= -0.71)
                    swipe = 'up';
                else if (normalized.x < 0.0)
                    swipe = 'left';
                else
                    swipe = 'right';
            }
            else
            {
                if (dotProd < 0.71 && dotProd > -0.71)
                {
                    if (normalized.x < 0.0)
                        swipe = 'left';
                    else
                        swipe = 'right';
                }
            }

            if (swipe)
            {
                if (originalTarget === el)
                {
                    ReUI.DomHelper.bind(el, 'click', touchClick, true);
                }

                ReUI.DomHelper.dispatch(el, 'swipe', {direction: swipe});
            }
        }
        else if (duration >= minLongClickTime && length <= maxLongClickLength)
        {
            if (originalTarget === el)
            {
                ReUI.DomHelper.bind(el, 'click', touchClick, true);
                ReUI.DomHelper.dispatch(el, 'clicklong');
            }
        }
    };

    if (!isMobile)
    {
        root.on('mousedown', touchStart);
        root.on('mouseup', touchEnd);
    }
    else
    {
        root.on('touchstart', touchStart);
        root.on('touchend', touchEnd);
    }
});
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import arrayFindIndex from 'array-find-index';
import classNames from 'classnames';

import createHTMLAudioElementWithLoopEvent from './createHTMLAudioElementWithLoopEvent';
import getDisplayText from './utils/getDisplayText';
import getControlComponent from './utils/getControlComponent';

import './index.scss';

const log = console.log.bind(console);
const logError = console.error ? console.error.bind(console) : log;
const logWarning = console.warn ? console.warn.bind(console) : log;

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'cycle' prop (default true,
 * starts playing at the beginning of the playlist
 * when finished if true).
 *
 * Accepts 'pauseOnSeekPreview' prop (default true,
 * pauses audio while user is selecting new time
 * for playback)
 *
 * Accepts 'disableSeek' prop (default false,
 * disables seeking through the audio if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'style' prop, object, is applied to
 * outermost div (React styles).
 *
 * Accepts 'onMediaEvent' prop, an object used for
 * listening to media events on the underlying audio element.
 *
 * Accepts 'audioElementRef' prop, a function called after
 * the component mounts and before it unmounts with the
 * internally-referenced HTML audio element as its only parameter.
 * Similar to: https://facebook.github.io/react/docs/refs-and-the-dom.html
 */
class AudioPlayer extends Component {

  constructor (props) {
    super(props);

    this.defaultState = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      // elapsed time for current track, in seconds
      currentTime: 0,
      // The most recent targeted time, in seconds, for seek preview
      seekPreviewTime: 0,
      /* true if the user is currently dragging the mouse
       * to seek a new track position
       */
      seekInProgress: false,
      /* true if audio was playing when seek previewing began,
       * it was paused, and it should be resumed on seek
       * complete
       */
      awaitingResumeOnSeekComplete: false,
      // the latest volume of the audio, between 0 and 1.
      volume: 0,
      // true if the audio has been muted
      muted: false,
      // the duration in seconds of the loaded track
      duration: 0,
      /* the TimeRanges object representing the buffered sections of the
       * loaded track
       */
      buffered: null,
      /* the TimeRanges object representing the played sections of the
       * loaded track
       */
      played: null,
      // whether to loop the current track
      loop: true,
      // Rate at which audio should be played. 1.0 is normal speed.
      playbackRate: 1.0
    };

    this.state = this.defaultState;

    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    // html audio element used for playback
    this.audio = null;

    // bind methods fired on React events
    this.togglePause = this.togglePause.bind(this);
    this.skipToNextTrack = this.skipToNextTrack.bind(this);
    this.backSkip = this.backSkip.bind(this);
    this.seekPreview = this.seekPreview.bind(this);
    this.seekComplete = this.seekComplete.bind(this);

    // bind audio event listeners to add on mount and remove on unmount
    this.handleAudioPlay = this.handleAudioPlay.bind(this);
    this.handleAudioPause = this.handleAudioPause.bind(this);
    this.handleAudioEnded = this.handleAudioEnded.bind(this);
    this.handleAudioStalled = this.handleAudioStalled.bind(this);
    this.handleAudioTimeupdate = this.handleAudioTimeupdate.bind(this);
    this.handleAudioLoadedmetadata = this.handleAudioLoadedmetadata.bind(this);
    this.handleAudioVolumechange = this.handleAudioVolumechange.bind(this);
    this.handleAudioDurationchange = this.handleAudioDurationchange.bind(this);
    this.handleAudioProgress = this.handleAudioProgress.bind(this);
    this.handleAudioLoopchange = this.handleAudioLoopchange.bind(this);
    this.handleAudioRatechange = this.handleAudioRatechange.bind(this);
  }

  componentDidMount () {
    const audio = this.audio = createHTMLAudioElementWithLoopEvent();

    // initialize audio properties
    audio.volume = this.state.volume;
    audio.muted = this.state.muted;
    audio.loop = this.state.loop;
    audio.playbackRate = this.state.playbackRate;

    // add event listeners on the audio element
    audio.preload = 'metadata';
    audio.addEventListener('play', this.handleAudioPlay);
    audio.addEventListener('pause', this.handleAudioPause);
    audio.addEventListener('ended', this.handleAudioEnded);
    audio.addEventListener('stalled', this.handleAudioStalled);
    audio.addEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.addEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.addEventListener('volumechange', this.handleAudioVolumechange);
    audio.addEventListener('durationchange', this.handleAudioDurationchange);
    audio.addEventListener('progress', this.handleAudioProgress);
    audio.addEventListener('loopchange', this.handleAudioLoopchange);
    audio.addEventListener('ratechange', this.handleAudioRatechange);
    this.addMediaEventListeners(this.props.onMediaEvent);

    if (this.props.playlist && this.props.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = setTimeout(() => {
          this.togglePause(false);
        }, this.props.autoplayDelayInSeconds * 1000);
      }
    }

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  componentWillReceiveProps (nextProps) {
    // Update media event listeners that may have changed
    this.removeMediaEventListeners(this.props.onMediaEvent);
    this.addMediaEventListeners(nextProps.onMediaEvent);

    const newPlaylist = nextProps.playlist;
    if (!newPlaylist || !newPlaylist.length) {
      this.audio.src = '';
      this.currentTrackIndex = 0;
      return this.setState(this.defaultState);
    }

    const oldPlaylist = this.props.playlist;

    const currentTrackUrl = ((oldPlaylist || [])[this.currentTrackIndex] || {}).url;
    this.currentTrackIndex = arrayFindIndex(newPlaylist, track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex.
     */
    if (this.currentTrackIndex !== -1) {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
  }

  componentDidUpdate () {
    /* if we loaded a new playlist and reset the current track marker, we
     * should load up the first one.
     */
    if (this.currentTrackIndex === -1) {
      this.skipToNextTrack(false);
    }
  }

  componentWillUnmount () {
    const { audio } = this;
    // remove event listeners on the audio element
    audio.removeEventListener('play', this.handleAudioPlay);
    audio.removeEventListener('pause', this.handleAudioPause);
    audio.removeEventListener('ended', this.handleAudioEnded);
    audio.removeEventListener('stalled', this.handleAudioStalled);
    audio.removeEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.removeEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.removeEventListener('volumechange', this.handleAudioVolumechange);
    audio.removeEventListener('durationchange', this.handleAudioDurationchange);
    audio.removeEventListener('progress', this.handleAudioProgress);
    audio.removeEventListener('loopchange', this.handleAudioLoopchange);
    audio.removeEventListener('ratechange', this.handleAudioRatechange);
    removeMediaEventListeners(this.props.onMediaEvent);

    clearTimeout(this.gapLengthTimeout);
    clearTimeout(this.delayTimeout);

    // pause the audio element before we unmount
    audio.pause();

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  addMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.addEventListener(type, mediaEvents[type]);
    });
  }

  removeMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.removeEventListener(type, mediaEvents[type]);
    });
  }

  handleAudioPlay () {
    this.setState({ paused: false });
  }

  handleAudioPause () {
    this.setState({ paused: true });
  }

  handleAudioEnded () {
    clearTimeout(this.gapLengthTimeout);
    this.gapLengthTimeout = setTimeout(
      this.skipToNextTrack,
      this.props.gapLengthInSeconds * 1000
    );
  }

  handleAudioStalled () {
    this.togglePause(true);
  }

  handleAudioTimeupdate () {
    const { currentTime, played } = this.audio;
    this.setState({ currentTime, played });
  }

  handleAudioLoadedmetadata () {
    this.setState({
      activeTrackIndex: this.currentTrackIndex
    });
  }

  handleAudioVolumechange () {
    const { volume, muted } = this.audio;
    this.setState({ volume, muted });
  }

  handleAudioDurationchange () {
    const { duration } = this.audio;
    this.setState({ duration });
  }

  handleAudioProgress () {
    const { buffered } = this.audio;
    this.setState({ buffered });
  }

  handleAudioLoopchange () {
    const { loop } = this.audio;
    this.setState({ loop });
  }

  handleAudioRatechange () {
    const { playbackRate } = this.audio;
    this.setState({ playbackRate });
  }

  updateSource () {
    this.audio.src = this.props.playlist[this.currentTrackIndex].url;
  }

  togglePause (value) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      return audio.pause();
    }
    const { playlist } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    try {
      audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    audio.pause();
    const { playlist, cycle } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      currentTime: 0
    }, () => {
      this.updateSource();
      const shouldPauseOnCycle = (!cycle && i === 0);
      const shouldPause = shouldPauseOnCycle || (typeof shouldPlay === 'boolean' ? !shouldPlay : false);
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    const { playlist, stayOnBackSkipThreshold } = this.props;
    const { audio } = this;
    if (!playlist || !playlist.length) {
      return;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  seekPreview (progress) {
    const { paused, awaitingResumeOnSeekComplete } = this.state;
    if (this.isSeekUnavailable()) {
      return;
    }
    if (!paused && this.props.pauseOnSeekPreview && !awaitingResumeOnSeekComplete) {
      this.setState({
        awaitingResumeOnSeekComplete: true
      });
      this.togglePause(true);
    }
    const progressInBounds = Math.max(0, Math.min(progress, 1));
    this.setState({
      seekPreviewTime: progressInBounds * this.audio.duration,
      seekInProgress: true
    });
  }

  seekComplete () {
    const { seekPreviewTime, awaitingResumeOnSeekComplete } = this.state;
    this.setState({
      seekInProgress: false,
    });
    if (isNaN(seekPreviewTime)) {
      return;
    }
    this.setState({
      /* we'll update currentTime on the audio listener hook anyway,
       * but the optimistic update helps avoid a visual glitch in
       * the progress bar, if seekInProgress changes before currentTime.
       */
      currentTime: seekPreviewTime
    });
    const { audio } = this;
    audio.currentTime = seekPreviewTime;
    if (awaitingResumeOnSeekComplete) {
      audio.play();
      this.setState({
        awaitingResumeOnSeekComplete: false
      });
    }
  }

  isSeekUnavailable () {
    return Boolean(
      !this.props.playlist ||
      !this.props.playlist.length ||
      this.props.disableSeek
    );
  }

  render () {
    const { playlist, controls, style } = this.props;
    return (
      <div
        className="audio_player"
        title={getDisplayText(playlist, this.state.activeTrackIndex)}
        style={style}
      >
        {controls.map((control, index) => {
          const ControlComponent = getControlComponent(control);
          if (!ControlComponent) {
            return null;
          }
          return (
            <ControlComponent
              key={index}
              playlist={playlist}
              seekUnavailable={this.isSeekUnavailable()}
              onTogglePause={this.togglePause}
              onBackSkip={this.backSkip}
              onForwardSkip={this.skipToNextTrack}
              onSeekPreview={this.seekPreview}
              onSeekComplete={this.seekComplete}
              {...this.state}
            />
          );
        })}
      </div>
    );
  }

}

AudioPlayer.propTypes = {
  playlist: PropTypes.arrayOf(PropTypes.shape({
    url: PropTypes.string.isRequired,
    displayText: PropTypes.string.isRequired
  }).isRequired),
  controls: PropTypes.arrayOf(PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.oneOf([
      'playpause',
      'backskip',
      'forwardskip',
      'progress',
      'spacer'
    ])
  ]).isRequired),
  autoplay: PropTypes.bool,
  autoplayDelayInSeconds: PropTypes.number,
  gapLengthInSeconds: PropTypes.number,
  cycle: PropTypes.bool,
  pauseOnSeekPreview: PropTypes.bool,
  disableSeek: PropTypes.bool,
  stayOnBackSkipThreshold: PropTypes.number,
  style: PropTypes.object,
  onMediaEvent: PropTypes.objectOf(PropTypes.func.isRequired),
  audioElementRef: PropTypes.func
};

AudioPlayer.defaultProps = {
  controls: [
    'spacer',
    'backskip',
    'playpause',
    'forwardskip',
    'spacer',
    'progress'
  ],
  autoplay: false,
  autoplayDelayInSeconds: 0,
  gapLengthInSeconds: 0,
  cycle: true,
  pauseOnSeekPreview: false,
  disableSeek: false,
  stayOnBackSkipThreshold: 5
};

module.exports = AudioPlayer;
<commit_msg>Default volume fixed.<commit_after>import React, { Component, PropTypes } from 'react';
import arrayFindIndex from 'array-find-index';
import classNames from 'classnames';

import createHTMLAudioElementWithLoopEvent from './createHTMLAudioElementWithLoopEvent';
import getDisplayText from './utils/getDisplayText';
import getControlComponent from './utils/getControlComponent';

import './index.scss';

const log = console.log.bind(console);
const logError = console.error ? console.error.bind(console) : log;
const logWarning = console.warn ? console.warn.bind(console) : log;

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'cycle' prop (default true,
 * starts playing at the beginning of the playlist
 * when finished if true).
 *
 * Accepts 'pauseOnSeekPreview' prop (default true,
 * pauses audio while user is selecting new time
 * for playback)
 *
 * Accepts 'disableSeek' prop (default false,
 * disables seeking through the audio if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'style' prop, object, is applied to
 * outermost div (React styles).
 *
 * Accepts 'onMediaEvent' prop, an object used for
 * listening to media events on the underlying audio element.
 *
 * Accepts 'audioElementRef' prop, a function called after
 * the component mounts and before it unmounts with the
 * internally-referenced HTML audio element as its only parameter.
 * Similar to: https://facebook.github.io/react/docs/refs-and-the-dom.html
 */
class AudioPlayer extends Component {

  constructor (props) {
    super(props);

    this.defaultState = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      // elapsed time for current track, in seconds
      currentTime: 0,
      // The most recent targeted time, in seconds, for seek preview
      seekPreviewTime: 0,
      /* true if the user is currently dragging the mouse
       * to seek a new track position
       */
      seekInProgress: false,
      /* true if audio was playing when seek previewing began,
       * it was paused, and it should be resumed on seek
       * complete
       */
      awaitingResumeOnSeekComplete: false,
      // the latest volume of the audio, between 0 and 1.
      volume: 1.0,
      // true if the audio has been muted
      muted: false,
      // the duration in seconds of the loaded track
      duration: 0,
      /* the TimeRanges object representing the buffered sections of the
       * loaded track
       */
      buffered: null,
      /* the TimeRanges object representing the played sections of the
       * loaded track
       */
      played: null,
      // whether to loop the current track
      loop: true,
      // Rate at which audio should be played. 1.0 is normal speed.
      playbackRate: 1.0
    };

    this.state = this.defaultState;

    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    // html audio element used for playback
    this.audio = null;

    // bind methods fired on React events
    this.togglePause = this.togglePause.bind(this);
    this.skipToNextTrack = this.skipToNextTrack.bind(this);
    this.backSkip = this.backSkip.bind(this);
    this.seekPreview = this.seekPreview.bind(this);
    this.seekComplete = this.seekComplete.bind(this);

    // bind audio event listeners to add on mount and remove on unmount
    this.handleAudioPlay = this.handleAudioPlay.bind(this);
    this.handleAudioPause = this.handleAudioPause.bind(this);
    this.handleAudioEnded = this.handleAudioEnded.bind(this);
    this.handleAudioStalled = this.handleAudioStalled.bind(this);
    this.handleAudioTimeupdate = this.handleAudioTimeupdate.bind(this);
    this.handleAudioLoadedmetadata = this.handleAudioLoadedmetadata.bind(this);
    this.handleAudioVolumechange = this.handleAudioVolumechange.bind(this);
    this.handleAudioDurationchange = this.handleAudioDurationchange.bind(this);
    this.handleAudioProgress = this.handleAudioProgress.bind(this);
    this.handleAudioLoopchange = this.handleAudioLoopchange.bind(this);
    this.handleAudioRatechange = this.handleAudioRatechange.bind(this);
  }

  componentDidMount () {
    const audio = this.audio = createHTMLAudioElementWithLoopEvent();

    // initialize audio properties
    audio.volume = this.state.volume;
    audio.muted = this.state.muted;
    audio.loop = this.state.loop;
    audio.playbackRate = this.state.playbackRate;

    // add event listeners on the audio element
    audio.preload = 'metadata';
    audio.addEventListener('play', this.handleAudioPlay);
    audio.addEventListener('pause', this.handleAudioPause);
    audio.addEventListener('ended', this.handleAudioEnded);
    audio.addEventListener('stalled', this.handleAudioStalled);
    audio.addEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.addEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.addEventListener('volumechange', this.handleAudioVolumechange);
    audio.addEventListener('durationchange', this.handleAudioDurationchange);
    audio.addEventListener('progress', this.handleAudioProgress);
    audio.addEventListener('loopchange', this.handleAudioLoopchange);
    audio.addEventListener('ratechange', this.handleAudioRatechange);
    this.addMediaEventListeners(this.props.onMediaEvent);

    if (this.props.playlist && this.props.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = setTimeout(() => {
          this.togglePause(false);
        }, this.props.autoplayDelayInSeconds * 1000);
      }
    }

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  componentWillReceiveProps (nextProps) {
    // Update media event listeners that may have changed
    this.removeMediaEventListeners(this.props.onMediaEvent);
    this.addMediaEventListeners(nextProps.onMediaEvent);

    const newPlaylist = nextProps.playlist;
    if (!newPlaylist || !newPlaylist.length) {
      this.audio.src = '';
      this.currentTrackIndex = 0;
      return this.setState(this.defaultState);
    }

    const oldPlaylist = this.props.playlist;

    const currentTrackUrl = ((oldPlaylist || [])[this.currentTrackIndex] || {}).url;
    this.currentTrackIndex = arrayFindIndex(newPlaylist, track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex.
     */
    if (this.currentTrackIndex !== -1) {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
  }

  componentDidUpdate () {
    /* if we loaded a new playlist and reset the current track marker, we
     * should load up the first one.
     */
    if (this.currentTrackIndex === -1) {
      this.skipToNextTrack(false);
    }
  }

  componentWillUnmount () {
    const { audio } = this;
    // remove event listeners on the audio element
    audio.removeEventListener('play', this.handleAudioPlay);
    audio.removeEventListener('pause', this.handleAudioPause);
    audio.removeEventListener('ended', this.handleAudioEnded);
    audio.removeEventListener('stalled', this.handleAudioStalled);
    audio.removeEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.removeEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.removeEventListener('volumechange', this.handleAudioVolumechange);
    audio.removeEventListener('durationchange', this.handleAudioDurationchange);
    audio.removeEventListener('progress', this.handleAudioProgress);
    audio.removeEventListener('loopchange', this.handleAudioLoopchange);
    audio.removeEventListener('ratechange', this.handleAudioRatechange);
    removeMediaEventListeners(this.props.onMediaEvent);

    clearTimeout(this.gapLengthTimeout);
    clearTimeout(this.delayTimeout);

    // pause the audio element before we unmount
    audio.pause();

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  addMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.addEventListener(type, mediaEvents[type]);
    });
  }

  removeMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.removeEventListener(type, mediaEvents[type]);
    });
  }

  handleAudioPlay () {
    this.setState({ paused: false });
  }

  handleAudioPause () {
    this.setState({ paused: true });
  }

  handleAudioEnded () {
    clearTimeout(this.gapLengthTimeout);
    this.gapLengthTimeout = setTimeout(
      this.skipToNextTrack,
      this.props.gapLengthInSeconds * 1000
    );
  }

  handleAudioStalled () {
    this.togglePause(true);
  }

  handleAudioTimeupdate () {
    const { currentTime, played } = this.audio;
    this.setState({ currentTime, played });
  }

  handleAudioLoadedmetadata () {
    this.setState({
      activeTrackIndex: this.currentTrackIndex
    });
  }

  handleAudioVolumechange () {
    const { volume, muted } = this.audio;
    this.setState({ volume, muted });
  }

  handleAudioDurationchange () {
    const { duration } = this.audio;
    this.setState({ duration });
  }

  handleAudioProgress () {
    const { buffered } = this.audio;
    this.setState({ buffered });
  }

  handleAudioLoopchange () {
    const { loop } = this.audio;
    this.setState({ loop });
  }

  handleAudioRatechange () {
    const { playbackRate } = this.audio;
    this.setState({ playbackRate });
  }

  updateSource () {
    this.audio.src = this.props.playlist[this.currentTrackIndex].url;
  }

  togglePause (value) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      return audio.pause();
    }
    const { playlist } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    try {
      audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    audio.pause();
    const { playlist, cycle } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      currentTime: 0
    }, () => {
      this.updateSource();
      const shouldPauseOnCycle = (!cycle && i === 0);
      const shouldPause = shouldPauseOnCycle || (typeof shouldPlay === 'boolean' ? !shouldPlay : false);
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    const { playlist, stayOnBackSkipThreshold } = this.props;
    const { audio } = this;
    if (!playlist || !playlist.length) {
      return;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  seekPreview (progress) {
    const { paused, awaitingResumeOnSeekComplete } = this.state;
    if (this.isSeekUnavailable()) {
      return;
    }
    if (!paused && this.props.pauseOnSeekPreview && !awaitingResumeOnSeekComplete) {
      this.setState({
        awaitingResumeOnSeekComplete: true
      });
      this.togglePause(true);
    }
    const progressInBounds = Math.max(0, Math.min(progress, 1));
    this.setState({
      seekPreviewTime: progressInBounds * this.audio.duration,
      seekInProgress: true
    });
  }

  seekComplete () {
    const { seekPreviewTime, awaitingResumeOnSeekComplete } = this.state;
    this.setState({
      seekInProgress: false,
    });
    if (isNaN(seekPreviewTime)) {
      return;
    }
    this.setState({
      /* we'll update currentTime on the audio listener hook anyway,
       * but the optimistic update helps avoid a visual glitch in
       * the progress bar, if seekInProgress changes before currentTime.
       */
      currentTime: seekPreviewTime
    });
    const { audio } = this;
    audio.currentTime = seekPreviewTime;
    if (awaitingResumeOnSeekComplete) {
      audio.play();
      this.setState({
        awaitingResumeOnSeekComplete: false
      });
    }
  }

  isSeekUnavailable () {
    return Boolean(
      !this.props.playlist ||
      !this.props.playlist.length ||
      this.props.disableSeek
    );
  }

  render () {
    const { playlist, controls, style } = this.props;
    return (
      <div
        className="audio_player"
        title={getDisplayText(playlist, this.state.activeTrackIndex)}
        style={style}
      >
        {controls.map((control, index) => {
          const ControlComponent = getControlComponent(control);
          if (!ControlComponent) {
            return null;
          }
          return (
            <ControlComponent
              key={index}
              playlist={playlist}
              seekUnavailable={this.isSeekUnavailable()}
              onTogglePause={this.togglePause}
              onBackSkip={this.backSkip}
              onForwardSkip={this.skipToNextTrack}
              onSeekPreview={this.seekPreview}
              onSeekComplete={this.seekComplete}
              {...this.state}
            />
          );
        })}
      </div>
    );
  }

}

AudioPlayer.propTypes = {
  playlist: PropTypes.arrayOf(PropTypes.shape({
    url: PropTypes.string.isRequired,
    displayText: PropTypes.string.isRequired
  }).isRequired),
  controls: PropTypes.arrayOf(PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.oneOf([
      'playpause',
      'backskip',
      'forwardskip',
      'progress',
      'spacer'
    ])
  ]).isRequired),
  autoplay: PropTypes.bool,
  autoplayDelayInSeconds: PropTypes.number,
  gapLengthInSeconds: PropTypes.number,
  cycle: PropTypes.bool,
  pauseOnSeekPreview: PropTypes.bool,
  disableSeek: PropTypes.bool,
  stayOnBackSkipThreshold: PropTypes.number,
  style: PropTypes.object,
  onMediaEvent: PropTypes.objectOf(PropTypes.func.isRequired),
  audioElementRef: PropTypes.func
};

AudioPlayer.defaultProps = {
  controls: [
    'spacer',
    'backskip',
    'playpause',
    'forwardskip',
    'spacer',
    'progress'
  ],
  autoplay: false,
  autoplayDelayInSeconds: 0,
  gapLengthInSeconds: 0,
  cycle: true,
  pauseOnSeekPreview: false,
  disableSeek: false,
  stayOnBackSkipThreshold: 5
};

module.exports = AudioPlayer;
<|endoftext|>


<commit_before><commit_msg>throw in some curly before tripping over it later<commit_after><|endoftext|>


<commit_before><commit_msg>Remove default ns trickery in xmlshim<commit_after><|endoftext|>


<commit_before>#!/usr/bin/env node

var _ = require('lodash');
var async = require('async-chainable');
var asyncFlush = require('async-chainable-flush');
var colors = require('chalk');
var doop = require('.');
var glob = require('glob');
var fs = require('fs');
var fspath = require('path');
var program = require('commander');
var sha1 = require('node-sha1');

program
	.version(require('./package.json').version)
	.description('List units installed for the current project')
	.option('-b, --basic', 'Display a simple list, do not attempt to hash file differences')
	.option('-s, --server', 'Limit list to only server units (cannot be used with `--client`)')
	.option('-c, --client', 'Limit list to only client units (cannot be used with `--server`)')
	.option('-v, --verbose', 'Be verbose. Specify multiple times for increasing verbosity', function(i, v) { return v + 1 }, 0)
	.parse(process.argv);

async()
	.use(asyncFlush)
	.then(doop.chProjectRoot)
	.then(doop.getUserSettings)
	// Get the list of units {{{
	.parallel({
		client: function(next) {
			if (program.server) return next();
			doop.getUnits(function(err, units) {
				if (err) return next(err);
				next(null, units.map(u => { return {id: u, path: fspath.join(doop.settings.paths.client, u), files:{}} }));
			}, 'client');
		},
		server: function(next) {
			if (program.server) return next();
			doop.getUnits(function(err, units) {
				if (err) return next(err);
				next(null, units.map(u => { return {id: u, path: fspath.join(doop.settings.paths.server, u), files:{}} }));
			}, 'server');
		},
	})
	// }}}
	// Hash file comparisons unless program.basic {{{
	// Get repo {{{
	.then('repo', function(next) {
		if (program.basic) return next();
		doop.getDoopPath(next, program.repo);
	})
	.then(function(next) {
		if (program.basic) return next();
		if (program.verbose) console.log('Using Doop source:', colors.cyan(this.repo));
		next();
	})
	// }}}
	// Scan project + Doop file list and hash all files (unless !program.basic) {{{
	.then(function(next) {
		if (program.basic) return next();

		// Make a list of all files in both this project and in the doop repo
		// For each file create an object with a `local` sha1 hash and `doop` sha1 hash

		var hashQueue = async(); // Hash tasks to perform

		async()
			.forEach(_.concat(this.client, this.server), function(next, unit) {
				async()
					.parallel([
						// Hash all project files {{{
						function(next) {
							glob(fspath.join(unit.path, '**'), {nodir: true}, function(err, files) {
								if (files.length) {
									unit.existsInProject = true;
									files.forEach(function(file) {
										hashQueue.defer(file, function(next) {
											if (program.verbose) console.log('Hash file (Proj)', colors.cyan(file));
											sha1(fs.createReadStream(file), function(err, hash) {
												if (!unit.files[file]) unit.files[file] = {path: file};
												unit.files[file].project = hash;
												next();
											});
										});
									});
								} else {
									unit.existsInProject = false;
								}
								next();
							});
						},
						// }}}
						// Hash all Doop files {{{
						function(next) {
							glob(fspath.join(doop.settings.paths.doop, unit.path, '**'), {nodir: true}, function(err, files) {
								if (files.length) {
									unit.existsInDoop = true;
									files.forEach(function(rawFile) {
										var croppedPath = rawFile.substr(doop.settings.paths.doop.length + 1);
										var file = fspath.join(doop.settings.paths.doop, croppedPath);

										hashQueue.defer(file, function(next) {
											if (program.verbose) console.log('Hash file (Doop)', colors.cyan(croppedPath));
											sha1(fs.createReadStream(file), function(err, hash) {
												if (!unit.files[croppedPath]) unit.files[croppedPath] = {path: file};
												unit.files[croppedPath].doop = hash;
												next();
											});
										});
									});
								} else {
									unit.existsInDoop = false;
								}
								next();
							});
						},
						// }}}
					])
					.end(next)
			})
			.end(function(err) {
				if (err) return next(err);

				// Wait for hashing queue to finish
				hashQueue.await().end(next);
			});
	})
	// }}}
	// }}}
	// Present the list {{{
	.then(function(next) {
		var task = this;
		if (program.verbose > 1) console.log();
		[
			!program.server ? 'client' : null,
			!program.client ? 'server' : null,
		]
			.filter(i => !!i) // Filter out empty
			.forEach(type => {
				console.log(colors.bold.blue(type));
				if (program.basic) { // Dont bother to do any weird hash comparison operations
					this[type].forEach(unit => console.log(' -', unit.id));
				} else {
					this[type].forEach(function(unit) {
						if (unit.existsInProject && !unit.existsInDoop) {
							console.log(colors.grey(' -', unit.id));
						} else if (!unit.existsInProject && unit.existsInDoop) {
							console.log(colors.red(' -', unit.id));
						} else { // In both Doop + Project - examine file differences
							var changes = [];

							// Edited {{{
							var items = _.filter(unit.files, f => f.project && f.doop && f.project != f.doop);
							if (_.get(doop.settings, 'list.changes.maxEdited') && items.length > doop.settings.list.changes.maxEdited) {
								changes.push(colors.yellow.bold('~' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.yellow.bold('~') + f.path.substr(unit.path.length+1)));
							}
							// }}}

							// Created {{{
							var items = _.filter(unit.files, f => f.project && !f.doop);
							if (_.get(doop.settings, 'list.changes.maxCreated') && items.length > doop.settings.list.changes.maxCreated) {
								changes.push(colors.green.bold('+' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.green.bold('+') + f.path.substr(unit.path.length+1)));
							}
							// }}}

							// Deleted {{{
							var items = _.filter(unit.files, f => f.doop && !f.project);
							if (_.get(doop.settings, 'list.changes.maxDeleted') && items.length > doop.settings.list.changes.maxDeleted) {
								changes.push(colors.red.bold('-' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.red.bold('-') + f.path.substr(doop.settings.paths.doop.length+unit.path.length+2)));
							}
							// }}}

							if (changes.length) {
								console.log(' -', unit.id, colors.blue('('), changes.join(', '), colors.blue(')'));
							} else {
								console.log(' -', unit.id);
							}
						}
					});
				}
			});
		next();
	})
	// }}}
	// End {{{
	.flush()
	.end(function(err) {
		if (err) {
			console.log(colors.red('Doop Error'), err.toString());
			process.exit(1);
		} else {
			process.exit(0);
		}
	});
	// }}}
<commit_msg>List adds new line spacing after first section output<commit_after>#!/usr/bin/env node

var _ = require('lodash');
var async = require('async-chainable');
var asyncFlush = require('async-chainable-flush');
var colors = require('chalk');
var doop = require('.');
var glob = require('glob');
var fs = require('fs');
var fspath = require('path');
var program = require('commander');
var sha1 = require('node-sha1');

program
	.version(require('./package.json').version)
	.description('List units installed for the current project')
	.option('-b, --basic', 'Display a simple list, do not attempt to hash file differences')
	.option('-s, --server', 'Limit list to only server units (cannot be used with `--client`)')
	.option('-c, --client', 'Limit list to only client units (cannot be used with `--server`)')
	.option('-v, --verbose', 'Be verbose. Specify multiple times for increasing verbosity', function(i, v) { return v + 1 }, 0)
	.parse(process.argv);

async()
	.use(asyncFlush)
	.then(doop.chProjectRoot)
	.then(doop.getUserSettings)
	// Get the list of units {{{
	.parallel({
		client: function(next) {
			if (program.server) return next();
			doop.getUnits(function(err, units) {
				if (err) return next(err);
				next(null, units.map(u => { return {id: u, path: fspath.join(doop.settings.paths.client, u), files:{}} }));
			}, 'client');
		},
		server: function(next) {
			if (program.server) return next();
			doop.getUnits(function(err, units) {
				if (err) return next(err);
				next(null, units.map(u => { return {id: u, path: fspath.join(doop.settings.paths.server, u), files:{}} }));
			}, 'server');
		},
	})
	// }}}
	// Hash file comparisons unless program.basic {{{
	// Get repo {{{
	.then('repo', function(next) {
		if (program.basic) return next();
		doop.getDoopPath(next, program.repo);
	})
	.then(function(next) {
		if (program.basic) return next();
		if (program.verbose) console.log('Using Doop source:', colors.cyan(this.repo));
		next();
	})
	// }}}
	// Scan project + Doop file list and hash all files (unless !program.basic) {{{
	.then(function(next) {
		if (program.basic) return next();

		// Make a list of all files in both this project and in the doop repo
		// For each file create an object with a `local` sha1 hash and `doop` sha1 hash

		var hashQueue = async(); // Hash tasks to perform

		async()
			.forEach(_.concat(this.client, this.server), function(next, unit) {
				async()
					.parallel([
						// Hash all project files {{{
						function(next) {
							glob(fspath.join(unit.path, '**'), {nodir: true}, function(err, files) {
								if (files.length) {
									unit.existsInProject = true;
									files.forEach(function(file) {
										hashQueue.defer(file, function(next) {
											if (program.verbose) console.log('Hash file (Proj)', colors.cyan(file));
											sha1(fs.createReadStream(file), function(err, hash) {
												if (!unit.files[file]) unit.files[file] = {path: file};
												unit.files[file].project = hash;
												next();
											});
										});
									});
								} else {
									unit.existsInProject = false;
								}
								next();
							});
						},
						// }}}
						// Hash all Doop files {{{
						function(next) {
							glob(fspath.join(doop.settings.paths.doop, unit.path, '**'), {nodir: true}, function(err, files) {
								if (files.length) {
									unit.existsInDoop = true;
									files.forEach(function(rawFile) {
										var croppedPath = rawFile.substr(doop.settings.paths.doop.length + 1);
										var file = fspath.join(doop.settings.paths.doop, croppedPath);

										hashQueue.defer(file, function(next) {
											if (program.verbose) console.log('Hash file (Doop)', colors.cyan(croppedPath));
											sha1(fs.createReadStream(file), function(err, hash) {
												if (!unit.files[croppedPath]) unit.files[croppedPath] = {path: file};
												unit.files[croppedPath].doop = hash;
												next();
											});
										});
									});
								} else {
									unit.existsInDoop = false;
								}
								next();
							});
						},
						// }}}
					])
					.end(next)
			})
			.end(function(err) {
				if (err) return next(err);

				// Wait for hashing queue to finish
				hashQueue.await().end(next);
			});
	})
	// }}}
	// }}}
	// Present the list {{{
	.then(function(next) {
		var task = this;
		if (program.verbose > 1) console.log();
		[
			!program.server ? 'client' : null,
			!program.client ? 'server' : null,
		]
			.filter(i => !!i) // Filter out empty
			.forEach((type, typeIndex) => {
				if (typeIndex > 0) console.log(); // Add newline space after first item output
				console.log(colors.bold.blue(type));
				if (program.basic) { // Dont bother to do any weird hash comparison operations
					this[type].forEach(unit => console.log(' -', unit.id));
				} else {
					this[type].forEach(function(unit) {
						if (unit.existsInProject && !unit.existsInDoop) {
							console.log(colors.grey(' -', unit.id));
						} else if (!unit.existsInProject && unit.existsInDoop) {
							console.log(colors.red(' -', unit.id));
						} else { // In both Doop + Project - examine file differences
							var changes = [];

							// Edited {{{
							var items = _.filter(unit.files, f => f.project && f.doop && f.project != f.doop);
							if (_.get(doop.settings, 'list.changes.maxEdited') && items.length > doop.settings.list.changes.maxEdited) {
								changes.push(colors.yellow.bold('~' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.yellow.bold('~') + f.path.substr(unit.path.length+1)));
							}
							// }}}

							// Created {{{
							var items = _.filter(unit.files, f => f.project && !f.doop);
							if (_.get(doop.settings, 'list.changes.maxCreated') && items.length > doop.settings.list.changes.maxCreated) {
								changes.push(colors.green.bold('+' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.green.bold('+') + f.path.substr(unit.path.length+1)));
							}
							// }}}

							// Deleted {{{
							var items = _.filter(unit.files, f => f.doop && !f.project);
							if (_.get(doop.settings, 'list.changes.maxDeleted') && items.length > doop.settings.list.changes.maxDeleted) {
								changes.push(colors.red.bold('-' + items.length + ' items'));
							} else {
								items.forEach(f => changes.push(colors.red.bold('-') + f.path.substr(doop.settings.paths.doop.length+unit.path.length+2)));
							}
							// }}}

							if (changes.length) {
								console.log(' -', unit.id, colors.blue('('), changes.join(', '), colors.blue(')'));
							} else {
								console.log(' -', unit.id);
							}
						}
					});
				}
			});
		next();
	})
	// }}}
	// End {{{
	.flush()
	.end(function(err) {
		if (err) {
			console.log(colors.red('Doop Error'), err.toString());
			process.exit(1);
		} else {
			process.exit(0);
		}
	});
	// }}}
<|endoftext|>