<commit_before>/**
* Helper function for visualising a sunburst plot as SVG using d3js
*
* @param {Object}   container - D3 selection to HTML object acting as container of the svg
* @param {Float[]}  data      - array with tree information
* @param {Object}   [config]  - configuration object (optional)
* @param {Int}      [config.breadcrumb]   - definition of polygon for breadcrumb (optional)
* @param {Object}   [config.color]        - D3 scale object for color coding (optional)
* @param {Callback} [config.cutoff]       - minimum relative contribution for a contribution to be shown (optional)
* @param {d3.scale} [config.legend_entry] - definition of polygon for legend entry (optional)
* @param {String}   [config.margin]       - specification of top/bottom/right/left margins (optional)
*
* This function requires the d3.js library and jQuery to be loaded.
*
* This function takes a data object describing a tree structure. The data object
* must have the following layout:
*
* var data = {
*  name: "root",
*  children: [{
*    name: "A",
*    children: [...],
*    size: 3
*   },{
*    name: "B",
*    children: [...],
*    size: 5
*   }
*   ...
*  ],
*  size: 0
* };
*
* The size for each node should describe the size of this specific node NOT
* including the contributions from child nodes. The function then plots the data
* as a sunburst diagram in a SVG inside the given HTML object/container. If the
* container contains a SVG element, it is overwritten and replaced by the
* sunburst SVG. The config object allows to customize the appearance and
* behaviour of the plot.
* The margin object must have bottom/top/left/right attributes specifying the
* individual margins. The color object should be a pre-configured d3 ordinal
* scale object. The domain is overwritten by the function to match the list of
* unique node names.
*
* The SVG elements can be styled using CSS. To this end, the following class
* tags are assigned:
* - The paths representing the sunburst nodes are classed as 'sunburst-node'.
* - Group objects representing the legend entries are classed as 'legend-entry'.
* - The text at the tip of the breadcrumb trail has the id 'endlabel'.
* - The text in the middle of the plot showing the relative percentage has the
*   id 'percentage'.
*/
function plotSunburst(container, data, config) {

  // define some default settings
  var defaults = {
    breadcrumb: {w: 75, h: 30, s: 3, t: 10},
    color: d3.scaleOrdinal(d3.schemeCategory20c),
    cutoff: 0.005,
    legend_entry: {w: 75, h: 30, s: 3, r: 3},
    margin: {top: 20, right: 20, bottom: 20, left: 20}
  };

  // augment user-defined input with default settings
  config = $.extend({}, defaults, config);

  // redraw whole histogram instead of updating it
  container.select("svg").remove();

  // restore back to standard layout
  container.on("mouseleave", mouseleave);

  // setup svg image with padding and margins
  var width = container.node().getBoundingClientRect().width;
  var height = container.node().getBoundingClientRect().height;
  var innerWidth = width - config.margin.left - config.margin.right;
  var innerHeight = height - config.margin.top - config.margin.bottom;
  var radius = Math.min(innerWidth, innerHeight) / 2;

  var svg = container.append("svg")
  .attr("width", width)
  .attr("height", height);

  var canvas = svg.append("g")
  .attr("transform", "translate(" + config.margin.left + "," + config.margin.top + ")")
  .append("g")
  .attr("transform", "translate(" + innerWidth / 2 + "," + innerHeight / 2 + ")");

  // setup partitioning of data
  var partition = d3.partition()
  .size([2 * Math.PI, radius * radius]);

  // generate hierarchy
  var root = d3.hierarchy(data)
  .sum(function (d) {return d.size;})
  .sort(function(a, b) { return b.value - a.value; });

  // get only nodes with visible contribution
  var nodes = partition(root).descendants()
    .filter(function(d) {
      return (d.x1 - d.x0 > config.cutoff);
    });

  // set domain for color scale to unique names in tree
  var uniqueNames = (function(graph) {
    var output = [];
    graph.forEach(function(node) {
      if (output.indexOf(node.data.name) === -1) {
        output.push(node.data.name);
      }
    });
    return output;
  })(nodes);

  config.color.domain(uniqueNames);

  // define generic template for circle segment
  var arc = d3.arc()
  .startAngle(function(d) { return d.x0; })
  .endAngle(function(d) { return d.x1; })
  .innerRadius(function(d) { return Math.sqrt(d.y0); })
  .outerRadius(function(d) { return Math.sqrt(d.y1); });

  // draw sunburst plot
  var path = canvas.selectAll("path")
  .data(nodes)
  .enter().append("path")
  .attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
  .attr("d", arc)
  .classed("sunburst-node", true)
  .style("stroke", "#fff")
  .style("fill", function(d) {return config.color(d.data.name); })
  .on("mouseover", mouseover);

  // get total size of the tree = value of root node from partition.
  totalSize = path.datum().value;

  // add legend
  var legend = canvas.append("g")
  .attr("transform", "translate(" + (-innerWidth/2) + "," + (-innerHeight/2 + config.breadcrumb.h) + ")");

  // draw legend entries
  var entries = legend.selectAll("g")
  .data(uniqueNames.slice(1,uniqueNames.length))
  .enter()
  .append("g")
  .attr("transform", function(d, i) {return "translate(0," + i * (config.legend_entry.h + config.legend_entry.s) + ")";})
  .classed("legend-entry", true);

  entries.append("rect")
  .attr("rx", config.legend_entry.r)
  .attr("ry", config.legend_entry.r)
  .attr("width", config.legend_entry.w)
  .attr("height", config.legend_entry.h)
  .style("fill", function(d) { return config.color(d); });

  entries.append("text")
  .attr("x", config.legend_entry.w / 2)
  .attr("y", config.legend_entry.h / 2)
  .attr("dy", "0.35em")
  .attr("text-anchor", "middle")
  .text(function(d) { return d; });

  // initialize breadcrump trail
  var trail = svg.append("svg")
  .attr("width", innerWidth)
  .attr("height", 50)
  .attr("id", "trail");

  trail
  .append("text")
  .attr("id", "endlabel");

  canvas
  .append("text")
  .attr("id","percentage")
  .attr("text-anchor", "middle");

  canvas
  .append("text")
  .attr("id","counts")
  .attr("dy","1em")
  .attr("text-anchor", "middle");

  // fade all but the current sequence, and show it in the breadcrumb trail
  function mouseover(d) {
    var percentage = (100 * d.value / totalSize).toPrecision(3);
    var percentageString = percentage + "%";
    if (percentage < 0.1) {
      percentageString = "< 0.1%";
    }

    d3.select("#percentage")
    .text(percentageString)
    .style("visibility","");

    d3.select("#counts")
    .text(d3.format(",")(d.value) + "/" + d3.format(",")(totalSize))
    .style("visibility","");

    var sequenceArray = d.ancestors().reverse();
    sequenceArray.shift(); // remove root node from the array
    updateBreadcrumbs(sequenceArray, percentageString);

    // fade all the segments
    d3.selectAll("path").style("opacity", 0.3);

    // then highlight only those that are an ancestor of the current segment.
    canvas.selectAll("path")
    .filter(function(node) {return (sequenceArray.indexOf(node) >= 0);})
    .style("opacity", 1);
  }

  // restore everything to full opacity when moving off the visualization.
  function mouseleave(d) {
    // hide the breadcrumb trail
    d3.select("#trail").style("visibility", "hidden");

    // deactivate all segments during transition.
    d3.selectAll("path").on("mouseover", null);

    // transition each segment to full opacity and then reactivate it.
    d3.selectAll("path")
    .transition()
    .duration(500)
    .style("opacity", 1)
    .on("end", function() {d3.select(this).on("mouseover", mouseover);});

    d3.select("#percentage").style("visibility","hidden");
    d3.select("#counts").style("visibility","hidden");
  }

  // update the breadcrumb trail to show the current sequence and percentage.
  function updateBreadcrumbs(nodeArray, percentageString) {
    // data join; key function combines name and depth (= position in sequence).
    var trail = d3.select("#trail")
    .selectAll("g")
    .data(nodeArray, function(d) { return d.data.name + d.depth; });

    // remove exiting nodes.
    trail.exit().remove();

    // ddd breadcrumb and label for entering nodes.
    var entering = trail.enter().append("g");
    entering.append("polygon")
    .attr("points", breadcrumbPoints)
    .style("fill", function(d) { return config.color(d.data.name); });

    entering.append("text")
    .attr("x", (config.breadcrumb.w + config.breadcrumb.t) / 2)
    .attr("y", config.breadcrumb.h / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "middle")
    .text(function(d) { return d.data.name; });

    // merge enter and update selections; set position for all nodes.
    entering.merge(trail).attr("transform", function(d, i) {return "translate(" + i * (config.breadcrumb.w + config.breadcrumb.s) + ", 0)";});

    // now move and update the percentage at the end.
    d3.select("#trail").select("#endlabel")
    .attr("x", (nodeArray.length + 0.5) * (config.breadcrumb.w + config.breadcrumb.s))
    .attr("y", config.breadcrumb.h / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "middle")
    .text(percentageString);

    // Make the breadcrumb trail visible, if it's hidden.
    d3.select("#trail").style("visibility", "");
  }

  // generate a string that describes the points of a breadcrumb polygon.
  function breadcrumbPoints(d, i) {
    var points = [];
    points.push("0,0");
    points.push(config.breadcrumb.w + ",0");
    points.push(config.breadcrumb.w + config.breadcrumb.t + "," + (config.breadcrumb.h / 2));
    points.push(config.breadcrumb.w + "," + config.breadcrumb.h);
    points.push("0," + config.breadcrumb.h);

    // leftmost breadcrumb -> don't include 6th vertex.
    if (i > 0) {
      points.push(config.breadcrumb.t + "," + (config.breadcrumb.h / 2));
    }

    return points.join(" ");
  }
}
<commit_msg>fix documentation<commit_after>/**
* Helper function for visualising a sunburst plot as SVG using d3js
*
* @param {Object}   container - D3 selection to HTML object acting as container of the svg
* @param {Object}   data      - array with tree information
* @param {Object}   [config]  - configuration object (optional)
* @param {Int}      [config.breadcrumb]   - definition of polygon for breadcrumb (optional)
* @param {Object}   [config.color]        - D3 scale object for color coding (optional)
* @param {Callback} [config.cutoff]       - minimum relative contribution for a contribution to be shown (optional)
* @param {d3.scale} [config.legend_entry] - definition of polygon for legend entry (optional)
* @param {String}   [config.margin]       - specification of top/bottom/right/left margins (optional)
*
* This function requires the d3.js library and jQuery to be loaded.
*
* This function takes a data object describing a tree structure. The data object
* must have the following layout:
*
* var data = {
*  name: "root",
*  children: [{
*    name: "A",
*    children: [...],
*    size: 3
*   },{
*    name: "B",
*    children: [...],
*    size: 5
*   }
*   ...
*  ],
*  size: 0
* };
*
* The size for each node should describe the size of this specific node NOT
* including the contributions from child nodes. The function then plots the data
* as a sunburst diagram in a SVG inside the given HTML object/container. If the
* container contains a SVG element, it is overwritten and replaced by the
* sunburst SVG. The config object allows to customize the appearance and
* behaviour of the plot.
* The margin object must have bottom/top/left/right attributes specifying the
* individual margins. The color object should be a pre-configured d3 ordinal
* scale object. The domain is overwritten by the function to match the list of
* unique node names.
*
* The SVG elements can be styled using CSS. To this end, the following class
* tags are assigned:
* - The paths representing the sunburst nodes are classed as 'sunburst-node'.
* - Group objects representing the legend entries are classed as 'legend-entry'.
* - The text at the tip of the breadcrumb trail has the id 'endlabel'.
* - The text in the middle of the plot showing the relative percentage has the
*   id 'percentage'.
*/
function plotSunburst(container, data, config) {

  // define some default settings
  var defaults = {
    breadcrumb: {w: 75, h: 30, s: 3, t: 10},
    color: d3.scaleOrdinal(d3.schemeCategory20c),
    cutoff: 0.005,
    legend_entry: {w: 75, h: 30, s: 3, r: 3},
    margin: {top: 20, right: 20, bottom: 20, left: 20}
  };

  // augment user-defined input with default settings
  config = $.extend({}, defaults, config);

  // redraw whole histogram instead of updating it
  container.select("svg").remove();

  // restore back to standard layout
  container.on("mouseleave", mouseleave);

  // setup svg image with padding and margins
  var width = container.node().getBoundingClientRect().width;
  var height = container.node().getBoundingClientRect().height;
  var innerWidth = width - config.margin.left - config.margin.right;
  var innerHeight = height - config.margin.top - config.margin.bottom;
  var radius = Math.min(innerWidth, innerHeight) / 2;

  var svg = container.append("svg")
  .attr("width", width)
  .attr("height", height);

  var canvas = svg.append("g")
  .attr("transform", "translate(" + config.margin.left + "," + config.margin.top + ")")
  .append("g")
  .attr("transform", "translate(" + innerWidth / 2 + "," + innerHeight / 2 + ")");

  // setup partitioning of data
  var partition = d3.partition()
  .size([2 * Math.PI, radius * radius]);

  // generate hierarchy
  var root = d3.hierarchy(data)
  .sum(function (d) {return d.size;})
  .sort(function(a, b) { return b.value - a.value; });

  // get only nodes with visible contribution
  var nodes = partition(root).descendants()
    .filter(function(d) {
      return (d.x1 - d.x0 > config.cutoff);
    });

  // set domain for color scale to unique names in tree
  var uniqueNames = (function(graph) {
    var output = [];
    graph.forEach(function(node) {
      if (output.indexOf(node.data.name) === -1) {
        output.push(node.data.name);
      }
    });
    return output;
  })(nodes);

  config.color.domain(uniqueNames);

  // define generic template for circle segment
  var arc = d3.arc()
  .startAngle(function(d) { return d.x0; })
  .endAngle(function(d) { return d.x1; })
  .innerRadius(function(d) { return Math.sqrt(d.y0); })
  .outerRadius(function(d) { return Math.sqrt(d.y1); });

  // draw sunburst plot
  var path = canvas.selectAll("path")
  .data(nodes)
  .enter().append("path")
  .attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
  .attr("d", arc)
  .classed("sunburst-node", true)
  .style("stroke", "#fff")
  .style("fill", function(d) {return config.color(d.data.name); })
  .on("mouseover", mouseover);

  // get total size of the tree = value of root node from partition.
  totalSize = path.datum().value;

  // add legend
  var legend = canvas.append("g")
  .attr("transform", "translate(" + (-innerWidth/2) + "," + (-innerHeight/2 + config.breadcrumb.h) + ")");

  // draw legend entries
  var entries = legend.selectAll("g")
  .data(uniqueNames.slice(1,uniqueNames.length))
  .enter()
  .append("g")
  .attr("transform", function(d, i) {return "translate(0," + i * (config.legend_entry.h + config.legend_entry.s) + ")";})
  .classed("legend-entry", true);

  entries.append("rect")
  .attr("rx", config.legend_entry.r)
  .attr("ry", config.legend_entry.r)
  .attr("width", config.legend_entry.w)
  .attr("height", config.legend_entry.h)
  .style("fill", function(d) { return config.color(d); });

  entries.append("text")
  .attr("x", config.legend_entry.w / 2)
  .attr("y", config.legend_entry.h / 2)
  .attr("dy", "0.35em")
  .attr("text-anchor", "middle")
  .text(function(d) { return d; });

  // initialize breadcrump trail
  var trail = svg.append("svg")
  .attr("width", innerWidth)
  .attr("height", 50)
  .attr("id", "trail");

  trail
  .append("text")
  .attr("id", "endlabel");

  canvas
  .append("text")
  .attr("id","percentage")
  .attr("text-anchor", "middle");

  canvas
  .append("text")
  .attr("id","counts")
  .attr("dy","1em")
  .attr("text-anchor", "middle");

  // fade all but the current sequence, and show it in the breadcrumb trail
  function mouseover(d) {
    var percentage = (100 * d.value / totalSize).toPrecision(3);
    var percentageString = percentage + "%";
    if (percentage < 0.1) {
      percentageString = "< 0.1%";
    }

    d3.select("#percentage")
    .text(percentageString)
    .style("visibility","");

    d3.select("#counts")
    .text(d3.format(",")(d.value) + "/" + d3.format(",")(totalSize))
    .style("visibility","");

    var sequenceArray = d.ancestors().reverse();
    sequenceArray.shift(); // remove root node from the array
    updateBreadcrumbs(sequenceArray, percentageString);

    // fade all the segments
    d3.selectAll("path").style("opacity", 0.3);

    // then highlight only those that are an ancestor of the current segment.
    canvas.selectAll("path")
    .filter(function(node) {return (sequenceArray.indexOf(node) >= 0);})
    .style("opacity", 1);
  }

  // restore everything to full opacity when moving off the visualization.
  function mouseleave(d) {
    // hide the breadcrumb trail
    d3.select("#trail").style("visibility", "hidden");

    // deactivate all segments during transition.
    d3.selectAll("path").on("mouseover", null);

    // transition each segment to full opacity and then reactivate it.
    d3.selectAll("path")
    .transition()
    .duration(500)
    .style("opacity", 1)
    .on("end", function() {d3.select(this).on("mouseover", mouseover);});

    d3.select("#percentage").style("visibility","hidden");
    d3.select("#counts").style("visibility","hidden");
  }

  // update the breadcrumb trail to show the current sequence and percentage.
  function updateBreadcrumbs(nodeArray, percentageString) {
    // data join; key function combines name and depth (= position in sequence).
    var trail = d3.select("#trail")
    .selectAll("g")
    .data(nodeArray, function(d) { return d.data.name + d.depth; });

    // remove exiting nodes.
    trail.exit().remove();

    // ddd breadcrumb and label for entering nodes.
    var entering = trail.enter().append("g");
    entering.append("polygon")
    .attr("points", breadcrumbPoints)
    .style("fill", function(d) { return config.color(d.data.name); });

    entering.append("text")
    .attr("x", (config.breadcrumb.w + config.breadcrumb.t) / 2)
    .attr("y", config.breadcrumb.h / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "middle")
    .text(function(d) { return d.data.name; });

    // merge enter and update selections; set position for all nodes.
    entering.merge(trail).attr("transform", function(d, i) {return "translate(" + i * (config.breadcrumb.w + config.breadcrumb.s) + ", 0)";});

    // now move and update the percentage at the end.
    d3.select("#trail").select("#endlabel")
    .attr("x", (nodeArray.length + 0.5) * (config.breadcrumb.w + config.breadcrumb.s))
    .attr("y", config.breadcrumb.h / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "middle")
    .text(percentageString);

    // Make the breadcrumb trail visible, if it's hidden.
    d3.select("#trail").style("visibility", "");
  }

  // generate a string that describes the points of a breadcrumb polygon.
  function breadcrumbPoints(d, i) {
    var points = [];
    points.push("0,0");
    points.push(config.breadcrumb.w + ",0");
    points.push(config.breadcrumb.w + config.breadcrumb.t + "," + (config.breadcrumb.h / 2));
    points.push(config.breadcrumb.w + "," + config.breadcrumb.h);
    points.push("0," + config.breadcrumb.h);

    // leftmost breadcrumb -> don't include 6th vertex.
    if (i > 0) {
      points.push(config.breadcrumb.t + "," + (config.breadcrumb.h / 2));
    }

    return points.join(" ");
  }
}
<|endoftext|>


