<commit_before>import {inject, customElement, bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {BindingEngine} from 'aurelia-binding';
import d3 from 'd3';
import GraphModel from './graph-model';
import {GraphLibD3} from './graphlib-d3';
import {GraphFinder} from './graph-finder';
import {GraphModifier} from './graph-modifier';
import {ActionManager} from './action-manager';

@customElement('graph')
@inject(Element, EventAggregator, GraphLibD3, GraphModel, GraphFinder, GraphModifier, ActionManager, BindingEngine)
export class Graph {
  @bindable data;
  @bindable graphInteractionModel;

  constructor(element, pubSub, graphLibD3, graphModel, graphFinder, graphModifier, actionManager, bindingEngine) {
    this.element = element;
    this.pubSub = pubSub;
    this.graphLibD3 = graphLibD3;
    this.graphModel = graphModel;
    this.graphFinder = graphFinder;
    this.graphModifier = graphModifier;
    this.actionManager = actionManager;
    this.bindingEngine = bindingEngine;

    this.windowSizeAdapter();

    this.displayNodes = [];
    this.displayEdges = [];

    this.pubSub.subscribe('search.node', nodeId => {
      this.addNodeAction([nodeId], true);
    });
  }

  /**
   * TODO Need to respond to window resize so that svg can resize.
   */
  windowSizeAdapter() {
    this.presentationSVG = {
      width: window.innerWidth - 100,
      height: window.innerHeight - 112
    };
  }

  /**
   * A new global graph (i.e. project analysis) is available.
   * Create an empty display graph and initialize it with some
   * sample data from the global graph.
   */
  dataChanged(newVal) {
    let unusedTypes;

    if (newVal) {
      unusedTypes = this.graphFinder.findUnusedTypes(this.graphModel.globalGraphModel);
      this.graphModel.initRadii();
      this.displayGraph = this.graphModel.emptyGraph();
      this.addNodeAction([unusedTypes[0]], true);
    }
  }

  /**
   * Perform the add action, and register undo and redo handlers.
   */
  addNodeAction(nodeIds, withNeighbours) {
    this.addAndDisplay(nodeIds, withNeighbours);
    this.actionManager.addAction(this,
      this.removeAndDisplay, [nodeIds, withNeighbours],
      this.addAndDisplay, [nodeIds, withNeighbours]
    );
  }

  addAndDisplay(nodeIds, withNeighbours) {
    this.addToDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  removeAndDisplay(nodeIds, withNeighbours) {
    this.removeFromDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  /**
   * Modify the graph display model to include the given node id's.
   * If withNeihbours is true, then also include each node's neighbours.
   */
  addToDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.addNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.addNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Modify the graph display model to remove the given node id's.
   * If withNeihbours is true, then also remove each node's neighbours.
   */
  removeFromDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.removeNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.removeNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Use D3 force layout to calculate node positions.
   * A high negative charge value avoids node overlap.
   * Setting link distance as function of width creates
   * a visually pleasing, uncluttered effect.
   */
  updateForceLayout() {
    let d3Data = this.graphLibD3.mapToD3(this.displayGraph),
      numNodes = d3Data.nodes.length,
      currentNode;

    let force = d3.layout.force()
       .nodes(d3Data.nodes)
       .links(d3Data.links)
       .size([this.presentationSVG.width, this.presentationSVG.height])
       .gravity(0.4)
       .linkDistance(this.presentationSVG.width/4)
       .charge(-3000);

    this.computeLayout(force, numNodes);
    this.updateDisplayData(d3Data);
  }

  /**
   * Technique to minimize D3 chaotic bouncing in force layout.
   * http://stackoverflow.com/questions/13463053/calm-down-initial-tick-of-a-force-layout
   */
  computeLayout(force, numIterations) {
    let safety = 0;

    force.start();

    while(force.alpha() > 0.05) {
      force.tick();
      if(safety++ > 500) {
        break;
      }
    }

    force.stop();

    if(safety >= 500) {
      console.warn('Unable to stabilize force layout.');
    }
  }

  /**
   * Update list of display nodes and edges from d3 data.
   */
  updateDisplayData(d3Data) {
    // add nodes that are in d3Data, but not in display
    d3Data.nodes.forEach( node => {
      if (!this.graphLibD3.containsNode(this.displayNodes, node)) {
        this.displayNodes.push(node);
      }
    });

    // add links that are in d3Data, but not in display
    d3Data.links.forEach( link => {
      if (!this.graphLibD3.containsEdge(this.displayEdges, link)) {
        this.displayEdges.push(link);
      }
    });

    // remove nodes that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayNodes = this.displayNodes.length;
    for (let i=numDisplayNodes-1; i >= 0; i--) {
      if (!this.graphLibD3.containsNode(d3Data.nodes, this.displayNodes[i])) {
        this.displayNodes.splice(i, 1);
      }
    }

    // remove edges that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayEdges = this.displayEdges.length;
    for (let j=numDisplayEdges-1; j >=0; j--) {
      if (!this.graphLibD3.containsEdge(d3Data.links, this.displayEdges[j])) {
        this.displayEdges.splice(j, 1);
      }
    }

  }

  /**
   * Based on the currently selected nodes, use 'interaction' and 'type'
   * to find nodes from globalGraph that should be added, but only if
   * they're not already in the display graph.
   */
  findNodesToAdd(interaction, type) {
    let selectedNodeIds,
      relationship,
      nodesByEdgeRelationship;

    selectedNodeIds = this.graphFinder.findSelectedNodeIds(this.displayGraph);
    relationship = type === 'of it' ? 'target' : 'source';
    nodesByEdgeRelationship = this.graphFinder.findNodesByEdgeRelationship(
      this.graphModel.globalGraphModel, selectedNodeIds, interaction, relationship
    );

    return this.graphFinder.filterAlreadyInGraph(nodesByEdgeRelationship, this.displayGraph);
  }

  /**
   * Determine nodes that should be added, and add them as an undo-able action.
   */
  addNodesToDisplay(interaction, type) {
    let nodesToAdd = this.findNodesToAdd(interaction, type);

    if (nodesToAdd.length > 0) {
      this.addNodeAction(nodesToAdd);
    } else {
      // TODO notify https://github.com/CANVE/canve-viz/issues/32
      console.warn('No interaction results');
    }
  }

  /**
   * Invoked by Aurelia when 'graphInteractionModel' binding value has changed.
   * In practice this object it set once in the parent view. After that,
   * register property observers to fire when specific properties of the interaction model change,
   * indicating that the graph should respond to the change by adding more nodes.
   */
  graphInteractionModelChanged(newValue) {
    if (newValue) {
      this.graphInteractionModel = newValue;

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'callsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('uses', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'extensionsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('extends', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'ownershipSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('declares member', newValue);
      });

      // TODO dispose in appropriate lifecycle method http://stackoverflow.com/questions/30283569/array-subscription-in-aurelia
    }
  }

}
<commit_msg>more mucking around with force values<commit_after>import {inject, customElement, bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {BindingEngine} from 'aurelia-binding';
import d3 from 'd3';
import GraphModel from './graph-model';
import {GraphLibD3} from './graphlib-d3';
import {GraphFinder} from './graph-finder';
import {GraphModifier} from './graph-modifier';
import {ActionManager} from './action-manager';

@customElement('graph')
@inject(Element, EventAggregator, GraphLibD3, GraphModel, GraphFinder, GraphModifier, ActionManager, BindingEngine)
export class Graph {
  @bindable data;
  @bindable graphInteractionModel;

  constructor(element, pubSub, graphLibD3, graphModel, graphFinder, graphModifier, actionManager, bindingEngine) {
    this.element = element;
    this.pubSub = pubSub;
    this.graphLibD3 = graphLibD3;
    this.graphModel = graphModel;
    this.graphFinder = graphFinder;
    this.graphModifier = graphModifier;
    this.actionManager = actionManager;
    this.bindingEngine = bindingEngine;

    this.windowSizeAdapter();

    this.displayNodes = [];
    this.displayEdges = [];

    this.pubSub.subscribe('search.node', nodeId => {
      this.addNodeAction([nodeId], true);
    });
  }

  /**
   * TODO Need to respond to window resize so that svg can resize.
   */
  windowSizeAdapter() {
    this.presentationSVG = {
      width: window.innerWidth - 100,
      height: window.innerHeight - 112
    };
  }

  /**
   * A new global graph (i.e. project analysis) is available.
   * Create an empty display graph and initialize it with some
   * sample data from the global graph.
   */
  dataChanged(newVal) {
    let unusedTypes;

    if (newVal) {
      unusedTypes = this.graphFinder.findUnusedTypes(this.graphModel.globalGraphModel);
      this.graphModel.initRadii();
      this.displayGraph = this.graphModel.emptyGraph();
      this.addNodeAction([unusedTypes[0]], true);
    }
  }

  /**
   * Perform the add action, and register undo and redo handlers.
   */
  addNodeAction(nodeIds, withNeighbours) {
    this.addAndDisplay(nodeIds, withNeighbours);
    this.actionManager.addAction(this,
      this.removeAndDisplay, [nodeIds, withNeighbours],
      this.addAndDisplay, [nodeIds, withNeighbours]
    );
  }

  addAndDisplay(nodeIds, withNeighbours) {
    this.addToDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  removeAndDisplay(nodeIds, withNeighbours) {
    this.removeFromDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  /**
   * Modify the graph display model to include the given node id's.
   * If withNeihbours is true, then also include each node's neighbours.
   */
  addToDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.addNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.addNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Modify the graph display model to remove the given node id's.
   * If withNeihbours is true, then also remove each node's neighbours.
   */
  removeFromDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.removeNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.removeNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Use D3 force layout to calculate node positions.
   * A high negative charge value avoids node overlap.
   * Setting link distance as function of width creates
   * a visually pleasing, uncluttered effect.
   */
  updateForceLayout() {
    let d3Data = this.graphLibD3.mapToD3(this.displayGraph),
      numNodes = d3Data.nodes.length,
      currentNode;

    let force = d3.layout.force()
       .nodes(d3Data.nodes)
       .links(d3Data.links)
       .size([this.presentationSVG.width, this.presentationSVG.height])
       .gravity(0.4)
       .linkDistance(this.presentationSVG.width/6)
       .charge(-4000);

    this.computeLayout(force, numNodes);
    this.updateDisplayData(d3Data);
  }

  /**
   * Technique to minimize D3 chaotic bouncing in force layout.
   * http://stackoverflow.com/questions/13463053/calm-down-initial-tick-of-a-force-layout
   */
  computeLayout(force, numIterations) {
    let safety = 0;

    force.start();

    while(force.alpha() > 0.05) {
      force.tick();
      if(safety++ > 500) {
        break;
      }
    }

    force.stop();

    if(safety >= 500) {
      console.warn('Unable to stabilize force layout.');
    }
  }

  /**
   * Update list of display nodes and edges from d3 data.
   */
  updateDisplayData(d3Data) {
    // add nodes that are in d3Data, but not in display
    d3Data.nodes.forEach( node => {
      if (!this.graphLibD3.containsNode(this.displayNodes, node)) {
        this.displayNodes.push(node);
      }
    });

    // add links that are in d3Data, but not in display
    d3Data.links.forEach( link => {
      if (!this.graphLibD3.containsEdge(this.displayEdges, link)) {
        this.displayEdges.push(link);
      }
    });

    // remove nodes that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayNodes = this.displayNodes.length;
    for (let i=numDisplayNodes-1; i >= 0; i--) {
      if (!this.graphLibD3.containsNode(d3Data.nodes, this.displayNodes[i])) {
        this.displayNodes.splice(i, 1);
      }
    }

    // remove edges that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayEdges = this.displayEdges.length;
    for (let j=numDisplayEdges-1; j >=0; j--) {
      if (!this.graphLibD3.containsEdge(d3Data.links, this.displayEdges[j])) {
        this.displayEdges.splice(j, 1);
      }
    }

  }

  /**
   * Based on the currently selected nodes, use 'interaction' and 'type'
   * to find nodes from globalGraph that should be added, but only if
   * they're not already in the display graph.
   */
  findNodesToAdd(interaction, type) {
    let selectedNodeIds,
      relationship,
      nodesByEdgeRelationship;

    selectedNodeIds = this.graphFinder.findSelectedNodeIds(this.displayGraph);
    relationship = type === 'of it' ? 'target' : 'source';
    nodesByEdgeRelationship = this.graphFinder.findNodesByEdgeRelationship(
      this.graphModel.globalGraphModel, selectedNodeIds, interaction, relationship
    );

    return this.graphFinder.filterAlreadyInGraph(nodesByEdgeRelationship, this.displayGraph);
  }

  /**
   * Determine nodes that should be added, and add them as an undo-able action.
   */
  addNodesToDisplay(interaction, type) {
    let nodesToAdd = this.findNodesToAdd(interaction, type);

    if (nodesToAdd.length > 0) {
      this.addNodeAction(nodesToAdd);
    } else {
      // TODO notify https://github.com/CANVE/canve-viz/issues/32
      console.warn('No interaction results');
    }
  }

  /**
   * Invoked by Aurelia when 'graphInteractionModel' binding value has changed.
   * In practice this object it set once in the parent view. After that,
   * register property observers to fire when specific properties of the interaction model change,
   * indicating that the graph should respond to the change by adding more nodes.
   */
  graphInteractionModelChanged(newValue) {
    if (newValue) {
      this.graphInteractionModel = newValue;

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'callsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('uses', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'extensionsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('extends', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'ownershipSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('declares member', newValue);
      });

      // TODO dispose in appropriate lifecycle method http://stackoverflow.com/questions/30283569/array-subscription-in-aurelia
    }
  }

}
<|endoftext|>


<commit_before>const dagreD3 = require('dagre-d3');
const d3 = require('d3');
const graphCreator = require('./../src/graphCreator.js');
const graphStyleUpdater = require('./../src/graphStyleUpdater.js');
const graphProperties = require('./../src/graphProperties.js');

function drawGraph(g) {

    let svg = d3.select("svg");
    let inner = svg.select("g");

    let w = 520, h = 600;

    // Create the renderer
    let render = new dagreD3.render();

    // Run the renderer. This is what draws the final graph.
    render(inner, g);

    let colors = d3.scale.category20();

    let vis = d3.select("#chart").append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var data = d3.range(20).map(function (i) {
        return { i: i, x: Math.random() * w, y: Math.random() * h, r: Math.random() * 30 }
    });

    vis.selectAll("g.node")
        .data(data)
        .enter().append("svg:g.node")
        .attr("stroke", "black")
        .attr("fill", function (d, i) { return colors(i); })
        .attr("cx", function (d, i) { return d.x; })
        .attr("cy", function (d, i) { return d.y; })
        .attr("r", function (d, i) { return d.r; });

    $('svg g.node').tipsy({
        gravity: 'w',
        html: true,
        title: function () {
            var d = this.__data__, c = colors(d.i);
            return d;
        }
    });

    graphStyleUpdater.change_node_color_onHover(svg);
    zoom_by_scroll(svg, inner)
    zoom_In_Out(w,h);

}

function zoom_by_scroll (svg, inner) {
    let zoom = d3.behavior.zoom().on("zoom", function () {
        inner.attr("transform", "translate(" + d3.event.translate + ")" +
            "scale(" + d3.event.scale + ")");
    });
    svg.call(zoom);
}

function zoom_In_Out  (w, h) {

     var makePanZoomCTRL = function (width, height) {
        var control = {}

        var zoomMin = -10, // Levels of Zoom Out
            zoomMax = 10, // Levels of Zoom In
            zoomCur = 0; // Current Zoom

        var transform = function () {
            var x = -((width * zoomCur / 10) / 2);
            var y = -((height * zoomCur / 10) / 2);
            var s = (zoomCur / 10) + 1;
            d3.select("g")
                .attr("transform", "translate(" + x + " " + y + ") scale(" + s + ")");
        };

        control.zoom = function (btnID) {
            if (btnID === "zoomIn") {
                if (zoomCur >= zoomMax) return;
                zoomCur++;
            } else if (btnID === "zoomOut") {
                if (zoomCur <= zoomMin) return;
                zoomCur--;
            }
            transform();
        };
        return control;
    }

    //***********************************************************
    // INSTANTIATE PAN-ZOOM CONTROL (CREATE INSTANCE)
    //***********************************************************
    var panZoom = makePanZoomCTRL(w, h);

    //***********************************************************
    // SET BUTTON EVENT LISTENERS
    //***********************************************************
    d3.selectAll("#zoomIn, #zoomOut")
        .on("click", function () {
            d3.event.preventDefault();
            var id = d3.select(this).attr("id");
            panZoom.zoom(id);
        });

}

module.exports.draw = (data) => {
    graph_dot = data;
    let g = graphCreator.getGraph(graph_dot, "TB", "ellipse");
    properties(g);
    drawGraph(g);

}

function properties(g) {

    // console.log(graphProperties.getNumberOfNodes());
    document.getElementById('nodesProoerty').innerHTML = "Nodes: " + graphProperties.getNumberOfNodes(g);
    document.getElementById('edgesProperty').innerHTML = "Edges: " + graphProperties.getNumberOfEdges(g);
    document.getElementById('nameProperty').innerHTML = "Name: " + graphProperties.getName(g);

}

<commit_msg>return g in function draw and remove unnecessary code<commit_after>const dagreD3 = require('dagre-d3');
const d3 = require('d3');
const graphCreator = require('./../src/graphCreator.js');
const graphStyleUpdater = require('./../src/graphStyleUpdater.js');
const graphProperties = require('./../src/graphProperties.js');

module.exports.drawGraph = (g) => {

    let svg = d3.select("svg");
    let inner = svg.select("g");

    let w = 520, h = 600;

    // Create the renderer
    let render = new dagreD3.render();

    // Run the renderer. This is what draws the final graph.
    render(inner, g);

    let colors = d3.scale.category20();

    let vis = d3.select("#chart").append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var data = d3.range(20).map(function (i) {
        return { i: i, x: Math.random() * w, y: Math.random() * h, r: Math.random() * 30 }
    });

    vis.selectAll("g.node")
        .data(data)
        .enter().append("svg:g.node")
        .attr("stroke", "black")
        .attr("fill", function (d, i) { return colors(i); })
        .attr("cx", function (d, i) { return d.x; })
        .attr("cy", function (d, i) { return d.y; })
        .attr("r", function (d, i) { return d.r; });

    $('svg g.node').tipsy({
        gravity: 'w',
        html: true,
        title: function () {
            var d = this.__data__, c = colors(d.i);
            return d;
        }
    });

    graphStyleUpdater.change_node_color_onHover(svg);
    zoom_by_scroll(svg, inner)
    zoom_In_Out(w, h);

}

function zoom_by_scroll(svg, inner) {
    let zoom = d3.behavior.zoom().on("zoom", function () {
        inner.attr("transform", "translate(" + d3.event.translate + ")" +
            "scale(" + d3.event.scale + ")");
    });
    svg.call(zoom);
}

function zoom_In_Out(w, h) {

    var makePanZoomCTRL = function (width, height) {
        var control = {}

        var zoomMin = -10, // Levels of Zoom Out
            zoomMax = 10, // Levels of Zoom In
            zoomCur = 0; // Current Zoom

        var transform = function () {
            var x = -((width * zoomCur / 10) / 2);
            var y = -((height * zoomCur / 10) / 2);
            var s = (zoomCur / 10) + 1;
            d3.select("g")
                .attr("transform", "translate(" + x + " " + y + ") scale(" + s + ")");
        };

        control.zoom = function (btnID) {
            if (btnID === "zoomIn") {
                if (zoomCur >= zoomMax) return;
                zoomCur++;
            } else if (btnID === "zoomOut") {
                if (zoomCur <= zoomMin) return;
                zoomCur--;
            }
            transform();
        };
        return control;
    }

    //***********************************************************
    // INSTANTIATE PAN-ZOOM CONTROL (CREATE INSTANCE)
    //***********************************************************
    var panZoom = makePanZoomCTRL(w, h);

    //***********************************************************
    // SET BUTTON EVENT LISTENERS
    //***********************************************************
    d3.selectAll("#zoomIn, #zoomOut")
        .on("click", function () {
            d3.event.preventDefault();
            var id = d3.select(this).attr("id");
            panZoom.zoom(id);
        });

}

module.exports.draw = (data) => {
    graph_dot = data;
    let g = graphCreator.getGraph(graph_dot, "TB", "ellipse");
    properties(g);
    this.drawGraph(g);
    return g ;
}

function properties(g) {

    document.getElementById('nodesProoerty').innerHTML = "Nodes: " + graphProperties.getNumberOfNodes(g);
    document.getElementById('edgesProperty').innerHTML = "Edges: " + graphProperties.getNumberOfEdges(g);
    document.getElementById('nameProperty').innerHTML = "Name: " + graphProperties.getName(g);

}

<|endoftext|>


<commit_before>var rows = 10;
var columns = 14;

// Generate the color palette
var colors = new Array(10);
//colors.push("#F8F8FF");
//colors.push("#F5F5F5");
var baseR = parseInt(Math.floor(Math.random() * 256));
var baseG = parseInt(Math.floor(Math.random() * 256));
var baseB = parseInt(Math.floor(Math.random() * 256));
for (var i = 0; i < 10; i++) {
    var baseR = (baseR + (Math.floor(Math.random() * 72))) % 256;
    var baseG = (baseG + (Math.floor(Math.random() * 72))) % 256;
    var baseB = (baseB + (Math.floor(Math.random() * 72))) % 256;
    colors.push("#" + baseR.toString(16) + baseG.toString(16) + baseB.toString(16));
}

var animationSpeed = 0.35;

var previousTimeStamp = 0;

var animations = new Array();

var pads = new Array(rows);
for (var i = 0; i < rows; i++) {
    pads[i] = new Array(columns);
}

function getRandomColor() {
    return colors[Math.floor(Math.random() * colors.length)];
}

function Animation(x, y, newColor, onDone) {
    this.x = x;
    this.y = y;
    this.newColor = newColor;
    this.onDone = onDone;
    this.progress = 0;
}

function addAnimation() {
    var anim = new Animation(Math.floor(Math.random() * rows),
                             Math.floor(Math.random() * columns),
                             getRandomColor(),
                             function() {
                                 setTimeout(function() { addAnimation(); }, 2000);
                             });
    animations.push(anim);
}

function updateAnimations(timestamp) {
    var delta = timestamp - previousTimeStamp;
    var deltaAnimation = delta * animationSpeed;

    for (var i = 0; i < animations.length; i++) {
        var animation = animations[i];

        if (animation.progress >= 180) {
            // Animation done, reset the rotation
            pads[animation.x][animation.y].style.transform = "rotateY(0deg)";

            // Remove the animation from the list
            animations.splice(i, 1);
            animation.onDone();
        } else if (Math.round(animation.progress) == 90) {
            pads[animation.x][animation.y].style.backgroundColor = animation.newColor;
        } else {
            pads[animation.x][animation.y].style.transform = "rotateY(" + animation.progress + "deg)";
        }

        animation.progress += deltaAnimation;
    }

    previousTimeStamp = timestamp;
    requestAnimationFrame(updateAnimations);
}

function clickHandler(event) {
    var i = event.currentTarget.getAttribute("row");
    var j = event.currentTarget.getAttribute("col");

    animations.push(new Animation(i, j, getRandomColor(), function() {}));
}

var background = document.getElementById("background");
for (var i = 0; i < rows; i++) {
    // Create a row element to contain the cells/pads in that row
    var row = document.createElement("div");
    row.style.width = "100%";
    row.style.height = (100 / rows) + "%";
    row.style.display = "table-row";
    for (var j = 0; j < columns; j++) {
        // Create the table cells (the pads)
        var childNode = document.createElement("div");
        childNode.className = "pad";
        childNode.style.width = (100 / columns) + "%";
        childNode.style.height = (100 / rows) + "%";
        childNode.style.backgroundColor = getRandomColor();
        childNode.style.display = "table-cell";

        childNode.addEventListener("click", clickHandler);
        childNode.setAttribute("row", i);
        childNode.setAttribute("col", j);

        pads[i][j] = childNode;
        row.appendChild(childNode);
    }
    background.appendChild(row);
}

addAnimation();
setTimeout(function() {
    updateAnimations(previousTimeStamp);
}, 2000);<commit_msg>Added procedural color generation<commit_after>// Dimensions of the pad grid
var rows = 10;
var columns = 14;

// Taken from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
function toRGB(h, s, l){
    var r, g, b;
    if (s == 0) {
        r = g = b = l; // achromatic
    } else {
        var hue2rgb = function hue2rgb(p, q, t) {
            if (t < 0) t += 1;
            if (t > 1) t -= 1;
            if (t < 1/6) return p + (q - p) * 6 * t;
            if (t < 1/2) return q;
            if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    return "rgb(" + Math.round(r * 255) + ", " + Math.round(g * 255) + ", " + Math.round(b * 255) + ")";
}

// Generate the color palette
var colors = new Array();
var saturation = 0.25 + Math.random() * 0.5;
var luminance = 0.25 + Math.random() * 0.5;

var baseOffset = Math.random() * 360;
var offset1 = -15;
var offset2 = 120;

var range1 = 30;
var range2 = 30;
var range3 = 60;
for (var i = 0; i < 10; i++) {
    var randAngle = Math.random() * (range1 + range2 + range3);
    if (randAngle > range1) { // It is not in the first range
        if (randAngle < (range1 + range2)) { // It is in the second range
            randAngle += offset1;
        } else { // It is in the third range
            randAngle += offset2;
        }
    }

    var hue = ((baseOffset + randAngle) / 360) % 1.0;
    colors.push(toRGB(hue, saturation, luminance));
}
console.log(colors);

// Speed of the animation
var animationSpeed = 0.35;

// Previous time stamp for delta calculation
var previousTimeStamp = 0;

// The animations to render at this time
var animations = new Array();

var pads = new Array(rows);
for (var i = 0; i < rows; i++) {
    pads[i] = new Array(columns);
}

function getRandomColor() {
    return colors[Math.floor(Math.random() * colors.length)];
}

function getRandomColorNot(color) {
    var index = colors.indexOf(color);
    if (index != -1) {
        var rand = Math.floor(Math.random() * (colors.length - 1));
        if (rand >= index) {
            // Skip the index
            ++rand;
        }
        return colors[rand];
    }
}

function Animation(x, y, newColor, onDone) {
    this.x = x;
    this.y = y;
    this.flipped = false;
    this.newColor = newColor;
    this.onDone = onDone;
    this.progress = 0;
}

function addAnimation() {
    var anim = new Animation(Math.floor(Math.random() * rows),
                             Math.floor(Math.random() * columns),
                             getRandomColor(),
                             function() {
                                 setTimeout(function() { addAnimation(); }, 2000);
                             });
    animations.push(anim);
}

function updateAnimations(timestamp) {
    var delta = timestamp - previousTimeStamp;
    var deltaAnimation = delta * animationSpeed;

    for (var i = 0; i < animations.length; i++) {
        var animation = animations[i];

        if (animation.progress >= 180) {
            // Animation done, reset the rotation
            pads[animation.x][animation.y].style.transform = "rotateY(0deg)";

            // Remove the animation from the list
            animations.splice(i, 1);
            animation.onDone();
        } else if (!animation.flipped && animation.progress >= 90) {
            pads[animation.x][animation.y].style.backgroundColor = animation.newColor;
            animation.flipped = true;
        } else {
            pads[animation.x][animation.y].style.transform = "rotateY(" + animation.progress + "deg)";
        }
        animation.progress += deltaAnimation;
    }

    previousTimeStamp = timestamp;
    requestAnimationFrame(updateAnimations);
}

function clickHandler(event) {
    var i = event.currentTarget.getAttribute("row");
    var j = event.currentTarget.getAttribute("col");
    var previousColor = event.currentTarget.style.backgroundColor;

    animations.push(new Animation(i, j, getRandomColorNot(previousColor), function() {}));
}

var background = document.getElementById("background");
for (var i = 0; i < rows; i++) {
    // Create a row element to contain the cells/pads in that row
    var row = document.createElement("div");
    row.style.width = "100%";
    row.style.height = (100 / rows) + "%";
    row.style.display = "table-row";
    for (var j = 0; j < columns; j++) {
        // Create the table cells (the pads)
        var childNode = document.createElement("div");
        childNode.className = "pad";
        childNode.style.width = (100 / columns) + "%";
        childNode.style.height = (100 / rows) + "%";
        childNode.style.backgroundColor = getRandomColor();
        childNode.style.display = "table-cell";

        childNode.addEventListener("click", clickHandler);
        childNode.setAttribute("row", i);
        childNode.setAttribute("col", j);

        pads[i][j] = childNode;
        row.appendChild(childNode);
    }
    background.appendChild(row);
}

addAnimation();
setTimeout(function() {
    updateAnimations(previousTimeStamp);
}, 2000);<|endoftext|>


<commit_before>// based on Electron's getting started guide

const { app, dialog, shell, globalShortcut, ipcMain, BrowserWindow, Menu, Tray } = require("electron")
const path = require("path")
const url = require("url")
const autoUpdater = require("electron-updater").autoUpdater
const fs = require("fs")
const i18n = require("./lib/i18n.js")
const request = require("request")

let win, tray // keep globals to avoid garbage collection

const package = require("./package.json");

const configDirName = "cumulonimbus"
const configDirPath = path.join( path.parse(app.getPath("userData")).dir, configDirName )
try {
  fs.mkdirSync(configDirPath)
} catch (exp) {
  console.log("Looks like config dir already exists, continuing")
}
app.setPath("userData", configDirPath)

const APP_NAME = "CPod" // only used in this file

const USERDATA_DIR = app.getPath("userData")
const WINDOW_SIZE_FILE = path.join(USERDATA_DIR, "window_size")
const SETTINGS_FILE = path.join(USERDATA_DIR, "user_settings.json")
const ICON_WIN = path.join(__dirname, "build/icon.ico")
const ICON_OTHER = path.join(__dirname, "build/icon.png")
const USER_AGENT = `CPod/${package.version} (github.com/z-------------)`;

let settings = {}
let size = {}

var beforeQuit = false

/* get settings and watch for changes */

console.log(`Using settings file: ${SETTINGS_FILE}`)

function updateSettings(json) {
  for (let key in json) {
    if (settings[key] != json[key]) console.log(`Updating setting in main: ${key}: ${settings[key]} --> ${json[key]}`);
    settings[key] = json[key]
  }
}

try {
  updateSettings(JSON.parse(fs.readFileSync(SETTINGS_FILE)));
} catch (exp) {
  // do nothing.
}

ipcMain.on("settingChanged", (e, message) => {
  let obj = {};
  obj[message.key] = message.value;
  updateSettings(obj);
});

/* create and handle window */

let windowOptions = {
  title: APP_NAME
}

function mergeObjects() {
  let finalObj = {}
  for (let obj of arguments) {
    for (let key in obj) {
      finalObj[key] = obj[key]
    }
  }
  return finalObj
}

function getIconPath() {
  return process.platform === "win32" ? ICON_WIN : ICON_OTHER;
}

function writeWindowSize(win, cb) {
  let dimens = win.getSize()

  size.width = dimens[0]
  size.height = dimens[1]
  size.maximized = win.isMaximized()

  fs.writeFile(WINDOW_SIZE_FILE, JSON.stringify(size) + "\n", {
    encoding: "utf8"
  }, (err) => {
    cb(err)
  })
}

function sizeWindow() {
  win.setSize(size.width, size.height, false)
  if (size.maximized) win.maximize()
}

function showWindow() {
  if (win.isMinimized()) win.restore()
  win.show()
  sizeWindow()
  win.focus()
}

function createWindow(width, height, maximize) {
  windowOptions.icon = getIconPath()

  if (maximize) {
    win = new BrowserWindow(windowOptions)
    win.maximize()
  } else if (width && height) {
    win = new BrowserWindow(mergeObjects(windowOptions, {
      width: width,
      height: height
    }))
  }

  win.webContents.setUserAgent(USER_AGENT);
  win.loadURL(url.format({
    pathname: path.join(__dirname, "public/app/index.html"),
    protocol: "file:",
    slashes: true
  }))

  /* taskbar item */
  tray = new Tray(getIconPath())
  let menuTemplate = [{
    id: "now_playing",
    enabled: false,
    visible: false
  }, {
    label: i18n.__("button_playback_playpause"),
    click: function() {
      win.webContents.send("playbackControl", "playpause")
    }
  }, {
    label: i18n.__("button_playback-next"),
    click: function() {
      win.webContents.send("playbackControl", "next")
    }
  }, {
    type: "separator"
  }, {
    id: "show_window",
    label: i18n.__("label_show_window"),
    visible: false,
    click: showWindow
  }, {
    label: i18n.__("label_quit"),
    click: app.quit
  }]
  let menu = Menu.buildFromTemplate(menuTemplate)
  if (process.platform === "linux") {
    menu.getMenuItemById("show_window").visible = true
  }
  tray.setToolTip(APP_NAME)
  tray.setContextMenu(menu)
  tray.on("click", showWindow)
  ipcMain.on("nowPlayingInfo", (e, arg) => {
    tray.setToolTip(
`${APP_NAME} - ${i18n.__("label_now_playing")}
${i18n.__("punc_quote_open")}${arg.episodeTitle}${i18n.__("punc_quote_close")}
${arg.podcastTitle}`)
    menuTemplate[0].label = `${i18n.__("label_now_playing")}${i18n.__("punc_quote_open")}${arg.episodeTitle}${i18n.__("punc_quote_close")} - ${arg.podcastTitle}`
    menuTemplate[0].visible = true
    menu = Menu.buildFromTemplate(menuTemplate)
    tray.setContextMenu(menu)
  })

  win.on("close", e => {
    if (settings.taskbarClose && !beforeQuit) {
      e.preventDefault()
      win.hide()
    }

    writeWindowSize(win, err => {
      if (err) {
        console.log("Error writing to window size file")
      }
      if (!settings.taskbarClose) {
        win = null
      }
    })
  })

  win.on("minimize", e => {
    if (settings.taskbarMinimize && !beforeQuit) {
      e.preventDefault()
      win.hide()
    }

    writeWindowSize(win, err => {
      if (err) {
        console.log("Error writing to window size file")
      }
    })
  })
}

const isSecondInstance = app.makeSingleInstance((argv, wd) => {
  if (win) showWindow()
})
if (isSecondInstance) app.quit();

app.on("ready", function() {
  fs.readFile(WINDOW_SIZE_FILE, {
    encoding: "utf8"
  }, (err, data) => {
    if (err) {
      console.log("Error reading window size file")
      createWindow(null, null, true)
    } else {
      try {
        data = JSON.parse(data)
        size.width = data.width
        size.height = data.height
        size.maximized = data.maximized
        createWindow(size.width, size.height, size.maximized)
      } catch (e) {
        console.log("Invalid window size file")
        createWindow(null, null, true)
      }
    }
  })

  for (let accelerator of ["mediaplaypause", "medianexttrack", "mediaprevioustrack"]) {
    globalShortcut.register(accelerator, function() {
      win.webContents.send("globalShortcut", accelerator)
    })
  }
})

app.on("before-quit", () => {
  beforeQuit = true
  tray.destroy()
})

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit()
  }
})

app.on("activate", () => {
  if (win === null) {
    createWindow()
  }
})

/* auto-update */

if (settings.autoUpdaterAllowPrerelease) {
  autoUpdater.allowPrerelease = true
}

// autoUpdater.fullChangelog = true
autoUpdater.autoDownload = false

var currentVersion

autoUpdater.checkForUpdates()
.then((updateCheckResult) => {
  let info = updateCheckResult.updateInfo
  console.log(info, info.releaseNotes)

  var isNewer = false;
  currentVersion = package.version
  let cvs = currentVersion.split(".").map(s => Number(s))
  let uvs = info.version.split(".").map(s => Number(s))
  if (
    uvs[0] > cvs[0] ||
    uvs[0] === cvs[0] && uvs[1] > cvs[1] ||
    uvs[0] === cvs[0] && uvs[1] === cvs[1] && uvs[2] > cvs[2]
  ) {
    isNewer = true;
  }

  if (isNewer) {
    fs.readFile(path.join(app.getPath("userData"), "skip_version"), {
      encoding: "utf8"
    }, (err, data) => {
      if (err) {
        console.log("Error reading skip_version file")
        data = "";
      }
      if (info.version !== data.trim()) {
        request({
          url: "https://api.github.com/repos/z-------------/cumulonimbus/releases/latest",
          headers: { "User-Agent": USER_AGENT }
        }, (err, response, body) => {
          try {
            var isPrerelease = true;
            if (JSON.parse(body).tag_name.substring(1) === info.version) {
              isPrerelease = false;
            }
            let messageBoxOptions = {
              type: "question",
              buttons: [
                i18n.__("dialog_update-available_button_download"),
                i18n.__("dialog_update-available_button_skip")
              ],
              defaultId: 0,
              cancelId: -1,
              title: i18n.__("dialog_update-available_title"),
              message: i18n.__(
                "dialog_update-available_body-" + (isPrerelease ? "prerelease" : "stable"),
                info.version, currentVersion
              )
            }

            dialog.showMessageBox(win, messageBoxOptions, (responseIndex) => {
              if (responseIndex === messageBoxOptions.defaultId) {
                autoUpdater.downloadUpdate(updateCheckResult.cancellationToken)
              } else if (responseIndex !== -1) {
                fs.writeFile(
                  path.join(app.getPath("userData"), "skip_version"),
                  info.version.toString(),
                  (err) => {
                    if (err) {
                      console.log("Error writing to skip_version file")
                    } else {
                      console.log("Wrote to skip_version file")
                    }
                  }
                )
              }
            })
          } catch (e) {
            console.log("Exception in update check", e)
          }
        })
      } else {
        console.log("Version " + info.version + " is skipped")
      }
    })
  } else {
    console.log("Update is not newer than current version, ignoring")
  }
})
.catch(e => {
  console.error("Auto-update error:")
  console.error(e)
})

autoUpdater.on("update-downloaded", (info) => {
  console.log(info)

  let messageBoxOptions = {
    type: "question",
    buttons: [
      i18n.__("dialog_update-downloaded_button_install"),
      i18n.__("dialog_update-downloaded_button_cancel")
    ],
    defaultId: 0,
    cancelId: 1,
    title: i18n.__("dialog_update-downloaded_title"),
    message: i18n.__("dialog_update-downloaded_body", info.version, currentVersion)
  }

  dialog.showMessageBox(win, messageBoxOptions, (responseIndex) => {
    if (responseIndex === messageBoxOptions.defaultId) {
      autoUpdater.quitAndInstall()
    }
  })
})

/* chromium flags */

if (!settings.smoothScrolling) {
  app.commandLine.appendSwitch("disable-smooth-scrolling")
}
<commit_msg>Fix tray icon on MacOS<commit_after>// based on Electron's getting started guide

const { app, dialog, shell, globalShortcut, ipcMain, BrowserWindow, Menu, Tray } = require("electron")
const path = require("path")
const url = require("url")
const autoUpdater = require("electron-updater").autoUpdater
const fs = require("fs")
const i18n = require("./lib/i18n.js")
const request = require("request")

let win, tray // keep globals to avoid garbage collection

const package = require("./package.json");

const configDirName = "cumulonimbus"
const configDirPath = path.join( path.parse(app.getPath("userData")).dir, configDirName )
try {
  fs.mkdirSync(configDirPath)
} catch (exp) {
  console.log("Looks like config dir already exists, continuing")
}
app.setPath("userData", configDirPath)

const APP_NAME = "CPod" // only used in this file

const USERDATA_DIR = app.getPath("userData")
const WINDOW_SIZE_FILE = path.join(USERDATA_DIR, "window_size")
const SETTINGS_FILE = path.join(USERDATA_DIR, "user_settings.json")
const ICON_WIN = path.join(__dirname, "build/icon.ico")
const ICON_MAC = path.join(__dirname, "build/icons/16x16.png")
const ICON_OTHER = path.join(__dirname, "build/icon.png")
const USER_AGENT = `CPod/${package.version} (github.com/z-------------)`;

let settings = {}
let size = {}

var beforeQuit = false

/* get settings and watch for changes */

console.log(`Using settings file: ${SETTINGS_FILE}`)

function updateSettings(json) {
  for (let key in json) {
    if (settings[key] != json[key]) console.log(`Updating setting in main: ${key}: ${settings[key]} --> ${json[key]}`);
    settings[key] = json[key]
  }
}

try {
  updateSettings(JSON.parse(fs.readFileSync(SETTINGS_FILE)));
} catch (exp) {
  // do nothing.
}

ipcMain.on("settingChanged", (e, message) => {
  let obj = {};
  obj[message.key] = message.value;
  updateSettings(obj);
});

/* create and handle window */

let windowOptions = {
  title: APP_NAME
}

function mergeObjects() {
  let finalObj = {}
  for (let obj of arguments) {
    for (let key in obj) {
      finalObj[key] = obj[key]
    }
  }
  return finalObj
}

function getIconPath() {
  if (process.platform === "win32") {
    return ICON_WIN;
  } else if (process.platform === "darwin") {
    return ICON_MAC;
  } else {
    return ICON_OTHER;
  }
}

function writeWindowSize(win, cb) {
  let dimens = win.getSize()

  size.width = dimens[0]
  size.height = dimens[1]
  size.maximized = win.isMaximized()

  fs.writeFile(WINDOW_SIZE_FILE, JSON.stringify(size) + "\n", {
    encoding: "utf8"
  }, (err) => {
    cb(err)
  })
}

function sizeWindow() {
  win.setSize(size.width, size.height, false)
  if (size.maximized) win.maximize()
}

function showWindow() {
  if (win.isMinimized()) win.restore()
  win.show()
  sizeWindow()
  win.focus()
}

function createWindow(width, height, maximize) {
  windowOptions.icon = getIconPath()

  if (maximize) {
    win = new BrowserWindow(windowOptions)
    win.maximize()
  } else if (width && height) {
    win = new BrowserWindow(mergeObjects(windowOptions, {
      width: width,
      height: height
    }))
  }

  win.webContents.setUserAgent(USER_AGENT);
  win.loadURL(url.format({
    pathname: path.join(__dirname, "public/app/index.html"),
    protocol: "file:",
    slashes: true
  }))

  /* taskbar item */
  tray = new Tray(getIconPath())
  let menuTemplate = [{
    id: "now_playing",
    enabled: false,
    visible: false
  }, {
    label: i18n.__("button_playback_playpause"),
    click: function() {
      win.webContents.send("playbackControl", "playpause")
    }
  }, {
    label: i18n.__("button_playback-next"),
    click: function() {
      win.webContents.send("playbackControl", "next")
    }
  }, {
    type: "separator"
  }, {
    id: "show_window",
    label: i18n.__("label_show_window"),
    visible: false,
    click: showWindow
  }, {
    label: i18n.__("label_quit"),
    click: app.quit
  }]
  let menu = Menu.buildFromTemplate(menuTemplate)
  if (process.platform === "linux") {
    menu.getMenuItemById("show_window").visible = true
  }
  tray.setToolTip(APP_NAME)
  tray.setContextMenu(menu)
  tray.on("click", showWindow)
  ipcMain.on("nowPlayingInfo", (e, arg) => {
    tray.setToolTip(
`${APP_NAME} - ${i18n.__("label_now_playing")}
${i18n.__("punc_quote_open")}${arg.episodeTitle}${i18n.__("punc_quote_close")}
${arg.podcastTitle}`)
    menuTemplate[0].label = `${i18n.__("label_now_playing")}${i18n.__("punc_quote_open")}${arg.episodeTitle}${i18n.__("punc_quote_close")} - ${arg.podcastTitle}`
    menuTemplate[0].visible = true
    menu = Menu.buildFromTemplate(menuTemplate)
    tray.setContextMenu(menu)
  })

  win.on("close", e => {
    if (settings.taskbarClose && !beforeQuit) {
      e.preventDefault()
      win.hide()
    }

    writeWindowSize(win, err => {
      if (err) {
        console.log("Error writing to window size file")
      }
      if (!settings.taskbarClose) {
        win = null
      }
    })
  })

  win.on("minimize", e => {
    if (settings.taskbarMinimize && !beforeQuit) {
      e.preventDefault()
      win.hide()
    }

    writeWindowSize(win, err => {
      if (err) {
        console.log("Error writing to window size file")
      }
    })
  })
}

const isSecondInstance = app.makeSingleInstance((argv, wd) => {
  if (win) showWindow()
})
if (isSecondInstance) app.quit();

app.on("ready", function() {
  fs.readFile(WINDOW_SIZE_FILE, {
    encoding: "utf8"
  }, (err, data) => {
    if (err) {
      console.log("Error reading window size file")
      createWindow(null, null, true)
    } else {
      try {
        data = JSON.parse(data)
        size.width = data.width
        size.height = data.height
        size.maximized = data.maximized
        createWindow(size.width, size.height, size.maximized)
      } catch (e) {
        console.log("Invalid window size file")
        createWindow(null, null, true)
      }
    }
  })

  for (let accelerator of ["mediaplaypause", "medianexttrack", "mediaprevioustrack"]) {
    globalShortcut.register(accelerator, function() {
      win.webContents.send("globalShortcut", accelerator)
    })
  }
})

app.on("before-quit", () => {
  beforeQuit = true
  tray.destroy()
})

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit()
  }
})

