<commit_before>import React from 'react';
import styled from 'styled-components';
import merge from 'lodash/merge';
import get from 'lodash/get';
import map from 'lodash/map';
import SelectBar from 'ui/components/SelectBar';
import Select from 'ui/components/Select';
import Tabs from 'ui/components/Tabs';
import CodeEditor from 'ui/components/CodeEditor';
import API from 'api';
import compact from 'lodash/compact';
import uniq from 'lodash/uniq';
import sortBy from 'lodash/sortBy';
import find from 'lodash/find';
import { getContentType } from 'api/utils/headers';
import { convertDelayToSeconds } from 'ui/utils/string';

const Container = styled.div`
  flex-grow: 1;
`;

const SettingsBar = styled.div`
  padding: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
`;

const Input = styled.input`
  border: none;
  background-color: #f0f0f0;
  border-radius: 4px;
  flex-grow: 1;
  height: 24px;
  outline: 0;
  font-size: 13px;
  line-height: 20px;
  padding: 0 6px;
`;

const Actions = styled.div`
  display: flex;
  align-items: center;
`;

const delayPreset = [
  { value: 500, label: '0.5s' },
  { value: 1000, label: '1s' },
  { value: 2000, label: '2s' },
  { value: 5000, label: '5s' },
  { value: 10000, label: '10s' },
  { value: 15000, label: '15s' }
];

const contentTypeHeaderToString = (headers) => {
  if (!headers || !headers['content-type']) {
    return null;
  }

  const contentType = headers['content-type'];

  switch (true) {
    case !!contentType.match('application/json'):
      return 'JSON';

    case !!contentType.match('text/xml'):
      return 'XML';

    case !!contentType.match('text/html'):
      return 'HTML';

    case !!contentType.match('text/plain'):
      return 'Plain text';

    case !!contentType.match('multipart'):
      return 'File';

    default:
      return null;
  }
};

class Editor extends React.Component {

  state = {
    selectedResponseContentTab: 'response body'
  };

  selectResponseContentTab = (selectedResponseContentTab) => {
    this.setState({ selectedResponseContentTab });
  };

  updateMock = (partialUpdate) => {
    const selectedMock = this.props.selectedMock;
    const mock         = merge({ ...selectedMock }, { ...partialUpdate });

    API.updateMock(selectedMock.id, mock)
  };

  handleCodeEditorChange = (value) => {
    this.updateMock({ response: { body: value } });
  };

  handleUrlChange = (event) => {
    this.updateMock({ url: event.target.value.trim() });
  };

  handleMethodChange = (method) => {
    this.updateMock({ method });

    if (method === 'GET' || method === 'DELETE') {
      this.setState({ selectedResponseContentTab: 'response body' });
      return;
    }

    this.forceUpdate();
  };

  handleResponseChange = (status) => {
    this.updateMock({
      response: { ...this.props.selectedMock.response, status }
    });

    this.forceUpdate();
  };

  handleContentTypeChange = (value) => {
    this.updateMock({
      response: {
        headers: {
          'content-type': getContentType(value)
        }
      }
    });

    this.forceUpdate();
  };

  handleDelayChange = (delay) => {
    this.updateMock({
      response: {
        delay
      }
    });

    this.forceUpdate();
  };

  getTabs() {
    const method = this.props.selectedMock.method;
    const tabs   = ['response headers', 'response body'];

    if (method === 'GET' || method === 'DELETE') {
      return tabs;
    }

    return ['request body', ...tabs];
  }

  render() {
    const { selectedMock }               = this.props;
    const { selectedResponseContentTab } = this.state;

    if (!selectedMock) {
      return null;
    }

    const delayInPreset = find(delayPreset, { value: selectedMock.response.delay });

    let delayOptions = delayPreset;
    if (!delayInPreset) {
      delayOptions = [...delayOptions, {
        value: selectedMock.response.delay,
        label: convertDelayToSeconds(selectedMock.response.delay)
      }];
    }

    return (
      <Container>
        <SettingsBar>
          <SelectBar values={['GET', 'POST', 'PUT', 'DELETE']}
                     selectedValue={ selectedMock.method }
                     onChange={ this.handleMethodChange }/>

          <Input type="text" value={ selectedMock.url } onChange={ this.handleUrlChange }/>
        </SettingsBar>

        <SettingsBar>
          <Actions>
            <SelectBar values={ sortBy(uniq(compact([200, 201, 401, 500, selectedMock.response.status]))) }
                       other={ true }
                       selectedValue={ selectedMock.response.status }
                       onChange={ this.handleResponseChange }/>

            <SelectBar
              values={['JSON', 'XML', 'HTML', 'Plain text']}
              selectedValue={ contentTypeHeaderToString(selectedMock.response.headers) }
              onChange={ this.handleContentTypeChange }/>

            <Select
              valueIcon="delay"
              value={ selectedMock.response.delay }
              onChange={ this.handleDelayChange }
              options={ delayOptions }/>
          </Actions>
        </SettingsBar>

        <Tabs options={ this.getTabs() }
              selectTab={ this.selectResponseContentTab }
              selectedTab={ selectedResponseContentTab }/>

        {
          selectedResponseContentTab === 'request body' &&
          <CodeEditor
            key={ `${selectedMock.id}-${selectedResponseContentTab}` }
            value={ get(selectedMock, 'params') }
            onChange={ this.handleCodeEditorChange }
            contentType={ get(selectedMock, 'headers[Content-Type]') }/>
        }

        {
          selectedResponseContentTab === 'response body' &&
          <CodeEditor
            key={ `${selectedMock.id}-${selectedResponseContentTab}` }
            value={ get(selectedMock, 'response.body') }
            onChange={ this.handleCodeEditorChange }
            contentType={ get(selectedMock, 'response.headers[content-type]') }/>
        }

        {
          selectedResponseContentTab === 'response headers' &&

            {
              map(selectedMock.response.headers, (value, header) => (

{header}: {value}

              ))
            }

        }

      </Container>
    );
  }
}

export default Editor;
<commit_msg>When switching between mocks go back to response body tab.<commit_after>import React from 'react';
import styled from 'styled-components';
import merge from 'lodash/merge';
import get from 'lodash/get';
import map from 'lodash/map';
import SelectBar from 'ui/components/SelectBar';
import Select from 'ui/components/Select';
import Tabs from 'ui/components/Tabs';
import CodeEditor from 'ui/components/CodeEditor';
import API from 'api';
import compact from 'lodash/compact';
import uniq from 'lodash/uniq';
import sortBy from 'lodash/sortBy';
import find from 'lodash/find';
import { getContentType } from 'api/utils/headers';
import { convertDelayToSeconds } from 'ui/utils/string';

const Container = styled.div`
  flex-grow: 1;
`;

const SettingsBar = styled.div`
  padding: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
`;

const Input = styled.input`
  border: none;
  background-color: #f0f0f0;
  border-radius: 4px;
  flex-grow: 1;
  height: 24px;
  outline: 0;
  font-size: 13px;
  line-height: 20px;
  padding: 0 6px;
`;

const Actions = styled.div`
  display: flex;
  align-items: center;
`;

const delayPreset = [
  { value: 500, label: '0.5s' },
  { value: 1000, label: '1s' },
  { value: 2000, label: '2s' },
  { value: 5000, label: '5s' },
  { value: 10000, label: '10s' },
  { value: 15000, label: '15s' }
];

const contentTypeHeaderToString = (headers) => {
  if (!headers || !headers['content-type']) {
    return null;
  }

  const contentType = headers['content-type'];

  switch (true) {
    case !!contentType.match('application/json'):
      return 'JSON';

    case !!contentType.match('text/xml'):
      return 'XML';

    case !!contentType.match('text/html'):
      return 'HTML';

    case !!contentType.match('text/plain'):
      return 'Plain text';

    case !!contentType.match('multipart'):
      return 'File';

    default:
      return null;
  }
};

class Editor extends React.Component {

  state = {
    selectedResponseContentTab: 'response body'
  };

  componentWillReceiveProps(nextProps) {
    if (this.props.selectedMock !== nextProps.selectedMock) {
      this.setState({ selectedResponseContentTab: 'response body' });
    }
  }

  selectResponseContentTab = (selectedResponseContentTab) => {
    this.setState({ selectedResponseContentTab });
  };

  updateMock = (partialUpdate) => {
    const selectedMock = this.props.selectedMock;
    const mock         = merge({ ...selectedMock }, { ...partialUpdate });

    API.updateMock(selectedMock.id, mock)
  };

  handleCodeEditorChange = (value) => {
    this.updateMock({ response: { body: value } });
  };

  handleUrlChange = (event) => {
    this.updateMock({ url: event.target.value.trim() });
  };

  handleMethodChange = (method) => {
    this.updateMock({ method });

    if (method === 'GET' || method === 'DELETE') {
      this.setState({ selectedResponseContentTab: 'response body' });
      return;
    }

    this.forceUpdate();
  };

  handleResponseChange = (status) => {
    this.updateMock({
      response: { ...this.props.selectedMock.response, status }
    });

    this.forceUpdate();
  };

  handleContentTypeChange = (value) => {
    this.updateMock({
      response: {
        headers: {
          'content-type': getContentType(value)
        }
      }
    });

    this.forceUpdate();
  };

  handleDelayChange = (delay) => {
    this.updateMock({
      response: {
        delay
      }
    });

    this.forceUpdate();
  };

  getTabs() {
    const method = this.props.selectedMock.method;
    const tabs   = ['response headers', 'response body'];

    if (method === 'GET' || method === 'DELETE') {
      return tabs;
    }

    return ['request body', ...tabs];
  }

  render() {
    const { selectedMock }               = this.props;
    const { selectedResponseContentTab } = this.state;

    if (!selectedMock) {
      return null;
    }

    const delayInPreset = find(delayPreset, { value: selectedMock.response.delay });

    let delayOptions = delayPreset;
    if (!delayInPreset) {
      delayOptions = [...delayOptions, {
        value: selectedMock.response.delay,
        label: convertDelayToSeconds(selectedMock.response.delay)
      }];
    }

    return (
      <Container>
        <SettingsBar>
          <SelectBar values={['GET', 'POST', 'PUT', 'DELETE']}
                     selectedValue={ selectedMock.method }
                     onChange={ this.handleMethodChange }/>

          <Input type="text" value={ selectedMock.url } onChange={ this.handleUrlChange }/>
        </SettingsBar>

        <SettingsBar>
          <Actions>
            <SelectBar values={ sortBy(uniq(compact([200, 201, 401, 500, selectedMock.response.status]))) }
                       other={ true }
                       selectedValue={ selectedMock.response.status }
                       onChange={ this.handleResponseChange }/>

            <SelectBar
              values={['JSON', 'XML', 'HTML', 'Plain text']}
              selectedValue={ contentTypeHeaderToString(selectedMock.response.headers) }
              onChange={ this.handleContentTypeChange }/>

            <Select
              valueIcon="delay"
              value={ selectedMock.response.delay }
              onChange={ this.handleDelayChange }
              options={ delayOptions }/>
          </Actions>
        </SettingsBar>

        <Tabs options={ this.getTabs() }
              selectTab={ this.selectResponseContentTab }
              selectedTab={ selectedResponseContentTab }/>

        {
          selectedResponseContentTab === 'request body' &&
          <CodeEditor
            key={ `${selectedMock.id}-${selectedResponseContentTab}` }
            value={ get(selectedMock, 'params') }
            onChange={ this.handleCodeEditorChange }
            contentType={ get(selectedMock, 'headers[Content-Type]') }/>
        }

        {
          selectedResponseContentTab === 'response body' &&
          <CodeEditor
            key={ `${selectedMock.id}-${selectedResponseContentTab}` }
            value={ get(selectedMock, 'response.body') }
            onChange={ this.handleCodeEditorChange }
            contentType={ get(selectedMock, 'response.headers[content-type]') }/>
        }

        {
          selectedResponseContentTab === 'response headers' &&

            {
              map(selectedMock.response.headers, (value, header) => (

{header}: {value}

              ))
            }

        }

      </Container>
    );
  }
}