<commit_before>/**
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

<<<<<<< HEAD
import {LitElement, html} from 'lit-element';
=======
import {Icon} from '@material/mwc-icon';
import {AudioRecorder} from '../utils/AudioRecorder';
import {UtteranceApiService} from '../utils/UtteranceApiService';
import {QualityControl} from '../utils/QualityControl';
>>>>>>> master

import {AudioRecorder} from '../utils/AudioRecorder.js';
import {UtteranceApiService} from '../utils/UtteranceApiService.js';

import style from '../styles/components/RecordButton.css.js';
import {dispatchErrorToast} from '../utils/ToastUtils.js';

export class RecordButton extends LitElement {
    static get properties() {
        return {
            isRecording: {type: Boolean},
            audioStream: {type: Object},
            context: {type: Object},
        };
    }

    static get styles() {
        return style;
    }

    constructor() {
        super();
        this.isRecording = false;
        this.audioRecorder = new AudioRecorder();
        this.utteranceService = new UtteranceApiService();
    }

    updated() {
        this.handleWaveCanvas();
    }

    /**
     * If the user is not currently recording, begin recording using the Microphone
     * component. Otherwise, stop recording and save and display the just-recorded
     * audio file.
     */
    async recordHandler() {
        if (!this.isRecording) {
            try {
                await this.audioRecorder.initRecorder();
            } catch (e) {
                dispatchErrorToast(
                    this,
                    `Microphone access is currently blocked for this site.
                    To unblock, please navigate to chrome://settings/content/microphone
                    and remove this site from the 'Block' section.`
                );
                return;
            }

            if (!this.audioRecorder.startRecording()) {
                dispatchErrorToast(this, 'Failed to start recording.');
                return;
            }

            this.isRecording = true;
            this.audioStream = this.audioRecorder.stream;
            this.context = new (window.AudioContext ||
                window.webkitAudioContext)();
        } else {

            this.isRecording = false;
            let audio;

            try {
                audio = await this.audioRecorder.stopRecording();
            } catch (e) {
                dispatchErrorToast(
                    this,
                    `Could not record successfully; ${e.name}: ${e.message}`
                );
            }

            const qualityCheck = new QualityControl(this.context, audio.blob);
            const qualityResult = qualityCheck.isQualitySound();
            if (!qualityResult.success) {
                return;
            }

            if (audio.recordingUrl) {
                try {
                    const resp = await this.utteranceService.saveAudio(audio);

                    if (!resp) throw new Error();
                } catch (e) {
                    dispatchErrorToast(
                        this,
                        `Could not upload recording successfully`
                    );
                }
            }

            this.handleFinish();
        }
    }

    /**
     * Returns whether or not the application is actively recording.
     * @return {Boolean} Whether or not the application is actively
     *  recording.
     */
    getIsRecording() {
        return this.isRecording;
    }

    /**
     * Returns the current audio stream being recorded.
     * @returns {Object} The current audio stream being
     *  recorded.
     */
    getAudioStream() {
        return this.audioStream;
    }

    /**
     * Returns the context of the audio.
     * @returns {Object} The current context for the audio.
     */
    getContext() {
        return this.context;
    }

    /**
     * Emits an event that causes the application to render a sound
     * wave that corresponds to the current audio stream.
     */
    handleWaveCanvas() {
        const event = new CustomEvent('update-wave', {
            detail: {
                isRecording: this.isRecording,
                audioStream: this.audioStream,
                context: this.context,
            },

            bubbles: true,
            composed: true,
        });
        this.dispatchEvent(event);
    }

    /**
     * Emits an event that causes a new prompt to be rendered
     * on the recording page.
     */
    handleFinish() {
        const event = new CustomEvent('change-prompt', {
            bubbles: true,
            composed: true,
        });
        this.dispatchEvent(event);
    }

    render() {
        return html`
            <mwc-icon-button
                id="record-button"
                icon=${this.isRecording ? 'stop' : 'mic'}
                class=${this.isRecording ? 'recording' : ''}
                @click=${this.recordHandler}
            >
            </mwc-icon-button>
        `;
    }
}