app.on("activate", () => {
  if (win === null) {
    createWindow()
  }
})

/* auto-update */

if (settings.autoUpdaterAllowPrerelease) {
  autoUpdater.allowPrerelease = true
}

// autoUpdater.fullChangelog = true
autoUpdater.autoDownload = false

var currentVersion

autoUpdater.checkForUpdates()
.then((updateCheckResult) => {
  let info = updateCheckResult.updateInfo
  console.log(info, info.releaseNotes)

  var isNewer = false;
  currentVersion = package.version
  let cvs = currentVersion.split(".").map(s => Number(s))
  let uvs = info.version.split(".").map(s => Number(s))
  if (
    uvs[0] > cvs[0] ||
    uvs[0] === cvs[0] && uvs[1] > cvs[1] ||
    uvs[0] === cvs[0] && uvs[1] === cvs[1] && uvs[2] > cvs[2]
  ) {
    isNewer = true;
  }

  if (isNewer) {
    fs.readFile(path.join(app.getPath("userData"), "skip_version"), {
      encoding: "utf8"
    }, (err, data) => {
      if (err) {
        console.log("Error reading skip_version file")
        data = "";
      }
      if (info.version !== data.trim()) {
        request({
          url: "https://api.github.com/repos/z-------------/cumulonimbus/releases/latest",
          headers: { "User-Agent": USER_AGENT }
        }, (err, response, body) => {
          try {
            var isPrerelease = true;
            if (JSON.parse(body).tag_name.substring(1) === info.version) {
              isPrerelease = false;
            }
            let messageBoxOptions = {
              type: "question",
              buttons: [
                i18n.__("dialog_update-available_button_download"),
                i18n.__("dialog_update-available_button_skip")
              ],
              defaultId: 0,
              cancelId: -1,
              title: i18n.__("dialog_update-available_title"),
              message: i18n.__(
                "dialog_update-available_body-" + (isPrerelease ? "prerelease" : "stable"),
                info.version, currentVersion
              )
            }

            dialog.showMessageBox(win, messageBoxOptions, (responseIndex) => {
              if (responseIndex === messageBoxOptions.defaultId) {
                autoUpdater.downloadUpdate(updateCheckResult.cancellationToken)
              } else if (responseIndex !== -1) {
                fs.writeFile(
                  path.join(app.getPath("userData"), "skip_version"),
                  info.version.toString(),
                  (err) => {
                    if (err) {
                      console.log("Error writing to skip_version file")
                    } else {
                      console.log("Wrote to skip_version file")
                    }
                  }
                )
              }
            })
          } catch (e) {
            console.log("Exception in update check", e)
          }
        })
      } else {
        console.log("Version " + info.version + " is skipped")
      }
    })
  } else {
    console.log("Update is not newer than current version, ignoring")
  }
})
.catch(e => {
  console.error("Auto-update error:")
  console.error(e)
})