export default Editor;
<|endoftext|>


<commit_before>'use strict';
'require rpc';

return L.Class.extend({
	__init__: function() {
		this.state = {
			newidx:  0,
			values:  { },
			creates: { },
			changes: { },
			deletes: { },
			reorder: { }
		};

		this.loaded = {};
	},

	callLoad: rpc.declare({
		object: 'uci',
		method: 'get',
		params: [ 'config' ],
		expect: { values: { } }
	}),

	callOrder: rpc.declare({
		object: 'uci',
		method: 'order',
		params: [ 'config', 'sections' ]
	}),

	callAdd: rpc.declare({
		object: 'uci',
		method: 'add',
		params: [ 'config', 'type', 'name', 'values' ],
		expect: { section: '' }
	}),

	callSet: rpc.declare({
		object: 'uci',
		method: 'set',
		params: [ 'config', 'section', 'values' ]
	}),

	callDelete: rpc.declare({
		object: 'uci',
		method: 'delete',
		params: [ 'config', 'section', 'options' ]
	}),

	callApply: rpc.declare({
		object: 'uci',
		method: 'apply',
		params: [ 'timeout', 'rollback' ]
	}),

	callConfirm: rpc.declare({
		object: 'uci',
		method: 'confirm'
	}),

	createSID: function(conf) {
		var v = this.state.values,
		    n = this.state.creates,
		    sid;

		do {
			sid = "new%06x".format(Math.random() * 0xFFFFFF);
		} while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));

		return sid;
	},

	resolveSID: function(conf, sid) {
		if (typeof(sid) != 'string')
			return sid;

		var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);

		if (m) {
			var type = m[1],
			    pos = +m[2],
			    sections = this.sections(conf, type),
			    section = sections[pos >= 0 ? pos : sections.length + pos];

			return section ? section['.name'] : null;
		}

		return sid;
	},

	reorderSections: function() {
		var v = this.state.values,
		    n = this.state.creates,
		    r = this.state.reorder,
		    tasks = [];

		if (Object.keys(r).length === 0)
			return Promise.resolve();

		/*
		 gather all created and existing sections, sort them according
		 to their index value and issue an uci order call
		*/
		for (var c in r) {
			var o = [ ];

			if (n[c])
				for (var s in n[c])
					o.push(n[c][s]);

			for (var s in v[c])
				o.push(v[c][s]);

			if (o.length > 0) {
				o.sort(function(a, b) {
					return (a['.index'] - b['.index']);
				});

				var sids = [ ];

				for (var i = 0; i < o.length; i++)
					sids.push(o[i]['.name']);

				tasks.push(this.callOrder(c, sids));
			}
		}

		this.state.reorder = { };
		return Promise.all(tasks);
	},

	loadPackage: function(packageName) {
		if (this.loaded[packageName] == null)
			return (this.loaded[packageName] = this.callLoad(packageName));

		return Promise.resolve(this.loaded[packageName]);
	},

	load: function(packages) {
		var self = this,
		    pkgs = [ ],
		    tasks = [];

		if (!Array.isArray(packages))
			packages = [ packages ];

		for (var i = 0; i < packages.length; i++)
			if (!self.state.values[packages[i]]) {
				pkgs.push(packages[i]);
				tasks.push(self.loadPackage(packages[i]));
			}

		return Promise.all(tasks).then(function(responses) {
			for (var i = 0; i < responses.length; i++)
				self.state.values[pkgs[i]] = responses[i];

			if (responses.length)
				document.dispatchEvent(new CustomEvent('uci-loaded'));

			return pkgs;
		});
	},

	unload: function(packages) {
		if (!Array.isArray(packages))
			packages = [ packages ];

		for (var i = 0; i < packages.length; i++) {
			delete this.state.values[packages[i]];
			delete this.state.creates[packages[i]];
			delete this.state.changes[packages[i]];
			delete this.state.deletes[packages[i]];

			delete this.loaded[packages[i]];
		}
	},

	add: function(conf, type, name) {
		var n = this.state.creates,
		    sid = name || this.createSID(conf);

		if (!n[conf])
			n[conf] = { };

		n[conf][sid] = {
			'.type':      type,
			'.name':      sid,
			'.create':    name,
			'.anonymous': !name,
			'.index':     1000 + this.state.newidx++
		};

		return sid;
	},

	remove: function(conf, sid) {
		var n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		/* requested deletion of a just created section */
		if (n[conf] && n[conf][sid]) {
			delete n[conf][sid];
		}
		else {
			if (c[conf])
				delete c[conf][sid];

			if (!d[conf])
				d[conf] = { };

			d[conf][sid] = true;
		}
	},

	sections: function(conf, type, cb) {
		var sa = [ ],
		    v = this.state.values[conf],
		    n = this.state.creates[conf],
		    c = this.state.changes[conf],
		    d = this.state.deletes[conf];

		if (!v)
			return sa;

		for (var s in v)
			if (!d || d[s] !== true)
				if (!type || v[s]['.type'] == type)
					sa.push(Object.assign({ }, v[s], c ? c[s] : undefined));

		if (n)
			for (var s in n)
				if (!type || n[s]['.type'] == type)
					sa.push(Object.assign({ }, n[s]));

		sa.sort(function(a, b) {
			return a['.index'] - b['.index'];
		});

		for (var i = 0; i < sa.length; i++)
			sa[i]['.index'] = i;

		if (typeof(cb) == 'function')
			for (var i = 0; i < sa.length; i++)
				cb.call(this, sa[i], sa[i]['.name']);

		return sa;
	},

	get: function(conf, sid, opt) {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		sid = this.resolveSID(conf, sid);

		if (sid == null)
			return null;

		/* requested option in a just created section */
		if (n[conf] && n[conf][sid]) {
			if (!n[conf])
				return undefined;

			if (opt == null)
				return n[conf][sid];

			return n[conf][sid][opt];
		}

		/* requested an option value */
		if (opt != null) {
			/* check whether option was deleted */
			if (d[conf] && d[conf][sid]) {
				if (d[conf][sid] === true)
					return undefined;

				for (var i = 0; i < d[conf][sid].length; i++)
					if (d[conf][sid][i] == opt)
						return undefined;
			}

			/* check whether option was changed */
			if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
				return c[conf][sid][opt];

			/* return base value */
			if (v[conf] && v[conf][sid])
				return v[conf][sid][opt];

			return undefined;
		}

		/* requested an entire section */
		if (v[conf])
			return v[conf][sid];

		return undefined;
	},

	set: function(conf, sid, opt, val) {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		sid = this.resolveSID(conf, sid);

		if (sid == null || opt == null || opt.charAt(0) == '.')
			return;

		if (n[conf] && n[conf][sid]) {
			if (val != null)
				n[conf][sid][opt] = val;
			else
				delete n[conf][sid][opt];
		}
		else if (val != null && val !== '') {
			/* do not set within deleted section */
			if (d[conf] && d[conf][sid] === true)
				return;

			/* only set in existing sections */
			if (!v[conf] || !v[conf][sid])
				return;

			if (!c[conf])
				c[conf] = {};

			if (!c[conf][sid])
				c[conf][sid] = {};

			/* undelete option */
			if (d[conf] && d[conf][sid])
				d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });

			c[conf][sid][opt] = val;
		}
		else {
			/* only delete in existing sections */
			if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) &&
			    !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt)))
			    return;

			if (!d[conf])
				d[conf] = { };

			if (!d[conf][sid])
				d[conf][sid] = [ ];

			if (d[conf][sid] !== true)
				d[conf][sid].push(opt);
		}
	},

	unset: function(conf, sid, opt) {
		return this.set(conf, sid, opt, null);
	},

	get_first: function(conf, type, opt) {
		var sid = null;

		this.sections(conf, type, function(s) {
			if (sid == null)
				sid = s['.name'];
		});

		return this.get(conf, sid, opt);
	},

	set_first: function(conf, type, opt, val) {
		var sid = null;

		this.sections(conf, type, function(s) {
			if (sid == null)
				sid = s['.name'];
		});

		return this.set(conf, sid, opt, val);
	},

	unset_first: function(conf, type, opt) {
		return this.set_first(conf, type, opt, null);
	},

	move: function(conf, sid1, sid2, after) {
		var sa = this.sections(conf),
		    s1 = null, s2 = null;

		sid1 = this.resolveSID(conf, sid1);
		sid2 = this.resolveSID(conf, sid2);

		for (var i = 0; i < sa.length; i++) {
			if (sa[i]['.name'] != sid1)
				continue;

			s1 = sa[i];
			sa.splice(i, 1);
			break;
		}

		if (s1 == null)
			return false;

		if (sid2 == null) {
			sa.push(s1);
		}
		else {
			for (var i = 0; i < sa.length; i++) {
				if (sa[i]['.name'] != sid2)
					continue;

				s2 = sa[i];
				sa.splice(i + !!after, 0, s1);
				break;
			}

			if (s2 == null)
				return false;
		}

		for (var i = 0; i < sa.length; i++)
			this.get(conf, sa[i]['.name'])['.index'] = i;

		this.state.reorder[conf] = true;

		return true;
	},

	save: function() {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes,
		    self = this,
		    snew = [ ],
		    pkgs = { },
		    tasks = [];

		if (n)
			for (var conf in n) {
				for (var sid in n[conf]) {
					var r = {
						config: conf,
						values: { }
					};

					for (var k in n[conf][sid]) {
						if (k == '.type')
							r.type = n[conf][sid][k];
						else if (k == '.create')
							r.name = n[conf][sid][k];
						else if (k.charAt(0) != '.')
							r.values[k] = n[conf][sid][k];
					}

					snew.push(n[conf][sid]);
					tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
				}

				pkgs[conf] = true;
			}

		if (c)
			for (var conf in c) {
				for (var sid in c[conf])
					tasks.push(self.callSet(conf, sid, c[conf][sid]));

				pkgs[conf] = true;
			}

		if (d)
			for (var conf in d) {
				for (var sid in d[conf]) {
					var o = d[conf][sid];
					tasks.push(self.callDelete(conf, sid, (o === true) ? null : o));
				}

				pkgs[conf] = true;
			}

		return Promise.all(tasks).then(function(responses) {
			/*
			 array "snew" holds references to the created uci sections,
			 use it to assign the returned names of the new sections
			*/
			for (var i = 0; i < snew.length; i++)
				snew[i]['.name'] = responses[i];

			return self.reorderSections();
		}).then(function() {
			pkgs = Object.keys(pkgs);

			self.unload(pkgs);

			return self.load(pkgs);
		});
	},

	apply: function(timeout) {
		var self = this,
		    date = new Date();

		if (typeof(timeout) != 'number' || timeout < 1)
			timeout = 10;

		return self.callApply(timeout, true).then(function(rv) {
			if (rv != 0)
				return Promise.reject(rv);

			var try_deadline = date.getTime() + 1000 * timeout;
			var try_confirm = function() {
				return self.callConfirm().then(function(rv) {
					if (rv != 0) {
						if (date.getTime() < try_deadline)
							window.setTimeout(try_confirm, 250);
						else
							return Promise.reject(rv);
					}

					return rv;
				});
			};

			window.setTimeout(try_confirm, 1000);
		});
	},

	changes: rpc.declare({
		object: 'uci',
		method: 'changes',
		expect: { changes: { } }
	})
});
<commit_msg>luci-base: uci.js: track packages affected by reorder operations<commit_after>'use strict';
'require rpc';

