<commit_before>// Copyright 2021, University of Colorado Boulder

/**
 * The Toolbar along the left edge of the screen with controls related to the sim or active screen. Currently, it
 * only contains controls related to the Voicing feature. It isn't always displayed and the user must request it
 * from the PreferencesDialog. In order to be used, both voicing AND the Toolbar must be enabled by the user.
 * Voicing can be used without this shown.
 *
 * When open, the sim will resize and shift to the right to create space. Screen bounds are adjusted so that
 * simulation components will never overlap with the Toolbar.
 *
 * @author Jesse Greenberg
 */

import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../axon/js/NumberProperty.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import Matrix3 from '../../../dot/js/Matrix3.js';
import Shape from '../../../kite/js/Shape.js';
import voicingUtteranceQueue from '../../../scenery/js/accessibility/voicing/voicingUtteranceQueue.js';
import webSpeaker from '../../../scenery/js/accessibility/voicing/webSpeaker.js';
import Node from '../../../scenery/js/nodes/Node.js';
import Path from '../../../scenery/js/nodes/Path.js';
import Rectangle from '../../../scenery/js/nodes/Rectangle.js';
import ButtonNode from '../../../sun/js/buttons/ButtonNode.js';
import RoundPushButton from '../../../sun/js/buttons/RoundPushButton.js';
import joist from '../joist.js';
import joistStrings from '../joistStrings.js';
import VoicingToolbarAlertManager from './VoicingToolbarAlertManager.js';
import VoicingToolbarItem from './VoicingToolbarItem.js';

// constants
const MAX_ANIMATION_SPEED = 250; // in view coordinates per second, assuming 60 fps
const CONTENT_TOP_MARGIN = 15; // margin between top of Toolbar and contents

// constants
const openToolbarString = joistStrings.a11y.toolbar.openToolbar;
const closeToolbarString = joistStrings.a11y.toolbar.closeToolbar;
const hideToolbarString = joistStrings.a11y.toolbar.hideToolbar;
const showToolbarString = joistStrings.a11y.toolbar.showToolbar;
const toolbarShownString = joistStrings.a11y.toolbar.toolbarShown;
const toolbarHiddenString = joistStrings.a11y.toolbar.toolbarHidden;

class Toolbar extends Node {

  /**
   * @param {Sim} sim
   */
  constructor( sim ) {

    super( {

      // pdom
      tagName: 'div'
    } );

    // @private {BooleanProperty} - Whether or not the Toolbar is enabled (visible to the user)
    this.isEnabledProperty = sim.preferencesManager.preferencesProperties.toolbarEnabledProperty;

    // @private {Rectangle} - the Rectangle for the Toolbar that surrounds all content, bounds set once
    // content is created and in layout to fill height of screen
    this.backgroundRectangle = new Rectangle( 0, 0, 0, 0, {
      fill: sim.lookAndFeel.navigationBarFillProperty
    } );

    // @public {NumberProperty} - The position of the right edge of the backgroundRectangle in local coordinates.
    // This is what controls the position of the Toolbar as it is open/closed/removed/animating.
    this.rightPositionProperty = new NumberProperty( 0 );

    // @private {number} - The target position for the rightPositionProperty, to support animation. In step,
    // the rightPositionProperty will be changed until the rightPositionProperty equals the rightDestinationPosition.
    this.rightDestinationPosition = 0;

    // @private {BooleanProperSty} - Whether or not the Toolbar is open or closed. This is different from
    // whether or not it is showing.
    this.openProperty = new BooleanProperty( true );

    // @public (read-only) {DerivedProperty.<boolean>} - Whether the Toolbar is shown to the user. At this time,
    // that is true if the toolbar is enabled, voicing is enabled, and if all audio is enabled. The Toolbar only
    // includes controls related to audio (voicing) so when audio is disabled there is no need to show it.
    this.isShowingProperty = DerivedProperty.and( [ this.isEnabledProperty, webSpeaker.enabledProperty, sim.allAudioEnabledProperty ] );

    // @private {number} - Scale applied to the Toolbar and its contents in response to layout and window resizing.
    this.layoutScale = 1;

    // @private {VoicingToolbarItem} - Contents for the Toolbar, currently only controls related to the voicing
    // feature.
    const voicingAlertManager = new VoicingToolbarAlertManager( sim.screenProperty );
    this.menuContent = new VoicingToolbarItem( voicingAlertManager, sim.lookAndFeel );

    // icon for the openButton
    const chevronIcon = new DoubleChevron();

    // @private {RoundPushButton}
    this.openButton = new RoundPushButton( {
      content: chevronIcon,
      listener: () => this.openProperty.toggle(),
      buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy,
      baseColor: 'lightgrey'
    } );

    // @private {number} - width of content for the toolbar in the local coordinate frame
    this.contentWidth = this.menuContent.localBounds.width;

    // @private {number} - Margin between toolbar content and edge of the backgroundRectangle, in the local coordinate
    // frame. Also used to determine the right position when closed. This is the width of the button so that when the
    // Toolbar is closed only the button is shown and all other content is hidden.
    this.contentMargin = this.openButton.localBounds.width;

    // a parent for all Nodes of the toolbar, so we can set visibility of this group internally when
    // the isShowingProperty changes
    const contentParent = new Node( {
      children: [ this.backgroundRectangle, this.menuContent, this.openButton ]
    } );
    this.addChild( contentParent );

    // move to destination position in the animation frame
    const timerListener = dt => {
      this.step( dt );
    };
    stepTimer.addListener( timerListener );

    const isOpenListener = ( open, oldValue ) => {

      // rotate chevron to indicate direction of toolbar movement
      chevronIcon.matrix = open ? Matrix3.scaling( -1, 1 ) : Matrix3.IDENTITY;

      // when closed, menu content should be hidden from screen readers and the navigation order
      this.menuContent.pdomVisible = open;

      this.openButton.innerContent = open ? closeToolbarString : openToolbarString;
      this.openButton.voicingNameResponse = open ? hideToolbarString : showToolbarString;

      this.updateDestinationPosition();

      if ( oldValue !== null ) {
        const alert = open ? toolbarShownString : toolbarHiddenString;

        voicingUtteranceQueue.addToBack( alert );
        phet.joist.sim.utteranceQueue.addToBack( alert );
      }
    };
    this.openProperty.link( isOpenListener );

    // when shown or hidden update destination positions so it animates open or close
    const isShowingListener = showing => {
      this.updateDestinationPosition();
      console.log( showing );

      // when now showing, this entire toolbar should be hidden for Interactive Description, but we don't use
      // visibility directly because we want to see the Toolbar animate away
      contentParent.pdomVisible = showing;
    };
    this.isShowingProperty.link( isShowingListener );

    // @private
    this.disposeToolbar = () => {
      this.isShowingProperty.unlink( isShowingListener );
      this.openProperty.unlink( isOpenListener );
    };
  }

  /**
   * Returns the width of the Toolbar that can be seen on screen. This can be any value from the full width of the
   * Toolbar to zero width, depending on whether it is open, closed, removed entirely, or animating.
   * @public
   *
   * @returns {number}
   */
  getDisplayedWidth() {
    return this.rightPositionProperty.value * this.layoutScale + this.openButton.width / 2;
  }

  /**
   * Update rightDestinationPosition so that the Toolbar will animate towards opening, closing, or being removed
   * entirely from view.
   * @private
   */
  updateDestinationPosition() {
    if ( this.isShowingProperty.value ) {
      // the Toolbar is enabled and should either show all content or just the openButton
      this.rightDestinationPosition = this.openProperty.value ? this.contentWidth + this.contentMargin * 2 : this.contentMargin;
    }
    else {
      // no aspect of the menu should be visible
      this.rightDestinationPosition = -this.contentMargin / 2;
    }
  }

  /**
   * Animated the Toolbar as it opens and closes.
   * @private
   *
   * @param {number} dt
   */
  step( dt ) {
    const distance = Math.abs( this.rightPositionProperty.value - this.rightDestinationPosition );
    if ( distance !== 0 ) {
      const animationDistance = Math.min( distance, MAX_ANIMATION_SPEED * dt );
      const currentPosition = this.rightPositionProperty.value;
      this.rightPositionProperty.value = this.rightDestinationPosition > currentPosition ?
                                         currentPosition + animationDistance :
                                         currentPosition - animationDistance;
    }
  }

  /**
   * Layout for the Toolbar, called whenever position changes or window is resized.
   * @public
   *
   * @param {number} scale
   * @param {number} height
   */
  layout( scale, height ) {
    this.layoutScale = scale;
    this.menuContent.setScaleMagnitude( scale );
    this.openButton.setScaleMagnitude( scale );
    this.backgroundRectangle.rectWidth = scale * ( this.contentWidth + this.contentMargin * 2 );
    this.backgroundRectangle.rectHeight = height;
    this.backgroundRectangle.right = this.rightPositionProperty.value * scale;
    this.openButton.center = this.backgroundRectangle.rightCenter;
    this.menuContent.centerTop = this.backgroundRectangle.centerTop.plusXY( 0, CONTENT_TOP_MARGIN );
  }

  /**
   * @public
   */
  dispose() {
    this.disposeToolbar();
    super.dispose();
  }
}

/**
 * The icon for the button that opens and closes the Toolbar.
 */
class DoubleChevron extends Path {
  constructor() {

    // spacing and dimensions for the arrows
    const chevronSpacing = 8;
    const chevronWidth = 5;
    const chevronHeight = 12;

    const chevronShape = new Shape();
    for ( let i = 0; i < 2; i++ ) {
      const left = i * chevronSpacing;
      chevronShape.moveTo( left, 0 );
      chevronShape.lineTo( left + chevronWidth, chevronHeight / 2 );
      chevronShape.lineTo( left, chevronHeight );
    }

    super( chevronShape, {
      stroke: 'black',
      lineWidth: 3.5,
      lineCap: 'round'
    } );
  }
}

joist.register( 'Toolbar', Toolbar );
export default Toolbar;<commit_msg>remove console statement<commit_after>// Copyright 2021, University of Colorado Boulder

/**
 * The Toolbar along the left edge of the screen with controls related to the sim or active screen. Currently, it
 * only contains controls related to the Voicing feature. It isn't always displayed and the user must request it
 * from the PreferencesDialog. In order to be used, both voicing AND the Toolbar must be enabled by the user.
 * Voicing can be used without this shown.
 *
 * When open, the sim will resize and shift to the right to create space. Screen bounds are adjusted so that
 * simulation components will never overlap with the Toolbar.
 *
 * @author Jesse Greenberg
 */

import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../axon/js/NumberProperty.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import Matrix3 from '../../../dot/js/Matrix3.js';
import Shape from '../../../kite/js/Shape.js';
import voicingUtteranceQueue from '../../../scenery/js/accessibility/voicing/voicingUtteranceQueue.js';
import webSpeaker from '../../../scenery/js/accessibility/voicing/webSpeaker.js';
import Node from '../../../scenery/js/nodes/Node.js';
import Path from '../../../scenery/js/nodes/Path.js';
import Rectangle from '../../../scenery/js/nodes/Rectangle.js';
import ButtonNode from '../../../sun/js/buttons/ButtonNode.js';
import RoundPushButton from '../../../sun/js/buttons/RoundPushButton.js';
import joist from '../joist.js';
import joistStrings from '../joistStrings.js';
import VoicingToolbarAlertManager from './VoicingToolbarAlertManager.js';
import VoicingToolbarItem from './VoicingToolbarItem.js';