customElements.define('vox-record-button', RecordButton);
<commit_msg>fix merge conflicts 2<commit_after>/**
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {LitElement, html} from 'lit-element';
import {Icon} from '@material/mwc-icon';
import {AudioRecorder} from '../utils/AudioRecorder';
import {UtteranceApiService} from '../utils/UtteranceApiService';
import {QualityControl} from '../utils/QualityControl';
import {dispatchErrorToast} from '../utils/ToastUtils.js';

import style from '../styles/components/RecordButton.css.js';

export class RecordButton extends LitElement {
    static get properties() {
        return {
            isRecording: {type: Boolean},
            audioStream: {type: Object},
            context: {type: Object},
        };
    }

    static get styles() {
        return style;
    }

    constructor() {
        super();
        this.isRecording = false;
        this.audioRecorder = new AudioRecorder();
        this.utteranceService = new UtteranceApiService();
    }

    updated() {
        this.handleWaveCanvas();
    }

    /**
     * If the user is not currently recording, begin recording using the Microphone
     * component. Otherwise, stop recording and save and display the just-recorded
     * audio file.
     */
    async recordHandler() {
        if (!this.isRecording) {
            try {
                await this.audioRecorder.initRecorder();
            } catch (e) {
                dispatchErrorToast(
                    this,
                    `Microphone access is currently blocked for this site.
                    To unblock, please navigate to chrome://settings/content/microphone
                    and remove this site from the 'Block' section.`
                );
                return;
            }

            if (!this.audioRecorder.startRecording()) {
                dispatchErrorToast(this, 'Failed to start recording.');
                return;
            }

            this.isRecording = true;
            this.audioStream = this.audioRecorder.stream;
            this.context = new (window.AudioContext ||
                window.webkitAudioContext)();
        } else {
            this.isRecording = false;
            let audio;

            try {
                audio = await this.audioRecorder.stopRecording();
            } catch (e) {
                dispatchErrorToast(
                    this,
                    `Could not record successfully; ${e.name}: ${e.message}`
                );
            }

            const qualityCheck = new QualityControl(this.context, audio.blob);
            const qualityResult = qualityCheck.isQualitySound();
            if (!qualityResult.success) {
                return;
            }

            if (audio.recordingUrl) {
                try {
                    const resp = await this.utteranceService.saveAudio(audio);

                    if (!resp) throw new Error();
                } catch (e) {
                    dispatchErrorToast(
                        this,
                        `Could not upload recording successfully`
                    );
                }
            }

            this.handleFinish();
        }
    }

    /**
     * Returns whether or not the application is actively recording.
     * @return {Boolean} Whether or not the application is actively
     *  recording.
     */
    getIsRecording() {
        return this.isRecording;
    }

    /**
     * Returns the current audio stream being recorded.
     * @returns {Object} The current audio stream being
     *  recorded.
     */
    getAudioStream() {
        return this.audioStream;
    }

    /**
     * Returns the context of the audio.
     * @returns {Object} The current context for the audio.
     */
    getContext() {
        return this.context;
    }

    /**
     * Emits an event that causes the application to render a sound
     * wave that corresponds to the current audio stream.
     */
    handleWaveCanvas() {
        const event = new CustomEvent('update-wave', {
            detail: {
                isRecording: this.isRecording,
                audioStream: this.audioStream,
                context: this.context,
            },

            bubbles: true,
            composed: true,
        });
        this.dispatchEvent(event);
    }

    /**
     * Emits an event that causes a new prompt to be rendered
     * on the recording page.
     */
    handleFinish() {
        const event = new CustomEvent('change-prompt', {
            bubbles: true,
            composed: true,
        });
        this.dispatchEvent(event);
    }

    render() {
        return html`
            <mwc-icon-button
                id="record-button"
                icon=${this.isRecording ? 'stop' : 'mic'}
                class=${this.isRecording ? 'recording' : ''}
                @click=${this.recordHandler}
            >
            </mwc-icon-button>
        `;
    }
}

customElements.define('vox-record-button', RecordButton);
<|endoftext|>


<commit_before>// -------------------------------------------------------------------------------------------------
// OpenSeaMap Water Depth - Web frontend for depth data handling.
//
// Written in 2012 by Dominik Fässler dfa@bezono.org
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
// -------------------------------------------------------------------------------------------------

OSeaM.models.Vessels = Backbone.Collection.extend({
    model: OSeaM.models.Vessel,
    url: OSeaM.apiUrl + 'vesselconfig',
    parse: function(response) {
    	var vessels = new Array();
        for (var i = 0; i < response.length; i++) {
        	var responseObject = response[i];

        	var newsbasoffset = new OSeaM.models.Offset();
        	var x = responseObject.sbasoffset;
        	newsbasoffset.set('distanceFromStern', x['distanceFromStern']);
        	newsbasoffset.set('distanceFromCenter', x['distanceFromCenter']);
        	newsbasoffset.set('distanceWaterline', x['distanceWaterline']);
        	newsbasoffset.set('manufacturer', x['manufacturer']);
        	newsbasoffset.set('model', x['model']);

        	var newdepthoffset = new OSeaM.models.Offset();
        	var y = responseObject.depthoffset;
        	newdepthoffset.set('distanceFromStern', y['distanceFromStern']);
        	newdepthoffset.set('distanceFromCenter', y['distanceFromCenter']);
        	newdepthoffset.set('distanceWaterline', y['distanceWaterline']);
			newdepthoffset.set('offsetKeel', y['offsetKeel']);
			newdepthoffset.set('manufacturer', x['manufacturer']);
			newdepthoffset.set('model', x['model']);

        	var vessel = new OSeaM.models.Vessel({
                id       : responseObject.id,
                name : responseObject.name,
                description : responseObject.description,
                manufacturer : responseObject.manufacturer,
                model : responseObject.model,
                loa : responseObject.loa,
                breadth : responseObject.breadth,
                draft : responseObject.draft,
                height : responseObject.height,
                displacement : responseObject.displacement,
                maximumspeed : responseObject.maximumspeed,
                sbasoffset : newsbasoffset,
                depthoffset : newdepthoffset
            });
        	vessels[i] = vessel;
        }
    	return vessels;
    }
});
<commit_msg>F: Fill model values correctly<commit_after>// -------------------------------------------------------------------------------------------------
// OpenSeaMap Water Depth - Web frontend for depth data handling.
//
// Written in 2012 by Dominik Fässler dfa@bezono.org
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
// -------------------------------------------------------------------------------------------------