return L.Class.extend({
	__init__: function() {
		this.state = {
			newidx:  0,
			values:  { },
			creates: { },
			changes: { },
			deletes: { },
			reorder: { }
		};

		this.loaded = {};
	},

	callLoad: rpc.declare({
		object: 'uci',
		method: 'get',
		params: [ 'config' ],
		expect: { values: { } }
	}),

	callOrder: rpc.declare({
		object: 'uci',
		method: 'order',
		params: [ 'config', 'sections' ]
	}),

	callAdd: rpc.declare({
		object: 'uci',
		method: 'add',
		params: [ 'config', 'type', 'name', 'values' ],
		expect: { section: '' }
	}),

	callSet: rpc.declare({
		object: 'uci',
		method: 'set',
		params: [ 'config', 'section', 'values' ]
	}),

	callDelete: rpc.declare({
		object: 'uci',
		method: 'delete',
		params: [ 'config', 'section', 'options' ]
	}),

	callApply: rpc.declare({
		object: 'uci',
		method: 'apply',
		params: [ 'timeout', 'rollback' ]
	}),

	callConfirm: rpc.declare({
		object: 'uci',
		method: 'confirm'
	}),

	createSID: function(conf) {
		var v = this.state.values,
		    n = this.state.creates,
		    sid;

		do {
			sid = "new%06x".format(Math.random() * 0xFFFFFF);
		} while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));

		return sid;
	},

	resolveSID: function(conf, sid) {
		if (typeof(sid) != 'string')
			return sid;

		var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);

		if (m) {
			var type = m[1],
			    pos = +m[2],
			    sections = this.sections(conf, type),
			    section = sections[pos >= 0 ? pos : sections.length + pos];

			return section ? section['.name'] : null;
		}

		return sid;
	},

	reorderSections: function() {
		var v = this.state.values,
		    n = this.state.creates,
		    r = this.state.reorder,
		    tasks = [];

		if (Object.keys(r).length === 0)
			return Promise.resolve();

		/*
		 gather all created and existing sections, sort them according
		 to their index value and issue an uci order call
		*/
		for (var c in r) {
			var o = [ ];

			if (n[c])
				for (var s in n[c])
					o.push(n[c][s]);

			for (var s in v[c])
				o.push(v[c][s]);

			if (o.length > 0) {
				o.sort(function(a, b) {
					return (a['.index'] - b['.index']);
				});

				var sids = [ ];

				for (var i = 0; i < o.length; i++)
					sids.push(o[i]['.name']);

				tasks.push(this.callOrder(c, sids));
			}
		}

		this.state.reorder = { };
		return Promise.all(tasks);
	},

	loadPackage: function(packageName) {
		if (this.loaded[packageName] == null)
			return (this.loaded[packageName] = this.callLoad(packageName));

		return Promise.resolve(this.loaded[packageName]);
	},

	load: function(packages) {
		var self = this,
		    pkgs = [ ],
		    tasks = [];

		if (!Array.isArray(packages))
			packages = [ packages ];

		for (var i = 0; i < packages.length; i++)
			if (!self.state.values[packages[i]]) {
				pkgs.push(packages[i]);
				tasks.push(self.loadPackage(packages[i]));
			}

		return Promise.all(tasks).then(function(responses) {
			for (var i = 0; i < responses.length; i++)
				self.state.values[pkgs[i]] = responses[i];

			if (responses.length)
				document.dispatchEvent(new CustomEvent('uci-loaded'));

			return pkgs;
		});
	},

	unload: function(packages) {
		if (!Array.isArray(packages))
			packages = [ packages ];

		for (var i = 0; i < packages.length; i++) {
			delete this.state.values[packages[i]];
			delete this.state.creates[packages[i]];
			delete this.state.changes[packages[i]];
			delete this.state.deletes[packages[i]];

			delete this.loaded[packages[i]];
		}
	},

	add: function(conf, type, name) {
		var n = this.state.creates,
		    sid = name || this.createSID(conf);

		if (!n[conf])
			n[conf] = { };

		n[conf][sid] = {
			'.type':      type,
			'.name':      sid,
			'.create':    name,
			'.anonymous': !name,
			'.index':     1000 + this.state.newidx++
		};

		return sid;
	},

	remove: function(conf, sid) {
		var n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		/* requested deletion of a just created section */
		if (n[conf] && n[conf][sid]) {
			delete n[conf][sid];
		}
		else {
			if (c[conf])
				delete c[conf][sid];

			if (!d[conf])
				d[conf] = { };

			d[conf][sid] = true;
		}
	},

	sections: function(conf, type, cb) {
		var sa = [ ],
		    v = this.state.values[conf],
		    n = this.state.creates[conf],
		    c = this.state.changes[conf],
		    d = this.state.deletes[conf];

		if (!v)
			return sa;

		for (var s in v)
			if (!d || d[s] !== true)
				if (!type || v[s]['.type'] == type)
					sa.push(Object.assign({ }, v[s], c ? c[s] : undefined));

		if (n)
			for (var s in n)
				if (!type || n[s]['.type'] == type)
					sa.push(Object.assign({ }, n[s]));

		sa.sort(function(a, b) {
			return a['.index'] - b['.index'];
		});

		for (var i = 0; i < sa.length; i++)
			sa[i]['.index'] = i;

		if (typeof(cb) == 'function')
			for (var i = 0; i < sa.length; i++)
				cb.call(this, sa[i], sa[i]['.name']);

		return sa;
	},

	get: function(conf, sid, opt) {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		sid = this.resolveSID(conf, sid);

		if (sid == null)
			return null;

		/* requested option in a just created section */
		if (n[conf] && n[conf][sid]) {
			if (!n[conf])
				return undefined;

			if (opt == null)
				return n[conf][sid];

			return n[conf][sid][opt];
		}

		/* requested an option value */
		if (opt != null) {
			/* check whether option was deleted */
			if (d[conf] && d[conf][sid]) {
				if (d[conf][sid] === true)
					return undefined;

				for (var i = 0; i < d[conf][sid].length; i++)
					if (d[conf][sid][i] == opt)
						return undefined;
			}

			/* check whether option was changed */
			if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
				return c[conf][sid][opt];

			/* return base value */
			if (v[conf] && v[conf][sid])
				return v[conf][sid][opt];

			return undefined;
		}

		/* requested an entire section */
		if (v[conf])
			return v[conf][sid];

		return undefined;
	},

	set: function(conf, sid, opt, val) {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes;

		sid = this.resolveSID(conf, sid);

		if (sid == null || opt == null || opt.charAt(0) == '.')
			return;

		if (n[conf] && n[conf][sid]) {
			if (val != null)
				n[conf][sid][opt] = val;
			else
				delete n[conf][sid][opt];
		}
		else if (val != null && val !== '') {
			/* do not set within deleted section */
			if (d[conf] && d[conf][sid] === true)
				return;

			/* only set in existing sections */
			if (!v[conf] || !v[conf][sid])
				return;

			if (!c[conf])
				c[conf] = {};

			if (!c[conf][sid])
				c[conf][sid] = {};

			/* undelete option */
			if (d[conf] && d[conf][sid])
				d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });

			c[conf][sid][opt] = val;
		}
		else {
			/* only delete in existing sections */
			if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) &&
			    !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt)))
			    return;

			if (!d[conf])
				d[conf] = { };

			if (!d[conf][sid])
				d[conf][sid] = [ ];

			if (d[conf][sid] !== true)
				d[conf][sid].push(opt);
		}
	},

	unset: function(conf, sid, opt) {
		return this.set(conf, sid, opt, null);
	},

	get_first: function(conf, type, opt) {
		var sid = null;

		this.sections(conf, type, function(s) {
			if (sid == null)
				sid = s['.name'];
		});

		return this.get(conf, sid, opt);
	},

	set_first: function(conf, type, opt, val) {
		var sid = null;

		this.sections(conf, type, function(s) {
			if (sid == null)
				sid = s['.name'];
		});

		return this.set(conf, sid, opt, val);
	},

	unset_first: function(conf, type, opt) {
		return this.set_first(conf, type, opt, null);
	},

	move: function(conf, sid1, sid2, after) {
		var sa = this.sections(conf),
		    s1 = null, s2 = null;

		sid1 = this.resolveSID(conf, sid1);
		sid2 = this.resolveSID(conf, sid2);

		for (var i = 0; i < sa.length; i++) {
			if (sa[i]['.name'] != sid1)
				continue;

			s1 = sa[i];
			sa.splice(i, 1);
			break;
		}

		if (s1 == null)
			return false;

		if (sid2 == null) {
			sa.push(s1);
		}
		else {
			for (var i = 0; i < sa.length; i++) {
				if (sa[i]['.name'] != sid2)
					continue;

				s2 = sa[i];
				sa.splice(i + !!after, 0, s1);
				break;
			}

			if (s2 == null)
				return false;
		}

		for (var i = 0; i < sa.length; i++)
			this.get(conf, sa[i]['.name'])['.index'] = i;

		this.state.reorder[conf] = true;

		return true;
	},

	save: function() {
		var v = this.state.values,
		    n = this.state.creates,
		    c = this.state.changes,
		    d = this.state.deletes,
		    r = this.state.reorder,
		    self = this,
		    snew = [ ],
		    pkgs = { },
		    tasks = [];

		if (n)
			for (var conf in n) {
				for (var sid in n[conf]) {
					var r = {
						config: conf,
						values: { }
					};

					for (var k in n[conf][sid]) {
						if (k == '.type')
							r.type = n[conf][sid][k];
						else if (k == '.create')
							r.name = n[conf][sid][k];
						else if (k.charAt(0) != '.')
							r.values[k] = n[conf][sid][k];
					}

					snew.push(n[conf][sid]);
					tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
				}

				pkgs[conf] = true;
			}

		if (c)
			for (var conf in c) {
				for (var sid in c[conf])
					tasks.push(self.callSet(conf, sid, c[conf][sid]));

				pkgs[conf] = true;
			}

		if (d)
			for (var conf in d) {
				for (var sid in d[conf]) {
					var o = d[conf][sid];
					tasks.push(self.callDelete(conf, sid, (o === true) ? null : o));
				}

				pkgs[conf] = true;
			}

		if (r)
			for (var conf in r)
				pkgs[conf] = true;

		return Promise.all(tasks).then(function(responses) {
			/*
			 array "snew" holds references to the created uci sections,
			 use it to assign the returned names of the new sections
			*/
			for (var i = 0; i < snew.length; i++)
				snew[i]['.name'] = responses[i];

			return self.reorderSections();
		}).then(function() {
			pkgs = Object.keys(pkgs);

			self.unload(pkgs);

			return self.load(pkgs);
		});
	},

	apply: function(timeout) {
		var self = this,
		    date = new Date();

		if (typeof(timeout) != 'number' || timeout < 1)
			timeout = 10;

		return self.callApply(timeout, true).then(function(rv) {
			if (rv != 0)
				return Promise.reject(rv);

			var try_deadline = date.getTime() + 1000 * timeout;
			var try_confirm = function() {
				return self.callConfirm().then(function(rv) {
					if (rv != 0) {
						if (date.getTime() < try_deadline)
							window.setTimeout(try_confirm, 250);
						else
							return Promise.reject(rv);
					}

					return rv;
				});
			};

			window.setTimeout(try_confirm, 1000);
		});
	},

	changes: rpc.declare({
		object: 'uci',
		method: 'changes',
		expect: { changes: { } }
	})
});
<|endoftext|>