autoUpdater.on("update-downloaded", (info) => {
  console.log(info)

  let messageBoxOptions = {
    type: "question",
    buttons: [
      i18n.__("dialog_update-downloaded_button_install"),
      i18n.__("dialog_update-downloaded_button_cancel")
    ],
    defaultId: 0,
    cancelId: 1,
    title: i18n.__("dialog_update-downloaded_title"),
    message: i18n.__("dialog_update-downloaded_body", info.version, currentVersion)
  }

  dialog.showMessageBox(win, messageBoxOptions, (responseIndex) => {
    if (responseIndex === messageBoxOptions.defaultId) {
      autoUpdater.quitAndInstall()
    }
  })
})

/* chromium flags */

if (!settings.smoothScrolling) {
  app.commandLine.appendSwitch("disable-smooth-scrolling")
}
<|endoftext|>


<commit_before>//Audiodevice by http://whoshacks.blogspot.com/2009/01/change-audio-devices-via-shell-script.html

//Shell and filesystem dependencies
require('shelljs/global');
var path = require('path');
var _ = require('lodash');

//Electron dependencies
const {app, dialog, Menu, MenuItem} = require('electron')
const storage = require('electron-json-storage');

var menubar = require('menubar');
var mb = menubar({dir: __dirname, icon: 'not-castingTemplate.png'});