OSeaM.models.Vessels = Backbone.Collection.extend({
    model: OSeaM.models.Vessel,
    url: OSeaM.apiUrl + 'vesselconfig',
    parse: function(response) {
    	var vessels = new Array();
        for (var i = 0; i < response.length; i++) {
        	var responseObject = response[i];

        	var newsbasoffset = new OSeaM.models.Offset();
        	var x = responseObject.sbasoffset;
        	newsbasoffset.set('distanceFromStern', x['distanceFromStern']);
        	newsbasoffset.set('distanceFromCenter', x['distanceFromCenter']);
        	newsbasoffset.set('distanceWaterline', x['distanceWaterline']);
        	newsbasoffset.set('manufacturer', x['manufacturer']);
        	newsbasoffset.set('model', x['model']);

        	var newdepthoffset = new OSeaM.models.Offset();
        	var y = responseObject.depthoffset;
        	newdepthoffset.set('distanceFromStern', y['distanceFromStern']);
        	newdepthoffset.set('distanceFromCenter', y['distanceFromCenter']);
        	newdepthoffset.set('distanceWaterline', y['distanceWaterline']);
			newdepthoffset.set('offsetKeel', y['offsetKeel']);
			newdepthoffset.set('manufacturer', y['manufacturer']);
			newdepthoffset.set('model', y['model']);

        	var vessel = new OSeaM.models.Vessel({
                id       : responseObject.id,
                name : responseObject.name,
                description : responseObject.description,
                manufacturer : responseObject.manufacturer,
                model : responseObject.model,
                loa : responseObject.loa,
                breadth : responseObject.breadth,
                draft : responseObject.draft,
                height : responseObject.height,
                displacement : responseObject.displacement,
                maximumspeed : responseObject.maximumspeed,
                sbasoffset : newsbasoffset,
                depthoffset : newdepthoffset
            });
        	vessels[i] = vessel;
        }
    	return vessels;
    }
});
<|endoftext|>


<commit_before>import Vue from 'vue'

import QIcon from '../icon/QIcon.js'
import QSpinner from '../spinner/QSpinner.js'

import ValidateMixin from '../../mixins/validate.js'
import slot from '../../utils/slot.js'
import { stop, prevent } from '../../utils/event.js'
import uid from '../../utils/uid.js'

export default Vue.extend({
  name: 'QField',

  inheritAttrs: false,

  mixins: [ ValidateMixin ],

  props: {
    label: String,
    stackLabel: Boolean,
    hint: String,
    hideHint: Boolean,
    prefix: String,
    suffix: String,

    color: String,
    bgColor: String,
    dark: Boolean,

    filled: Boolean,
    outlined: Boolean,
    borderless: Boolean,
    standout: [Boolean, String],

    square: Boolean,

    loading: Boolean,

    bottomSlots: Boolean,
    hideBottomSpace: Boolean,

    rounded: Boolean,
    dense: Boolean,
    itemAligned: Boolean,

    counter: Boolean,

    clearable: Boolean,
    clearIcon: String,

    disable: Boolean,
    readonly: Boolean,

    autofocus: Boolean,

    maxlength: [Number, String],
    maxValues: [Number, String] // private, do not add to JSON; internally needed by QSelect
  },

  data () {
    return {
      focused: false,

      // used internally by validation for QInput
      // or menu handling for QSelect
      innerLoading: false,
      targetUid: this.$attrs.for === void 0 ? 'qf_' + uid() : this.$attrs.for
    }
  },

  computed: {
    editable () {
      return this.disable !== true && this.readonly !== true
    },

    hasValue () {
      const value = this.__getControl === void 0 ? this.value : this.innerValue

      return value !== void 0 &&
        value !== null &&
        ('' + value).length > 0
    },

    computedCounter () {
      if (this.counter !== false) {
        const len = typeof this.value === 'string' || typeof this.value === 'number'
          ? ('' + this.value).length
          : (Array.isArray(this.value) === true ? this.value.length : 0)
        const max = this.maxlength !== void 0 ? this.maxlength : this.maxValues

        return len + (max !== void 0 ? ' / ' + max : '')
      }
    },

    floatingLabel () {
      return this.hasError === true ||
        this.stackLabel === true ||
        this.focused === true ||
        (
          this.inputValue !== void 0 && this.hideSelected === true
            ? this.inputValue.length > 0
            : this.hasValue === true
        ) ||
        (
          this.displayValue !== void 0 &&
          this.displayValue !== null &&
          ('' + this.displayValue).length > 0
        )
    },

    shouldRenderBottom () {
      return this.bottomSlots === true ||
        this.hint !== void 0 ||
        this.rules !== void 0 ||
        this.counter === true ||
        this.error !== null
    },

    classes () {
      return {
        [this.fieldClass]: this.fieldClass !== void 0,

        [`q-field--${this.styleType}`]: true,
        'q-field--rounded': this.rounded,
        'q-field--square': this.square,

        'q-field--focused': this.focused === true || this.hasError === true,
        'q-field--float': this.floatingLabel,
        'q-field--labeled': this.label !== void 0,

        'q-field--dense': this.dense,
        'q-field--item-aligned q-item-type': this.itemAligned,
        'q-field--dark': this.dark,

        'q-field--auto-height': this.__getControl === void 0,

        'q-field--with-bottom': this.hideBottomSpace !== true && this.shouldRenderBottom === true,
        'q-field--error': this.hasError,

        'q-field--readonly': this.readonly === true && this.disable !== true,
        'q-field--disabled': this.disable
      }
    },

    styleType () {
      if (this.filled === true) { return 'filled' }
      if (this.outlined === true) { return 'outlined' }
      if (this.borderless === true) { return 'borderless' }
      if (this.standout) { return 'standout' }
      return 'standard'
    },

    contentClass () {
      const cls = []

      if (this.hasError === true) {
        cls.push('text-negative')
      }
      else if (typeof this.standout === 'string' && this.standout.length > 0 && this.focused === true) {
        return this.standout
      }
      else if (this.color !== void 0) {
        cls.push('text-' + this.color)
      }

      if (this.bgColor !== void 0) {
        cls.push(`bg-${this.bgColor}`)
      }

      return cls
    },

    controlSlotScope () {
      return {
        id: this.targetUid,
        field: this.$el,
        editable: this.editable,
        focused: this.focused,
        floatingLabel: this.floatingLabel,
        value: this.value,
        emitValue: this.__emitValue
      }
    }
  },

  methods: {
    focus () {
      if (this.showPopup !== void 0 && this.hasDialog === true) {
        this.showPopup()
        return
      }

      this.__focus()
    },

    blur () {
      const el = document.activeElement
      // IE can have null document.activeElement
      if (el !== null && this.$el.contains(el)) {
        el.blur()
      }
    },

    __focus () {
      const el = document.activeElement
      let target = this.$refs.target
      // IE can have null document.activeElement
      if (target !== void 0 && (el === null || el.id !== this.targetUid)) {
        target.matches('[tabindex]') || (target = target.querySelector('[tabindex]'))
        target !== null && target !== el && target.focus()
      }
    },

    __getContent (h) {
      const node = []

      this.$scopedSlots.prepend !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__prepend q-field__marginal row no-wrap items-center',
          key: 'prepend'
        }, this.$scopedSlots.prepend())
      )

      node.push(
        h('div', {
          staticClass: 'q-field__control-container col relative-position row no-wrap q-anchor--skip'
        }, this.__getControlContainer(h))
      )

      this.$scopedSlots.append !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__append q-field__marginal row no-wrap items-center',
          key: 'append'
        }, this.$scopedSlots.append())
      )

      this.hasError === true && this.noErrorIcon === false && node.push(
        this.__getInnerAppendNode(h, 'error', [
          h(QIcon, { props: { name: this.$q.iconSet.field.error, color: 'negative' } })
        ])
      )

      if (this.loading === true || this.innerLoading === true) {
        node.push(
          this.__getInnerAppendNode(
            h,
            'inner-loading-append',
            this.$scopedSlots.loading !== void 0
              ? this.$scopedSlots.loading()
              : [ h(QSpinner, { props: { color: this.color } }) ]
          )
        )
      }
      else if (this.clearable === true && this.hasValue === true && this.editable === true) {
        node.push(
          this.__getInnerAppendNode(h, 'inner-clearable-append', [
            h(QIcon, {
              staticClass: 'cursor-pointer',
              props: { name: this.clearIcon || this.$q.iconSet.field.clear },
              on: {
                click: this.__clearValue
              }
            })
          ])
        )
      }

      this.__getInnerAppend !== void 0 && node.push(
        this.__getInnerAppendNode(h, 'inner-append', this.__getInnerAppend(h))
      )

      this.__getPopup !== void 0 && node.push(
        this.__getPopup(h)
      )

      return node
    },

    __getControlContainer (h) {
      const node = []

      this.prefix !== void 0 && this.prefix !== null && node.push(
        h('div', {
          staticClass: 'q-field__prefix no-pointer-events row items-center'
        }, [ this.prefix ])
      )

      if (this.__getControl !== void 0) {
        node.push(
          this.__getControl(h)
        )
      }
      // internal usage only:
      else if (this.$scopedSlots.rawControl !== void 0) {
        node.push(this.$scopedSlots.rawControl())
      }
      else if (this.$scopedSlots.control !== void 0) {
        node.push(
          h('div', {
            ref: 'target',
            staticClass: 'q-field__native row',
            attrs: {
              ...this.$attrs,
              autofocus: this.autofocus
            }
          }, this.$scopedSlots.control(this.controlSlotScope))
        )
      }

      this.label !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__label no-pointer-events absolute ellipsis'
        }, [ this.label ])
      )

      this.suffix !== void 0 && this.suffix !== null && node.push(
        h('div', {
          staticClass: 'q-field__suffix no-pointer-events row items-center'
        }, [ this.suffix ])
      )

      return node.concat(
        this.__getDefaultSlot !== void 0
          ? this.__getDefaultSlot(h)
          : slot(this, 'default')
      )
    },

    __getBottom (h) {
      let msg, key

      if (this.hasError === true) {
        if (this.computedErrorMessage !== void 0) {
          msg = [ h('div', [ this.computedErrorMessage ]) ]
          key = this.computedErrorMessage
        }
        else {
          msg = slot(this, 'error')
          key = 'q--slot-error'
        }
      }
      else if (this.hideHint !== true || this.focused === true) {
        if (this.hint !== void 0) {
          msg = [ h('div', [ this.hint ]) ]
          key = this.hint
        }
        else {
          msg = slot(this, 'hint')
          key = 'q--slot-hint'
        }
      }

      const hasCounter = this.counter === true || this.$scopedSlots.counter !== void 0

      if (this.hideBottomSpace === true && hasCounter === false && msg === void 0) {
        return
      }

      const main = h('div', {
        key,
        staticClass: 'q-field__messages col'
      }, msg)

      return h('div', {
        staticClass: 'q-field__bottom row items-start q-field__bottom--' +
          (this.hideBottomSpace !== true ? 'animated' : 'stale')
      }, [
        this.hideBottomSpace === true
          ? main
          : h('transition', { props: { name: 'q-transition--field-message' } }, [
            main
          ]),

        hasCounter === true
          ? h('div', {
            staticClass: 'q-field__counter'
          }, this.$scopedSlots.counter !== void 0 ? this.$scopedSlots.counter() : [ this.computedCounter ])
          : null
      ])
    },

    __getInnerAppendNode (h, key, content) {
      return content === null ? null : h('div', {
        staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',
        key
      }, content)
    },

    __onControlPopupShow (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-show', e)
      this.hasPopupOpen = true
      this.__onControlFocusin(e)
    },

    __onControlPopupHide (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-hide', e)
      this.hasPopupOpen = false
      this.__onControlFocusout(e)
    },

    __onControlFocusin (e) {
      if (this.editable === true && this.focused === false) {
        this.focused = true
        this.$emit('focus', e)
      }
    },

    __onControlFocusout (e, then) {
      clearTimeout(this.focusoutTimer)
      this.focusoutTimer = setTimeout(() => {
        if (
          document.hasFocus() === true && (
            this.hasPopupOpen === true ||
            this.$refs === void 0 ||
            this.$refs.control === void 0 ||
            this.$refs.control.contains(document.activeElement) !== false
          )
        ) {
          return
        }

        if (this.focused === true) {
          this.focused = false
          this.$emit('blur', e)
        }

        then !== void 0 && then()
      })
    },

    __clearValue (e) {
      stop(e)
      if (this.type === 'file') {
        // do not let focus be triggered
        // as it will make the native file dialog
        // appear for another selection
        prevent(e)
        this.$refs.input.value = null
      }
      this.$emit('input', null)
      this.$emit('clear', this.value)
    },

    __emitValue (value) {
      this.$emit('input', value)
    }
  },

  render (h) {
    this.__onPreRender !== void 0 && this.__onPreRender()
    this.__onPostRender !== void 0 && this.$nextTick(this.__onPostRender)

    return h('label', {
      staticClass: 'q-field row no-wrap items-start',
      class: this.classes,
      attrs: {
        for: this.targetUid
      }
    }, [
      this.$scopedSlots.before !== void 0 ? h('div', {
        staticClass: 'q-field__before q-field__marginal row no-wrap items-center'
      }, this.$scopedSlots.before()) : null,

      h('div', {
        staticClass: 'q-field__inner relative-position col self-stretch column justify-center'
      }, [
        h('div', {
          ref: 'control',
          staticClass: 'q-field__control relative-position row no-wrap',
          class: this.contentClass,
          attrs: { tabindex: -1 },
          on: this.controlEvents
        }, this.__getContent(h)),

        this.shouldRenderBottom === true
          ? this.__getBottom(h)
          : null
      ]),

      this.$scopedSlots.after !== void 0 ? h('div', {
        staticClass: 'q-field__after q-field__marginal row no-wrap items-center'
      }, this.$scopedSlots.after()) : null
    ])
  },

  created () {
    this.__onPreRender !== void 0 && this.__onPreRender()

    this.controlEvents = this.__getControlEvents !== void 0
      ? this.__getControlEvents()
      : {
        focus: this.focus,
        focusin: this.__onControlFocusin,
        focusout: this.__onControlFocusout,
        'popup-show': this.__onControlPopupShow,
        'popup-hide': this.__onControlPopupHide
      }
  },

  mounted () {
    this.autofocus === true && this.$el.focus()
  },

  beforeDestroy () {
    clearTimeout(this.focusoutTimer)
  }
})
<commit_msg>fix(QField): Use component's focus method on mounted if autofocus (#5263)<commit_after>import Vue from 'vue'