<commit_before>(function() { // Don't use ES6 function decloration syntax to test if the user has a supported
	console.log('Chrome T-Rex Runner');
	try {
		class Test { }
	} catch (e) {
		alert('Your browser does not support this site.\nPlease use the latest version of Chrome.');
		return;
	}
	class Module {
		constructor(file, name) {
			this.fileName = file;
			this.name = name;
		}
		load() {
			var me = this;
			return new Promise((resolve, reject) => {
				var s = document.createElement('script');
				s.src = me.fileName;
				s.type = "text/javascript";
				s.addEventListener('load', function onLoad() {
					setTimeout(() => {
						console.log("Loaded module '" + me.name + "'");
						s.removeEventListener('load', onLoad, false);
						resolve();
					});
				});
				document.body.appendChild(s);
			});
		}
	}
	window.modules = {
		runner: new Module('site/js/dino.js', 'Runner'),
		synaptic: new Module('site/js/synaptic.js', 'Synaptic'),
		jquery: new Module('site/js/jquery.js', 'JQuery'),
		eventEmitter: new Module('site/js/EventEmitter.min.js', 'EventEmitter')
	}
	window.addEventListener('load', () => {
		modules.jquery.load()
		.then(() => modules.eventEmitter.load())
		.then(() => modules.synaptic.load())
		.then(() => modules.runner.load())
		.then(() => {
			// Every module has loaded, start the Runner
		    setTimeout(() => {
		        new Runner('.interstitial-wrapper');
		    }, 30);
		});
	});
})();<commit_msg>Store script to module class<commit_after>(function() { // Don't use ES6 function decloration syntax to test if the user has a supported
	console.log('Chrome T-Rex Runner');
	try {
		class Test { }
	} catch (e) {
		alert('Your browser does not support this site.\nPlease use the latest version of Chrome.');
		return;
	}
	class Module {
		constructor(file, name) {
			this.fileName = file;
			this.name = name;
			this.elem = document.createElement('script');
		}
		load() {
			var me = this;
			return new Promise((resolve, reject) => {
				me.elem.src = me.fileName;
				me.elem.type = "text/javascript";
				me.elem.addEventListener('load', function onLoad() {
					setTimeout(() => {
						console.log("Loaded module '" + me.name + "'");
						me.elem.removeEventListener('load', onLoad, false);
						resolve();
					});
				});
				document.body.appendChild(me.elem);
			});
		}
	}
	window.modules = {
		runner: new Module('site/js/dino.js', 'Runner'),
		synaptic: new Module('site/js/synaptic.js', 'Synaptic'),
		jquery: new Module('site/js/jquery.js', 'JQuery'),
		eventEmitter: new Module('site/js/EventEmitter.min.js', 'EventEmitter')
	}
	window.addEventListener('load', () => {
		modules.jquery.load()
		.then(() => modules.eventEmitter.load())
		.then(() => modules.synaptic.load())
		.then(() => modules.runner.load())
		.then(() => {
			// Every module has loaded, start the Runner
		    setTimeout(() => {
		        new Runner('.interstitial-wrapper');
		    }, 30);
		});
	});
})();<|endoftext|>


<commit_before>var _ = require('underscore');
var formatter = require('../../formatter');
var HistogramTitleView = require('./histogram-title-view');
var WidgetContent = require('../standard/widget-content-view');
var WidgetViewModel = require('../widget-content-model');
var HistogramChartView = require('./chart');
var placeholder = require('./placeholder.tpl');
var template = require('./content.tpl');
var AnimateValues = require('../animate-values.js');
var animationTemplate = require('./animation-template.tpl');

/**
 * Widget content view for a histogram
 */
module.exports = WidgetContent.extend({
  defaults: {
    chartHeight: 48 + 20 + 4
  },

  events: {
    'click .js-clear': '_clear',
    'click .js-zoom': '_zoom'
  },

  initialize: function () {
    this.model = this.options.dataModel;
    this.viewModel = new WidgetViewModel();
    this.lockedByUser = false;
    WidgetContent.prototype.initialize.call(this);
  },

  _initViews: function () {
    var titleView = new HistogramTitleView({
      viewModel: this.viewModel,
      dataModel: this.model
    });
    this.$('.js-title').html(titleView.render().el);
    this.addView(titleView);

    this._renderMiniChart();
    this._renderMainChart();
  },

  _initBinds: function () {
    this.model.once('change:data', this._onFirstLoad, this);
    this.model.bind('change:collapsed', function (mdl, isCollapsed) {
      this.$el.toggleClass('is-collapsed', !!isCollapsed);
    }, this);
  },

  _onFirstLoad: function () {
    this.render();
    this._storeBounds();

    this.model.bind('change', this._onChangeModel, this);
    this.model._fetch();
  },

  _storeBounds: function () {
    var data = this.model.getData();
    if (data && data.length > 0) {
      this.start = data[0].start;
      this.end = data[data.length - 1].end;
      this.binsCount = data.length;
      this.model.set({ start: this.start, end: this.end, bins: this.binsCount });
    }
  },

  _isZoomed: function () {
    return this.viewModel.get('zoomed');
  },

  _onChangeModel: function () {
    // When the histogram is zoomed, we don't need to rely
    // on the change url to update the histogram
    if (this.model.changed.url && this._isZoomed()) {
      return;
    }

    // if the action was initiated by the user
    // don't replace the stored data
    if (this.lockedByUser) {
      this.lockedByUser = false;
    } else {
      if (this._isZoomed()) {
        this.zoomedData = this.model.getData();
      } else {
        this.histogramChartView.showShadowBars();
        this.originalData = this.model.getData();
      }
      this.histogramChartView.replaceData(this.model.getData());
    }

    if (this.unsettingRange) {
      this._unsetRange();
    } else {
      if (this._isZoomed() && !this.lockZoomedData) {
        this.lockZoomedData = true;
        this.zoomedData = this.model.getData();
      }
    }

    this._updateStats();
  },

  render: function () {
    this.clearSubViews();

    var data = this.model.getData();
    var isDataEmpty = _.isEmpty(data) || _.size(data) === 0;

    this.$el.html(
      template({
        title: this.model.get('title'),
        itemsCount: !isDataEmpty ? data.length : '-'
      })
    );

    if (isDataEmpty) {
      this._addPlaceholder();
    } else {
      this.originalData = this.model.getData();
      this._setupBindings();
      this._initViews();
    }

    return this;
  },

  _unsetRange: function () {
    this.unsettingRange = false;
    this.histogramChartView.replaceData(this.originalData);
    this.viewModel.set({ lo_index: null, hi_index: null });

    if (!this._isZoomed()) {
      this.histogramChartView.showShadowBars();
    }
  },

  _addPlaceholder: function () {
    this.$('.js-content').append(placeholder());
  },

  _renderMainChart: function () {
    this.histogramChartView = new HistogramChartView(({
      margin: { top: 4, right: 4, bottom: 4, left: 4 },
      hasShadowBards: true,
      hasHandles: true,
      hasAxisTip: true,
      width: this.canvasWidth,
      height: this.defaults.chartHeight,
      data: this.model.getData(),
      shadowData: this.model.getData()
    }));

    this.$('.js-content').append(this.histogramChartView.el);
    this.addView(this.histogramChartView);

    this.histogramChartView.bind('range_updated', this._onRangeUpdated, this);
    this.histogramChartView.bind('on_brush_end', this._onBrushEnd, this);
    this.histogramChartView.bind('hover', this._onValueHover, this);
    this.histogramChartView.render().show();

    this._updateStats();
  },

  _renderMiniChart: function () {
    this.miniHistogramChartView = new HistogramChartView(({
      className: 'CDB-Chart--mini',
      margin: { top: 0, right: 0, bottom: 4, left: 4 },
      height: 40,
      showOnWidthChange: false,
      data: this.model.getData()
    }));

    this.addView(this.miniHistogramChartView);
    this.$('.js-content').append(this.miniHistogramChartView.el);
    this.miniHistogramChartView.bind('on_brush_end', this._onMiniRangeUpdated, this);
    this.miniHistogramChartView.render();
  },

  _setupBindings: function () {
    this.viewModel.bind('change:zoomed', this._onChangeZoomed, this);
    this.viewModel.bind('change:zoom_enabled', this._onChangeZoomEnabled, this);
    this.viewModel.bind('change:filter_enabled', this._onChangeFilterEnabled, this);
    this.viewModel.bind('change:total', this._onChangeTotal, this);
    this.viewModel.bind('change:nulls', this._onChangeNulls, this);
    this.viewModel.bind('change:max', this._onChangeMax, this);
    this.viewModel.bind('change:min', this._onChangeMin, this);
    this.viewModel.bind('change:avg', this._onChangeAvg, this);
  },

  _clearTooltip: function () {
    this.$('.js-tooltip').stop().hide();
  },

  _onValueHover: function (info) {
    var $tooltip = this.$('.js-tooltip');

    if (info && info.data) {
      var bottom = this.defaults.chartHeight + 3 - info.top;

      $tooltip.css({ bottom: bottom, left: info.left });
      $tooltip.text(info.data);
      $tooltip.css({ left: info.left - $tooltip.width() / 2 });
      $tooltip.fadeIn(70);
    } else {
      this._clearTooltip();
    }
  },

  _onMiniRangeUpdated: function (loBarIndex, hiBarIndex) {
    this.lockedByUser = false;
    this.lockZoomedData = false;

    this._clearTooltip();
    this.histogramChartView.removeSelection();

    var data = this.originalData;

    if (loBarIndex >= 0 && loBarIndex < data.length && (hiBarIndex - 1) >= 0 && (hiBarIndex - 1) < data.length) {
      this.filter.setRange(
        data[loBarIndex].start,
        data[hiBarIndex - 1].end
      );
      this._updateStats();
    } else {
      console.error('Error accessing array bounds', loBarIndex, hiBarIndex, data);
    }
  },

  _onBrushEnd: function (loBarIndex, hiBarIndex) {
    var data = this._getData();

    if (!data || !data.length) {
      return;
    }

    if (this._isZoomed()) {
      this.lockedByUser = true;
    }

    var properties = { filter_enabled: true, lo_index: loBarIndex, hi_index: hiBarIndex };

    if (!this.viewModel.get('zoomed')) {
      properties.zoom_enabled = true;
    }

    this.viewModel.set(properties);

    if (loBarIndex >= 0 && loBarIndex < data.length && (hiBarIndex - 1) >= 0 && (hiBarIndex - 1) < data.length) {
      this.filter.setRange(
        data[loBarIndex].start,
        data[hiBarIndex - 1].end
      );
      this._updateStats();
    } else {
      console.error('Error accessing array bounds', loBarIndex, hiBarIndex, data);
    }
  },

  _onRangeUpdated: function (loBarIndex, hiBarIndex) {
    var self = this;
    if (this.viewModel.get('zoomed')) {
      this.viewModel.set({ zoom_enabled: false, lo_index: loBarIndex, hi_index: hiBarIndex });
    } else {
      this.viewModel.set({ lo_index: loBarIndex, hi_index: hiBarIndex });
    }

    var updateStats = _.debounce(function () { self._updateStats(); }, 400);
    updateStats();
  },

  _getData: function () {
    var data = this.model.getData();

    if (this._isZoomed()) {
      data = this.zoomedData;
    }
    return data;
  },

  _onChangeFilterEnabled: function () {
    this.$('.js-filter').toggleClass('is-hidden', !this.viewModel.get('filter_enabled'));
  },

  _onChangeZoomEnabled: function () {
    this.$('.js-zoom').toggleClass('is-hidden', !this.viewModel.get('zoom_enabled'));
  },

  _changeHeaderValue: function (className, what, suffix) {
    if (this.viewModel.get(what) === undefined) {
      this.$(className).text('0 ' + suffix);
      return;
    }

    this._addTitleForValue(className, what, suffix);

    var animator = new AnimateValues({
      el: this.$el
    });

    animator.animateValue(this.viewModel, what, className, animationTemplate, {
      formatter: formatter.formatNumber,
      templateData: { suffix: ' ' + suffix }
    });
  },

  _onChangeNulls: function () {
    this._changeHeaderValue('.js-nulls', 'nulls', 'NULL ROWS');
  },

  _onChangeTotal: function () {
    this._changeHeaderValue('.js-val', 'total', 'SELECTED');
  },

  _onChangeMax: function () {
    this._changeHeaderValue('.js-max', 'max', 'MAX');
  },

  _onChangeMin: function () {
    this._changeHeaderValue('.js-min', 'min', 'MIN');
  },

  _onChangeAvg: function () {
    this._changeHeaderValue('.js-avg', 'avg', 'AVG');
  },

  _addTitleForValue: function (className, what, unit) {
    this.$(className).attr('title', this._formatNumberWithCommas(this.viewModel.get(what).toFixed(2)) + ' ' + unit);
  },

  _formatNumberWithCommas: function (x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  },

  _updateStats: function () {
    var data = this.originalData;

    if (this._isZoomed()) {
      data = this.zoomedData;
    }

    var nulls = this.model.get('nulls');

    var min, max;

    if (data && data.length) {
      var loBarIndex = this.viewModel.get('lo_index') || 0;
      var hiBarIndex = this.viewModel.get('hi_index') || data.length;

      var sum = this._calcSum(data, loBarIndex, hiBarIndex);
      var avg = this._calcAvg(data, loBarIndex, hiBarIndex);

      if (loBarIndex >= 0 && loBarIndex < data.length) {
        min = data[loBarIndex].start;
      }

      if (hiBarIndex >= 0 && hiBarIndex - 1 < data.length) {
        max = data[hiBarIndex - 1].end;
      }

      this.viewModel.set({ total: sum, nulls: nulls, min: min, max: max, avg: avg });
    }
  },

  _calcAvg: function (data, start, end) {
    var selectedData = data.slice(start, end);

    var total = this._calcSum(data, start, end, total);

    if (!total) {
      return 0;
    }

    var area = _.reduce(selectedData, function (memo, d) {
      return (d.avg && d.freq) ? (d.avg * d.freq) + memo : memo;
    }, 0);

    return area / total;
  },

  _calcSum: function (data, start, end) {
    return _.reduce(data.slice(start, end), function (memo, d) {
      return d.freq + memo;
    }, 0);
  },

  _onChangeZoomed: function () {
    if (this.viewModel.get('zoomed')) {
      this._onZoomIn();
    } else {
      this._onZoomOut();
    }
  },

  _onZoomIn: function () {
    this._showMiniRange();
    this.histogramChartView.expand(20);

    this.histogramChartView.removeShadowBars();

    this.model.set({ start: null, end: null, bins: null, own_filter: 1 });
    this.model._fetch();
    this.lockedByUser = false;
  },

  _zoom: function () {
    this.lockedByUser = true;
    this.viewModel.set({ zoomed: true, zoom_enabled: false });
    this.histogramChartView.removeSelection();
  },

  _onZoomOut: function () {
    this.lockedByUser = true;
    this.lockZoomedData = false;
    this.unsettingRange = true;

    this.model.set({ start: this.start, end: this.end, bins: this.binsCount, own_filter: null });

    this.viewModel.set({ zoom_enabled: false, filter_enabled: false, lo_index: null, hi_index: null });

    this.filter.unsetRange();

    this.histogramChartView.contract(this.defaults.chartHeight);
    this.histogramChartView.resetIndexes();

    this.miniHistogramChartView.hide();
  },

  _showMiniRange: function () {
    var loBarIndex = this.viewModel.get('lo_index');
    var hiBarIndex = this.viewModel.get('hi_index');

    this.miniHistogramChartView.selectRange(loBarIndex, hiBarIndex);
    this.miniHistogramChartView.show();
  },

  _clear: function () {
    this.histogramChartView.removeSelection();
    this.viewModel.set({ zoomed: false, zoom_enabled: false });
    this.viewModel.trigger('change:zoomed');
  }
});
<commit_msg>Adds dropdown<commit_after>var _ = require('underscore');
var formatter = require('../../formatter');
var HistogramTitleView = require('./histogram-title-view');
var WidgetContent = require('../standard/widget-content-view');
var WidgetViewModel = require('../widget-content-model');
var HistogramChartView = require('./chart');
var placeholder = require('./placeholder.tpl');
var template = require('./content.tpl');
var DropdownView = require('../dropdown/widget-dropdown-view');
var AnimateValues = require('../animate-values.js');
var animationTemplate = require('./animation-template.tpl');