/* Internals */
var MenuFactory = require('./lib/native/MenuFactory');
var NotificationService = require('./lib/native/NotificationService');

var DeviceLookupService = require('./lib/device/utils/DeviceLookupService');
var DeviceMatcher = require('./lib/device/utils/DeviceMatcher');
var LocalSourceSwitcher = require('./lib/device/utils/LocalSourceSwitcher');
var UpnpMediaClientUtils = require('./lib/device/utils/UpnpMediaClientUtils');

var LocalSoundStreamer = require('./lib/sound/LocalSoundStreamerWebcast');

/* Various Device controllers */
var RaumfeldZone = require('./lib/device/controls/RaumfeldZone');
var ChromeCast = require('./lib/device/controls/ChromeCast');
var logger = require('./lib/common/logger');
const osxsleep = require ('osxsleep');

var currentDevice = null;
var menu = null;
var deviceListMenu = null;
var devicesAdded = [];
var streamingAddress;
var reconnect = false;
var reconnectName = null;
var switchingDevice = false;
var isStreaming = false;

var stopCurrentDevice = function () {

	if (currentDevice) {

	    var device = currentDevice;

	    device.controls.stop();

	    NotificationService.notifyCastingStopped(device.controls);

	    // Clean up playing speaker icon
	    deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

	    // Switch tray icon
	    mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));
	}
    };