// constants
const MAX_ANIMATION_SPEED = 250; // in view coordinates per second, assuming 60 fps
const CONTENT_TOP_MARGIN = 15; // margin between top of Toolbar and contents

// constants
const openToolbarString = joistStrings.a11y.toolbar.openToolbar;
const closeToolbarString = joistStrings.a11y.toolbar.closeToolbar;
const hideToolbarString = joistStrings.a11y.toolbar.hideToolbar;
const showToolbarString = joistStrings.a11y.toolbar.showToolbar;
const toolbarShownString = joistStrings.a11y.toolbar.toolbarShown;
const toolbarHiddenString = joistStrings.a11y.toolbar.toolbarHidden;

class Toolbar extends Node {

  /**
   * @param {Sim} sim
   */
  constructor( sim ) {

    super( {

      // pdom
      tagName: 'div'
    } );

    // @private {BooleanProperty} - Whether or not the Toolbar is enabled (visible to the user)
    this.isEnabledProperty = sim.preferencesManager.preferencesProperties.toolbarEnabledProperty;

    // @private {Rectangle} - the Rectangle for the Toolbar that surrounds all content, bounds set once
    // content is created and in layout to fill height of screen
    this.backgroundRectangle = new Rectangle( 0, 0, 0, 0, {
      fill: sim.lookAndFeel.navigationBarFillProperty
    } );

    // @public {NumberProperty} - The position of the right edge of the backgroundRectangle in local coordinates.
    // This is what controls the position of the Toolbar as it is open/closed/removed/animating.
    this.rightPositionProperty = new NumberProperty( 0 );

    // @private {number} - The target position for the rightPositionProperty, to support animation. In step,
    // the rightPositionProperty will be changed until the rightPositionProperty equals the rightDestinationPosition.
    this.rightDestinationPosition = 0;

    // @private {BooleanProperSty} - Whether or not the Toolbar is open or closed. This is different from
    // whether or not it is showing.
    this.openProperty = new BooleanProperty( true );

    // @public (read-only) {DerivedProperty.<boolean>} - Whether the Toolbar is shown to the user. At this time,
    // that is true if the toolbar is enabled, voicing is enabled, and if all audio is enabled. The Toolbar only
    // includes controls related to audio (voicing) so when audio is disabled there is no need to show it.
    this.isShowingProperty = DerivedProperty.and( [ this.isEnabledProperty, webSpeaker.enabledProperty, sim.allAudioEnabledProperty ] );

    // @private {number} - Scale applied to the Toolbar and its contents in response to layout and window resizing.
    this.layoutScale = 1;

    // @private {VoicingToolbarItem} - Contents for the Toolbar, currently only controls related to the voicing
    // feature.
    const voicingAlertManager = new VoicingToolbarAlertManager( sim.screenProperty );
    this.menuContent = new VoicingToolbarItem( voicingAlertManager, sim.lookAndFeel );

    // icon for the openButton
    const chevronIcon = new DoubleChevron();

    // @private {RoundPushButton}
    this.openButton = new RoundPushButton( {
      content: chevronIcon,
      listener: () => this.openProperty.toggle(),
      buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy,
      baseColor: 'lightgrey'
    } );

    // @private {number} - width of content for the toolbar in the local coordinate frame
    this.contentWidth = this.menuContent.localBounds.width;

    // @private {number} - Margin between toolbar content and edge of the backgroundRectangle, in the local coordinate
    // frame. Also used to determine the right position when closed. This is the width of the button so that when the
    // Toolbar is closed only the button is shown and all other content is hidden.
    this.contentMargin = this.openButton.localBounds.width;

    // a parent for all Nodes of the toolbar, so we can set visibility of this group internally when
    // the isShowingProperty changes
    const contentParent = new Node( {
      children: [ this.backgroundRectangle, this.menuContent, this.openButton ]
    } );
    this.addChild( contentParent );

    // move to destination position in the animation frame
    const timerListener = dt => {
      this.step( dt );
    };
    stepTimer.addListener( timerListener );

    const isOpenListener = ( open, oldValue ) => {

      // rotate chevron to indicate direction of toolbar movement
      chevronIcon.matrix = open ? Matrix3.scaling( -1, 1 ) : Matrix3.IDENTITY;

      // when closed, menu content should be hidden from screen readers and the navigation order
      this.menuContent.pdomVisible = open;

      this.openButton.innerContent = open ? closeToolbarString : openToolbarString;
      this.openButton.voicingNameResponse = open ? hideToolbarString : showToolbarString;

      this.updateDestinationPosition();

      if ( oldValue !== null ) {
        const alert = open ? toolbarShownString : toolbarHiddenString;

        voicingUtteranceQueue.addToBack( alert );
        phet.joist.sim.utteranceQueue.addToBack( alert );
      }
    };
    this.openProperty.link( isOpenListener );

    // when shown or hidden update destination positions so it animates open or close
    const isShowingListener = showing => {
      this.updateDestinationPosition();

      // when now showing, this entire toolbar should be hidden for Interactive Description, but we don't use
      // visibility directly because we want to see the Toolbar animate away
      contentParent.pdomVisible = showing;
    };
    this.isShowingProperty.link( isShowingListener );

    // @private
    this.disposeToolbar = () => {
      this.isShowingProperty.unlink( isShowingListener );
      this.openProperty.unlink( isOpenListener );
    };
  }

  /**
   * Returns the width of the Toolbar that can be seen on screen. This can be any value from the full width of the
   * Toolbar to zero width, depending on whether it is open, closed, removed entirely, or animating.
   * @public
   *
   * @returns {number}
   */
  getDisplayedWidth() {
    return this.rightPositionProperty.value * this.layoutScale + this.openButton.width / 2;
  }

  /**
   * Update rightDestinationPosition so that the Toolbar will animate towards opening, closing, or being removed
   * entirely from view.
   * @private
   */
  updateDestinationPosition() {
    if ( this.isShowingProperty.value ) {
      // the Toolbar is enabled and should either show all content or just the openButton
      this.rightDestinationPosition = this.openProperty.value ? this.contentWidth + this.contentMargin * 2 : this.contentMargin;
    }
    else {
      // no aspect of the menu should be visible
      this.rightDestinationPosition = -this.contentMargin / 2;
    }
  }

  /**
   * Animated the Toolbar as it opens and closes.
   * @private
   *
   * @param {number} dt
   */
  step( dt ) {
    const distance = Math.abs( this.rightPositionProperty.value - this.rightDestinationPosition );
    if ( distance !== 0 ) {
      const animationDistance = Math.min( distance, MAX_ANIMATION_SPEED * dt );
      const currentPosition = this.rightPositionProperty.value;
      this.rightPositionProperty.value = this.rightDestinationPosition > currentPosition ?
                                         currentPosition + animationDistance :
                                         currentPosition - animationDistance;
    }
  }

  /**
   * Layout for the Toolbar, called whenever position changes or window is resized.
   * @public
   *
   * @param {number} scale
   * @param {number} height
   */
  layout( scale, height ) {
    this.layoutScale = scale;
    this.menuContent.setScaleMagnitude( scale );
    this.openButton.setScaleMagnitude( scale );
    this.backgroundRectangle.rectWidth = scale * ( this.contentWidth + this.contentMargin * 2 );
    this.backgroundRectangle.rectHeight = height;
    this.backgroundRectangle.right = this.rightPositionProperty.value * scale;
    this.openButton.center = this.backgroundRectangle.rightCenter;
    this.menuContent.centerTop = this.backgroundRectangle.centerTop.plusXY( 0, CONTENT_TOP_MARGIN );
  }

  /**
   * @public
   */
  dispose() {
    this.disposeToolbar();
    super.dispose();
  }
}

/**
 * The icon for the button that opens and closes the Toolbar.
 */
class DoubleChevron extends Path {
  constructor() {

    // spacing and dimensions for the arrows
    const chevronSpacing = 8;
    const chevronWidth = 5;
    const chevronHeight = 12;

    const chevronShape = new Shape();
    for ( let i = 0; i < 2; i++ ) {
      const left = i * chevronSpacing;
      chevronShape.moveTo( left, 0 );
      chevronShape.lineTo( left + chevronWidth, chevronHeight / 2 );
      chevronShape.lineTo( left, chevronHeight );
    }

    super( chevronShape, {
      stroke: 'black',
      lineWidth: 3.5,
      lineCap: 'round'
    } );
  }
}

joist.register( 'Toolbar', Toolbar );
export default Toolbar;<|endoftext|>