/**
 * Widget content view for a histogram
 */
module.exports = WidgetContent.extend({
  defaults: {
    chartHeight: 48 + 20 + 4
  },

  events: {
    'click .js-clear': '_clear',
    'click .js-zoom': '_zoom'
  },

  initialize: function () {
    this.model = this.options.dataModel;
    this.viewModel = new WidgetViewModel();
    this.lockedByUser = false;
    WidgetContent.prototype.initialize.call(this);
  },

  _initViews: function () {
    var titleView = new HistogramTitleView({
      viewModel: this.viewModel,
      dataModel: this.model
    });

    this.$('.js-title').html(titleView.render().el);
    this.addView(titleView);

    var dropdown = new DropdownView({
      target: this.$('.js-actions'),
      container: this.$('.js-header')
    });

    dropdown.bind('click', function (action) {
      if (action === 'toggle') {
        this.model.toggleCollapsed();
      }
    }, this);

    this.addView(dropdown);

    this._renderMiniChart();
    this._renderMainChart();
  },

  _initBinds: function () {
    this.model.once('change:data', this._onFirstLoad, this);
    this.model.bind('change:collapsed', function (mdl, isCollapsed) {
      this.$el.toggleClass('is-collapsed', !!isCollapsed);
    }, this);
  },

  _onFirstLoad: function () {
    this.render();
    this._storeBounds();

    this.model.bind('change', this._onChangeModel, this);
    this.model._fetch();
  },

  _storeBounds: function () {
    var data = this.model.getData();
    if (data && data.length > 0) {
      this.start = data[0].start;
      this.end = data[data.length - 1].end;
      this.binsCount = data.length;
      this.model.set({ start: this.start, end: this.end, bins: this.binsCount });
    }
  },

  _isZoomed: function () {
    return this.viewModel.get('zoomed');
  },

  _onChangeModel: function () {
    // When the histogram is zoomed, we don't need to rely
    // on the change url to update the histogram
    if (this.model.changed.url && this._isZoomed()) {
      return;
    }

    // if the action was initiated by the user
    // don't replace the stored data
    if (this.lockedByUser) {
      this.lockedByUser = false;
    } else {
      if (this._isZoomed()) {
        this.zoomedData = this.model.getData();
      } else {
        this.histogramChartView.showShadowBars();
        this.originalData = this.model.getData();
      }
      this.histogramChartView.replaceData(this.model.getData());
    }

    if (this.unsettingRange) {
      this._unsetRange();
    } else {
      if (this._isZoomed() && !this.lockZoomedData) {
        this.lockZoomedData = true;
        this.zoomedData = this.model.getData();
      }
    }

    this._updateStats();
  },

  render: function () {
    this.clearSubViews();

    var data = this.model.getData();
    var isDataEmpty = _.isEmpty(data) || _.size(data) === 0;

    this.$el.html(
      template({
        title: this.model.get('title'),
        itemsCount: !isDataEmpty ? data.length : '-'
      })
    );

    if (isDataEmpty) {
      this._addPlaceholder();
    } else {
      this.originalData = this.model.getData();
      this._setupBindings();
      this._initViews();
    }

    return this;
  },

  _unsetRange: function () {
    this.unsettingRange = false;
    this.histogramChartView.replaceData(this.originalData);
    this.viewModel.set({ lo_index: null, hi_index: null });

    if (!this._isZoomed()) {
      this.histogramChartView.showShadowBars();
    }
  },

  _addPlaceholder: function () {
    this.$('.js-content').append(placeholder());
  },

  _renderMainChart: function () {
    this.histogramChartView = new HistogramChartView(({
      margin: { top: 4, right: 4, bottom: 4, left: 4 },
      hasShadowBards: true,
      hasHandles: true,
      hasAxisTip: true,
      width: this.canvasWidth,
      height: this.defaults.chartHeight,
      data: this.model.getData(),
      shadowData: this.model.getData()
    }));

    this.$('.js-content').append(this.histogramChartView.el);
    this.addView(this.histogramChartView);

    this.histogramChartView.bind('range_updated', this._onRangeUpdated, this);
    this.histogramChartView.bind('on_brush_end', this._onBrushEnd, this);
    this.histogramChartView.bind('hover', this._onValueHover, this);
    this.histogramChartView.render().show();

    this._updateStats();
  },

  _renderMiniChart: function () {
    this.miniHistogramChartView = new HistogramChartView(({
      className: 'CDB-Chart--mini',
      margin: { top: 0, right: 0, bottom: 4, left: 4 },
      height: 40,
      showOnWidthChange: false,
      data: this.model.getData()
    }));

    this.addView(this.miniHistogramChartView);
    this.$('.js-content').append(this.miniHistogramChartView.el);
    this.miniHistogramChartView.bind('on_brush_end', this._onMiniRangeUpdated, this);
    this.miniHistogramChartView.render();
  },

  _setupBindings: function () {
    this.viewModel.bind('change:zoomed', this._onChangeZoomed, this);
    this.viewModel.bind('change:zoom_enabled', this._onChangeZoomEnabled, this);
    this.viewModel.bind('change:filter_enabled', this._onChangeFilterEnabled, this);
    this.viewModel.bind('change:total', this._onChangeTotal, this);
    this.viewModel.bind('change:nulls', this._onChangeNulls, this);
    this.viewModel.bind('change:max', this._onChangeMax, this);
    this.viewModel.bind('change:min', this._onChangeMin, this);
    this.viewModel.bind('change:avg', this._onChangeAvg, this);
  },

  _clearTooltip: function () {
    this.$('.js-tooltip').stop().hide();
  },

  _onValueHover: function (info) {
    var $tooltip = this.$('.js-tooltip');

    if (info && info.data) {
      var bottom = this.defaults.chartHeight + 3 - info.top;

      $tooltip.css({ bottom: bottom, left: info.left });
      $tooltip.text(info.data);
      $tooltip.css({ left: info.left - $tooltip.width() / 2 });
      $tooltip.fadeIn(70);
    } else {
      this._clearTooltip();
    }
  },

  _onMiniRangeUpdated: function (loBarIndex, hiBarIndex) {
    this.lockedByUser = false;
    this.lockZoomedData = false;

    this._clearTooltip();
    this.histogramChartView.removeSelection();

    var data = this.originalData;

    if (loBarIndex >= 0 && loBarIndex < data.length && (hiBarIndex - 1) >= 0 && (hiBarIndex - 1) < data.length) {
      this.filter.setRange(
        data[loBarIndex].start,
        data[hiBarIndex - 1].end
      );
      this._updateStats();
    } else {
      console.error('Error accessing array bounds', loBarIndex, hiBarIndex, data);
    }
  },

  _onBrushEnd: function (loBarIndex, hiBarIndex) {
    var data = this._getData();

    if (!data || !data.length) {
      return;
    }

    if (this._isZoomed()) {
      this.lockedByUser = true;
    }

    var properties = { filter_enabled: true, lo_index: loBarIndex, hi_index: hiBarIndex };

    if (!this.viewModel.get('zoomed')) {
      properties.zoom_enabled = true;
    }

    this.viewModel.set(properties);

    if (loBarIndex >= 0 && loBarIndex < data.length && (hiBarIndex - 1) >= 0 && (hiBarIndex - 1) < data.length) {
      this.filter.setRange(
        data[loBarIndex].start,
        data[hiBarIndex - 1].end
      );
      this._updateStats();
    } else {
      console.error('Error accessing array bounds', loBarIndex, hiBarIndex, data);
    }
  },

  _onRangeUpdated: function (loBarIndex, hiBarIndex) {
    var self = this;
    if (this.viewModel.get('zoomed')) {
      this.viewModel.set({ zoom_enabled: false, lo_index: loBarIndex, hi_index: hiBarIndex });
    } else {
      this.viewModel.set({ lo_index: loBarIndex, hi_index: hiBarIndex });
    }

    var updateStats = _.debounce(function () { self._updateStats(); }, 400);
    updateStats();
  },

  _getData: function () {
    var data = this.model.getData();

    if (this._isZoomed()) {
      data = this.zoomedData;
    }
    return data;
  },

  _onChangeFilterEnabled: function () {
    this.$('.js-filter').toggleClass('is-hidden', !this.viewModel.get('filter_enabled'));
  },

  _onChangeZoomEnabled: function () {
    this.$('.js-zoom').toggleClass('is-hidden', !this.viewModel.get('zoom_enabled'));
  },

  _changeHeaderValue: function (className, what, suffix) {
    if (this.viewModel.get(what) === undefined) {
      this.$(className).text('0 ' + suffix);
      return;
    }

    this._addTitleForValue(className, what, suffix);

    var animator = new AnimateValues({
      el: this.$el
    });

    animator.animateValue(this.viewModel, what, className, animationTemplate, {
      formatter: formatter.formatNumber,
      templateData: { suffix: ' ' + suffix }
    });
  },

  _onChangeNulls: function () {
    this._changeHeaderValue('.js-nulls', 'nulls', 'NULL ROWS');
  },

  _onChangeTotal: function () {
    this._changeHeaderValue('.js-val', 'total', 'SELECTED');
  },

  _onChangeMax: function () {
    this._changeHeaderValue('.js-max', 'max', 'MAX');
  },

  _onChangeMin: function () {
    this._changeHeaderValue('.js-min', 'min', 'MIN');
  },

  _onChangeAvg: function () {
    this._changeHeaderValue('.js-avg', 'avg', 'AVG');
  },

  _addTitleForValue: function (className, what, unit) {
    this.$(className).attr('title', this._formatNumberWithCommas(this.viewModel.get(what).toFixed(2)) + ' ' + unit);
  },

  _formatNumberWithCommas: function (x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  },

  _updateStats: function () {
    var data = this.originalData;

    if (this._isZoomed()) {
      data = this.zoomedData;
    }

    var nulls = this.model.get('nulls');

    var min, max;

    if (data && data.length) {
      var loBarIndex = this.viewModel.get('lo_index') || 0;
      var hiBarIndex = this.viewModel.get('hi_index') || data.length;

      var sum = this._calcSum(data, loBarIndex, hiBarIndex);
      var avg = this._calcAvg(data, loBarIndex, hiBarIndex);

      if (loBarIndex >= 0 && loBarIndex < data.length) {
        min = data[loBarIndex].start;
      }

      if (hiBarIndex >= 0 && hiBarIndex - 1 < data.length) {
        max = data[hiBarIndex - 1].end;
      }

      this.viewModel.set({ total: sum, nulls: nulls, min: min, max: max, avg: avg });
    }
  },

  _calcAvg: function (data, start, end) {
    var selectedData = data.slice(start, end);

    var total = this._calcSum(data, start, end, total);

    if (!total) {
      return 0;
    }

    var area = _.reduce(selectedData, function (memo, d) {
      return (d.avg && d.freq) ? (d.avg * d.freq) + memo : memo;
    }, 0);

    return area / total;
  },

  _calcSum: function (data, start, end) {
    return _.reduce(data.slice(start, end), function (memo, d) {
      return d.freq + memo;
    }, 0);
  },

  _onChangeZoomed: function () {
    if (this.viewModel.get('zoomed')) {
      this._onZoomIn();
    } else {
      this._onZoomOut();
    }
  },

  _onZoomIn: function () {
    this._showMiniRange();
    this.histogramChartView.expand(20);

    this.histogramChartView.removeShadowBars();

    this.model.set({ start: null, end: null, bins: null, own_filter: 1 });
    this.model._fetch();
    this.lockedByUser = false;
  },

  _zoom: function () {
    this.lockedByUser = true;
    this.viewModel.set({ zoomed: true, zoom_enabled: false });
    this.histogramChartView.removeSelection();
  },

  _onZoomOut: function () {
    this.lockedByUser = true;
    this.lockZoomedData = false;
    this.unsettingRange = true;

    this.model.set({ start: this.start, end: this.end, bins: this.binsCount, own_filter: null });

    this.viewModel.set({ zoom_enabled: false, filter_enabled: false, lo_index: null, hi_index: null });

    this.filter.unsetRange();

    this.histogramChartView.contract(this.defaults.chartHeight);
    this.histogramChartView.resetIndexes();

    this.miniHistogramChartView.hide();
  },

  _showMiniRange: function () {
    var loBarIndex = this.viewModel.get('lo_index');
    var hiBarIndex = this.viewModel.get('hi_index');

    this.miniHistogramChartView.selectRange(loBarIndex, hiBarIndex);
    this.miniHistogramChartView.show();
  },

  _clear: function () {
    this.histogramChartView.removeSelection();
    this.viewModel.set({ zoomed: false, zoom_enabled: false });
    this.viewModel.trigger('change:zoomed');
  }
});
<|endoftext|>