var onStartStream = function() {

    if (isStreaming) {
	return;
    }

    isStreaming = true;

    LocalSoundStreamer.startStream(function (streamUrl) {
        streamingAddress = streamUrl;
    }, function(err){
    }, function() {
	onStop();
    });
};

var onStop = function() {

    if (switchingDevice) {
       return;
    }

    isStreaming = false;

    if (currentDevice) {

	    NotificationService.notifyCastingStopped(currentDevice.controls);

	    // Clean up playing speaker icon
	    deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

	    // Switch tray icon
	    mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));

	    LocalSourceSwitcher.resetOriginSource();
    }
};

var setSpeakIcon = function (item) {

	logger.info("label: %s  name: %s", item.label, this.device.name);

        if (item.label === this.device.name) {
            MenuFactory.setSpeaker(item);
        } else {
            MenuFactory.removeSpeaker(item);
        }
};

var onStreamingUpdateUI = function () {

	switchingDevice = false;

        // set speak icon when playing
        deviceListMenu.items.forEach(setSpeakIcon.bind({device: this.device}));
    	mb.tray.setContextMenu(menu);

        // Changes tray icon to "Casting"
        mb.tray.setImage(path.join(__dirname, 'castingTemplate.png'));
    };

var scanForDevices = function(self) {

    DeviceLookupService.lookUpDevices(function onDevice(device) {

	var found = false;

        switch (device.type) {
            case DeviceMatcher.TYPES.CHROMECAST:
		device.name = device.name + " (c)";
		break;
	    case DeviceMatcher.TYPES.UPNP:
		device.name = device.name + " (u)";
		break;
		default:
			break;
	}

	for (var n = 0; n < devicesAdded.length; n++) {
		if(device.name.localeCompare(devicesAdded[n].name) == 0) {
			devicesAdded[n] = device;
			found = true;
			break;
		}
	}

	if (!found) {
	   devicesAdded.push(device);
	} else {
	   return;
        }

        switch (device.type) {
            case DeviceMatcher.TYPES.CHROMECAST:

		device.controls = new ChromeCast(device);

		var doConnectCast = function onClicked() {

		    onStartStream();

		    switchingDevice = true;

		    logger.info('Attempting to play to Chromecast', device.name);

		    stopCurrentDevice();

		    // Sets OSX selected input and output audio devices to Soundflower
		    LocalSourceSwitcher.switchSource({
			output: 'Soundflower (2ch)',
			input: 'Soundflower (2ch)'
		    });

		    storage.set('reconnect', {
				    setting: reconnect,
				    name: device.name
			    }, function(error){
				    if (error == null) {
					    return;
				    }
				    logger.info("error while storing setting: %s", error.toString());
			    });

		    currentDevice = device;

		    device.controls.play(streamingAddress, onStreamingUpdateUI.bind({device: device}));
		};

		device.doConnect = doConnectCast;

		deviceListMenu.append(MenuFactory.chromeCastItem(device, doConnectCast));

		if (reconnectName != null && (reconnectName.localeCompare(device.name) == 0)) {
		    doConnectCast();
		}

                break;

            case DeviceMatcher.TYPES.UPNP:

                if (DeviceMatcher.isRaumfeld(device)) {

                    device.controls = new RaumfeldZone(device);

		    var doConnectUPnP = function onClicked() {

		    	onStartStream();

			switchingDevice = true;

                        logger.info('Attempting to play to Raumfeld device', device.name);

		    	stopCurrentDevice();

                        // Sets OSX selected input and output audio devices to Soundflower
                        LocalSourceSwitcher.switchSource({
                            output: 'Soundflower (2ch)',
                            input: 'Soundflower (2ch)'
                        });

			currentDevice = device;

			storage.set('reconnect', {
					setting: reconnect,
					name: device.name
				}, function(error){
					if (error == null) {
						return;
					}
					logger.info("error while storing setting: %s", error.toString());
				});

			device.controls.registerErrorHandler(function(err){
				/*dialog.showErrorBox("devicecast - An Error Occurred",
						err.toString());*/
				onStop();
			});

                        device.controls.play(streamingAddress, onStreamingUpdateUI.bind({device: device}));
                    };

		    device.doConnect = doConnectUPnP;

                    deviceListMenu.append(MenuFactory.raumfeldDeviceItem(device, doConnectUPnP));

		    logger.info('Added Raumfeld menu item (reconnect name: %s)', reconnectName);

		    if (reconnectName != null && (reconnectName.localeCompare(device.name) == 0)) {
			doConnectUPnP();
		    }
                }
                break;
            default:
                logger.error('Unknown recognised device found', logger.level === 'verbose' ? device : device.name);
        }

        mb.tray.setContextMenu(menu);
   });

};