<commit_before>//function Seq(_seq, speed, gen, _outputMsg) {
function Seq() {
	var _seq = arguments[0] || null;

	if(typeof _seq === "function") { // wrap anonymous function in array as sugar
		_seq = [_seq];
	}

	var speed = arguments[1] || null;
	if(speed == null && _seq != null) {
		speed = (arguments.length != 0) ? window["_" + arguments[0].length] : _4;
	}

	var _outputMsg = arguments[2] || null;

	if(_outputMsg === null) {
		_outputMsg = "note";
	}

	var that = {
		name : "Seq",
		type : "control",
		_sequence : null,
		sequence : _seq,
		_start : true,
		offset : 0,	 // used to sequence Gibber object
		counter : 0, // position in seqeuence
		speed: speed,
		outputMessage:_outputMsg,
		active:true,
		slaves: [],
		phase: 0,
		memory: [],
		init: false,
		mods: [],
		oddEven : 0,
	}

	that.once = function() {
		this.end = true;
		return this;
	};

	that.schedule = function() {
	    var phase = 0;
	    var _offset = this.offset;

		var events = (_1 + _offset) / this.speed;
		for(var i = 0; i < events; i++) {
			this.oddEven = !this.oddEven;
			var pos = (this.oddEven) ? (i * Math.floor(this.speed)) : (i * Math.ceil(this.speed));

			G.callback.addEvent(pos - _offset, this); // sequence on global object
			phase = pos - _offset;

	        this.offset = 0;
		}

		this.offset += _1 - phase;
	};

	that.kill = function() {
		this.free();
		Gibber.callback.slaves.remove(this);

		for(var i = 0; i < this.slaves.length; i++) {
			var slave = this.slaves[i];
			slave.masters.remove(this);
		}
		this.slaves.length = 0;

		this.mods.length = 0;
	},

	that.setSequence = function(seq, _speed, _reset) {
		if(typeof _speed !== "undefined") {
			//console.log("SPEED = " + _speed);
			this.speed = _speed;
		}

		if(_reset) {
			this.phase = 0;
			this.counter = 0;
		}

		this.sequence = [];

		if(typeof seq === "string") {
			for(var c = 0; c < seq.length; c++) {
				var _c = seq.charAt(c);
				this.sequence.push(_c);
			}
		}else{
			for(var i = 0; i < seq.length; i++) {
				var n = seq[i];
				this.sequence[i] = n;
			}
		}

		if(this.init === false) {
			Gibber.callback.slaves.push(this);
			this._sequence = this.sequence.slice(0);
			this.init = true;
		}
	};

	that.slave = function(gen) {
		//console.log("slaving " + gen);
		var idx = jQuery.inArray( gen, this.slaves);
		if(idx === -1) {
			this.slaves.push(gen);
			if(typeof gen.masters === "undefined") {
				gen.masters = [this];
			}else{
				gen.masters.push(this);
			}
			if(typeof gen.note === "undefined" && this.outputMessage == "note") { this.outputMessage = "freq"; }
		}
		return this;
	};

	that.free = function() {
		if(arguments.length == 0) {
			this.slaves.length = 0;
		}else{
			this.slaves.splice(arguments[0], 1);
		}
	};

	that.stop = function() {
		this.active = false;
		this.phase = 0;
		this.counter = 0;
		return this;
	};

	that.pause = function() {
		this.active = false;
		return this;
	};

	that.play = function() {
		this.active = true;
		return this;
	};

	that.advance = function() {
		if(this.active) {
			var val = this.sequence[this.counter % this.sequence.length];
			this.counter++;

			var shouldReturn = false;
			// Function sequencing
			// TODO: there should probably be a more robust way to to this
			// but it will look super nice and clean on screen...
			if(typeof val === "function") {
				val();
				return;
			}else if(typeof val === "undefined") {
				return;
			}

			for(var j = 0; j < this.slaves.length; j++) {
				var _slave = this.slaves[j];

				if(this.outputMessage === "freq") {
					if(typeof val === "string" ) {
						var nt = teoria.note(val);
						val = nt.fq();
					}else if(typeof val === "object"){
						val = val.fq();
					}// else val is a number and is fine to send as a freq...
				}
				if(typeof _slave[this.outputMessage] === "function") {
					_slave[this.outputMessage](val);
				}else{
					_slave[this.outputMessage] = val;
				}
			}
		}
	};

	that.break = function(newSeq, breakToOriginal) {
		this.shouldBreak = true;

		if(breakToOriginal) {
			this.breakToOriginal = true;
		}
		console.log("BREAK");
		this.preBreakSequence = jQuery.extend(true, {}, this._sequence);
		console.log(this.preBreakSequence);
	};

	that.getMix = function(){
		return 0;
	};

	that.out = function() {
		this.generate();
		return 0;
	};
	that.set = function(newSequence, speed, shouldReset) {
		if(typeof speed != "undefined") {
			if(!shouldReset) {
				this.phase -= this.speed - speed;
			}
			this.speed = speed;
		}

		if(shouldReset) {
			this.phase = 0;
		}

		this.sequence = newSequence;
		this.setSequence(this.sequence, speed, shouldReset);
	};

	that.shuffle = function() {
		that.sequence.shuffle();
		that.setSequence(that.sequence, that.speed);
		return that;
	};

	that.reset = function() {
		if(arguments.length === 0) {
			that.setSequence(that._sequence, that.speed);
		}else{
			that.setSequence(that.memory[arguments[0]]);
		}
		return that;
	};

	that.retain = function() {
		if(arguments.length != 0) {
			this.memory.push(this.sequence);
		}else{
			this.memory[arguments[0]] = this.sequence;
		}
	};

	that.bpmCallback = function(obj) {
		var _that = obj;
		return function(percentageChangeForBPM) {
			_that.speed *= percentageChangeForBPM;
			//_that.setSequence(_that.sequence, _that.speed); // don't need this, not sure why it causes errors.
		}
	};

	Gibber.registerObserver( "bpm", that.bpmCallback(that) );

	(function() {
		var speed = that.speed;
		var _that = that;

		Object.defineProperties(that, {
			"speed": {
				get: function(){ return speed; },
				set: function(value) {
					if(value < 65) {
						value = window["_"+value];
					}
					speed = value;
					if(this.sequence != null) {
						this.setSequence(this.sequence);
					}
				}
			}
		});
	})();

	if(that.sequence != null && typeof that.sequence != "undefined") {
		//console.log("setting inital sequence")
		that.setSequence(that.sequence, that.speed);
	}

	that.setParam = function(param, _value){
		this[param] = _value;
	};

	Gibber.addModsAndFX.call(that);

	return that;
}<commit_msg>changed slave method to allow slaving of multiple things at once<commit_after>//function Seq(_seq, speed, gen, _outputMsg) {
function Seq() {
	var _seq = arguments[0] || null;

	if(typeof _seq === "function") { // wrap anonymous function in array as sugar
		_seq = [_seq];
	}

	var speed = arguments[1] || null;
	if(speed == null && _seq != null) {
		speed = (arguments.length != 0) ? window["_" + arguments[0].length] : _4;
	}

	var _outputMsg = arguments[2] || null;

	if(_outputMsg === null) {
		_outputMsg = "note";
	}

	var that = {
		name : "Seq",
		type : "control",
		_sequence : null,
		sequence : _seq,
		_start : true,
		offset : 0,	 // used to sequence Gibber object
		counter : 0, // position in seqeuence
		speed: speed,
		outputMessage:_outputMsg,
		active:true,
		slaves: [],
		phase: 0,
		memory: [],
		init: false,
		mods: [],
		oddEven : 0,
	}

	that.once = function() {
		this.end = true;
		return this;
	};

	that.schedule = function() {
	    var phase = 0;
	    var _offset = this.offset;

		var events = (_1 + _offset) / this.speed;
		for(var i = 0; i < events; i++) {
			this.oddEven = !this.oddEven;
			var pos = (this.oddEven) ? (i * Math.floor(this.speed)) : (i * Math.ceil(this.speed));

			G.callback.addEvent(pos - _offset, this); // sequence on global object
			phase = pos - _offset;

	        this.offset = 0;
		}

		this.offset += _1 - phase;
	};

	that.kill = function() {
		this.free();
		Gibber.callback.slaves.remove(this);

		for(var i = 0; i < this.slaves.length; i++) {
			var slave = this.slaves[i];
			slave.masters.remove(this);
		}
		this.slaves.length = 0;

		this.mods.length = 0;
	},

	that.setSequence = function(seq, _speed, _reset) {
		if(typeof _speed !== "undefined") {
			//console.log("SPEED = " + _speed);
			this.speed = _speed;
		}

		if(_reset) {
			this.phase = 0;
			this.counter = 0;
		}

		this.sequence = [];

		if(typeof seq === "string") {
			for(var c = 0; c < seq.length; c++) {
				var _c = seq.charAt(c);
				this.sequence.push(_c);
			}
		}else{
			for(var i = 0; i < seq.length; i++) {
				var n = seq[i];
				this.sequence[i] = n;
			}
		}

		if(this.init === false) {
			Gibber.callback.slaves.push(this);
			this._sequence = this.sequence.slice(0);
			this.init = true;
		}
	};

	that.slave = function() {
		//console.log("slaving " + gen);
		for(var i = 0; i < arguments.length; i++) {
			var gen = arguments[i];

			var idx = jQuery.inArray( gen, this.slaves);
			if(idx === -1) {
				this.slaves.push(gen);
				if(typeof gen.masters === "undefined") {
					gen.masters = [this];
				}else{
					gen.masters.push(this);
				}
				if(typeof gen.note === "undefined" && this.outputMessage == "note") { this.outputMessage = "freq"; }
			}
		}
		return this;
	};

	that.free = function() {
		if(arguments.length == 0) {
			this.slaves.length = 0;
		}else{
			this.slaves.splice(arguments[0], 1);
		}
	};

	that.stop = function() {
		this.active = false;
		this.phase = 0;
		this.counter = 0;
		return this;
	};

	that.pause = function() {
		this.active = false;
		return this;
	};

	that.play = function() {
		this.active = true;
		return this;
	};

	that.advance = function() {
		if(this.active) {
			var val = this.sequence[this.counter % this.sequence.length];
			this.counter++;

			var shouldReturn = false;
			// Function sequencing
			// TODO: there should probably be a more robust way to to this
			// but it will look super nice and clean on screen...
			if(typeof val === "function") {
				val();
				return;
			}else if(typeof val === "undefined") {
				return;
			}

			for(var j = 0; j < this.slaves.length; j++) {
				var _slave = this.slaves[j];

				if(this.outputMessage === "freq") {
					if(typeof val === "string" ) {
						var nt = teoria.note(val);
						val = nt.fq();
					}else if(typeof val === "object"){
						val = val.fq();
					}// else val is a number and is fine to send as a freq...
				}
				if(typeof _slave[this.outputMessage] === "function") {
					_slave[this.outputMessage](val);
				}else{
					_slave[this.outputMessage] = val;
				}
			}
		}
	};

	that.break = function(newSeq, breakToOriginal) {
		this.shouldBreak = true;

		if(breakToOriginal) {
			this.breakToOriginal = true;
		}
		console.log("BREAK");
		this.preBreakSequence = jQuery.extend(true, {}, this._sequence);
		console.log(this.preBreakSequence);
	};

	that.getMix = function(){
		return 0;
	};

	that.out = function() {
		this.generate();
		return 0;
	};
	that.set = function(newSequence, speed, shouldReset) {
		if(typeof speed != "undefined") {
			if(!shouldReset) {
				this.phase -= this.speed - speed;
			}
			this.speed = speed;
		}

		if(shouldReset) {
			this.phase = 0;
		}

		this.sequence = newSequence;
		this.setSequence(this.sequence, speed, shouldReset);
	};

	that.shuffle = function() {
		that.sequence.shuffle();
		that.setSequence(that.sequence, that.speed);
		return that;
	};

	that.reset = function() {
		if(arguments.length === 0) {
			that.setSequence(that._sequence, that.speed);
		}else{
			that.setSequence(that.memory[arguments[0]]);
		}
		return that;
	};

	that.retain = function() {
		if(arguments.length != 0) {
			this.memory.push(this.sequence);
		}else{
			this.memory[arguments[0]] = this.sequence;
		}
	};

	that.bpmCallback = function(obj) {
		var _that = obj;
		return function(percentageChangeForBPM) {
			_that.speed *= percentageChangeForBPM;
			//_that.setSequence(_that.sequence, _that.speed); // don't need this, not sure why it causes errors.
		}
	};

	Gibber.registerObserver( "bpm", that.bpmCallback(that) );

	(function() {
		var speed = that.speed;
		var _that = that;

		Object.defineProperties(that, {
			"speed": {
				get: function(){ return speed; },
				set: function(value) {
					if(value < 65) {
						value = window["_"+value];
					}
					speed = value;
					if(this.sequence != null) {
						this.setSequence(this.sequence);
					}
				}
			}
		});
	})();

	if(that.sequence != null && typeof that.sequence != "undefined") {
		//console.log("setting inital sequence")
		that.setSequence(that.sequence, that.speed);
	}

	that.setParam = function(param, _value){
		this[param] = _value;
	};

	Gibber.addModsAndFX.call(that);

	return that;
}<|endoftext|>


<commit_before>if(typeof translations == 'undefined'){
    translations = {};
}