<commit_before>describe("About Mutability", function() {

  it("should expect object properties to be public and mutable", function () {
    var aPerson = {firstname: "John", lastname: "Smith" };
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe(FILL_ME_IN);
  });

  it("should understand that constructed properties are public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    var aPerson = new Person ("John", "Smith");
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe(FILL_ME_IN);
  });

  it("should expect prototype properties to be public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    Person.prototype.getFullName = function () {
      return this.firstname + " " + this.lastname;
    };

    var aPerson = new Person ("John", "Smith");
    expect(aPerson.getFullName()).toBe(FILL_ME_IN);

    aPerson.getFullName = function () {
      return this.lastname + ", " + this.firstname;
    };

    expect(aPerson.getFullName()).toBe(FILL_ME_IN);
  });

  it("should know that variables inside a constructor and constructor args are private", function () {
    function Person(firstname, lastname)
    {
      var fullName = firstname + " " + lastname;

      this.getFirstName = function () { return firstname; };
      this.getLastName = function () { return lastname; };
      this.getFullName = function () { return fullName; };
    }
    var aPerson = new Person ("John", "Smith");

    aPerson.firstname = "Penny";
    aPerson.lastname = "Andrews";
    aPerson.fullName = "Penny Andrews";

    expect(aPerson.getFirstName()).toBe(FILL_ME_IN);
    expect(aPerson.getLastName()).toBe(FILL_ME_IN);
    expect(aPerson.getFullName()).toBe(FILL_ME_IN);

    aPerson.getFullName = function () {
      return aPerson.lastname + ", " + aPerson.firstname;
    };

    expect(aPerson.getFullName()).toBe(FILL_ME_IN);
  });

});
<commit_msg>Worked through AboutMutability koans<commit_after>describe("About Mutability", function() {

  it("should expect object properties to be public and mutable", function () {
    var aPerson = {firstname: "John", lastname: "Smith" };
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should understand that constructed properties are public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    var aPerson = new Person ("John", "Smith");
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should expect prototype properties to be public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    Person.prototype.getFullName = function () {
      return this.firstname + " " + this.lastname;
    };

    var aPerson = new Person ("John", "Smith");
    expect(aPerson.getFullName()).toBe("John Smith");

    aPerson.getFullName = function () {
      return this.lastname + ", " + this.firstname;
    };

    expect(aPerson.getFullName()).toBe("Smith, John");
  });

  it("should know that variables inside a constructor and constructor args are private", function () {
    function Person(firstname, lastname)
    {
      var fullName = firstname + " " + lastname;

      this.getFirstName = function () { return firstname; };
      this.getLastName = function () { return lastname; };
      this.getFullName = function () { return fullName; };
    }
    var aPerson = new Person ("John", "Smith");

    aPerson.firstname = "Penny";
    aPerson.lastname = "Andrews";
    aPerson.fullName = "Penny Andrews";

    expect(aPerson.getFirstName()).toBe("John");
    expect(aPerson.getLastName()).toBe("Smith");
    expect(aPerson.getFullName()).toBe("John Smith");

    aPerson.getFullName = function () {
      return aPerson.lastname + ", " + aPerson.firstname;
    };

    expect(aPerson.getFullName()).toBe("Andrews, Penny");
  });

});
<|endoftext|>


<commit_before>var Discord = require('discord.js');

var ytdl = require('ytdl-core');
var request = require('request');
var url = require('url');

var shouldDisallowQueue = require('./lib/permission-checks.js');
var VideoFormat = require('./lib/video-format.js');
var YoutubeStream = require('./lib/youtube-stream.js');
var Saved = require('./lib/saved.js');
Saved.read();

var Config = require('./lib/config.js');

var client = new Discord.Client();

// Handle discord.js warnings
client.on('warn', (m) => console.log('[warn]', m));
client.on('debug', (m) => console.log('[debug]', m));

var playQueue = [];
var boundChannel = false;
var currentStream = false;
var currentVideo = false;

var botMention = false;

// Handling api key
if (process.argv[4]) {
  var apiKey = process.argv[4];
} else {
  var apiKey = false;
}

client.on('ready', () => {
  botMention = `<@${client.user.id}>`;
  console.log(`Bot mention: ${botMention}`);
});