//Menubar construction
mb.on('ready', function ready() {

    osxsleep.OSXSleep.start(function(state) {
	logger.info("sleep state: %d", state);

	switch (state) {
		case osxsleep.HAS_POWERED_ON:
			if (currentDevice) {
				currentDevice.doConnect();
			}
			break;
		case osxsleep.WILL_SLEEP:
			stopCurrentDevice();
			LocalSoundStreamer.stopStream();
			break;
	}
    });

    storage.get('reconnect',
		function(error, object){

			if (object == null) {
		    		return;
			}

			reconnect = object.setting;
			reconnectName = object.name;

			logger.info("reconnect: "+reconnect+" name: "+reconnectName);
	    	});

    process.on('uncaughtException', function(err) {
	dialog.showErrorBox("devicecast - An Error Occurred", err.toString());
    });

    //Menu startup message
    menu = new Menu();
    deviceListMenu = new Menu();

    menu.append(MenuFactory.castToDeviceMenu(deviceListMenu));

    /*
    menu.append(new MenuItem({
        label: 'Reconnect on Start',
	type: 'checkbox',
        click: function (item, window, event) {
	    reconnect = item.checked;
	    reconnectName = self.currentDevice.name;
	    storage.set('reconnect', {
		setting: reconnect,
		name: self.currentDevice.name
		 }, function(error){
			if (error == null) {
				return;
			}
			logger.info("error while storing setting: %s", error.toString());
		});
        }
    }));
    */

    menu.append(MenuFactory.separator());

    menu.append(new MenuItem({
        label: 'Volume Up',
        click: function () {

		if (!currentDevice) return;

		currentDevice.controls.volumeUp();
        }
    }));

    menu.append(new MenuItem({
        label: 'Volume Down',
        click: function () {

		if (!currentDevice) return;

		currentDevice.controls.volumeDown();
        }
    }));

    // Stream Options
    var streamMenu = new Menu();
    streamMenu.append(new MenuItem({
        label: 'OSX Output (default)',
        click: function () {
            LocalSourceSwitcher.switchSource({
                output: 'Soundflower (2ch)',
                input: 'Soundflower (2ch)'
            });
            NotificationService.notify({
                title: 'Audio Source Switched',
                message: 'OSX Audio via Soundflower'
            });
        }
    }));
    streamMenu.append(new MenuItem({
        label: 'Internal Microphone',
        click: function () {
            LocalSourceSwitcher.switchSource({
                output: 'Soundflower (2ch)',
                input: 'Internal Microphone'
            });
            NotificationService.notify({
                title: 'Audio Source Switched',
                message: 'Internal Microphone via Soundflower'
            });
        }
    }));
    streamMenu.append(MenuFactory.separator());
    streamMenu.append(MenuFactory.aboutStreamFeature());

    // Streaming Menu
    menu.append(MenuFactory.separator());
    menu.append(MenuFactory.steamMenu(streamMenu));
    menu.append(MenuFactory.separator());

    //Clicking this option stops casting audio to Chromecast
    menu.append(new MenuItem({
        label: 'Stop casting',
        enabled: true, // default disabled as not initially playing
        click: function () {

            // Attempt to stop all controls
            stopCurrentDevice();

            // Clean up playing speaker icon
            deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

            // Switch tray icon
            mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));

            LocalSourceSwitcher.resetOriginSource();
        }
    }));

    var onQuitHandler = function () {
        mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));
        stopCurrentDevice();
        LocalSoundStreamer.stopStream();
        LocalSourceSwitcher.resetOriginSource();
        mb.app.quit();
    };

    // About
    menu.append(MenuFactory.about());

    // Quit
    menu.append(MenuFactory.quit(onQuitHandler));

    // Clicking this option starts casting audio to Cast
    menu.append(MenuFactory.separator());

    // CMD + C death
    mb.app.on('quit', onQuitHandler);

    // Set the menu items
    mb.tray.setContextMenu(menu);

    scanForDevices();
});
<commit_msg>Unregister for power changes on app quit<commit_after>//Audiodevice by http://whoshacks.blogspot.com/2009/01/change-audio-devices-via-shell-script.html

//Shell and filesystem dependencies
require('shelljs/global');
var path = require('path');
var _ = require('lodash');

//Electron dependencies
const {app, dialog, Menu, MenuItem} = require('electron')
const storage = require('electron-json-storage');

var menubar = require('menubar');
var mb = menubar({dir: __dirname, icon: 'not-castingTemplate.png'});

/* Internals */
var MenuFactory = require('./lib/native/MenuFactory');
var NotificationService = require('./lib/native/NotificationService');

var DeviceLookupService = require('./lib/device/utils/DeviceLookupService');
var DeviceMatcher = require('./lib/device/utils/DeviceMatcher');
var LocalSourceSwitcher = require('./lib/device/utils/LocalSourceSwitcher');
var UpnpMediaClientUtils = require('./lib/device/utils/UpnpMediaClientUtils');