import QIcon from '../icon/QIcon.js'
import QSpinner from '../spinner/QSpinner.js'

import ValidateMixin from '../../mixins/validate.js'
import slot from '../../utils/slot.js'
import { stop, prevent } from '../../utils/event.js'
import uid from '../../utils/uid.js'

export default Vue.extend({
  name: 'QField',

  inheritAttrs: false,

  mixins: [ ValidateMixin ],

  props: {
    label: String,
    stackLabel: Boolean,
    hint: String,
    hideHint: Boolean,
    prefix: String,
    suffix: String,

    color: String,
    bgColor: String,
    dark: Boolean,

    filled: Boolean,
    outlined: Boolean,
    borderless: Boolean,
    standout: [Boolean, String],

    square: Boolean,

    loading: Boolean,

    bottomSlots: Boolean,
    hideBottomSpace: Boolean,

    rounded: Boolean,
    dense: Boolean,
    itemAligned: Boolean,

    counter: Boolean,

    clearable: Boolean,
    clearIcon: String,

    disable: Boolean,
    readonly: Boolean,

    autofocus: Boolean,

    maxlength: [Number, String],
    maxValues: [Number, String] // private, do not add to JSON; internally needed by QSelect
  },

  data () {
    return {
      focused: false,

      // used internally by validation for QInput
      // or menu handling for QSelect
      innerLoading: false,
      targetUid: this.$attrs.for === void 0 ? 'qf_' + uid() : this.$attrs.for
    }
  },

  computed: {
    editable () {
      return this.disable !== true && this.readonly !== true
    },

    hasValue () {
      const value = this.__getControl === void 0 ? this.value : this.innerValue

      return value !== void 0 &&
        value !== null &&
        ('' + value).length > 0
    },

    computedCounter () {
      if (this.counter !== false) {
        const len = typeof this.value === 'string' || typeof this.value === 'number'
          ? ('' + this.value).length
          : (Array.isArray(this.value) === true ? this.value.length : 0)
        const max = this.maxlength !== void 0 ? this.maxlength : this.maxValues

        return len + (max !== void 0 ? ' / ' + max : '')
      }
    },

    floatingLabel () {
      return this.hasError === true ||
        this.stackLabel === true ||
        this.focused === true ||
        (
          this.inputValue !== void 0 && this.hideSelected === true
            ? this.inputValue.length > 0
            : this.hasValue === true
        ) ||
        (
          this.displayValue !== void 0 &&
          this.displayValue !== null &&
          ('' + this.displayValue).length > 0
        )
    },

    shouldRenderBottom () {
      return this.bottomSlots === true ||
        this.hint !== void 0 ||
        this.rules !== void 0 ||
        this.counter === true ||
        this.error !== null
    },

    classes () {
      return {
        [this.fieldClass]: this.fieldClass !== void 0,

        [`q-field--${this.styleType}`]: true,
        'q-field--rounded': this.rounded,
        'q-field--square': this.square,

        'q-field--focused': this.focused === true || this.hasError === true,
        'q-field--float': this.floatingLabel,
        'q-field--labeled': this.label !== void 0,

        'q-field--dense': this.dense,
        'q-field--item-aligned q-item-type': this.itemAligned,
        'q-field--dark': this.dark,

        'q-field--auto-height': this.__getControl === void 0,

        'q-field--with-bottom': this.hideBottomSpace !== true && this.shouldRenderBottom === true,
        'q-field--error': this.hasError,

        'q-field--readonly': this.readonly === true && this.disable !== true,
        'q-field--disabled': this.disable
      }
    },

    styleType () {
      if (this.filled === true) { return 'filled' }
      if (this.outlined === true) { return 'outlined' }
      if (this.borderless === true) { return 'borderless' }
      if (this.standout) { return 'standout' }
      return 'standard'
    },

    contentClass () {
      const cls = []

      if (this.hasError === true) {
        cls.push('text-negative')
      }
      else if (typeof this.standout === 'string' && this.standout.length > 0 && this.focused === true) {
        return this.standout
      }
      else if (this.color !== void 0) {
        cls.push('text-' + this.color)
      }

      if (this.bgColor !== void 0) {
        cls.push(`bg-${this.bgColor}`)
      }

      return cls
    },

    controlSlotScope () {
      return {
        id: this.targetUid,
        field: this.$el,
        editable: this.editable,
        focused: this.focused,
        floatingLabel: this.floatingLabel,
        value: this.value,
        emitValue: this.__emitValue
      }
    }
  },

  methods: {
    focus () {
      if (this.showPopup !== void 0 && this.hasDialog === true) {
        this.showPopup()
        return
      }

      this.__focus()
    },

    blur () {
      const el = document.activeElement
      // IE can have null document.activeElement
      if (el !== null && this.$el.contains(el)) {
        el.blur()
      }
    },

    __focus () {
      const el = document.activeElement
      let target = this.$refs.target
      // IE can have null document.activeElement
      if (target !== void 0 && (el === null || el.id !== this.targetUid)) {
        target.matches('[tabindex]') || (target = target.querySelector('[tabindex]'))
        target !== null && target !== el && target.focus()
      }
    },

    __getContent (h) {
      const node = []

      this.$scopedSlots.prepend !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__prepend q-field__marginal row no-wrap items-center',
          key: 'prepend'
        }, this.$scopedSlots.prepend())
      )

      node.push(
        h('div', {
          staticClass: 'q-field__control-container col relative-position row no-wrap q-anchor--skip'
        }, this.__getControlContainer(h))
      )

      this.$scopedSlots.append !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__append q-field__marginal row no-wrap items-center',
          key: 'append'
        }, this.$scopedSlots.append())
      )

      this.hasError === true && this.noErrorIcon === false && node.push(
        this.__getInnerAppendNode(h, 'error', [
          h(QIcon, { props: { name: this.$q.iconSet.field.error, color: 'negative' } })
        ])
      )

      if (this.loading === true || this.innerLoading === true) {
        node.push(
          this.__getInnerAppendNode(
            h,
            'inner-loading-append',
            this.$scopedSlots.loading !== void 0
              ? this.$scopedSlots.loading()
              : [ h(QSpinner, { props: { color: this.color } }) ]
          )
        )
      }
      else if (this.clearable === true && this.hasValue === true && this.editable === true) {
        node.push(
          this.__getInnerAppendNode(h, 'inner-clearable-append', [
            h(QIcon, {
              staticClass: 'cursor-pointer',
              props: { name: this.clearIcon || this.$q.iconSet.field.clear },
              on: {
                click: this.__clearValue
              }
            })
          ])
        )
      }

      this.__getInnerAppend !== void 0 && node.push(
        this.__getInnerAppendNode(h, 'inner-append', this.__getInnerAppend(h))
      )

      this.__getPopup !== void 0 && node.push(
        this.__getPopup(h)
      )

      return node
    },

    __getControlContainer (h) {
      const node = []

      this.prefix !== void 0 && this.prefix !== null && node.push(
        h('div', {
          staticClass: 'q-field__prefix no-pointer-events row items-center'
        }, [ this.prefix ])
      )

      if (this.__getControl !== void 0) {
        node.push(
          this.__getControl(h)
        )
      }
      // internal usage only:
      else if (this.$scopedSlots.rawControl !== void 0) {
        node.push(this.$scopedSlots.rawControl())
      }
      else if (this.$scopedSlots.control !== void 0) {
        node.push(
          h('div', {
            ref: 'target',
            staticClass: 'q-field__native row',
            attrs: {
              ...this.$attrs,
              autofocus: this.autofocus
            }
          }, this.$scopedSlots.control(this.controlSlotScope))
        )
      }

      this.label !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__label no-pointer-events absolute ellipsis'
        }, [ this.label ])
      )

      this.suffix !== void 0 && this.suffix !== null && node.push(
        h('div', {
          staticClass: 'q-field__suffix no-pointer-events row items-center'
        }, [ this.suffix ])
      )

      return node.concat(
        this.__getDefaultSlot !== void 0
          ? this.__getDefaultSlot(h)
          : slot(this, 'default')
      )
    },

    __getBottom (h) {
      let msg, key

      if (this.hasError === true) {
        if (this.computedErrorMessage !== void 0) {
          msg = [ h('div', [ this.computedErrorMessage ]) ]
          key = this.computedErrorMessage
        }
        else {
          msg = slot(this, 'error')
          key = 'q--slot-error'
        }
      }
      else if (this.hideHint !== true || this.focused === true) {
        if (this.hint !== void 0) {
          msg = [ h('div', [ this.hint ]) ]
          key = this.hint
        }
        else {
          msg = slot(this, 'hint')
          key = 'q--slot-hint'
        }
      }

      const hasCounter = this.counter === true || this.$scopedSlots.counter !== void 0

      if (this.hideBottomSpace === true && hasCounter === false && msg === void 0) {
        return
      }

      const main = h('div', {
        key,
        staticClass: 'q-field__messages col'
      }, msg)

      return h('div', {
        staticClass: 'q-field__bottom row items-start q-field__bottom--' +
          (this.hideBottomSpace !== true ? 'animated' : 'stale')
      }, [
        this.hideBottomSpace === true
          ? main
          : h('transition', { props: { name: 'q-transition--field-message' } }, [
            main
          ]),

        hasCounter === true
          ? h('div', {
            staticClass: 'q-field__counter'
          }, this.$scopedSlots.counter !== void 0 ? this.$scopedSlots.counter() : [ this.computedCounter ])
          : null
      ])
    },

    __getInnerAppendNode (h, key, content) {
      return content === null ? null : h('div', {
        staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',
        key
      }, content)
    },

    __onControlPopupShow (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-show', e)
      this.hasPopupOpen = true
      this.__onControlFocusin(e)
    },

    __onControlPopupHide (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-hide', e)
      this.hasPopupOpen = false
      this.__onControlFocusout(e)
    },

    __onControlFocusin (e) {
      if (this.editable === true && this.focused === false) {
        this.focused = true
        this.$emit('focus', e)
      }
    },

    __onControlFocusout (e, then) {
      clearTimeout(this.focusoutTimer)
      this.focusoutTimer = setTimeout(() => {
        if (
          document.hasFocus() === true && (
            this.hasPopupOpen === true ||
            this.$refs === void 0 ||
            this.$refs.control === void 0 ||
            this.$refs.control.contains(document.activeElement) !== false
          )
        ) {
          return
        }

        if (this.focused === true) {
          this.focused = false
          this.$emit('blur', e)
        }

        then !== void 0 && then()
      })
    },

    __clearValue (e) {
      stop(e)
      if (this.type === 'file') {
        // do not let focus be triggered
        // as it will make the native file dialog
        // appear for another selection
        prevent(e)
        this.$refs.input.value = null
      }
      this.$emit('input', null)
      this.$emit('clear', this.value)
    },

    __emitValue (value) {
      this.$emit('input', value)
    }
  },

  render (h) {
    this.__onPreRender !== void 0 && this.__onPreRender()
    this.__onPostRender !== void 0 && this.$nextTick(this.__onPostRender)

    return h('label', {
      staticClass: 'q-field row no-wrap items-start',
      class: this.classes,
      attrs: {
        for: this.targetUid
      }
    }, [
      this.$scopedSlots.before !== void 0 ? h('div', {
        staticClass: 'q-field__before q-field__marginal row no-wrap items-center'
      }, this.$scopedSlots.before()) : null,

      h('div', {
        staticClass: 'q-field__inner relative-position col self-stretch column justify-center'
      }, [
        h('div', {
          ref: 'control',
          staticClass: 'q-field__control relative-position row no-wrap',
          class: this.contentClass,
          attrs: { tabindex: -1 },
          on: this.controlEvents
        }, this.__getContent(h)),

        this.shouldRenderBottom === true
          ? this.__getBottom(h)
          : null
      ]),

      this.$scopedSlots.after !== void 0 ? h('div', {
        staticClass: 'q-field__after q-field__marginal row no-wrap items-center'
      }, this.$scopedSlots.after()) : null
    ])
  },

  created () {
    this.__onPreRender !== void 0 && this.__onPreRender()

    this.controlEvents = this.__getControlEvents !== void 0
      ? this.__getControlEvents()
      : {
        focus: this.focus,
        focusin: this.__onControlFocusin,
        focusout: this.__onControlFocusout,
        'popup-show': this.__onControlPopupShow,
        'popup-hide': this.__onControlPopupHide
      }
  },

  mounted () {
    this.autofocus === true && this.focus()
  },

  beforeDestroy () {
    clearTimeout(this.focusoutTimer)
  }
})
<|endoftext|>