translations.de_DE = {
// header
    'Search': 'Suche',
    // Nav menu
    'Add': 'Hinzufügen',
    'By URIs': 'mit URIs',
    'By Torrents': 'mit Torrents',
    'By Metalinks': 'mit Metalinks',
    'Manage': 'Verwalten',
    'Pause All': 'Alle anhalten',
    'Resume Paused': 'Angehaltene fortsetzen',
    'Purge Completed': 'Fertige entfernen',
    'Settings': 'Einstellungen',
    'Connection Settings': 'Verbindungseinstellungen',
    'Global Settings': 'Globale Einstellungen',
    'Server info': 'Server Information',
    'About and contribute': 'Über webui-aria2',
    'Toggle navigation': 'Navigation an/ausschalten',
// body
// nav side bar
    'Miscellaneous': 'Verschiedenes',
    'Global Statistics': 'Globale Statistiken',
    'About': 'Über',
    'Displaying': 'Anzeige',
    'of': 'von',
    'downloads': 'Downloads',
    'Language': 'Sprache',
// download filters
    'Download Filters': 'Download Filter',
    'Running': 'Laufende',
    'Active': 'Aktive',
    'Waiting': 'Wartende',
    'Complete': 'Fertige',
    'Error': 'Fehler',
    'Paused': 'Angehaltene',
    'Removed': 'Gelöschte',
    'Hide linked meta-data': 'Blende verlinkte Meta-Daten aus',
    'Displaying': 'Anzeige',
    'of': 'von',
    'downloads': 'Downloads',
    'Toggle': 'Umschalten',
    'Reset filters': 'Filter zurücksetzen',
// starred properties
    'Quick Access Settings': 'Ausgewählte Einstellungen',
    'Save settings': 'Einstellungen speichern',
    'Currently no download in line to display, use the': 'Aktuell sind keine Downloads vorhanden, bitte benutz den',
    'download button to start downloading files!': 'Download Link um den Download von Dateien zu beginnen!',
    'Peers': 'Peers',
    'More Info': 'Mehr Infos',
    'Remove': 'Entfernen',
    '# of': '# von',
    'Length': 'Länge',
// modals
    'Add Downloads By URIs': 'Downloads anhand von URIs hinzufügen',
    '- You can add multiple downloads (files) at the same time by putting URIs for each file on a separate line.':
        '- Es können mehrere Downloads (Dateien) gleichzeitig hinzugefügt werden, indem jede URI in eine separate Zeile eingegeben wird.',
    '- You can also add multiple URIs (mirrors) for the *same* file. To do this, separate the URIs by a space.':
        '- Es können auch mehrere URIs (Spiegelserver) für *dieselbe* Datei durch Leerzeichen getrennt angegeben werden.',
    '- A URI can be HTTP(S)/FTP/BitTorrent-Magnet.': '- Eine URI kann folgende Protokolle besitzen: HTTP(S)/FTP/BitTorrent-Magnet.',
    'E.g. to add 2 files (downloads) f1.jpg and f2.mp4 with 2 URIs (mirrors) each, add URIs as follows':
        'Um z.B. 2 Dateien (Downloads) f1.jpg und f2.mp4 mit jeweis einem zweiten Spiegelserver herunterzuladen, kann folgendes angegeben werden',
    'Download settings': 'Download Einstellungen',
    'Advanced settings': 'Erweiterte Einstellungen',
    'Cancel': 'Abbrechen',
    'Start': 'Beginnen',
    'Choose': 'Auswählen',
    'Quick Access (shown on the main page)': 'Schnellzugriff (Anzeige auf der Hauptseite)',
// add torrent modal
    'Add Downloads By Torrents': 'Downloads mit Torrents hinzufügen',
    '- Select the torrent from the local filesystem to start the download.': '- Wähle ein Torrent vom lokalen Dateisystem um den Download zu starten',
    '- You can select multiple torrents to start multiple downloads.': '- Es können mehrere Torrents ausgewählt werden um mehrere Downloads zu starten',
    '- To add a BitTorrent-Magnet URL, use the Add By URI option and add it there.': '- Für BitTorrent-Magnet URLs benutz die Option \'Mit URIs hinzufügen\'',
    'Select Torrents': 'Wähle Torrents',
    'Select a Torrent': 'Wähle ein Torrent',
// add metalink modal
    'Add Downloads By Metalinks': 'Download mit Metalinks hinzufügen',
    'Select Metalinks': 'Wähle Metalinks',
    '- Select the Metalink from the local filesystem to start the download.': '- Wähle ein Metalink vom lokalen Dateisystem um den Download zu starten',
    '- You can select multiple Metalinks to start multiple downloads.': '- Es können mehrere Metalinks ausgewählt werden um mehrere Downloads zu starten',
    'Select a Metalink': 'Wähle einen Metalink',
// select file modal
    'Choose files to start download for': 'Wähle Dateien für den Download aus',
    'Select to download': 'Wähle zum Download',
// settings modal
    'Aria2 RPC host and port': 'Aria2 RPC host und port',
    'Enter the host': 'Host',
    'Enter the IP or DNS name of the server on which the RPC for Aria2 is running (default: localhost)':
        'Gib die IP oder den DNS Namen des Servers ein, auf dem Aria2 läuft und mit dem du eine RPC-Verbindung etablieren willst (Standard: localhost)',
    'Enter the port': 'Port',
    'Enter the port of the server on which the RPC for Aria2 is running (default: 6800)':
        'Gib den Port des Servers ein, auf dem der RPC-Dienst von Aria2 läuft (Standard: 6800)',
    'Enter the RPC path': 'RPC path',
    'Enter the path for the Aria2 RPC endpoint (default: /jsonrpc)': 'Gib den Pfad zum Aria2 RPC Endpunkt an (Standard: /jsonrpc)',
    'SSL/TLS encryption': 'SSL/TLS',
    'Enable SSL/TLS encryption': 'Aktiviere SSL/TLS Verschlüsselung',
    'Enter the secret token (optional)': 'secret token (optional)',
    'Enter the Aria2 RPC secret token (leave empty if authentication is not enabled)':
        'Gib den Aria2 RPC secret Token ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter the username (optional)': 'Benutzername (optional)',
    'Enter the Aria2 RPC username (empty if authentication not enabled)':
        'Gib den Aria2 RPC Benutzernamen ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter the password (optional)': 'Password (optional)',
    'Enter the Aria2 RPC password (empty if authentication not enabled)': 'Gib das Aria2 RPC Passwort ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter base URL (optional)': 'Base URL (optional)',
    'Direct Download': 'Directer Download',
    'If supplied, links will be created to enable direct download from the Aria2 server.':
        'Falls angegeben, werden Links erstellt um einen direkten Download vom Aria2 Server zu ermöglichen',
    '(Requires appropriate webserver to be configured.)': '(Es wird ein entsprechend konfigurierter WebServer benötigt.)',
    'Save Connection configuration': 'Speichern der Verbindungseinstellung',
// server info modal
    'Aria2 server info': 'Aria2 Server Info',
    'Aria2 Version': 'Aria2 Version',
    'Features Enabled': 'Aktive Funktionen',
// about modal
    'To download the latest version of the project, add issues or to contribute back, head on to':
        'Um die neuste Version des Projects zu laden, Fehler zu melden oder sich zu beteiligen, besuch',
    'Or you can open the latest version in the browser through': 'Oder du kannst die neueste Version direkt in deinem Browser verwenden',
    'Close': 'Schließen',
// lables
    'Download status':'Download Status',
    'Download Speed':'Download Geschwindigkeit',
    'Upload Speed':'Upload Geschwindigkeit',
    'Estimated time':'Geschlätzte Zeit',
    'Download Size':'Download Größe',
    'Downloaded':'Heruntergeladen',
    'Progress':'Fortschritt',
    'Download Path':'Download Pfad',
    'Uploaded':'Hochgeladen',
    'Download GID':'Download GID',
    'Number of Pieces':'Anzahl der Stücken',
    'Piece Length':'Größe der Stücken'
};
<commit_msg>Typos in german language<commit_after>if(typeof translations == 'undefined'){
    translations = {};
}

translations.de_DE = {
// header
    'Search': 'Suche',
    // Nav menu
    'Add': 'Hinzufügen',
    'By URIs': 'mit URIs',
    'By Torrents': 'mit Torrents',
    'By Metalinks': 'mit Metalinks',
    'Manage': 'Verwalten',
    'Pause All': 'Alle anhalten',
    'Resume Paused': 'Angehaltene fortsetzen',
    'Purge Completed': 'Fertige entfernen',
    'Settings': 'Einstellungen',
    'Connection Settings': 'Verbindungseinstellungen',
    'Global Settings': 'Globale Einstellungen',
    'Server info': 'Server Information',
    'About and contribute': 'Über webui-aria2',
    'Toggle navigation': 'Navigation an/ausschalten',
// body
// nav side bar
    'Miscellaneous': 'Verschiedenes',
    'Global Statistics': 'Globale Statistiken',
    'About': 'Über',
    'Displaying': 'Anzeige',
    'of': 'von',
    'downloads': 'Downloads',
    'Language': 'Sprache',
// download filters
    'Download Filters': 'Download Filter',
    'Running': 'Laufende',
    'Active': 'Aktive',
    'Waiting': 'Wartende',
    'Complete': 'Fertige',
    'Error': 'Fehler',
    'Paused': 'Angehaltene',
    'Removed': 'Gelöschte',
    'Hide linked meta-data': 'Blende verlinkte Meta-Daten aus',
    'Displaying': 'Anzeige',
    'of': 'von',
    'downloads': 'Downloads',
    'Toggle': 'Umschalten',
    'Reset filters': 'Filter zurücksetzen',
// starred properties
    'Quick Access Settings': 'Ausgewählte Einstellungen',
    'Save settings': 'Einstellungen speichern',
    'Currently no download in line to display, use the': 'Aktuell sind keine Downloads vorhanden, bitte benutz den',
    'download button to start downloading files!': 'Download Link um den Download von Dateien zu beginnen!',
    'Peers': 'Peers',
    'More Info': 'Mehr Infos',
    'Remove': 'Entfernen',
    '# of': '# von',
    'Length': 'Länge',
// modals
    'Add Downloads By URIs': 'Downloads anhand von URIs hinzufügen',
    '- You can add multiple downloads (files) at the same time by putting URIs for each file on a separate line.':
        '- Es können mehrere Downloads (Dateien) gleichzeitig hinzugefügt werden, indem jede URI in eine separate Zeile eingegeben wird.',
    '- You can also add multiple URIs (mirrors) for the *same* file. To do this, separate the URIs by a space.':
        '- Es können auch mehrere URIs (Spiegelserver) für *dieselbe* Datei durch Leerzeichen getrennt angegeben werden.',
    '- A URI can be HTTP(S)/FTP/BitTorrent-Magnet.': '- Eine URI kann folgende Protokolle besitzen: HTTP(S)/FTP/BitTorrent-Magnet.',
    'E.g. to add 2 files (downloads) f1.jpg and f2.mp4 with 2 URIs (mirrors) each, add URIs as follows':
        'Um z.B. 2 Dateien (Downloads) f1.jpg und f2.mp4 mit jeweis einem zweiten Spiegelserver herunterzuladen, kann folgendes angegeben werden',
    'Download settings': 'Download Einstellungen',
    'Advanced settings': 'Erweiterte Einstellungen',
    'Cancel': 'Abbrechen',
    'Start': 'Beginnen',
    'Choose': 'Auswählen',
    'Quick Access (shown on the main page)': 'Schnellzugriff (Anzeige auf der Hauptseite)',
// add torrent modal
    'Add Downloads By Torrents': 'Downloads mit Torrents hinzufügen',
    '- Select the torrent from the local filesystem to start the download.': '- Wähle ein Torrent vom lokalen Dateisystem um den Download zu starten',
    '- You can select multiple torrents to start multiple downloads.': '- Es können mehrere Torrents ausgewählt werden um mehrere Downloads zu starten',
    '- To add a BitTorrent-Magnet URL, use the Add By URI option and add it there.': '- Für BitTorrent-Magnet URLs benutz die Option \'Mit URIs hinzufügen\'',
    'Select Torrents': 'Wähle Torrents',
    'Select a Torrent': 'Wähle ein Torrent',
// add metalink modal
    'Add Downloads By Metalinks': 'Download mit Metalinks hinzufügen',
    'Select Metalinks': 'Wähle Metalinks',
    '- Select the Metalink from the local filesystem to start the download.': '- Wähle ein Metalink vom lokalen Dateisystem um den Download zu starten',
    '- You can select multiple Metalinks to start multiple downloads.': '- Es können mehrere Metalinks ausgewählt werden um mehrere Downloads zu starten',
    'Select a Metalink': 'Wähle einen Metalink',
// select file modal
    'Choose files to start download for': 'Wähle Dateien für den Download aus',
    'Select to download': 'Wähle zum Download',
// settings modal
    'Aria2 RPC host and port': 'Aria2 RPC host und port',
    'Enter the host': 'Host',
    'Enter the IP or DNS name of the server on which the RPC for Aria2 is running (default: localhost)':
        'Gib die IP oder den DNS Namen des Servers ein, auf dem Aria2 läuft und mit dem du eine RPC-Verbindung etablieren willst (Standard: localhost)',
    'Enter the port': 'Port',
    'Enter the port of the server on which the RPC for Aria2 is running (default: 6800)':
        'Gib den Port des Servers ein, auf dem der RPC-Dienst von Aria2 läuft (Standard: 6800)',
    'Enter the RPC path': 'RPC Pfad',
    'Enter the path for the Aria2 RPC endpoint (default: /jsonrpc)': 'Gib den Pfad zum Aria2 RPC Endpunkt an (Standard: /jsonrpc)',
    'SSL/TLS encryption': 'SSL/TLS',
    'Enable SSL/TLS encryption': 'Aktiviere SSL/TLS Verschlüsselung',
    'Enter the secret token (optional)': 'Secret Token (optional)',
    'Enter the Aria2 RPC secret token (leave empty if authentication is not enabled)':
        'Gib den Aria2 RPC secret Token ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter the username (optional)': 'Benutzername (optional)',
    'Enter the Aria2 RPC username (empty if authentication not enabled)':
        'Gib den Aria2 RPC Benutzernamen ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter the password (optional)': 'Passwort (optional)',
    'Enter the Aria2 RPC password (empty if authentication not enabled)': 'Gib das Aria2 RPC Passwort ein (leer lassen falls keine Authentifizierung aktiv)',
    'Enter base URL (optional)': 'Base URL (optional)',
    'Direct Download': 'Direkter Download',
    'If supplied, links will be created to enable direct download from the Aria2 server.':
        'Falls angegeben, werden Links erstellt um einen direkten Download vom Aria2 Server zu ermöglichen',
    '(Requires appropriate webserver to be configured.)': '(Es wird ein entsprechend konfigurierter WebServer benötigt.)',
    'Save Connection configuration': 'Speichern der Verbindungseinstellung',
// server info modal
    'Aria2 server info': 'Aria2 Server Info',
    'Aria2 Version': 'Aria2 Version',
    'Features Enabled': 'Aktive Funktionen',
// about modal
    'To download the latest version of the project, add issues or to contribute back, head on to':
        'Um die neuste Version des Projects zu laden, Fehler zu melden oder sich zu beteiligen, besuch',
    'Or you can open the latest version in the browser through': 'Oder du kannst die neueste Version direkt in deinem Browser verwenden',
    'Close': 'Schließen',
// lables
    'Download status':'Download Status',
    'Download Speed':'Download Geschwindigkeit',
    'Upload Speed':'Upload Geschwindigkeit',
    'Estimated time':'Geschätzte Zeit',
    'Download Size':'Download Größe',
    'Downloaded':'Heruntergeladen',
    'Progress':'Fortschritt',
    'Download Path':'Download Pfad',
    'Uploaded':'Hochgeladen',
    'Download GID':'Download GID',
    'Number of Pieces':'Anzahl der Stücken',
    'Piece Length':'Größe der Stücken'
};
<|endoftext|>