var LocalSoundStreamer = require('./lib/sound/LocalSoundStreamerWebcast');

/* Various Device controllers */
var RaumfeldZone = require('./lib/device/controls/RaumfeldZone');
var ChromeCast = require('./lib/device/controls/ChromeCast');
var logger = require('./lib/common/logger');
const osxsleep = require ('osxsleep');

var currentDevice = null;
var menu = null;
var deviceListMenu = null;
var devicesAdded = [];
var streamingAddress;
var reconnect = false;
var reconnectName = null;
var switchingDevice = false;
var isStreaming = false;

var stopCurrentDevice = function () {

	if (currentDevice) {

	    var device = currentDevice;

	    device.controls.stop();

	    NotificationService.notifyCastingStopped(device.controls);

	    // Clean up playing speaker icon
	    deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

	    // Switch tray icon
	    mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));
	}
    };

var onStartStream = function() {

    if (isStreaming) {
	return;
    }

    isStreaming = true;

    LocalSoundStreamer.startStream(function (streamUrl) {
        streamingAddress = streamUrl;
    }, function(err){
    }, function() {
	onStop();
    });
};

var onStop = function() {

    if (switchingDevice) {
       return;
    }

    isStreaming = false;

    if (currentDevice) {

	    NotificationService.notifyCastingStopped(currentDevice.controls);

	    // Clean up playing speaker icon
	    deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

	    // Switch tray icon
	    mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));

	    LocalSourceSwitcher.resetOriginSource();
    }
};

var setSpeakIcon = function (item) {

	logger.info("label: %s  name: %s", item.label, this.device.name);

        if (item.label === this.device.name) {
            MenuFactory.setSpeaker(item);
        } else {
            MenuFactory.removeSpeaker(item);
        }
};

var onStreamingUpdateUI = function () {

	switchingDevice = false;

        // set speak icon when playing
        deviceListMenu.items.forEach(setSpeakIcon.bind({device: this.device}));
    	mb.tray.setContextMenu(menu);

        // Changes tray icon to "Casting"
        mb.tray.setImage(path.join(__dirname, 'castingTemplate.png'));
    };

var scanForDevices = function(self) {

    DeviceLookupService.lookUpDevices(function onDevice(device) {

	var found = false;

        switch (device.type) {
            case DeviceMatcher.TYPES.CHROMECAST:
		device.name = device.name + " (c)";
		break;
	    case DeviceMatcher.TYPES.UPNP:
		device.name = device.name + " (u)";
		break;
		default:
			break;
	}

	for (var n = 0; n < devicesAdded.length; n++) {
		if(device.name.localeCompare(devicesAdded[n].name) == 0) {
			devicesAdded[n] = device;
			found = true;
			break;
		}
	}

	if (!found) {
	   devicesAdded.push(device);
	} else {
	   return;
        }

        switch (device.type) {
            case DeviceMatcher.TYPES.CHROMECAST:

		device.controls = new ChromeCast(device);

		var doConnectCast = function onClicked() {

		    onStartStream();

		    switchingDevice = true;

		    logger.info('Attempting to play to Chromecast', device.name);

		    stopCurrentDevice();

		    // Sets OSX selected input and output audio devices to Soundflower
		    LocalSourceSwitcher.switchSource({
			output: 'Soundflower (2ch)',
			input: 'Soundflower (2ch)'
		    });

		    storage.set('reconnect', {
				    setting: reconnect,
				    name: device.name
			    }, function(error){
				    if (error == null) {
					    return;
				    }
				    logger.info("error while storing setting: %s", error.toString());
			    });

		    currentDevice = device;

		    device.controls.play(streamingAddress, onStreamingUpdateUI.bind({device: device}));
		};

		device.doConnect = doConnectCast;

		deviceListMenu.append(MenuFactory.chromeCastItem(device, doConnectCast));

		if (reconnectName != null && (reconnectName.localeCompare(device.name) == 0)) {
		    doConnectCast();
		}

                break;

            case DeviceMatcher.TYPES.UPNP:

                if (DeviceMatcher.isRaumfeld(device)) {

                    device.controls = new RaumfeldZone(device);

		    var doConnectUPnP = function onClicked() {

		    	onStartStream();

			switchingDevice = true;

                        logger.info('Attempting to play to Raumfeld device', device.name);

		    	stopCurrentDevice();

                        // Sets OSX selected input and output audio devices to Soundflower
                        LocalSourceSwitcher.switchSource({
                            output: 'Soundflower (2ch)',
                            input: 'Soundflower (2ch)'
                        });

			currentDevice = device;

			storage.set('reconnect', {
					setting: reconnect,
					name: device.name
				}, function(error){
					if (error == null) {
						return;
					}
					logger.info("error while storing setting: %s", error.toString());
				});

			device.controls.registerErrorHandler(function(err){
				/*dialog.showErrorBox("devicecast - An Error Occurred",
						err.toString());*/
				onStop();
			});

                        device.controls.play(streamingAddress, onStreamingUpdateUI.bind({device: device}));
                    };

		    device.doConnect = doConnectUPnP;

                    deviceListMenu.append(MenuFactory.raumfeldDeviceItem(device, doConnectUPnP));

		    logger.info('Added Raumfeld menu item (reconnect name: %s)', reconnectName);

		    if (reconnectName != null && (reconnectName.localeCompare(device.name) == 0)) {
			doConnectUPnP();
		    }
                }
                break;
            default:
                logger.error('Unknown recognised device found', logger.level === 'verbose' ? device : device.name);
        }

        mb.tray.setContextMenu(menu);
   });

};

//Menubar construction
mb.on('ready', function ready() {

    osxsleep.OSXSleep.start(function(state) {
	logger.info("sleep state: %d", state);

	switch (state) {
		case osxsleep.HAS_POWERED_ON:
			if (currentDevice) {
				currentDevice.doConnect();
			}
			break;
		case osxsleep.WILL_SLEEP:
			stopCurrentDevice();
			LocalSoundStreamer.stopStream();
			break;
	}
    });

    storage.get('reconnect',
		function(error, object){

			if (object == null) {
		    		return;
			}

			reconnect = object.setting;
			reconnectName = object.name;

			logger.info("reconnect: "+reconnect+" name: "+reconnectName);
	    	});

    process.on('uncaughtException', function(err) {
	dialog.showErrorBox("devicecast - An Error Occurred", err.toString());
    });

    //Menu startup message
    menu = new Menu();
    deviceListMenu = new Menu();

    menu.append(MenuFactory.castToDeviceMenu(deviceListMenu));

    /*
    menu.append(new MenuItem({
        label: 'Reconnect on Start',
	type: 'checkbox',
        click: function (item, window, event) {
	    reconnect = item.checked;
	    reconnectName = self.currentDevice.name;
	    storage.set('reconnect', {
		setting: reconnect,
		name: self.currentDevice.name
		 }, function(error){
			if (error == null) {
				return;
			}
			logger.info("error while storing setting: %s", error.toString());
		});
        }
    }));
    */

    menu.append(MenuFactory.separator());

    menu.append(new MenuItem({
        label: 'Volume Up',
        click: function () {

		if (!currentDevice) return;

		currentDevice.controls.volumeUp();
        }
    }));

    menu.append(new MenuItem({
        label: 'Volume Down',
        click: function () {

		if (!currentDevice) return;

		currentDevice.controls.volumeDown();
        }
    }));

    // Stream Options
    var streamMenu = new Menu();
    streamMenu.append(new MenuItem({
        label: 'OSX Output (default)',
        click: function () {
            LocalSourceSwitcher.switchSource({
                output: 'Soundflower (2ch)',
                input: 'Soundflower (2ch)'
            });
            NotificationService.notify({
                title: 'Audio Source Switched',
                message: 'OSX Audio via Soundflower'
            });
        }
    }));
    streamMenu.append(new MenuItem({
        label: 'Internal Microphone',
        click: function () {
            LocalSourceSwitcher.switchSource({
                output: 'Soundflower (2ch)',
                input: 'Internal Microphone'
            });
            NotificationService.notify({
                title: 'Audio Source Switched',
                message: 'Internal Microphone via Soundflower'
            });
        }
    }));
    streamMenu.append(MenuFactory.separator());
    streamMenu.append(MenuFactory.aboutStreamFeature());

    // Streaming Menu
    menu.append(MenuFactory.separator());
    menu.append(MenuFactory.steamMenu(streamMenu));
    menu.append(MenuFactory.separator());

    //Clicking this option stops casting audio to Chromecast
    menu.append(new MenuItem({
        label: 'Stop casting',
        enabled: true, // default disabled as not initially playing
        click: function () {

            // Attempt to stop all controls
            stopCurrentDevice();

            // Clean up playing speaker icon
            deviceListMenu.items.forEach(MenuFactory.removeSpeaker);

            // Switch tray icon
            mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));

            LocalSourceSwitcher.resetOriginSource();
        }
    }));

    var onQuitHandler = function () {
	osxsleep.OSXSleep.stop();
        mb.tray.setImage(path.join(__dirname, 'not-castingTemplate.png'));
        stopCurrentDevice();
        LocalSoundStreamer.stopStream();
        LocalSourceSwitcher.resetOriginSource();
        mb.app.quit();
    };

    // About
    menu.append(MenuFactory.about());

    // Quit
    menu.append(MenuFactory.quit(onQuitHandler));

    // Clicking this option starts casting audio to Cast
    menu.append(MenuFactory.separator());

    // CMD + C death
    mb.app.on('quit', onQuitHandler);

    // Set the menu items
    mb.tray.setContextMenu(menu);

    scanForDevices();
});
<|endoftext|>


<commit_before>var util = require('util');
var stream = require('stream');
var PassThrough = stream.PassThrough || require('readable-stream').PassThrough;