client.on('message', m => {
  if (!botMention) return;

  if (m.content.startsWith(`${botMention} i`)) { // init
    if (boundChannel) return;
    var channelToJoin = spliceArguments(m.content)[1];
    for (var channel of m.channel.server.channels) {
      if (channel instanceof Discord.VoiceChannel) {
        if (!channelToJoin || channel.name === channelToJoin) {
          boundChannel = m.channel;
          client.reply(m, `Binding to text channel <#${boundChannel.id}> and voice channel **${channel.name}** \`(${channel.id})\``);
          client.joinVoiceChannel(channel).catch(error);
          break;
        }
      }
    }
  }

  if (m.content.startsWith(`${botMention} d`)) { // destroy
    if (!boundChannel) return;
    client.reply(m, `Unbinding from <#${boundChannel.id}> and destroying voice connection`);
    playQueue = [];
    client.internal.leaveVoiceChannel();
    boundChannel = false;
    currentStream = false;
    currentVideo = false;
    return;
  }

  // Only respond to other messages inside the bound channel
  if (!m.channel.equals(boundChannel)) return;

  if (m.content.startsWith(`${botMention} n`)) { // next
    playStopped();
  }

  if (m.content.startsWith(`${botMention} yq`) // youtube query
    || m.content.startsWith(`${botMention} qq`) // queue query
    || m.content.startsWith(`${botMention} pq`) // play query
    || m.content.startsWith(`${botMention} ytq`)) {

    if (apiKey == false) {
      client.reply(m, 'Search is disabled (no API KEY found).');
      return;
    }

    var args = spliceArguments(m.content)[1];

    if (!args) {
      client.reply(m, 'You need to specify a search parameter.');
      return;
    }

    var requestUrl = 'https://www.googleapis.com/youtube/v3/search' +
      `?part=snippet&q=${escape(args)}&key=${apiKey}`;

    request(requestUrl, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        body = JSON.parse(body);
        if (body.items.length == 0) {
          client.reply(m, 'Your query gave 0 results.');
          return;
        }

        var vid = body.items[0].id.videoId;
        getInfoAndQueue(vid, m);
      } else {
        client.reply(m, 'There was an error searching.');
        return;
      }
    });

    return; // have to stop propagation
  }

  if (m.content.startsWith(`${botMention} pl`)) { // playlist
    if (apiKey == false) {
      client.reply(m, 'Playlist adding is disabled (no API KEY found).');
      return;
    }

    var pid = spliceArguments(m.content)[1];

    if (!pid) {
      client.reply(m, 'You need to specify a playlist ID!');
      return;
    }

    var requestUrl = 'https://www.googleapis.com/youtube/v3/playlistItems' +
      `?part=contentDetails&maxResults=50&playlistId=${pid}&key=${apiKey}`;

    request(requestUrl, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        body = JSON.parse(body);
        if (body.items.length == 0) {
          client.reply(m, 'That playlist has no videos.');
          return;
        }

        client.reply(m, `Loading ${body.items.length} videos...`);
        var suppress = 0;
        body.items.forEach((elem, idx) => {
          var vid = elem.contentDetails.videoId;
          if (idx == 1) suppress = body.items.length - 2;
          if (idx == 2) suppress = -1;
          getInfoAndQueue(vid, m, suppress);
        });
      } else {
        client.reply(m, 'There was an error finding playlist with that id.');
        return;
      }
    });

    return;
  }

  if (m.content.startsWith(`${botMention} y`) // youtube
    || m.content.startsWith(`${botMention} q`) // queue
    || m.content.startsWith(`${botMention} p`)) { // play

    var vidList = spliceArguments(m.content)[1];

    var vids = vidList.split(',');
    var suppress = 0;
    vids.forEach((vid, idx) => {
      if (idx == 1) suppress = vids.length - 2;
      if (idx == 2) suppress = -1;
      parseVidAndQueue(vid, m, suppress);
    });
  }

  if (m.content.startsWith(`${botMention} r`)) { // replay
    playQueue.push(currentVideo);
    client.reply(m, `Queued ${VideoFormat.prettyPrint(currentVideo)}`);
  }

  if (m.content.startsWith(`${botMention} sh`)) { // shuffle
    if (playQueue.length < 2) {
      client.reply(m, 'Not enough songs in the queue.');
      return;
    } else {
      shuffle(playQueue);
      client.reply(m, 'Songs in the queue have been shuffled.');
    }

    return;
  }

  if (m.content.startsWith(`${botMention} link`)) {
    if (currentVideo) client.reply(m, `<${currentVideo.loaderUrl}>`);
    return; // stop propagation
  }

  if (m.content.startsWith(`${botMention} list s`)) { // list saved
    var formattedList = 'Here are the videos currently saved: \n';
    for (var key in Saved.saved.videos) {
      if (Saved.saved.videos.hasOwnProperty(key)) {
        formattedList += `*${key}*: ${VideoFormat.prettyPrint(Saved.saved.videos[key])}\n`;
      }
    }

    client.reply(m, formattedList);
    return; // so list doesn't get triggered
  }

  if (m.content.startsWith(`${botMention} l`)) { // list
    var formattedList = 'Here are the videos currently in the play queue, from first added to last added: \n';
    formattedList += `Currently playing: ${VideoFormat.prettyPrintWithUser(currentVideo)}\n`;

    var shouldBreak = false;

    playQueue.forEach((video, idx) => {
      if (shouldBreak) return;

      var formattedVideo = `${idx + 1}. ${VideoFormat.prettyPrintWithUser(video)}\n`;

      if ((formattedList.length + formattedVideo.length) > 1950) {
        formattedList += `... and ${playQueue.length - idx} more`;
        shouldBreak = true;
      } else {
        formattedList += formattedVideo;
      }
    });
    client.reply(m, formattedList);
  }

  if (m.content.startsWith(`${botMention} s`)) { // save
    var argument = spliceArguments(m.content)[1];
    if (!argument) {
      client.reply(m, 'You need to specify a video and a keyword!');
      return;
    }

    var splitArgs = spliceArguments(argument, 1);

    var vid = splitArgs[0];

    if (vid === 'current') {
      if (currentVideo) vid = currentVideo.vid;
      else {
        client.reply(m, `Couldn't retrieve video information of current video!`);
      }
    }

    var requestUrl = 'http://www.youtube.com/watch?v=' + vid;
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else saveVideo(info, vid, splitArgs[1], m);
    });
  }

  if (m.content.startsWith(`${botMention} t`)) { // time
    var streamTime = client.internal.voiceConnection.streamTime; // in ms
    var videoTime = currentVideo.length_seconds;
    client.reply(m, `${VideoFormat.prettyTime(streamTime)} / ${VideoFormat.prettyTime(videoTime * 1000)} (${(streamTime / (videoTime * 10)).toFixed(2)} %)`);
  }
});

function parseVidAndQueue(vid, m, suppress) {
  if (/^http/.test(vid)) {
    if (url.parse(vid, true).query.v) {
      vid = url.parse(vid, true).query.v;
    }
  }

  vid = Saved.possiblyRetrieveVideo(vid);
  if (!vid) {
    client.reply(m, 'You need to specify a video!');
    return;
  }

  getInfoAndQueue(vid, m, suppress);
}

function getInfoAndQueue(vid, m, suppress) {
  requestUrl = 'http://www.youtube.com/watch?v=' + vid;
  ytdl.getInfo(requestUrl, (err, info) => {
    if (err) handleYTError(err);
    else {
      info.vid = vid;
      possiblyQueue(info, m.author.id, m, suppress);
    }
  });
}

function spliceArguments(message, after) {
  after = after || 2;
  var rest = message.split(' ');
  var removed = rest.splice(0, after);
  return [removed.join(' '), rest.join(' ')];
}

function saveVideo(video, vid, keywords, m) {
  simplified = VideoFormat.simplify(video, vid);
  if (Saved.saved.videos.hasOwnProperty(keywords)) client.reply(m, `Warning: ${VideoFormat.simplePrint(Saved.saved.videos[keywords])} is already saved as *${keywords}*! Overwriting.`);

  var key;
  if (key = Saved.isVideoSaved(vid)) client.reply(m, `Warning: This video is already saved as *${key}*! Adding it anyway as *${keywords}*.`);

  Saved.saved.videos[keywords] = simplified;
  client.reply(m, `Saved video ${VideoFormat.prettyPrint(video)} as *${keywords}*`);
  Saved.write();
}

function possiblyQueue(video, userId, m, suppress) {
  video.userId = userId;
  suppress = (suppress === undefined) ? false : suppress;
  reason = shouldDisallowQueue(playQueue, video);
  if (reason) {
    client.reply(m, `You can't queue this video right now! Reason: ${reason}`);
  } else {
    playQueue.push(video);
    if (suppress == 0) client.reply(m, `Queued ${VideoFormat.prettyPrint(video)}`);
    else if (suppress > -1) client.reply(m, `Queued ${VideoFormat.prettyPrint(video)} and ${suppress} other videos`);

    // Start playing if not playing yet
    if (!currentVideo) nextInQueue();
  }
}

function handleYTError(err) {
  if (err.toString().indexOf('Code 150') > -1) {
    // Video unavailable in country
    boundChannel.sendMessage('This video is unavailable in the country the bot is running in! Please try a different video.');
  } else {
    boundChannel.sendMessage('An error occurred while getting video information! Please try a different video.');
  }

  console.log(err.toString());
}

function playStopped() {
  if (client.internal.voiceConnection) client.internal.voiceConnection.stopPlaying();

  boundChannel.sendMessage(`Finished playing ${VideoFormat.simplePrint(currentVideo)}`);
  currentVideo = false;
  nextInQueue();
}

function play(video) {
  currentVideo = video;
  if (client.internal.voiceConnection) {
    var connection = client.internal.voiceConnection;
    currentStream = YoutubeStream.getStream(video.loaderUrl);

    currentStream.on('error', (err) => {
      boundChannel.sendMessage(`There was an error during playback! **${err}**`);
    });

    currentStream.on('end', () => setTimeout(playStopped, 8000)); // 8 second leeway for bad timing
    connection.playRawStream(currentStream).then(intent => {
      boundChannel.sendMessage(`Playing ${VideoFormat.prettyPrint(video)}`);
    });
  }
}

function nextInQueue() {
  if (playQueue.length > 0) {
    next = playQueue.shift();
    play(next);
  }
}

function shuffle(array) {
  var counter = array.length;
  var temp;
  var index;

  // While there are elements in the array
  while (counter > 0) {
    index = Math.floor(Math.random() * counter);

    counter--;

    temp = array[counter];
    array[counter] = array[index];
    array[index] = temp;
  }

  return array;
}

function error(argument) {
  console.log(argument.stack);
}

// Email and password over command line
client.login(process.argv[2], process.argv[3]).catch((e) => console.log(e));
<commit_msg>Add info command<commit_after>var Discord = require('discord.js');

var ytdl = require('ytdl-core');
var request = require('request');
var url = require('url');
var git = require('git-rev');

var shouldDisallowQueue = require('./lib/permission-checks.js');
var VideoFormat = require('./lib/video-format.js');
var YoutubeStream = require('./lib/youtube-stream.js');
var Saved = require('./lib/saved.js');
Saved.read();

var Config = require('./lib/config.js');

var client = new Discord.Client();

// Handle discord.js warnings
client.on('warn', (m) => console.log('[warn]', m));
client.on('debug', (m) => console.log('[debug]', m));

var playQueue = [];
var boundChannel = false;
var currentStream = false;
var currentVideo = false;