<commit_before>// March 2012, Franziska Hinkelmann, Mike Stillman, and Lars Kastner
//
// This is server-side JavaScript, intended to be run with Node.js.
// This file defines a Node.js server for serving 'tryM2'.
//   run
//      node m2server.js
// in a terminal in this directory.
// Then in a browser, use:
//      http://localhost:8000/
// Required Node.js libraries: cookies.  Install via:
//   npm install cookies, or sudo npm install -g cookies
// Required on path: M2

//
// A message on / : possibly creates a cookie, and serves back index.html
//   and related js/css/png files
// A POST message on /chat: input should be Macaulay2 commands to perform.
// A message on /chat: start an event emitter, which will return the output of
// the M2 process.

var http = require('http');  // NodeJS HTTP server API
var Cookies = require("cookies");

// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("index.html");
var totalUsers = 0;

// An array of Client objects.  Each has an M2 process, and a response object
// It is possible that this.m2 is not defined, and/or that this.response is not
// defined.
var clients = {};

function Client(m2process, resp) {
    this.m2 = m2process;
    this.response = resp;
}

startUser = function(cookies) {
    totalUsers = totalUsers + 1;
    var clientID = Math.random()*1000000;
    clientID = Math.floor(clientID);
    clientID = "user" + clientID.toString(10);
    cookies.set( "tryM2", clientID, { httpOnly: false } );
    clients[clientID] = new Client(); // will be populated with a Client in /chat
    return clientID;
}

startM2Process = function() {
    var spawn = require('child_process').spawn;
    //var m2 = spawn('M2');
    var m2 = spawn('sudo', ['../sandbox/sandbox', '../../sandbox-dir', 'bin/M2', '-q', '-e', 'limitResources()']);
    //var m2 = spawn('../sandbox/sandbox', ['../../sandbox-dir', 'bin/M2']);
    m2.stdout.setEncoding("utf8");
    m2.stderr.setEncoding("utf8");
    console.log('Spawned m2 pid: ' + m2.pid);
    return m2;
};

startM2 = function(client) {
    // client is an object of type Client
    client.m2 = startM2Process();
    console.log("starting emitter");
    var ondata = function(data) {
        console.log('ondata: ' + data);
        message = 'data: ' + data.replace(/\n/g, '\ndata: ') + "\r\n\r\n";
        client.response.write(message);
    };
    client.m2.stdout.on('data', ondata);
    client.m2.stderr.on('data', ondata);
};

restartM2 = function(client) {
    if (client.m2) {
        client.m2.kill();
    }
    startM2(client);
};

getCurrentClientID = function(cookies) {
    var clientID = cookies.get("tryM2");
    //console.log("Client has cookie value: " + clientID);

// Start new user for users coming with invalid, i.e., old, cookie
    if (!clients[clientID]) {
        console.log("startUser");
        clientID = startUser(cookies);
    }
    return clientID;
};