var ansi_styles = require('./lib/ansi_styles'),
    ansi_styles_flat = {},
    color_regex = /\x1b\[[0-9;]*m/g,
    attr;

util.inherits(ANSIState, PassThrough);

// build ansi_styles_flat table object with each element of the form:
// code: [attribute, description]

for (var attr_name in ansi_styles) {
    attr = ansi_styles[attr_name];
    for (var code in attr) {
        ansi_styles_flat[code] = [attr_name, attr[code]];
    }
}

// ANSI state constructor

function ANSIState(legacy) {

    var feed = '',
        last_match = '',
        stream_match = [],
        _this = this;

    PassThrough.call(this);

    this.attrs = {};
    this.is_reset = true;
    this.reset();
    this.setEncoding('utf8');

    this.on('readable', function() {
        feed += _this.read();
        if (feed.indexOf('\033') === -1) {
            feed = '';
        } else {
            stream_match = feed.match(color_regex);
            if (stream_match.length > 0) {
                last_match = stream_match[stream_match.length - 1];
                feed = feed.slice(feed.lastIndexOf(last_match) + last_match.length);
                _this.updateWithArray(stream_match);
            }
        }
    });

    if (legacy !== undefined) {
        this.update(legacy);
    }
}

// ansi state prototype getter/setter properties

Object.defineProperty(ANSIState.prototype, 'code', {
    get: function() {
        return this.buildCode();
    }
});

// ANSI state main API prototype methods

ANSIState.prototype.update = function(date) {

    if (Array.isArray(data)) {
        this.updateWithArray(data);
    } else if (typeof data === 'string') {
        this.updateWithString(data);
    } else if (data instanceof ANSIState) {
        this.updateWithState(data);
    }

    return this;
};

ANSIState.prototype.restore = function(date) {

    this.push(this.code);
    return this;
};

ANSIState.prototype.reset = function() {

    var attributes = this.attrs;

    for (var attr_name in ansi_styles) {
        attributes[attr_name] = null;
    }

    this.is_reset = true;
    this.xterm_foreground = undefined;
    this.xterm_background = undefined;

    return this;
};

// integral helper methods

ANSIState.prototype.updateRawArray = function(codes) {

    var code;
    this.is_reset = false;

    for (var i = 0; i < codes.length; i++) {
        code = codes[i];
        if (code === '38' || code === '48') {
            this.updateWithSpecialCode(code, codes, i);
        } else {
            this.updateWithCode(code);
        }
    }
    return this;
};

ANSIState.prototype.updateWithCode = function(code) {

    if (code === '0') {
        this.reset();
    } else {
        if (ansi_styles_flat[code]) {
            this.attrs[ansi_styles_flat[code][0]] = code;
        }
    }
    return this;
};

ANSIState.prototype.updateWithSpecialCode = function(code, codes, i) {

    switch (code) {
        case '38':
            this.xterm_foreground = xtermColor(codes, i);
            break;
        case '48':
            this.xterm_background = xtermColor(codes, i);
            break;
    }

    this.updateWithCode(code);
    return this;
};

ANSIState.prototype.updateWithState = function(ansi_state) {

    var state_attributes = ansi_state.attrs;

    if (ansi_state.is_reset === true) {
        return;
    } else {
        this.is_reset = false;
        for (var attr_name in state_attributes) {
            this.attrs[attr_name] = state_attributes[attr_name];
        }
    }

    if (ansi_state.xterm_foreground) this.xterm_foreground = ansi_state.xterm_foreground;
    if (ansi_state.xterm_background) this.xterm_background = ansi_state.xterm_background;

    return this;
};

ANSIState.prototype.updateWithArray = function(codes) {

    var code,
        codeList = [];

    if (codes !== null) {
        for (var i = 0; i < codes.length; i++) {
            code = codes[i];
            if (typeof code === 'string') {
                codeList = codeList.concat(code.replace(/[\x1b\[|m]/g, '').split(';'));
            } else if (typeof code === 'number') {
                codeList.push('' + code);
            }
        }
    }

    this.updateRawArray(codeList);
    return this;
};

ANSIState.prototype.updateWithString = function(line) {

    var codes = line.match(color_regex),
        codeList = [],
        code;

    if (codes !== null) {
        for (var i = 0; i < codes.length; i++) {
            code = codes[i];
            codeList = codeList.concat(code.replace(/[\x1b\[|m]/g, '').split(';'));
        }
    }

    this.updateRawArray(codeList);

    return this;
};

ANSIState.prototype.buildCode = function() {

    var attributes = this.attrs,
        line = '\033[',
        val,
        i = 0;

    if (this.is_reset === true) {

        line += '0m';

    } else {

        for (var attr_name in ansi_styles) {

            val = attributes[attr_name];

            if (val !== null) {

                if (val === '38') val += ';' + this.xterm_foreground.join(';');
                if (val === '48') val += ';' + this.xterm_background.join(';');

                line += (i === 0 ? '' : ';') + val;
                i++;
            }
        }

        line += 'm';
    }

    return line;
};

function xtermColor(codes, i) {

    var type = codes[i + 1];

    if (type === '2') {
        return codes.splice(i + 1, 4);
    } else {
        return codes.splice(i + 1, 2);
    }
}

module.exports = exports = ANSIState;
<commit_msg>Fixing bug with updating with state that has been reset.<commit_after>var util = require('util');
var stream = require('stream');
var PassThrough = stream.PassThrough || require('readable-stream').PassThrough;

var ansi_styles = require('./lib/ansi_styles'),
    ansi_styles_flat = {},
    color_regex = /\x1b\[[0-9;]*m/g,
    attr;

util.inherits(ANSIState, PassThrough);

// build ansi_styles_flat table object with each element of the form:
// code: [attribute, description]

for (var attr_name in ansi_styles) {
    attr = ansi_styles[attr_name];
    for (var code in attr) {
        ansi_styles_flat[code] = [attr_name, attr[code]];
    }
}

// ANSI state constructor

function ANSIState(legacy) {

    var feed = '',
        last_match = '',
        stream_match = [],
        _this = this;

    PassThrough.call(this);

    this.attrs = {};
    this.is_reset = true;
    this.reset();
    this.setEncoding('utf8');

    this.on('readable', function() {
        feed += _this.read();
        if (feed.indexOf('\033') === -1) {
            feed = '';
        } else {
            stream_match = feed.match(color_regex);
            if (stream_match.length > 0) {
                last_match = stream_match[stream_match.length - 1];
                feed = feed.slice(feed.lastIndexOf(last_match) + last_match.length);
                _this.updateWithArray(stream_match);
            }
        }
    });

    if (legacy !== undefined) {
        this.update(legacy);
    }
}

// ansi state prototype getter/setter properties

Object.defineProperty(ANSIState.prototype, 'code', {
    get: function() {
        return this.buildCode();
    }
});

// ANSI state main API prototype methods

ANSIState.prototype.update = function(date) {

    if (Array.isArray(data)) {
        this.updateWithArray(data);
    } else if (typeof data === 'string') {
        this.updateWithString(data);
    } else if (data instanceof ANSIState) {
        this.updateWithState(data);
    }

    return this;
};

ANSIState.prototype.restore = function(date) {

    this.push(this.code);
    return this;
};

ANSIState.prototype.reset = function() {

    var attributes = this.attrs;

    for (var attr_name in ansi_styles) {
        attributes[attr_name] = null;
    }

    this.is_reset = true;
    this.xterm_foreground = undefined;
    this.xterm_background = undefined;

    return this;
};

// integral helper methods

ANSIState.prototype.updateRawArray = function(codes) {

    var code;
    this.is_reset = false;

    for (var i = 0; i < codes.length; i++) {
        code = codes[i];
        if (code === '38' || code === '48') {
            this.updateWithSpecialCode(code, codes, i);
        } else {
            this.updateWithCode(code);
        }
    }
    return this;
};

ANSIState.prototype.updateWithCode = function(code) {

    if (code === '0') {
        this.reset();
    } else {
        if (ansi_styles_flat[code]) {
            this.attrs[ansi_styles_flat[code][0]] = code;
        }
    }
    return this;
};

ANSIState.prototype.updateWithSpecialCode = function(code, codes, i) {

    switch (code) {
        case '38':
            this.xterm_foreground = xtermColor(codes, i);
            break;
        case '48':
            this.xterm_background = xtermColor(codes, i);
            break;
    }

    this.updateWithCode(code);
    return this;
};

ANSIState.prototype.updateWithState = function(ansi_state) {

    var state_attributes = ansi_state.attrs;

    if (ansi_state.is_reset === true) {
        this.is_reset = true;
        return this;
    } else {
        this.is_reset = false;
        for (var attr_name in state_attributes) {
            this.attrs[attr_name] = state_attributes[attr_name];
        }
    }

    if (ansi_state.xterm_foreground) this.xterm_foreground = ansi_state.xterm_foreground;
    if (ansi_state.xterm_background) this.xterm_background = ansi_state.xterm_background;

    return this;
};

ANSIState.prototype.updateWithArray = function(codes) {

    var code,
        codeList = [];

    if (codes !== null) {
        for (var i = 0; i < codes.length; i++) {
            code = codes[i];
            if (typeof code === 'string') {
                codeList = codeList.concat(code.replace(/[\x1b\[|m]/g, '').split(';'));
            } else if (typeof code === 'number') {
                codeList.push('' + code);
            }
        }
    }

    this.updateRawArray(codeList);
    return this;
};

ANSIState.prototype.updateWithString = function(line) {

    var codes = line.match(color_regex),
        codeList = [],
        code;

    if (codes !== null) {
        for (var i = 0; i < codes.length; i++) {
            code = codes[i];
            codeList = codeList.concat(code.replace(/[\x1b\[|m]/g, '').split(';'));
        }
    }

    this.updateRawArray(codeList);

    return this;
};

ANSIState.prototype.buildCode = function() {

    var attributes = this.attrs,
        line = '\033[',
        val,
        i = 0;

    if (this.is_reset === true) {

        line += '0m';

    } else {

        for (var attr_name in ansi_styles) {

            val = attributes[attr_name];

            if (val !== null) {

                if (val === '38') val += ';' + this.xterm_foreground.join(';');
                if (val === '48') val += ';' + this.xterm_background.join(';');

                line += (i === 0 ? '' : ';') + val;
                i++;
            }
        }

        line += 'm';
    }

    return line;
};

function xtermColor(codes, i) {

    var type = codes[i + 1];

    if (type === '2') {
        return codes.splice(i + 1, 4);
    } else {
        return codes.splice(i + 1, 2);
    }
}

module.exports = exports = ANSIState;
<|endoftext|>