<commit_before>import React from 'react';
import classNames from 'classnames';

import UserAvatar from './../shared/UserAvatar';
import Button from './../shared/Button';
import Dropdown from './../shared/Dropdown';
import Badge from './../shared/Badge';
import AgentsCount from './../organization/AgentsCount';
import BuildsCountBadge from './../user/BuildsCountBadge';
import NewChangelogsBadge from './../user/NewChangelogsBadge';

const buttonClassNames = "btn black hover-lime focus-lime flex flex-center flex-none semi-bold";

const NavigationButton = (props) => {
  return (
    {props.children}
  );
}

NavigationButton.propTypes = {
  style: React.PropTypes.object,
  className: React.PropTypes.string,
  href: React.PropTypes.string,
  children: React.PropTypes.node
};

const DropdownButton = (props) => {
  return (
    <button style={props.style} className={buttonClassNames + " " + props.className}>{props.children}</button>
  );
}

DropdownButton.propTypes = {
  style: React.PropTypes.object,
  className: React.PropTypes.string,
  children: React.PropTypes.node
};

class Navigation extends React.Component {
  static propTypes = {
    organization: React.PropTypes.object,
    viewer: React.PropTypes.shape({
      user: React.PropTypes.shape({
        name: React.PropTypes.string.isRequired,
        avatar: React.PropTypes.shape({
          url: React.PropTypes.string.isRequired
        })
      }),
      organizations: React.PropTypes.shape({
        edges: React.PropTypes.array
      }),
      unreadChangelogs: React.PropTypes.shape({
        count: React.PropTypes.integer
      })
    })
  };

  state = {
    showingOrgDropdown: false,
    showingUserDropdown: false
  };