var stats = function(response) {
    // to do: authorization
     response.writeHead(200, {"Content-Type": "text/html"});
     var currentUsers = 0;
     for( var c in clients) {
         if(clients.hasOwnProperty(c))
                 currentUsers = currentUsers + 1;
     }
     response.write('<link rel="stylesheet" href="m2.css" type="text/css" media="screen">' );
     response.write('
Macaulay2 User Statistics
');
     response.write("There are currently " + currentUsers + " users using M2.
" );
     response.write("In total, there were " + totalUsers + " users since the server started.
");
     response.write("Enjoy M2!");
     response.end();
}

// Send a comment to the clients every 20 seconds so they don't
// close the connection and then reconnect
setInterval(function() {
    for (var prop in clients) {
        if (clients.hasOwnProperty(prop)
            && clients[prop]
            && clients[prop].response) {
            clients[prop].response.write(":ping\n");
        }
    }
}, 20000);

// Create a new server
var server = new http.Server();
// When the server gets a new request, run this function
server.on("request", function (request, response) {
    //console.log( "got on");
    // Parse the requested URL
    var cookies = new Cookies(request, response);
    var url = require('url').parse(request.url);

    if(url.pathname === "/admin") {
        stats(response);
        return;
    }
    // If the request was for "/", start new user and send index.html
    if (url.pathname === "/" || url.pathname === "/index.html") {
        startUser(cookies);
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(clientui);
        response.end();
        return;
    }
    // send css files requested by index.html
    if (/\.css/.test(url.pathname) ) {
        response.writeHead(200, {'Content-Type': 'text/css'});
        var u = url.pathname.replace("/", '');
        response.write(require('fs').readFileSync(u));
        response.end();
        return;
    }
    // communicate with M2 process
    if (url.pathname === "/chat") {
        var clientID = getCurrentClientID(cookies);
        // Client is sending M2 input
        if (request.method === "POST") {
            request.setEncoding("utf8");
            var body = "";
            // When we get a chunk of data, add it to the body
            request.on("data", function(chunk) { body += chunk; });

            // Send input to M2 when we have received the complete body
            request.on("end", function() {
                try {
                    //console.log("Send M2 input: " + body);
                    clients[clientID].m2.stdin.write(body);
                }
                catch (err) {
                    //console.log("Socket was closed for clients[" + clientID + "]");
                    // send error back to user, user needs to start eventStream and resend 'body'
                    response.writeHead(200, {
                      'notEventSourceError': 'No socket for client...' });
                    response.end();
                    return;
                }
                response.writeHead(200);
                response.end();
            });
            return;
        // Client starts eventStream to obtain M2 output
        } else {
            response.writeHead(200, {'Content-Type': "text/event-stream" });
            if (!clients[clientID].response) {
                clients[clientID].response = response;
                startM2(clients[clientID]);
            }

            // If the client closes the connection, remove client from the list of active clients
            request.connection.on("end", function() {
                console.log("close connection: clients[" + clientID + "]");
                if( clients[clientID] && clients[clientID].m2) {
                    clients[clientID].m2.kill();
                }
                delete clients[clientID];
                response.end();
            });
            return;
        }
    }
    if (url.pathname === "/restart") {
        var clientID = getCurrentClientID(cookies);
        restartM2(clients[clientID]);
        console.log("received: /restart from " + clientID);
        response.writeHead(200);
        response.end();
        return;
    }
    if (url.pathname === "/interrupt") {
        var clientID = getCurrentClientID(cookies);
        if (clients[clientID] && clients[clientID].m2) {
            clients[clientID].m2.kill('SIGINT');
        }
        console.log("received: /interrupt from " + clientID);
        response.writeHead(200);
        response.end();
        return;
    }
    // Send file (e.g. images and tutorial) or 404 if not found
    if (url.pathname !== "/chat") {
        var u = url.pathname.replace("/", '');
        try {
            data = require('fs').readFileSync(u);
            response.writeHead(200, {"Content-Type": "text/html"});
            response.write(data);
        }
        catch (err) {
            console.error("There was an error opening the file:");
            console.log(err);
            response.writeHead(404,{"Content-Type": "text/html"});
            response.write( '
Page not found. Return to TryM2
');
        }
        response.end();
        return;
    }
});

// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
console.log("Listening on port 8000...");
server.listen(8000);
<commit_msg>debug<commit_after>// March 2012, Franziska Hinkelmann, Mike Stillman, and Lars Kastner
//
// This is server-side JavaScript, intended to be run with Node.js.
// This file defines a Node.js server for serving 'tryM2'.
//   run
//      node m2server.js
// in a terminal in this directory.
// Then in a browser, use:
//      http://localhost:8000/
// Required Node.js libraries: cookies.  Install via:
//   npm install cookies, or sudo npm install -g cookies
// Required on path: M2

//
// A message on / : possibly creates a cookie, and serves back index.html
//   and related js/css/png files
// A POST message on /chat: input should be Macaulay2 commands to perform.
// A message on /chat: start an event emitter, which will return the output of
// the M2 process.

var http = require('http');  // NodeJS HTTP server API
var Cookies = require("cookies");

// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("index.html");
var totalUsers = 0;

// An array of Client objects.  Each has an M2 process, and a response object
// It is possible that this.m2 is not defined, and/or that this.response is not
// defined.
var clients = {};

function Client(m2process, resp) {
    this.m2 = m2process;
    this.response = resp;
}

startUser = function(cookies) {
    totalUsers = totalUsers + 1;
    var clientID = Math.random()*1000000;
    clientID = Math.floor(clientID);
    clientID = "user" + clientID.toString(10);
    cookies.set( "tryM2", clientID, { httpOnly: false } );
    clients[clientID] = new Client(); // will be populated with a Client in /chat
    return clientID;
}

startM2Process = function() {
    var spawn = require('child_process').spawn;
    console.log("spawning...");
    //var m2 = spawn('M2');
    var m2 = spawn('sudo', ['../sandbox/sandbox', '../../sandbox-dir', 'bin/M2', '-q', '-e', 'limitResources()']);
    //var m2 = spawn('../sandbox/sandbox', ['../../sandbox-dir', 'bin/M2']);
    m2.stdout.setEncoding("utf8");
    m2.stderr.setEncoding("utf8");
    console.log('Spawned m2 pid: ' + m2.pid);
    return m2;
};

startM2 = function(client) {
    // client is an object of type Client
    client.m2 = startM2Process();
    console.log("starting emitter");
    var ondata = function(data) {
        console.log('ondata: ' + data);
        message = 'data: ' + data.replace(/\n/g, '\ndata: ') + "\r\n\r\n";
        client.response.write(message);
    };
    client.m2.stdout.on('data', ondata);
    client.m2.stderr.on('data', ondata);
};

restartM2 = function(client) {
    console.log("in restartM2");
    if (client.m2) {
        client.m2.kill();
        console.log("killed it!");
    }
    startM2(client);
};

getCurrentClientID = function(cookies) {
    var clientID = cookies.get("tryM2");
    //console.log("Client has cookie value: " + clientID);

// Start new user for users coming with invalid, i.e., old, cookie
    if (!clients[clientID]) {
        console.log("startUser");
        clientID = startUser(cookies);
    }
    return clientID;
};

var stats = function(response) {
    // to do: authorization
     response.writeHead(200, {"Content-Type": "text/html"});
     var currentUsers = 0;
     for( var c in clients) {
         if(clients.hasOwnProperty(c))
                 currentUsers = currentUsers + 1;
     }
     response.write('<link rel="stylesheet" href="m2.css" type="text/css" media="screen">' );
     response.write('
Macaulay2 User Statistics
');
     response.write("There are currently " + currentUsers + " users using M2.
" );
     response.write("In total, there were " + totalUsers + " users since the server started.
");
     response.write("Enjoy M2!");
     response.end();
}

// Send a comment to the clients every 20 seconds so they don't
// close the connection and then reconnect
setInterval(function() {
    for (var prop in clients) {
        if (clients.hasOwnProperty(prop)
            && clients[prop]
            && clients[prop].response) {
            clients[prop].response.write(":ping\n");
        }
    }
}, 20000);

// Create a new server
var server = new http.Server();
// When the server gets a new request, run this function
server.on("request", function (request, response) {
    //console.log( "got on");
    // Parse the requested URL
    var cookies = new Cookies(request, response);
    var url = require('url').parse(request.url);

    if(url.pathname === "/admin") {
        stats(response);
        return;
    }
    // If the request was for "/", start new user and send index.html
    if (url.pathname === "/" || url.pathname === "/index.html") {
        startUser(cookies);
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(clientui);
        response.end();
        return;
    }
    // send css files requested by index.html
    if (/\.css/.test(url.pathname) ) {
        response.writeHead(200, {'Content-Type': 'text/css'});
        var u = url.pathname.replace("/", '');
        response.write(require('fs').readFileSync(u));
        response.end();
        return;
    }
    // communicate with M2 process
    if (url.pathname === "/chat") {
        var clientID = getCurrentClientID(cookies);
        // Client is sending M2 input
        if (request.method === "POST") {
            request.setEncoding("utf8");
            var body = "";
            // When we get a chunk of data, add it to the body
            request.on("data", function(chunk) { body += chunk; });

            // Send input to M2 when we have received the complete body
            request.on("end", function() {
                try {
                    //console.log("Send M2 input: " + body);
                    clients[clientID].m2.stdin.write(body);
                }
                catch (err) {
                    //console.log("Socket was closed for clients[" + clientID + "]");
                    // send error back to user, user needs to start eventStream and resend 'body'
                    response.writeHead(200, {
                      'notEventSourceError': 'No socket for client...' });
                    response.end();
                    return;
                }
                response.writeHead(200);
                response.end();
            });
            return;
        // Client starts eventStream to obtain M2 output
        } else {
            response.writeHead(200, {'Content-Type': "text/event-stream" });
            if (!clients[clientID].response) {
                clients[clientID].response = response;
                startM2(clients[clientID]);
            }

            // If the client closes the connection, remove client from the list of active clients
            request.connection.on("end", function() {
                console.log("close connection: clients[" + clientID + "]");
                if( clients[clientID] && clients[clientID].m2) {
                    clients[clientID].m2.kill();
                }
                delete clients[clientID];
                response.end();
            });
            return;
        }
    }
    if (url.pathname === "/restart") {
        var clientID = getCurrentClientID(cookies);
        restartM2(clients[clientID]);
        console.log("received: /restart from " + clientID);
        response.writeHead(200);
        response.end();
        return;
    }
    if (url.pathname === "/interrupt") {
        var clientID = getCurrentClientID(cookies);
        if (clients[clientID] && clients[clientID].m2) {
            clients[clientID].m2.kill('SIGINT');
        }
        console.log("received: /interrupt from " + clientID);
        response.writeHead(200);
        response.end();
        return;
    }
    // Send file (e.g. images and tutorial) or 404 if not found
    if (url.pathname !== "/chat") {
        var u = url.pathname.replace("/", '');
        try {
            data = require('fs').readFileSync(u);
            response.writeHead(200, {"Content-Type": "text/html"});
            response.write(data);
        }
        catch (err) {
            console.error("There was an error opening the file:");
            console.log(err);
            response.writeHead(404,{"Content-Type": "text/html"});
            response.write( '
Page not found. Return to TryM2
');
        }
        response.end();
        return;
    }
});

// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
console.log("Listening on port 8000...");
server.listen(8000);
<|endoftext|>


<commit_before>// March 2012, Franziska Hinkelmann, Mike Stillman, and Lars Kastner
//
// This is server-side JavaScript, intended to be run with Node.js.
// This file defines a Node.js server for serving 'tryM2'.
//   run
//      node m2server.js
// in a terminal in this directory.
// Then in a browser, use:
//      http://localhost:8000/
// Required Node.js libraries: cookies.  Install via:
//   npm install cookies, or sudo npm install -g cookies
// Required on path: M2

//
// A message on / : possibly creates a cookie, and serves back index.html
//   and related js/css/png files
// A POST message on /chat: input should be Macaulay2 commands to perform.
// A message on /chat: start an event emitter, which will return the output of
// the M2 process.

var http = require('http');  // NodeJS HTTP server API
var Cookies = require("cookies");

// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("index.html");

// An array of Client objects.  Each has an M2 process, and a response object
// It is possible that this.m2 is not defined, and/or that this.response is not
// defined.
var clients = [];
var nextClientId = 0;

function Client(m2process, resp) {
    this.m2 = m2process;
    this.response = resp;
}

startM2Process = function() {
    var spawn = require('child_process').spawn;
    var m2 = spawn('M2');
    m2.stdout.setEncoding("utf8");
    m2.stderr.setEncoding("utf8");
    console.log('Spawned m2 pid: ' + m2.pid);
    return m2;
};

startM2 = function(client) {
    client.m2 = startM2Process();
    console.log("starting emitter");
    var ondata = function(data) {
        //console.log('ondata: ' + data);
        message = 'data: ' + data.replace(/\n/g, '\ndata: ') + "\r\n\r\n";
        client.response.write(message);
    };
    client.m2.stdout.on('data', ondata);
    client.m2.stderr.on('data', ondata);
};

// Send a comment to the clients every 20 seconds so they don't
// close the connection and then reconnect
setInterval(function() {
    clients.forEach(function(client) {
        client.response.write(":ping\n");
    });
}, 20000);

// Create a new server
var server = new http.Server();

// When the server gets a new request, run this function
server.on("request", function (request, response) {
    // Parse the requested URL
    var cookies = new Cookies(request, response);
    var url = require('url').parse(request.url);

    // If the request was for "/", send the client-side chat UI.
    if (url.pathname === "/" || url.pathname === "/index.html") {  // A request for the chat UI
        cookies.set( "trym2cookie", nextClientId.toString(10), { httpOnly: false } );
        clients.push(new Client); // will be populated with a Client in /chat
        nextClientId = nextClientId + 1;

        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(clientui);
        response.end();
        return;
    }
    if (/\.css/.test(url.pathname) ) {
        response.writeHead(200, {'Content-Type': 'text/css'});
        var u = url.pathname.replace("/", '');
        response.write(require('fs').readFileSync(u));
        response.end();
        return;
    }
    if (url.pathname === "/chat") {
        // If the request was a post, then a client is posting a new message
        var clientID = parseInt(cookies.get("trym2cookie"),10);
        console.log("cookie value from client: " + clientID);
        if (request.method === "POST") {
            request.setEncoding("utf8");

            //   for (e in request.headers) {
            //       console.log(e+": "+request.headers[e]);
            //   }

            var body = "";
            // When we get a chunk of data, add it to the body
            request.on("data", function(chunk) { body += chunk; });

           request.on("end", function() {
                response.writeHead(200);   // Respond to the request
                response.end();
                // Send 'body' to M2
                clients[clientID].m2.stdin.write(body);
            });
            return;
        } else {
            // Set the content type and send an initial message event
            response.writeHead(200, {'Content-Type': "text/event-stream" });
            if (!clients[clientID].response) {
                clients[clientID].response = response;
                startM2(clients[clientID]);
            }

            // If the client closes the connection, remove the corresponding
            // response object from the array of active clients
            request.connection.on("end", function() {
                //TODO: kill client gracefully
                response.end();
            });

            return;
        }
    }
    // Send file (e.g. images and tutorial) or 404 if not found
    if (url.pathname !== "/chat") {

        var u = url.pathname.replace("/", '');
        try {
            data = require('fs').readFileSync(u);
            response.writeHead(200, {"Content-Type": "text/html"});
            response.write(data);
        }
        catch (err) {
            console.error("There was an error opening the file:");
            console.log(err);
            response.writeHead(404,{"Content-Type": "text/html"});
            response.write( '
Page not found. Return to TryM2
');
        }

        response.end();
        return;
    }
});

// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
console.log("Listening on port 8000...");
server.listen(8000);<commit_msg>m2server has more graceful restarting and so is more robust<commit_after>// March 2012, Franziska Hinkelmann, Mike Stillman, and Lars Kastner
//
// This is server-side JavaScript, intended to be run with Node.js.
// This file defines a Node.js server for serving 'tryM2'.
//   run
//      node m2server.js
// in a terminal in this directory.
// Then in a browser, use:
//      http://localhost:8000/
// Required Node.js libraries: cookies.  Install via:
//   npm install cookies, or sudo npm install -g cookies
// Required on path: M2

//
// A message on / : possibly creates a cookie, and serves back index.html
//   and related js/css/png files
// A POST message on /chat: input should be Macaulay2 commands to perform.
// A message on /chat: start an event emitter, which will return the output of
// the M2 process.

var http = require('http');  // NodeJS HTTP server API
var Cookies = require("cookies");

// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("index.html");

// An array of Client objects.  Each has an M2 process, and a response object
// It is possible that this.m2 is not defined, and/or that this.response is not
// defined.
var clients = {};
var nextClientId = 0;

function Client(m2process, resp) {
    this.m2 = m2process;
    this.response = resp;
}

startM2Process = function() {
    var spawn = require('child_process').spawn;
    var m2 = spawn('M2');
    m2.stdout.setEncoding("utf8");
    m2.stderr.setEncoding("utf8");
    console.log('Spawned m2 pid: ' + m2.pid);
    return m2;
};

startM2 = function(client) {
    // client is an object of type Client
    client.m2 = startM2Process();
    console.log("starting emitter");
    var ondata = function(data) {
        //console.log('ondata: ' + data);
        message = 'data: ' + data.replace(/\n/g, '\ndata: ') + "\r\n\r\n";
        client.response.write(message);
    };
    client.m2.stdout.on('data', ondata);
    client.m2.stderr.on('data', ondata);
};

restartM2 = function(client) {
    if (client.m2) { client.m2.kill(); }
    startM2(client);
}

// Send a comment to the clients every 20 seconds so they don't
// close the connection and then reconnect
setInterval(function() {
    for (var prop in clients) {
        if (clients.hasOwnProperty(prop)
            && clients[prop]
            && clients[prop].response) {
            clients[prop].response.write(":ping\n");
        }
    }
}, 20000);

// Create a new server
var server = new http.Server();

// When the server gets a new request, run this function
server.on("request", function (request, response) {
    // Parse the requested URL
    var cookies = new Cookies(request, response);
    var url = require('url').parse(request.url);

    // If the request was for "/", send the client-side chat UI.
    if (url.pathname === "/" || url.pathname === "/index.html") {  // A request for the chat UI
        cookies.set( "trym2cookie", nextClientId.toString(10), { httpOnly: false } );
        clients[nextClientId.toString()] = new Client(); // will be populated with a Client in /chat
        nextClientId = nextClientId + 1;

        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(clientui);
        response.end();
        return;
    }
    if (/\.css/.test(url.pathname) ) {
        response.writeHead(200, {'Content-Type': 'text/css'});
        var u = url.pathname.replace("/", '');
        response.write(require('fs').readFileSync(u));
        response.end();
        return;
    }
    if (url.pathname === "/chat") {
        // If the request was a post, then a client is posting a new message
        var clientID = cookies.get("trym2cookie");
        console.log("cookie value from client: " + clientID);
        if (request.method === "POST") {
            request.setEncoding("utf8");

            //   for (e in request.headers) {
            //       console.log(e+": "+request.headers[e]);
            //   }

            var body = "";
            // When we get a chunk of data, add it to the body
            request.on("data", function(chunk) { body += chunk; });

           request.on("end", function() {
                response.writeHead(200);   // Respond to the request
                response.end();
                // Send 'body' to M2
                try {
                    clients[clientID].m2.stdin.write(body);
                }
                catch (err) {
                    console.log("socket was closed for clients[" + clientID + "]");
                    restartM2(clients[clientID]);
                }
            });
            return;
        } else {
            // Set the content type and send an initial message event
            response.writeHead(200, {'Content-Type': "text/event-stream" });
            if (!clients[clientID].response) {
                clients[clientID].response = response;
                startM2(clients[clientID]);
            }

            // If the client closes the connection, remove the corresponding
            // response object from the array of active clients
            request.connection.on("end", function() {
                console.log("close connection: clients[" + clientID + "]");
                clients[clientID].m2.kill();
                clients[clientID].m2 = null;
                clients[clientID].response = null;
                response.end();
            });

            return;
        }
    }
    // Send file (e.g. images and tutorial) or 404 if not found
    if (url.pathname !== "/chat") {

        var u = url.pathname.replace("/", '');
        try {
            data = require('fs').readFileSync(u);
            response.writeHead(200, {"Content-Type": "text/html"});
            response.write(data);
        }
        catch (err) {
            console.error("There was an error opening the file:");
            console.log(err);
            response.writeHead(404,{"Content-Type": "text/html"});
            response.write( '
Page not found. Return to TryM2
');
        }

        response.end();
        return;
    }
});

// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
console.log("Listening on port 8000...");
server.listen(8000);<|endoftext|>


<commit_before>var instanceUrl = OC.generateUrl('/apps/owncollab_timetracker/');

(function (OC, window, $, undefined) {
'use strict';

$(document).ready(function () {

var translations = {
    newProject: $('#new-project-string').text()
};

// this projects object holds all our projects
var Projects = function (baseUrl) {
    this._baseUrl = baseUrl;
    this._projects = [];
    this._activeProject = undefined;
};

Projects.prototype = {
    load: function (id) {
        var self = this;
        this._projects.forEach(function (project) {
            if (project.id === id) {
                project.active = true;
                self._activeProject = project;
            } else {
                project.active = false;
            }
        });

    },
    getActive: function () {
        return this._activeProject;
    },
    removeActive: function () {
        var index;
        var deferred = $.Deferred();
        var id = this._activeProject.id;
        this._projects.forEach(function (project, counter) {
            if (project.id === id) {
                index = counter;
            }
        });

        if (index !== undefined) {
            // delete cached active project if necessary
            if (this._activeProject === this._projects[index]) {
                delete this._activeProject;
            }

            this._projects.splice(index, 1);

            $.ajax({
                url: this._baseUrl + '/' + id,
                method: 'DELETE'
            }).done(function () {
                deferred.resolve();
            }).fail(function () {
                deferred.reject();
            });
        } else {
            deferred.reject();
        }
        return deferred.promise();
    },
    create: function (project) {
        var deferred = $.Deferred();
        var self = this;
        $.ajax({
            url: this._baseUrl,
            method: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(project)
        }).done(function (project) {
            self._projects.push(project);
            self._activeProject = project;
            self.load(project.id);
            deferred.resolve();
        }).fail(function () {
            deferred.reject();
        });
        return deferred.promise();
    },
    getAll: function () {
        return this._projects;
    },
    loadAll: function () {
        var deferred = $.Deferred();
        var self = this;
        $.get(this._baseUrl).done(function (projects) {
            self._activeProject = undefined;
            self._projects = projects;
            deferred.resolve();
        }).fail(function () {
            deferred.reject();
        });
        return deferred.promise();
    },
    updateActive: function (name, clientid) {
        var project = this.getActive();
        project.name = name;
        project.clientid = clientid;
        return $.ajax({
            url: this._baseUrl + '/' + project.id,
            method: 'PUT',
            contentType: 'application/json',
            data: JSON.stringify(project)
        });
    }
};

// this will be the view that is used to update the html
var View = function (projects) {
    this._projects = projects;
};

View.prototype = {
    renderContent: function () {
        var source = $('#project-content-tpl').html();
        var template = Handlebars.compile(source);
        var html = template({project: this._projects.getActive()});
        $('#project-editor').html(html);
        if (this._projects.getActive()){
		    if (this._projects.getActive().clientid) {
                	if (this._projects.getActive().clientid == 0){
                        $('#select_client').changeSelect(instanceUrl, 'clients');
                	}
		    	    else{
				        $('#select_client').changeSelectbyID(instanceUrl, 'clients', this._projects.getActive().clientid, '', '');
		    	}
		}

        }

        // handle saves
        var self = this;
        $('#project-save').click(function () {
	    if ($('#select_client').val() != ''){
	            var name = $('#name').val();
	            var clientid = $('#select_client').val();
	            self._projects.updateActive(name, clientid).done(function () {
	                self.render();
	            }).fail(function () {
	                alert('Could not update project, not found');
	            });
	    }
	    else{
		alert('Please provide client');
	    }
	 });
    },
    renderNavigation: function () {
        var source = $('#projects-tpl').html();
        var template = Handlebars.compile(source);
        var html = template({projects: this._projects.getAll()});
        $('#project-sub-navigation ul').html(html);

        // create a new project
        var self = this;
        $('#new-project').click(function () {
            var project = {
                name: translations.newProject,
                clientid: ''
            };

            self._projects.create(project).done(function() {
                self.render();
                $('#select_client').changeSelect(instanceUrl, 'clients');
                $('#name').focus();
            }).fail(function () {
                alert('Could not create project');
            });
        });

        // show app menu
        $('#project-sub-navigation .app-navigation-entry-utils-menu-button').click(function () {
            var entry = $(this).closest('.project');
            entry.find('.app-navigation-entry-menu').toggleClass('open');
        });

        // delete a project
        $('#project-sub-navigation .project .delete').click(function () {
            var entry = $(this).closest('.project');
            entry.find('.app-navigation-entry-menu').removeClass('open');

            self._projects.removeActive().done(function () {
                self.render();
            }).fail(function () {
                alert('Could not delete project, not found');
            });
        });

        // load a project
        $('#project-sub-navigation .project > a').click(function () {

            var id = parseInt($(this).parent().data('id'), 10);
            self._projects.load(id);
            self.render();
            $('#name').focus();

        });
    },
    render: function () {
        this.renderNavigation();
        this.renderContent();
    }
};

var projects = new Projects(OC.generateUrl('/apps/owncollab_timetracker/projects'));
var view = new View(projects);
projects.loadAll().done(function () {
    view.render();
}).fail(function () {
    alert('Could not load projects');
});

});

})(OC, window, jQuery);
<commit_msg>fixed delete button for projects<commit_after>var instanceUrl = OC.generateUrl('/apps/owncollab_timetracker/');

(function (OC, window, $, undefined) {
'use strict';

$(document).ready(function () {

var translations = {
    newProject: $('#new-project-string').text()
};

// this projects object holds all our projects
var Projects = function (baseUrl) {
    this._baseUrl = baseUrl;
    this._projects = [];
    this._activeProject = undefined;
};

Projects.prototype = {
    load: function (id) {
        var self = this;
        this._projects.forEach(function (project) {
            if (project.id === id) {
                project.active = true;
                self._activeProject = project;
            } else {
                project.active = false;
            }
        });

    },
    getActive: function () {
        return this._activeProject;
    },
    removeActive: function () {
        var index;
        var deferred = $.Deferred();
        var id = this._activeProject.id;
        this._projects.forEach(function (project, counter) {
            if (project.id === id) {
                index = counter;
            }
        });

        if (index !== undefined) {
            // delete cached active project if necessary
            if (this._activeProject === this._projects[index]) {
                delete this._activeProject;
            }

            this._projects.splice(index, 1);

            $.ajax({
                url: this._baseUrl + '/' + id,
                method: 'DELETE'
            }).done(function () {
                deferred.resolve();
            }).fail(function () {
                deferred.reject();
            });
        } else {
            deferred.reject();
        }
        return deferred.promise();
    },
    create: function (project) {
        var deferred = $.Deferred();
        var self = this;
        $.ajax({
            url: this._baseUrl,
            method: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(project)
        }).done(function (project) {
            self._projects.push(project);
            self._activeProject = project;
            self.load(project.id);
            deferred.resolve();
        }).fail(function () {
            deferred.reject();
        });
        return deferred.promise();
    },
    getAll: function () {
        return this._projects;
    },
    loadAll: function () {
        var deferred = $.Deferred();
        var self = this;
        $.get(this._baseUrl).done(function (projects) {
            self._activeProject = undefined;
            self._projects = projects;
            deferred.resolve();
        }).fail(function () {
            deferred.reject();
        });
        return deferred.promise();
    },
    updateActive: function (name, clientid) {
        var project = this.getActive();
        project.name = name;
        project.clientid = clientid;
        return $.ajax({
            url: this._baseUrl + '/' + project.id,
            method: 'PUT',
            contentType: 'application/json',
            data: JSON.stringify(project)
        });
    }
};

// this will be the view that is used to update the html
var View = function (projects) {
    this._projects = projects;
};

View.prototype = {
    renderContent: function () {
        var source = $('#project-content-tpl').html();
        var template = Handlebars.compile(source);
        var html = template({project: this._projects.getActive()});
        $('#project-editor').html(html);
        if (this._projects.getActive()){
		    if (this._projects.getActive().clientid) {
                	if (this._projects.getActive().clientid == 0){
                        $('#select_client').changeSelect(instanceUrl, 'clients');
                	}
		    	    else{
				        $('#select_client').changeSelectbyID(instanceUrl, 'clients', this._projects.getActive().clientid, '', '');
		    	}
		}

        }

        // handle saves
        var self = this;
        $('#project-save').click(function () {
	    if ($('#select_client').val() != ''){
	            var name = $('#name').val();
	            var clientid = $('#select_client').val();
	            self._projects.updateActive(name, clientid).done(function () {
	                self.render();
	            }).fail(function () {
	                alert('Could not update project, not found');
	            });
	    }
	    else{
		alert('Please provide client');
	    }
	 });
    },
    renderNavigation: function () {
        var source = $('#projects-tpl').html();
        var template = Handlebars.compile(source);
        var html = template({projects: this._projects.getAll()});
        $('#project-sub-navigation ul').html(html);

        // create a new project
        var self = this;
        $('#new-project').click(function () {
            var project = {
                name: translations.newProject,
                clientid: ''
            };

            self._projects.create(project).done(function() {
                self.render();
                $('#select_client').changeSelect(instanceUrl, 'clients');
                $('#name').focus();
            }).fail(function () {
                alert('Could not create project');
            });
        });

        // show app menu
        $('#project-sub-navigation .sub-navigation-entry-utils-menu-button').click(function () {
            var entry = $(this).closest('.project');
            entry.find('.sub-navigation-entry-menu').toggleClass('open');
        });

        // delete a project
        $('#project-sub-navigation .project .delete').click(function () {
            var entry = $(this).closest('.project');
            entry.find('.sub-navigation-entry-menu').removeClass('open');

            self._projects.removeActive().done(function () {
                self.render();
            }).fail(function () {
                alert('Could not delete project, not found');
            });
        });

        // load a project
        $('#project-sub-navigation .project > a').click(function () {

            var id = parseInt($(this).parent().data('id'), 10);
            self._projects.load(id);
            self.render();
            $('#name').focus();

        });
    },
    render: function () {
        this.renderNavigation();
        this.renderContent();
    }
};

var projects = new Projects(OC.generateUrl('/apps/owncollab_timetracker/projects'));
var view = new View(projects);
projects.loadAll().done(function () {
    view.render();
}).fail(function () {
    alert('Could not load projects');
});

});

})(OC, window, jQuery);
<|endoftext|>


<commit_before>/* Typewriter text animation */

document.addEventListener('DOMContentLoaded', () => {
  window.typewriter = {
    element: document.getElementById('typewriter'),
    contents: [
      'I write code.\xa0',
      'I write software.\xa0',
      'I write websites.\xa0',
      'I write apps.\xa0',
      'I write libraries.\xa0',
      'I write simulations.\xa0',
      'I design experiences.\xa0',
      'I design interfaces.\xa0',
    ],
    contentIndex: 0,

    // A random delay in ms will be chosen from this range. The resulting delay
    // is multiplied for typing, so this isn't exact.
    typingDelay: [50, 100],

    /* Find common words that two strings start with. */
    commonStart(a, b) {
      const aWords = a.split(' ');
      const bWords = b.split(' ');

      const commonWords = [];
      for (let i = 0; i < aWords.length; i++) {
        if (aWords[i] === bWords[i]) {
          commonWords.push(aWords[i]);
        } else {
          break;
        }
      }
      return commonWords.join(' ');
    },

    /* Get instructions for transforming between two strings in the form of
     * number of characters to delete and characters to add.
     */
    transitionDescription(a, b) {
      const common = this.commonStart(a, b);
      const numCharactersToDelete = a.length - common.length;
      const charactersToAdd = b.slice(common.length);
      return {
        del: numCharactersToDelete,
        add: charactersToAdd,
      };
    },

    getTypingDelay() {
      return Math.floor((Math.random() * (this.typingDelay[1] - this.typingDelay[0])) + this.typingDelay[0]);
    },

    /* Animate backspacing by a given number of characters */
    backspace(num, callback) {
      let content;
      let toGo = num;
      const callback2 = callback || (() => {});
      this.backspaceOne = () => {
        if (toGo === 0) {
          callback2();
        } else {
          content = this.element.textContent;
          this.element.textContent = content.slice(0, content.length - 1);
          toGo--;
          setTimeout(() => window.typewriter.backspaceOne(), this.getTypingDelay());
        }
      };
      this.backspaceOne();
    },

    /* Animate inserting a given string at the end of the text */
    type(text, callback) {
      let i = 0;
      const callback2 = callback || (() => {});
      this.typeOne = () => {
        if (i === text.length) {
          callback2();
        } else {
          this.element.textContent += text[i];
          i++;
          setTimeout(() => window.typewriter.typeOne(), this.getTypingDelay() * 1.5);
        }
      };
      this.typeOne();
    },

    /* Animate advancing to the next phrase */
    next(callback) {
      this.contentIndex++;
      // Loop back
      if (this.contentIndex === this.contents.length) {
        this.contentIndex = 0;
      }
      // Make changes
      const changesNeeded = this.transitionDescription(
        this.element.textContent,
        this.contents[this.contentIndex]
      );
      this.backspace(changesNeeded.del, () => {
        setTimeout(() => window.typewriter.type(changesNeeded.add, callback), 200);
      });
    },

    /* Fill the element with the first phrase (non-animated) and set up other
     * miscellaneous details.
     */
    init() {
      this.element.textContent = this.contents[0];
      this.startOnScroll = () => {
        if (
          window.scrollY + window.innerHeight > this.element.offsetTop + (this.element.offsetHeight / 2)
        ) {
          setTimeout(() => window.typewriter.play(), 1000);
          window.removeEventListener('scroll', scrollFunc);
        }
      };
      function scrollFunc() {
        window.typewriter.startOnScroll();
      }
      window.addEventListener('scroll', scrollFunc);
    },

    /* Recursively continue advancing */
    play() {
      this.next(() => {
        setTimeout(() => window.typewriter.play(), 3000);
      });
    },

  };

  window.typewriter.init();
});
<commit_msg>Shorten long lines<commit_after>/* Typewriter text animation */

document.addEventListener('DOMContentLoaded', () => {
  window.typewriter = {
    element: document.getElementById('typewriter'),
    contents: [
      'I write code.\xa0',
      'I write software.\xa0',
      'I write websites.\xa0',
      'I write apps.\xa0',
      'I write libraries.\xa0',
      'I write simulations.\xa0',
      'I design experiences.\xa0',
      'I design interfaces.\xa0',
    ],
    contentIndex: 0,

    // A random delay in ms will be chosen from this range. The resulting delay
    // is multiplied for typing, so this isn't exact.
    typingDelay: [50, 100],

    /* Find common words that two strings start with. */
    commonStart(a, b) {
      const aWords = a.split(' ');
      const bWords = b.split(' ');

      const commonWords = [];
      for (let i = 0; i < aWords.length; i++) {
        if (aWords[i] === bWords[i]) {
          commonWords.push(aWords[i]);
        } else {
          break;
        }
      }
      return commonWords.join(' ');
    },

    /* Get instructions for transforming between two strings in the form of
     * number of characters to delete and characters to add.
     */
    transitionDescription(a, b) {
      const common = this.commonStart(a, b);
      const numCharactersToDelete = a.length - common.length;
      const charactersToAdd = b.slice(common.length);
      return {
        del: numCharactersToDelete,
        add: charactersToAdd,
      };
    },

    getTypingDelay() {
      return Math.floor(
        (Math.random() * (this.typingDelay[1] - this.typingDelay[0])) + this.typingDelay[0]
      );
    },

    /* Animate backspacing by a given number of characters */
    backspace(num, callback) {
      let content;
      let toGo = num;
      const callback2 = callback || (() => {});
      this.backspaceOne = () => {
        if (toGo === 0) {
          callback2();
        } else {
          content = this.element.textContent;
          this.element.textContent = content.slice(0, content.length - 1);
          toGo--;
          setTimeout(() => window.typewriter.backspaceOne(), this.getTypingDelay());
        }
      };
      this.backspaceOne();
    },

    /* Animate inserting a given string at the end of the text */
    type(text, callback) {
      let i = 0;
      const callback2 = callback || (() => {});
      this.typeOne = () => {
        if (i === text.length) {
          callback2();
        } else {
          this.element.textContent += text[i];
          i++;
          setTimeout(() => window.typewriter.typeOne(), this.getTypingDelay() * 1.5);
        }
      };
      this.typeOne();
    },

    /* Animate advancing to the next phrase */
    next(callback) {
      this.contentIndex++;
      // Loop back
      if (this.contentIndex === this.contents.length) {
        this.contentIndex = 0;
      }
      // Make changes
      const changesNeeded = this.transitionDescription(
        this.element.textContent,
        this.contents[this.contentIndex]
      );
      this.backspace(changesNeeded.del, () => {
        setTimeout(() => window.typewriter.type(changesNeeded.add, callback), 200);
      });
    },

    /* Fill the element with the first phrase (non-animated) and set up other
     * miscellaneous details.
     */
    init() {
      this.element.textContent = this.contents[0];
      this.startOnScroll = () => {
        if (
          window.scrollY + window.innerHeight >
          this.element.offsetTop + (this.element.offsetHeight / 2)
        ) {
          setTimeout(() => window.typewriter.play(), 1000);
          window.removeEventListener('scroll', scrollFunc);
        }
      };
      function scrollFunc() {
        window.typewriter.startOnScroll();
      }
      window.addEventListener('scroll', scrollFunc);
    },

    /* Recursively continue advancing */
    play() {
      this.next(() => {
        setTimeout(() => window.typewriter.play(), 3000);
      });
    },

  };

  window.typewriter.init();
});
<|endoftext|>