var botMention = false;

// Handling api key
if (process.argv[4]) {
  var apiKey = process.argv[4];
} else {
  var apiKey = false;
}

client.on('ready', () => {
  botMention = `<@${client.user.id}>`;
  console.log(`Bot mention: ${botMention}`);
});

client.on('message', m => {
  if (!botMention) return;

  if (m.content.startsWith(`${botMention} info`)) {
    git.short(commit => git.branch(branch => {
      client.reply(m, `Version: \`Lethe#${branch}@${commit}\`. Info about Lethe can be found at https://github.com/meew0/Lethe.`);
    }));
    return;
  }

  if (m.content.startsWith(`${botMention} i`)) { // init
    if (boundChannel) return;
    var channelToJoin = spliceArguments(m.content)[1];
    for (var channel of m.channel.server.channels) {
      if (channel instanceof Discord.VoiceChannel) {
        if (!channelToJoin || channel.name === channelToJoin) {
          boundChannel = m.channel;
          client.reply(m, `Binding to text channel <#${boundChannel.id}> and voice channel **${channel.name}** \`(${channel.id})\``);
          client.joinVoiceChannel(channel).catch(error);
          break;
        }
      }
    }
  }

  if (m.content.startsWith(`${botMention} d`)) { // destroy
    if (!boundChannel) return;
    client.reply(m, `Unbinding from <#${boundChannel.id}> and destroying voice connection`);
    playQueue = [];
    client.internal.leaveVoiceChannel();
    boundChannel = false;
    currentStream = false;
    currentVideo = false;
    return;
  }

  // Only respond to other messages inside the bound channel
  if (!m.channel.equals(boundChannel)) return;

  if (m.content.startsWith(`${botMention} n`)) { // next
    playStopped();
  }

  if (m.content.startsWith(`${botMention} yq`) // youtube query
    || m.content.startsWith(`${botMention} qq`) // queue query
    || m.content.startsWith(`${botMention} pq`) // play query
    || m.content.startsWith(`${botMention} ytq`)) {

    if (apiKey == false) {
      client.reply(m, 'Search is disabled (no API KEY found).');
      return;
    }

    var args = spliceArguments(m.content)[1];

    if (!args) {
      client.reply(m, 'You need to specify a search parameter.');
      return;
    }

    var requestUrl = 'https://www.googleapis.com/youtube/v3/search' +
      `?part=snippet&q=${escape(args)}&key=${apiKey}`;

    request(requestUrl, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        body = JSON.parse(body);
        if (body.items.length == 0) {
          client.reply(m, 'Your query gave 0 results.');
          return;
        }

        var vid = body.items[0].id.videoId;
        getInfoAndQueue(vid, m);
      } else {
        client.reply(m, 'There was an error searching.');
        return;
      }
    });

    return; // have to stop propagation
  }

  if (m.content.startsWith(`${botMention} pl`)) { // playlist
    if (apiKey == false) {
      client.reply(m, 'Playlist adding is disabled (no API KEY found).');
      return;
    }

    var pid = spliceArguments(m.content)[1];

    if (!pid) {
      client.reply(m, 'You need to specify a playlist ID!');
      return;
    }

    var requestUrl = 'https://www.googleapis.com/youtube/v3/playlistItems' +
      `?part=contentDetails&maxResults=50&playlistId=${pid}&key=${apiKey}`;

    request(requestUrl, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        body = JSON.parse(body);
        if (body.items.length == 0) {
          client.reply(m, 'That playlist has no videos.');
          return;
        }

        client.reply(m, `Loading ${body.items.length} videos...`);
        var suppress = 0;
        body.items.forEach((elem, idx) => {
          var vid = elem.contentDetails.videoId;
          if (idx == 1) suppress = body.items.length - 2;
          if (idx == 2) suppress = -1;
          getInfoAndQueue(vid, m, suppress);
        });
      } else {
        client.reply(m, 'There was an error finding playlist with that id.');
        return;
      }
    });

    return;
  }

  if (m.content.startsWith(`${botMention} y`) // youtube
    || m.content.startsWith(`${botMention} q`) // queue
    || m.content.startsWith(`${botMention} p`)) { // play

    var vidList = spliceArguments(m.content)[1];

    var vids = vidList.split(',');
    var suppress = 0;
    vids.forEach((vid, idx) => {
      if (idx == 1) suppress = vids.length - 2;
      if (idx == 2) suppress = -1;
      parseVidAndQueue(vid, m, suppress);
    });
  }

  if (m.content.startsWith(`${botMention} r`)) { // replay
    playQueue.push(currentVideo);
    client.reply(m, `Queued ${VideoFormat.prettyPrint(currentVideo)}`);
  }

  if (m.content.startsWith(`${botMention} sh`)) { // shuffle
    if (playQueue.length < 2) {
      client.reply(m, 'Not enough songs in the queue.');
      return;
    } else {
      shuffle(playQueue);
      client.reply(m, 'Songs in the queue have been shuffled.');
    }

    return;
  }

  if (m.content.startsWith(`${botMention} link`)) {
    if (currentVideo) client.reply(m, `<${currentVideo.loaderUrl}>`);
    return; // stop propagation
  }

  if (m.content.startsWith(`${botMention} list s`)) { // list saved
    var formattedList = 'Here are the videos currently saved: \n';
    for (var key in Saved.saved.videos) {
      if (Saved.saved.videos.hasOwnProperty(key)) {
        formattedList += `*${key}*: ${VideoFormat.prettyPrint(Saved.saved.videos[key])}\n`;
      }
    }

    client.reply(m, formattedList);
    return; // so list doesn't get triggered
  }

  if (m.content.startsWith(`${botMention} l`)) { // list
    var formattedList = 'Here are the videos currently in the play queue, from first added to last added: \n';
    formattedList += `Currently playing: ${VideoFormat.prettyPrintWithUser(currentVideo)}\n`;

    var shouldBreak = false;

    playQueue.forEach((video, idx) => {
      if (shouldBreak) return;

      var formattedVideo = `${idx + 1}. ${VideoFormat.prettyPrintWithUser(video)}\n`;

      if ((formattedList.length + formattedVideo.length) > 1950) {
        formattedList += `... and ${playQueue.length - idx} more`;
        shouldBreak = true;
      } else {
        formattedList += formattedVideo;
      }
    });
    client.reply(m, formattedList);
  }

  if (m.content.startsWith(`${botMention} s`)) { // save
    var argument = spliceArguments(m.content)[1];
    if (!argument) {
      client.reply(m, 'You need to specify a video and a keyword!');
      return;
    }

    var splitArgs = spliceArguments(argument, 1);

    var vid = splitArgs[0];

    if (vid === 'current') {
      if (currentVideo) vid = currentVideo.vid;
      else {
        client.reply(m, `Couldn't retrieve video information of current video!`);
      }
    }

    var requestUrl = 'http://www.youtube.com/watch?v=' + vid;
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else saveVideo(info, vid, splitArgs[1], m);
    });
  }

  if (m.content.startsWith(`${botMention} t`)) { // time
    var streamTime = client.internal.voiceConnection.streamTime; // in ms
    var videoTime = currentVideo.length_seconds;
    client.reply(m, `${VideoFormat.prettyTime(streamTime)} / ${VideoFormat.prettyTime(videoTime * 1000)} (${(streamTime / (videoTime * 10)).toFixed(2)} %)`);
  }
});

function parseVidAndQueue(vid, m, suppress) {
  if (/^http/.test(vid)) {
    if (url.parse(vid, true).query.v) {
      vid = url.parse(vid, true).query.v;
    }
  }

  vid = Saved.possiblyRetrieveVideo(vid);
  if (!vid) {
    client.reply(m, 'You need to specify a video!');
    return;
  }

  getInfoAndQueue(vid, m, suppress);
}

function getInfoAndQueue(vid, m, suppress) {
  requestUrl = 'http://www.youtube.com/watch?v=' + vid;
  ytdl.getInfo(requestUrl, (err, info) => {
    if (err) handleYTError(err);
    else {
      info.vid = vid;
      possiblyQueue(info, m.author.id, m, suppress);
    }
  });
}

function spliceArguments(message, after) {
  after = after || 2;
  var rest = message.split(' ');
  var removed = rest.splice(0, after);
  return [removed.join(' '), rest.join(' ')];
}

function saveVideo(video, vid, keywords, m) {
  simplified = VideoFormat.simplify(video, vid);
  if (Saved.saved.videos.hasOwnProperty(keywords)) client.reply(m, `Warning: ${VideoFormat.simplePrint(Saved.saved.videos[keywords])} is already saved as *${keywords}*! Overwriting.`);

  var key;
  if (key = Saved.isVideoSaved(vid)) client.reply(m, `Warning: This video is already saved as *${key}*! Adding it anyway as *${keywords}*.`);

  Saved.saved.videos[keywords] = simplified;
  client.reply(m, `Saved video ${VideoFormat.prettyPrint(video)} as *${keywords}*`);
  Saved.write();
}

function possiblyQueue(video, userId, m, suppress) {
  video.userId = userId;
  suppress = (suppress === undefined) ? false : suppress;
  reason = shouldDisallowQueue(playQueue, video);
  if (reason) {
    client.reply(m, `You can't queue this video right now! Reason: ${reason}`);
  } else {
    playQueue.push(video);
    if (suppress == 0) client.reply(m, `Queued ${VideoFormat.prettyPrint(video)}`);
    else if (suppress > -1) client.reply(m, `Queued ${VideoFormat.prettyPrint(video)} and ${suppress} other videos`);

    // Start playing if not playing yet
    if (!currentVideo) nextInQueue();
  }
}

function handleYTError(err) {
  if (err.toString().indexOf('Code 150') > -1) {
    // Video unavailable in country
    boundChannel.sendMessage('This video is unavailable in the country the bot is running in! Please try a different video.');
  } else {
    boundChannel.sendMessage('An error occurred while getting video information! Please try a different video.');
  }

  console.log(err.toString());
}

function playStopped() {
  if (client.internal.voiceConnection) client.internal.voiceConnection.stopPlaying();

  boundChannel.sendMessage(`Finished playing ${VideoFormat.simplePrint(currentVideo)}`);
  currentVideo = false;
  nextInQueue();
}

function play(video) {
  currentVideo = video;
  if (client.internal.voiceConnection) {
    var connection = client.internal.voiceConnection;
    currentStream = YoutubeStream.getStream(video.loaderUrl);

    currentStream.on('error', (err) => {
      boundChannel.sendMessage(`There was an error during playback! **${err}**`);
    });

    currentStream.on('end', () => setTimeout(playStopped, 8000)); // 8 second leeway for bad timing
    connection.playRawStream(currentStream).then(intent => {
      boundChannel.sendMessage(`Playing ${VideoFormat.prettyPrint(video)}`);
    });
  }
}

function nextInQueue() {
  if (playQueue.length > 0) {
    next = playQueue.shift();
    play(next);
  }
}

function shuffle(array) {
  var counter = array.length;
  var temp;
  var index;

  // While there are elements in the array
  while (counter > 0) {
    index = Math.floor(Math.random() * counter);

    counter--;

    temp = array[counter];
    array[counter] = array[index];
    array[index] = temp;
  }

  return array;
}

function error(argument) {
  console.log(argument.stack);
}

// Email and password over command line
client.login(process.argv[2], process.argv[3]).catch((e) => console.log(e));
<|endoftext|>