  render() {
    return (

              <NavigationButton href="/" className="px3 hover-faded-children" style={{paddingLeft: 0, paddingTop: 0, paddingBottom: 0}}>

              </NavigationButton>
              {this._newChangelogsBadge()}

            <Dropdown align="left" width={250} className="flex" style={{ minWidth: "5em"}} onToggle={this.handleOrgDropdownToggle}>
              <DropdownButton className={classNames({ "lime": this.state.showingOrgDropdown })}
                              style={{
                                backgroundImage: 'url(' + require('../../images/nav-button-right-arrow.svg') + ')',
                                backgroundRepeat: 'no-repeat',
                                backgroundPosition: 'center right',
                                paddingRight: 20}}>

                  {this._organizationSelectorLabel()}

                  ▾

              </DropdownButton>
              {this._organizationsList()}
              <NavigationButton href="/organizations/new" className="block">Create New Organization</NavigationButton>
            </Dropdown>

            {this._topOrganizationMenu()}

            <NavigationButton className="md-flex" href={`/builds`}>My Builds <BuildsCountBadge className="hover-lime-child" viewer={this.props.viewer} /></NavigationButton>
            <NavigationButton className="md-flex" href={`/docs`}>Documentation</NavigationButton>
            <NavigationButton className="md-flex" href="mailto:support@buildkite.com">Support</NavigationButton>

            <Dropdown align="right" width={170} className="flex" onToggle={this.handleUserDropdownToggle}>
              <DropdownButton className={classNames({ "lime": this.state.showingUserDropdown })}
                              style={{paddingRight: 0, paddingLeft: '1rem'}}>
                <UserAvatar user={this.props.viewer.user} className="flex-none flex flex-center" style={{width: 26, height: 26, marginRight: '.6rem'}} />
                {this.props.viewer.user.name}

                  ▾

              </DropdownButton>

              <NavigationButton className="md-hide" href={`/builds`}>My Builds <BuildsCountBadge className="hover-lime-child" viewer={this.props.viewer} /></NavigationButton>
              <NavigationButton href="/user/settings">Personal Settings</NavigationButton>

                <NavigationButton className="md-hide" href={`/docs`}>Documentation</NavigationButton>
                <NavigationButton className="md-hide" href="mailto:support@buildkite.com">Support</NavigationButton>

              <Button action="/logout" method="delete" className="black hover-lime focus-lime block left-align" style={{width: "100%"}}>Logout</Button>
            </Dropdown>

        {this._bottomOrganizationMenu()}

    );
  }

  _newChangelogsBadge() {
    return (
      <NewChangelogsBadge className="mr2 relative" style={{ top: -5, marginLeft: -8 }} viewer={this.props.viewer} />
    )
  }

  _topOrganizationMenu() {
    if (this.props.organization) {
      return (

          {this._organizationMenu()}

      )
    }
  }

  _bottomOrganizationMenu() {
    if (this.props.organization) {
      return (

            {this._organizationMenu({ paddingLeft: 0 })}

      )
    }
  }

  handleOrgDropdownToggle = (visible) => {
    this.setState({ showingOrgDropdown: visible });
  };

  handleUserDropdownToggle = (visible) => {
    this.setState({ showingUserDropdown: visible });
  };

  _organizationSelectorLabel() {
    if(this.props.organization) {
      return this.props.organization.name;
    } else {
      return "Organizations"
    }
  }

  _organizationsList() {
    let nodes = [];
    this.props.viewer.organizations.edges.forEach((org) => {
      // Don't show the active organization in the selector
      if(!this.props.organization || (org.node.slug != this.props.organization.slug)) {
        nodes.push(<NavigationButton key={org.node.slug} href={`/${org.node.slug}`} className="block">{org.node.name}</NavigationButton>);
      }
    });

    if(nodes.length > 0) {
      return nodes;
    } else {
      return You’ve no other organizations
    }
  }

  _organizationMenu(options = {}) {
    var organization = this.props.organization;
    var paddingLeft = options.paddingLeft != undefined ? options.paddingLeft : 15;

    // Find the first settings permission that we're allowed to perform
    let permission;
    for(let name in organization.permissions) {
      let allowed = organization.permissions[name].allowed;
      if(allowed) {
        permission = name;
      }
    }

    // If there was a settings permission, show the button
    let settingsButton;
    if(permission) {
      settingsButton = (
        <NavigationButton href={`/organizations/${organization.slug}/settings`}>Settings</NavigationButton>
      );
    }

    if(organization) {
      return (

          <NavigationButton style={{ paddingLeft: paddingLeft }} href={`/${organization.slug}`}>Pipelines</NavigationButton>
          <NavigationButton href={`/organizations/${organization.slug}/agents`}>
            {'Agents'}
            <Badge className="hover-lime-child"><AgentsCount organization={organization} /></Badge>
          </NavigationButton>
          {settingsButton}

      )
    }
  }
}

export default Navigation;
<commit_msg>Fix new nav dropdown for new orgs<commit_after>import React from 'react';
import classNames from 'classnames';

import UserAvatar from './../shared/UserAvatar';
import Button from './../shared/Button';
import Dropdown from './../shared/Dropdown';
import Badge from './../shared/Badge';
import AgentsCount from './../organization/AgentsCount';
import BuildsCountBadge from './../user/BuildsCountBadge';
import NewChangelogsBadge from './../user/NewChangelogsBadge';

const buttonClassNames = "btn black hover-lime focus-lime flex flex-center flex-none semi-bold";

const NavigationButton = (props) => {
  return (
    {props.children}
  );
}

NavigationButton.propTypes = {
  style: React.PropTypes.object,
  className: React.PropTypes.string,
  href: React.PropTypes.string,
  children: React.PropTypes.node
};

const DropdownButton = (props) => {
  return (
    <button style={props.style} className={buttonClassNames + " " + props.className}>{props.children}</button>
  );
}

DropdownButton.propTypes = {
  style: React.PropTypes.object,
  className: React.PropTypes.string,
  children: React.PropTypes.node
};

class Navigation extends React.Component {
  static propTypes = {
    organization: React.PropTypes.object,
    viewer: React.PropTypes.shape({
      user: React.PropTypes.shape({
        name: React.PropTypes.string.isRequired,
        avatar: React.PropTypes.shape({
          url: React.PropTypes.string.isRequired
        })
      }),
      organizations: React.PropTypes.shape({
        edges: React.PropTypes.array
      }),
      unreadChangelogs: React.PropTypes.shape({
        count: React.PropTypes.integer
      })
    })
  };

  state = {
    showingOrgDropdown: false,
    showingUserDropdown: false
  };

  render() {
    return (

              <NavigationButton href="/" className="px3 hover-faded-children" style={{paddingLeft: 0, paddingTop: 0, paddingBottom: 0}}>

              </NavigationButton>
              {this._newChangelogsBadge()}

            <Dropdown align="left" width={250} className="flex" style={{ minWidth: "5em"}} onToggle={this.handleOrgDropdownToggle}>
              <DropdownButton className={classNames({ "lime": this.state.showingOrgDropdown })}
                              style={{
                                backgroundImage: 'url(' + require('../../images/nav-button-right-arrow.svg') + ')',
                                backgroundRepeat: 'no-repeat',
                                backgroundPosition: 'center right',
                                paddingRight: 20}}>

                  {this._organizationSelectorLabel()}

                  ▾

              </DropdownButton>
              {this._organizationsList()}
              <NavigationButton href="/organizations/new" className="block">Create New Organization</NavigationButton>
            </Dropdown>

            {this._topOrganizationMenu()}

            <NavigationButton className="md-flex" href={`/builds`}>My Builds <BuildsCountBadge className="hover-lime-child" viewer={this.props.viewer} /></NavigationButton>
            <NavigationButton className="md-flex" href={`/docs`}>Documentation</NavigationButton>
            <NavigationButton className="md-flex" href="mailto:support@buildkite.com">Support</NavigationButton>

            <Dropdown align="right" width={170} className="flex" onToggle={this.handleUserDropdownToggle}>
              <DropdownButton className={classNames({ "lime": this.state.showingUserDropdown })}
                              style={{paddingRight: 0, paddingLeft: '1rem'}}>
                <UserAvatar user={this.props.viewer.user} className="flex-none flex flex-center" style={{width: 26, height: 26, marginRight: '.6rem'}} />
                {this.props.viewer.user.name}

                  ▾

              </DropdownButton>

              <NavigationButton className="md-hide" href={`/builds`}>My Builds <BuildsCountBadge className="hover-lime-child" viewer={this.props.viewer} /></NavigationButton>
              <NavigationButton href="/user/settings">Personal Settings</NavigationButton>

                <NavigationButton className="md-hide" href={`/docs`}>Documentation</NavigationButton>
                <NavigationButton className="md-hide" href="mailto:support@buildkite.com">Support</NavigationButton>

              <Button action="/logout" method="delete" className="black hover-lime focus-lime block left-align" style={{width: "100%"}}>Logout</Button>
            </Dropdown>

        {this._bottomOrganizationMenu()}

    );
  }

  _newChangelogsBadge() {
    return (
      <NewChangelogsBadge className="mr2 relative" style={{ top: -5, marginLeft: -8 }} viewer={this.props.viewer} />
    )
  }

  _topOrganizationMenu() {
    if (this.props.organization) {
      return (

          {this._organizationMenu()}

      )
    }
  }

  _bottomOrganizationMenu() {
    if (this.props.organization) {
      return (

            {this._organizationMenu({ paddingLeft: 0 })}

      )
    }
  }

  handleOrgDropdownToggle = (visible) => {
    this.setState({ showingOrgDropdown: visible });
  };

  handleUserDropdownToggle = (visible) => {
    this.setState({ showingUserDropdown: visible });
  };

  _organizationSelectorLabel() {
    if(this.props.organization) {
      return this.props.organization.name;
    } else {
      return "Organizations"
    }
  }

  _organizationsList() {
    let nodes = [];
    this.props.viewer.organizations.edges.forEach((org) => {
      // Don't show the active organization in the selector
      if(!this.props.organization || (org.node.slug != this.props.organization.slug)) {
        nodes.push(<NavigationButton key={org.node.slug} href={`/${org.node.slug}`} className="block">{org.node.name}</NavigationButton>);
      }
    });

    if(nodes.length > 0) {
      return nodes;
    }
  }

  _organizationMenu(options = {}) {
    var organization = this.props.organization;
    var paddingLeft = options.paddingLeft != undefined ? options.paddingLeft : 15;

    // Find the first settings permission that we're allowed to perform
    let permission;
    for(let name in organization.permissions) {
      let allowed = organization.permissions[name].allowed;
      if(allowed) {
        permission = name;
      }
    }

    // If there was a settings permission, show the button
    let settingsButton;
    if(permission) {
      settingsButton = (
        <NavigationButton href={`/organizations/${organization.slug}/settings`}>Settings</NavigationButton>
      );
    }

    if(organization) {
      return (

          <NavigationButton style={{ paddingLeft: paddingLeft }} href={`/${organization.slug}`}>Pipelines</NavigationButton>
          <NavigationButton href={`/organizations/${organization.slug}/agents`}>
            {'Agents'}
            <Badge className="hover-lime-child"><AgentsCount organization={organization} /></Badge>
          </NavigationButton>
          {settingsButton}

      )
    }
  }
}

export default Navigation;
<|endoftext|>


<commit_before>'use strict';

//var util = require('util');
//var _ = require('lodash');
//var log = require('../util/log');
//var preconditions = require('preconditions').singleton();
//var request = require('request');

/*
  This class lets interfaces with BitPay's exchange rate API.
*/

var RateService = function(opts) {
  var self = this;

  opts = opts || {};
  self.httprequest = opts.httprequest; // || request;
  self.lodash = opts.lodash;

  self.SAT_TO_BTC = 1 / 1e8;
  self.BTC_TO_SAT = 1e8;
  self.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable() or use service.whenAvailable()';
  self.UNSUPPORTED_CURRENCY_ERROR = 'Currency not supported';

  self._url = opts.url || 'https://insight.bitpay.com:443/api/rates';

  self._isAvailable = false;
  self._rates = {};
  self._alternatives = [];
  self._queued = [];

  self._fetchCurrencies();
};

var _instance;
RateService.singleton = function(opts) {
  if (!_instance) {
    _instance = new RateService(opts);
  }
  return _instance;
};

RateService.prototype._fetchCurrencies = function() {
  var self = this;

  var backoffSeconds = 5;
  var updateFrequencySeconds = 3600;
  var rateServiceUrl = 'https://bitpay.com/api/rates';

  var retrieve = function() {
    //log.info('Fetching exchange rates');
    self.httprequest.get(rateServiceUrl).success(function(res) {
      self.lodash.each(res, function(currency) {
        self._rates[currency.code] = currency.rate;
        self._alternatives.push({
          name: currency.name,
          isoCode: currency.code,
          rate: currency.rate
        });
      });
      self._isAvailable = true;
      self.lodash.each(self._queued, function(callback) {
        setTimeout(callback, 1);
      });
      setTimeout(retrieve, updateFrequencySeconds * 1000);
    }).error(function(err) {
      //log.debug('Error fetching exchange rates', err);
      setTimeout(function() {
        backoffSeconds *= 1.5;
        retrieve();
      }, backoffSeconds * 1000);
      return;
    });

  };

  retrieve();
};

RateService.prototype.getRate = function(code) {
  return this._rates[code];
};

RateService.prototype.getHistoricRate = function(code, date, cb) {
  var self = this;

  self.httprequest.get(self._url + '/' + code + '?ts=' + date)
    .success(function(body) {
      return cb(null, body.rate)
    })
    .error(function(err) {
      return cb(err)
    });

};

RateService.prototype.getHistoricRates = function(code, dates, cb) {
  var self = this;

  var tsList = dates.join(',');

  self.httprequest.get(self._url + '/' + code + '?ts=' + tsList)
    .success(function(body) {
      if (!self.lodash.isArray(body)) {
        body = [{
          ts: dates[0],
          rate: body.rate
        }];
      }
      return cb(null, body);
    })
    .error(function(err) {
      return cb(err)
    });
};

RateService.prototype.getAlternatives = function() {
  return this._alternatives;
};

RateService.prototype.isAvailable = function() {
  return this._isAvailable;
};

RateService.prototype.whenAvailable = function(callback) {
  if (this.isAvailable()) {
    setTimeout(callback, 1);
  } else {
    this._queued.push(callback);
  }
};

RateService.prototype.toFiat = function(satoshis, code) {
  if (!this.isAvailable()) {
    return null;
  }

  return satoshis * this.SAT_TO_BTC * this.getRate(code);
};

RateService.prototype.toFiatHistoric = function(satoshis, code, date, cb) {
  var self = this;

  self.getHistoricRate(code, date, function(err, rate) {
    if (err) return cb(err);
    return cb(null, satoshis * self.SAT_TO_BTC * rate);
  });
};

RateService.prototype.fromFiat = function(amount, code) {
  if (!this.isAvailable()) {
    return null;
  }
  return amount / this.getRate(code) * this.BTC_TO_SAT;
};

RateService.prototype.listAlternatives = function() {
  var self = this;
  if (!this.isAvailable()) {
    return [];
  }

  return self.lodash.map(this.getAlternatives(), function(item) {
    return {
      name: item.name,
      isoCode: item.isoCode
    }
  });
};

angular.module('copayApp.services').factory('rateService', function($http, lodash) {
  // var cfg = _.extend(config.rates, {
  //   httprequest: $http
  // });

  var cfg = {
    httprequest: $http,
    lodash: lodash
  };
  return RateService.singleton(cfg);
});
<commit_msg>update exchange rate at 5 min interval<commit_after>'use strict';

//var util = require('util');
//var _ = require('lodash');
//var log = require('../util/log');
//var preconditions = require('preconditions').singleton();
//var request = require('request');

/*
  This class lets interfaces with BitPay's exchange rate API.
*/

var RateService = function(opts) {
  var self = this;

  opts = opts || {};
  self.httprequest = opts.httprequest; // || request;
  self.lodash = opts.lodash;

  self.SAT_TO_BTC = 1 / 1e8;
  self.BTC_TO_SAT = 1e8;
  self.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable() or use service.whenAvailable()';
  self.UNSUPPORTED_CURRENCY_ERROR = 'Currency not supported';

  self._url = opts.url || 'https://insight.bitpay.com:443/api/rates';

  self._isAvailable = false;
  self._rates = {};
  self._alternatives = [];
  self._queued = [];

  self._fetchCurrencies();
};

var _instance;
RateService.singleton = function(opts) {
  if (!_instance) {
    _instance = new RateService(opts);
  }
  return _instance;
};

RateService.prototype._fetchCurrencies = function() {
  var self = this;

  var backoffSeconds = 5;
  var updateFrequencySeconds = 5 * 60;
  var rateServiceUrl = 'https://bitpay.com/api/rates';

  var retrieve = function() {
    //log.info('Fetching exchange rates');
    self.httprequest.get(rateServiceUrl).success(function(res) {
      self.lodash.each(res, function(currency) {
        self._rates[currency.code] = currency.rate;
        self._alternatives.push({
          name: currency.name,
          isoCode: currency.code,
          rate: currency.rate
        });
      });
      self._isAvailable = true;
      self.lodash.each(self._queued, function(callback) {
        setTimeout(callback, 1);
      });
      setTimeout(retrieve, updateFrequencySeconds * 1000);
    }).error(function(err) {
      //log.debug('Error fetching exchange rates', err);
      setTimeout(function() {
        backoffSeconds *= 1.5;
        retrieve();
      }, backoffSeconds * 1000);
      return;
    });

  };

  retrieve();
};

RateService.prototype.getRate = function(code) {
  return this._rates[code];
};

RateService.prototype.getHistoricRate = function(code, date, cb) {
  var self = this;

  self.httprequest.get(self._url + '/' + code + '?ts=' + date)
    .success(function(body) {
      return cb(null, body.rate)
    })
    .error(function(err) {
      return cb(err)
    });

};

RateService.prototype.getHistoricRates = function(code, dates, cb) {
  var self = this;

  var tsList = dates.join(',');

  self.httprequest.get(self._url + '/' + code + '?ts=' + tsList)
    .success(function(body) {
      if (!self.lodash.isArray(body)) {
        body = [{
          ts: dates[0],
          rate: body.rate
        }];
      }
      return cb(null, body);
    })
    .error(function(err) {
      return cb(err)
    });
};

RateService.prototype.getAlternatives = function() {
  return this._alternatives;
};

RateService.prototype.isAvailable = function() {
  return this._isAvailable;
};

RateService.prototype.whenAvailable = function(callback) {
  if (this.isAvailable()) {
    setTimeout(callback, 1);
  } else {
    this._queued.push(callback);
  }
};

RateService.prototype.toFiat = function(satoshis, code) {
  if (!this.isAvailable()) {
    return null;
  }

  return satoshis * this.SAT_TO_BTC * this.getRate(code);
};

RateService.prototype.toFiatHistoric = function(satoshis, code, date, cb) {
  var self = this;

  self.getHistoricRate(code, date, function(err, rate) {
    if (err) return cb(err);
    return cb(null, satoshis * self.SAT_TO_BTC * rate);
  });
};

RateService.prototype.fromFiat = function(amount, code) {
  if (!this.isAvailable()) {
    return null;
  }
  return amount / this.getRate(code) * this.BTC_TO_SAT;
};

RateService.prototype.listAlternatives = function() {
  var self = this;
  if (!this.isAvailable()) {
    return [];
  }

  return self.lodash.map(this.getAlternatives(), function(item) {
    return {
      name: item.name,
      isoCode: item.isoCode
    }
  });
};

angular.module('copayApp.services').factory('rateService', function($http, lodash) {
  // var cfg = _.extend(config.rates, {
  //   httprequest: $http
  // });

  var cfg = {
    httprequest: $http,
    lodash: lodash
  };
  return RateService.singleton(cfg);
});
<|endoftext|>