<commit_before>import React, { Component, PropTypes } from 'react';
import electron, { shell, remote } from 'electron';
const Menu = remote.Menu
const MenuItem = remote.MenuItem
import fetch from 'node-fetch';
import config from '../../server/config';
import classnames from 'classnames';
import $ from 'jquery';
import util from "../utils/util";
import live from '../backend/live';

class GuessPage extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            list: [],
        };
        this.match_id = 0;
    }

    componentDidMount() {
    }

    getTypeArr(){
        return {
            "guess_result": {name: "赛果竞猜", className:"label-important"},
            "guess_award": {name: "有奖预测", className:"label-success"},
            "guess_interact": {name: "互动竞猜", className:" label-info"},
        }
    }

    getType(value){
        let arr = this.getTypeArr();
        if(arr.hasOwnProperty(value)){
            return arr[value];
        }else{
            return {};
        }
    }

    getStatusArr(){
        return {
            0: {name: "未开始", className:"label-info", bsStyle:"default"},
            1: {name: "进行中", className:" label-success", bsStyle:"info"},
            9: {name: "已结束", className:" label-inverse", bsStyle:"success"},
        }
    }

    getStatus(value){
        let arr = this.getStatusArr();
        if(arr.hasOwnProperty(value)){
            return arr[value];
        }else{
            return {};
        }

    }

    getGuessInfo(match_id, callback){
        let self = this;
        if(!callback) callback = (guess) => self.setState({list: guess})
        fetch(config.FRONTEND_HOST + '?r=guess/getguess&no_cache=1&match_id='+match_id)
        .then(function(res) {
            return res.json();
        }).then(function(data) {
            if(data.hasOwnProperty("code") && data.code==1){
                callback(data.result.data)
            }else{
                callback([], data.message)
            }
        });
    }

    shouldComponentUpdate(nextProps, nextState){
        let bool = this.match_id != nextProps.matchId || this.state !== nextState
        if(this.match_id != nextProps.matchId){
            let self = this;
            this.match_id = nextProps.matchId;
            this.getGuessInfo(this.match_id)
        }
        return bool
    }

    sendGuess(data, callback){
        data.r = "appguessapi/addguess"
        data.match_id = this.match_id
        live.sendMessage(data, function(error, message, data){
            callback(error ? null : message)
        });
    }

    handlerDeleteGuess(guess_id){
        let self = this;
        if(!confirm("你确定要删除该信息吗?")){
            return false;
        }
        live.sendMessage({
            r: "appguessapi/delete",
            guess_id: guess_id,
        }, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerSelectResult(guess_id, e){
        let self = this;
        live.sendMessage({
            r: "appguessapi/update",
            guess_id: guess_id,
            result_id: e.target.value,
            status: 9,
        }, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerUpdateStatus(guess_id, e){
        let self = this;
        let data = {
            r: "appguessapi/update",
            guess_id: guess_id,
            status: e.target.value,
        }
        if(data.status == 1) data.start_time = parseInt(new Date().getTime() / 1000)
        if(data.status == 9) data.end_time = parseInt(new Date().getTime() / 1000)
        live.sendMessage(data, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerSubmitGuess(e){
        let self = this;
        let inputTitle = this.refs.guess_textarea.getDOMNode()
        let inputType = this.refs.guess_type.getDOMNode()
        let inputOption1 = this.refs.guess_option_1.getDOMNode()
        let inputOption2 = this.refs.guess_option_2.getDOMNode()
        let inputOption3 = this.refs.guess_option_3.getDOMNode()
        let inputOption4 = this.refs.guess_option_4.getDOMNode()
        let inputOption5 = this.refs.guess_option_5.getDOMNode()
        if(!inputTitle.value){
            return alert("竞猜描述必须填写")
        }
        if(!inputOption1.value || !inputOption2.value){
            return alert("前二项竞猜选项必须填写")
        }
        let data = {options: []}
        data.title = inputTitle.value
        data.type = inputType.value
        data.options.push(inputOption1.value)
        data.options.push(inputOption2.value)
        if(inputOption3.value) data.options.push(inputOption3.value)
        if(inputOption4.value) data.options.push(inputOption4.value)
        if(inputOption4.value) data.options.push(inputOption4.value)

        self.sendGuess(data, function(err){
            if(err) alert(err)
            else{
                self.getGuessInfo(self.match_id)
                inputTitle.value = "";
                inputOption1.value = "";
                inputOption2.value = "";
                inputOption3.value = "";
                inputOption4.value = "";
                inputOption5.value = "";
            }
        })
    }

    renderList(list){
        let self = this;
        return list.map((item, index) => {
            let typeItem = self.getType(item.type)
            let activeStatus = self.getStatus(item.status);
            let title = item.type == 'guess_result' ? "谁将取得比赛胜利？" : item.title
            let optionHtml = item.options.map((option, n) => {
                let checked = item.result_id == option.option_id ? true : false;
                let right = checked ? √ : ""
                let input = item.status == 9 ? <input type="radio" onClick={self.handlerSelectResult.bind(this, item.guess_id)} checked={checked} value={option.option_id} name={`option_${index}`}/> : ""
                return
{input} {n + 1}. {option.title}  {right}

            })
            let statusOption = Object.keys(self.getStatusArr()).map((status) => {
                let statusItem = self.getStatus(status);
                let selected = status == item.status ? "selected" : false;
                return <option selected={selected} value={status}>{statusItem.name}</option>
            })
            return (

                    <button type="button"  className="close" onClick={this.handlerDeleteGuess.bind(this, item.guess_id)}>×</button>

                        {/*{index+1}*/} {title}

 {optionHtml}

                    <span className={`label label-type ${typeItem.className}`}>{typeItem.name}
                    <select className={`label label-status ${activeStatus.className}`} onChange={this.handlerUpdateStatus.bind(this, item.guess_id)}>{statusOption}</select>

            )
        });
    }

    renderForm(){
        let self = this;
        let submitButton = <button className="btn btn-info pull-right ">提交</button>;
        return (

                    <textarea ref="guess_textarea" rows="3" id="guess-title"  placeholder="竞猜问题描述" ></textarea>

                    竞猜类型：
                    <select ref="guess_type" className="guess-type">
                        {
                            Object.keys(this.getTypeArr()).map((type, index) => {
                                if(type != 'guess_result'){
                                    let item = self.getType(type)
                                    return <option value={type}>{item.name}</option>
                                }
                            })
                        }
                    </select>

                    <input ref="guess_option_1" type="text" className="guess-option" placeholder="选项一(必填)" />
                    <input ref="guess_option_2" type="text" className="guess-option" placeholder="选项二(必填)" />
                    <input ref="guess_option_3" type="text" className="guess-option" placeholder="选项三" />
                    <input ref="guess_option_4" type="text" className="guess-option" placeholder="选项四" />
                    <input ref="guess_option_5" type="text" className="guess-option" placeholder="选项五" />

 {submitButton}

        )
    }

    render() {
        const {list} = this.state;
        return (

竞猜列表

                        { this.renderList(this.state.list) }

                    { this.renderForm() }

        );
    }

}

GuessPage.propTypes = {
    matchId: PropTypes.string,
};

export default GuessPage;
<commit_msg>add guess list and form +4<commit_after>import React, { Component, PropTypes } from 'react';
import electron, { shell, remote } from 'electron';
const Menu = remote.Menu
const MenuItem = remote.MenuItem
import fetch from 'node-fetch';
import config from '../../server/config';
import classnames from 'classnames';
import $ from 'jquery';
import util from "../utils/util";
import live from '../backend/live';

class GuessPage extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            list: [],
        };
        this.match_id = 0;
    }

    componentDidMount() {
    }

    getTypeArr(){
        return {
            "guess_result": {name: "赛果竞猜", className:"label-important"},
            "guess_award": {name: "有奖预测", className:"label-success"},
            "guess_interact": {name: "互动竞猜", className:" label-info"},
        }
    }

    getType(value){
        let arr = this.getTypeArr();
        if(arr.hasOwnProperty(value)){
            return arr[value];
        }else{
            return {};
        }
    }

    getStatusArr(){
        return {
            0: {name: "未开始", className:"label-info", bsStyle:"default"},
            1: {name: "进行中", className:" label-success", bsStyle:"info"},
            9: {name: "已结束", className:" label-inverse", bsStyle:"success"},
        }
    }

    getStatus(value){
        let arr = this.getStatusArr();
        if(arr.hasOwnProperty(value)){
            return arr[value];
        }else{
            return {};
        }

    }

    getGuessInfo(match_id, callback){
        let self = this;
        if(!callback) callback = (guess) => self.setState({list: guess})
        fetch(config.FRONTEND_HOST + '?r=guess/getguess&no_cache=1&match_id='+match_id)
        .then(function(res) {
            return res.json();
        }).then(function(data) {
            if(data.hasOwnProperty("code") && data.code==1){
                callback(data.result.data)
            }else{
                callback([], data.message)
            }
        });
    }

    shouldComponentUpdate(nextProps, nextState){
        let bool = this.match_id != nextProps.matchId || this.state !== nextState
        if(this.match_id != nextProps.matchId){
            let self = this;
            this.match_id = nextProps.matchId;
            this.getGuessInfo(this.match_id)
        }
        return bool
    }

    sendGuess(data, callback){
        data.r = "appguessapi/addguess"
        data.match_id = this.match_id
        live.sendMessage(data, function(error, message, data){
            callback(error ? null : message)
        });
    }

    handlerDeleteGuess(guess_id){
        let self = this;
        if(!confirm("你确定要删除该信息吗?")){
            return false;
        }
        live.sendMessage({
            r: "appguessapi/delete",
            guess_id: guess_id,
        }, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerSelectResult(guess_id, e){
        let self = this;
        live.sendMessage({
            r: "appguessapi/update",
            guess_id: guess_id,
            result_id: e.target.value,
            status: 9,
        }, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerUpdateStatus(guess_id, e){
        let self = this;
        let data = {
            r: "appguessapi/update",
            guess_id: guess_id,
            status: e.target.value,
        }
        if(data.status == 1) data.start_time = parseInt(new Date().getTime() / 1000)
        if(data.status == 9) data.end_time = parseInt(new Date().getTime() / 1000)
        live.sendMessage(data, function(error, message, data){
            if(error)alert(message);
            else{
                self.getGuessInfo(self.match_id)
            }
        });
    }

    handlerSubmitGuess(e){
        let self = this;
        let inputTitle = this.refs.guess_textarea.getDOMNode()
        let inputType = this.refs.guess_type.getDOMNode()
        let inputOption1 = this.refs.guess_option_1.getDOMNode()
        let inputOption2 = this.refs.guess_option_2.getDOMNode()
        let inputOption3 = this.refs.guess_option_3.getDOMNode()
        let inputOption4 = this.refs.guess_option_4.getDOMNode()
        let inputOption5 = this.refs.guess_option_5.getDOMNode()
        if(!inputTitle.value){
            return alert("竞猜描述必须填写")
        }
        if(!inputOption1.value || !inputOption2.value){
            return alert("前二项竞猜选项必须填写")
        }
        let data = {options: []}
        data.title = inputTitle.value
        data.type = inputType.value
        data.options.push(inputOption1.value)
        data.options.push(inputOption2.value)
        if(inputOption3.value) data.options.push(inputOption3.value)
        if(inputOption4.value) data.options.push(inputOption4.value)
        if(inputOption4.value) data.options.push(inputOption4.value)

        self.sendGuess(data, function(err){
            if(err) alert(err)
            else{
                self.getGuessInfo(self.match_id)
                inputTitle.value = "";
                inputOption1.value = "";
                inputOption2.value = "";
                inputOption3.value = "";
                inputOption4.value = "";
                inputOption5.value = "";
            }
        })
    }

    renderList(list){
        let self = this;
        return list.map((item, index) => {
            let typeItem = self.getType(item.type)
            let activeStatus = self.getStatus(item.status);
            let title = item.type == 'guess_result' ? "谁将取得比赛胜利？" : item.title
            let optionHtml = item.options.map((option, n) => {
                let checked = item.result_id == option.option_id ? true : false;
                let right = checked ? √ : ""
                let input = item.status == 9 ? <input type="radio" onClick={self.handlerSelectResult.bind(this, item.guess_id)} checked={checked} value={option.option_id} name={`option_${index}`}/> : ""
                return
{input} {n + 1}. {option.title} {right}

            })
            let statusOption = Object.keys(self.getStatusArr()).map((status) => {
                let statusItem = self.getStatus(status);
                let selected = status == item.status ? "selected" : false;
                return <option selected={selected} value={status}>{statusItem.name}</option>
            })
            return (

                    <button type="button"  className="close" onClick={this.handlerDeleteGuess.bind(this, item.guess_id)}>×</button>

                        {/*{index+1}*/} {title}

 {optionHtml}

                    <span className={`label label-type ${typeItem.className}`}>{typeItem.name}
                    <select className={`label label-status ${activeStatus.className}`} onChange={this.handlerUpdateStatus.bind(this, item.guess_id)}>{statusOption}</select>

            )
        });
    }

    renderForm(){
        let self = this;
        let submitButton = <button className="btn btn-info pull-right ">提交</button>;
        return (

                    <textarea ref="guess_textarea" rows="3" id="guess-title"  placeholder="竞猜问题描述" ></textarea>

                    竞猜类型：
                    <select ref="guess_type" className="guess-type">
                        {
                            Object.keys(this.getTypeArr()).map((type, index) => {
                                if(type != 'guess_result'){
                                    let item = self.getType(type)
                                    return <option value={type}>{item.name}</option>
                                }
                            })
                        }
                    </select>

                    <input ref="guess_option_1" type="text" className="guess-option" placeholder="选项一(必填)" />
                    <input ref="guess_option_2" type="text" className="guess-option" placeholder="选项二(必填)" />
                    <input ref="guess_option_3" type="text" className="guess-option" placeholder="选项三" />
                    <input ref="guess_option_4" type="text" className="guess-option" placeholder="选项四" />
                    <input ref="guess_option_5" type="text" className="guess-option" placeholder="选项五" />

 {submitButton}

        )
    }

    render() {
        const {list} = this.state;
        return (

竞猜列表

                        { this.renderList(this.state.list) }

                    { this.renderForm() }

        );
    }

}

GuessPage.propTypes = {
    matchId: PropTypes.string,
};

export default GuessPage;
<|endoftext|>


<commit_before>'use strict';
const Qs = require('qs');
const Sortable = require('sortablejs');
require('jquery-modal');

const utils = require('./utils');
const Playlist = require('./playlist');
const Storage = require('./storage');
const Track = require('./track');
const Reporting = require('./reporting');

const sortedFields = Track.fields.filter(e => !e.hidden);
function compareByLabel(a, b) {
  const aName = a.label.toLowerCase();
  const bName = b.label.toLowerCase();
  return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); // eslint-disable-line no-nested-ternary
}
sortedFields.sort(compareByLabel);

function getRulesData(playlists, playlistId, splaylistcache) {
  const variables = [];
  sortedFields.forEach(field => {
    variables.push({
      name: field.name,
      label: field.label,
      field_type: field.is_datetime ? 'datetime' : Track.lfToBusinessTypes[field.type],
      options: [],
    });
  });

  const otherPlaylists = playlists.filter(p => p.localId !== playlistId);
  if (otherPlaylists.length > 0 || Object.keys(splaylistcache.splaylists).length > 0) {
    const playlistOptions = [];

    // Playlists and splaylists are stored under the same label.
    // They can be distinguished by the first letter of the value:
    //   splaylists: 'P' (prepended to the remoteId)
    //   playlists: a digit (localIds are timestamps)
    // If splaylist support works out, we can unify them by treating them all like splaylists.
    for (let i = 0; i < otherPlaylists.length; i++) {
      const playlist = otherPlaylists[i];
      playlistOptions.push({label: playlist.title, value: playlist.localId});
    }

    for (const splaylistId in splaylistcache.splaylists) {
      const splaylist = splaylistcache.splaylists[splaylistId];
      if (!(splaylist.isAutoplaylist)) {
        playlistOptions.push({label: splaylist.title, value: 'P' + splaylist.id});
      }
    }

    variables.push({
      name: 'playlist',
      label: 'playlist',
      field_type: 'select',
      options: playlistOptions,
    });
  }

  variables.sort(compareByLabel);

  return {
    variables,
    actions: [],
    variable_type_operators: Track.operators,
  };
}

function createSort(fields, isLocked) {
  const $sort = $('
');
  const $sortBy = $('<select class="sort-by">');
  const $sortByOrder = $(
    '<select class="sort-by-order">' +
    '   <option value="ASC">ascending</option>' +
    '   <option value="DESC">descending</option>' +
    '</select');
  const $remove = $('Remove');
  $remove.click(e => {
    e.preventDefault();
    $sort.remove();
  });

  fields.forEach(field => {
    $('<option>').val(field.name).text(field.label).appendTo($sortBy);
  });
  $('<option>').val('random').text('random (explanation below)').appendTo($sortBy);

  $sort.append($sortBy);
  $sort.append($sortByOrder);
  if (!isLocked) {
    $sort.append($remove);
  } else {
    $sortBy.prop('disabled', true);
    $sortByOrder.prop('disabled', true);
  }

  return $sort;
}

function parseSorts($sorts) {
  const sorts = [];

  $sorts.children().each((idx, li) => {
    const sort = [];
    $(li).children('select').each((idx2, select) => {
      sort.push($(select).val());
    });
    sorts.push({sortBy: sort[0], sortByOrder: sort[1]});
  });

  return sorts;
}

function initializeForm(userId, playlistId, isLocked, playlists, splaylistcache) {
  const initConditions = getRulesData(playlists, playlistId, splaylistcache);
  let initialPlaylist = null;
  const $conditions = $('#conditions');

  console.log(userId, playlistId);

  const $sorts = $('#sorts');
  if (!isLocked) {
    Sortable.create($sorts[0]);
  }
  const $explanations = $('#explanations');

  sortedFields.forEach(field => {
    if (field.explanation) {
      $('
').text(`${field.label}: ${field.explanation}`).appendTo($explanations);
    }
  });
  $('
').text('playlist: a playlist whose contents will be included or excluded.' +
                ' Hidden if no other playlists are available.').appendTo($explanations);
  $('
').html('random sort: a random total ordering, different on each load.' +
                ' See ' +
                ' the wiki for more details.').appendTo($explanations);

  if (playlistId) {
    Storage.getPlaylist(userId, playlistId, loadedPlaylist => {
      initialPlaylist = loadedPlaylist;
      $('#playlist-title').val(loadedPlaylist.title);

      console.log('loading playlist', loadedPlaylist);
      initConditions.data = loadedPlaylist.rules;
      if (isLocked) {
        initConditions.disabled = true;
      }
      $conditions.conditionsBuilder(initConditions);

      $('#limit-to').val(loadedPlaylist.limit);

      for (let i = 0; i < loadedPlaylist.sorts.length; i++) {
        const sort = loadedPlaylist.sorts[i];
        const $sort = createSort(sortedFields, isLocked);
        $sort.children('.sort-by').val(sort.sortBy);
        $sort.children('.sort-by-order').val(sort.sortByOrder);
        $sorts.append($sort);
      }
    });
  } else {
    console.log('creating empty form');
    $conditions.conditionsBuilder(initConditions);
    $('#playlist-title').val('[auto] new playlist').focus();
    $sorts.append(createSort(sortedFields, isLocked));
    $('#delete').hide();
  }

  function readForm() {
    const playlist = initialPlaylist || {};
    const playlistRules = $('#conditions').conditionsBuilder('data');

    if (!('localId' in playlist)) {
      playlist.localId = `${new Date().getTime()}`;
    }

    playlist.title = $('#playlist-title').val() || '[untitled autoplaylist]';
    playlist.rules = playlistRules;
    playlist.userId = userId;
    playlist.limit = Math.min(1000, parseInt($('#limit-to').val(), 10));
    playlist.sorts = parseSorts($('#sorts'));

    return playlist;
  }

  $('#add-sort').click(e => {
    e.preventDefault();
    $('#sorts').append(createSort(sortedFields, isLocked));
  });

  $('#submit').click(e => {
    e.preventDefault();
    const playlist = readForm();

    console.log('writing', playlist);

    Storage.savePlaylist(playlist, () => {
      utils.goToManager(userId);
    });
  });

  $('#test').click(e => {
    e.preventDefault();

    const playlist = readForm();
    console.log('testing', playlist);

    // When testing, show the total number of matched tracks.
    playlist.limit = null;

    chrome.runtime.sendMessage({action: 'query', playlist}, response => {
      const columnNames = new Set();
      columnNames.add('title');
      columnNames.add('artist');
      columnNames.add('album');

      for (const fieldName in Playlist.involvedFields(playlist)) {
        if (fieldName !== 'random') {
          columnNames.add(fieldName);
        }
      }

      const columns = [];
      const datetimeRender = val => new Date(val / 1000).toLocaleString();
      for (const name of columnNames) {
        const field = Track.fieldsByName[name];
        const column = {data: name, title: field.label};
        if (field.is_datetime) {
          column.render = datetimeRender;
        }
        columns.push(column);
      }

      if ($.fn.DataTable.isDataTable('#query-result')) {
        // option destroy: true does not work when changing the number of columns,
        // so clear the table and the dom manually.
        $('#query-result').dataTable().fnDestroy();
        $('#query-result').empty();
      }

      $('#query-result').DataTable({ // eslint-disable-line new-cap
        data: response.tracks,
        columns,
        aaSorting: [],
      });
      $('#query-modal').modal();
    });
  });

  $('#delete').click(e => {
    e.preventDefault();
    Storage.deletePlaylist(userId, playlistId, () => {
      utils.goToManager(userId);
    });
  });

  if (isLocked) {
    $('#drag-explanation').remove();
    $('#limit-explanation').remove();
    $('input').prop('disabled', true);
    $('select').prop('disabled', true);
    $('a').remove();

    $('#submit, #test')
    .addClass('locked')
    .addClass('disabled')
    .wrap('
');
  }
}

function main() {
  Reporting.reportHit('playlist_editor.js');
  const qstring = Qs.parse(location.search.substring(1));

  Storage.getPlaylistsForUser(qstring.userId, playlists => {
    chrome.runtime.sendMessage({action: 'getSplaylistcache', userId: qstring.userId}, splaylistcache => {
      initializeForm(qstring.userId, qstring.id, qstring.locked === 'true', playlists, splaylistcache);
    });
  });
}

$(main);
<commit_msg>improve random sort explanation [closes #112]<commit_after>'use strict';
const Qs = require('qs');
const Sortable = require('sortablejs');
require('jquery-modal');

const utils = require('./utils');
const Playlist = require('./playlist');
const Storage = require('./storage');
const Track = require('./track');
const Reporting = require('./reporting');

const sortedFields = Track.fields.filter(e => !e.hidden);
function compareByLabel(a, b) {
  const aName = a.label.toLowerCase();
  const bName = b.label.toLowerCase();
  return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); // eslint-disable-line no-nested-ternary
}
sortedFields.sort(compareByLabel);

function getRulesData(playlists, playlistId, splaylistcache) {
  const variables = [];
  sortedFields.forEach(field => {
    variables.push({
      name: field.name,
      label: field.label,
      field_type: field.is_datetime ? 'datetime' : Track.lfToBusinessTypes[field.type],
      options: [],
    });
  });

  const otherPlaylists = playlists.filter(p => p.localId !== playlistId);
  if (otherPlaylists.length > 0 || Object.keys(splaylistcache.splaylists).length > 0) {
    const playlistOptions = [];

    // Playlists and splaylists are stored under the same label.
    // They can be distinguished by the first letter of the value:
    //   splaylists: 'P' (prepended to the remoteId)
    //   playlists: a digit (localIds are timestamps)
    // If splaylist support works out, we can unify them by treating them all like splaylists.
    for (let i = 0; i < otherPlaylists.length; i++) {
      const playlist = otherPlaylists[i];
      playlistOptions.push({label: playlist.title, value: playlist.localId});
    }

    for (const splaylistId in splaylistcache.splaylists) {
      const splaylist = splaylistcache.splaylists[splaylistId];
      if (!(splaylist.isAutoplaylist)) {
        playlistOptions.push({label: splaylist.title, value: 'P' + splaylist.id});
      }
    }

    variables.push({
      name: 'playlist',
      label: 'playlist',
      field_type: 'select',
      options: playlistOptions,
    });
  }

  variables.sort(compareByLabel);

  return {
    variables,
    actions: [],
    variable_type_operators: Track.operators,
  };
}

function createSort(fields, isLocked) {
  const $sort = $('
');
  const $sortBy = $('<select class="sort-by">');
  const $sortByOrder = $(
    '<select class="sort-by-order">' +
    '   <option value="ASC">ascending</option>' +
    '   <option value="DESC">descending</option>' +
    '</select');
  const $remove = $('Remove');
  $remove.click(e => {
    e.preventDefault();
    $sort.remove();
  });

  fields.forEach(field => {
    $('<option>').val(field.name).text(field.label).appendTo($sortBy);
  });
  $('<option>').val('random').text('random (explanation below)').appendTo($sortBy);

  $sort.append($sortBy);
  $sort.append($sortByOrder);
  if (!isLocked) {
    $sort.append($remove);
  } else {
    $sortBy.prop('disabled', true);
    $sortByOrder.prop('disabled', true);
  }

  return $sort;
}

function parseSorts($sorts) {
  const sorts = [];

  $sorts.children().each((idx, li) => {
    const sort = [];
    $(li).children('select').each((idx2, select) => {
      sort.push($(select).val());
    });
    sorts.push({sortBy: sort[0], sortByOrder: sort[1]});
  });

  return sorts;
}

function initializeForm(userId, playlistId, isLocked, playlists, splaylistcache) {
  const initConditions = getRulesData(playlists, playlistId, splaylistcache);
  let initialPlaylist = null;
  const $conditions = $('#conditions');

  console.log(userId, playlistId);

  const $sorts = $('#sorts');
  if (!isLocked) {
    Sortable.create($sorts[0]);
  }
  const $explanations = $('#explanations');

  sortedFields.forEach(field => {
    if (field.explanation) {
      $('
').text(`${field.label}: ${field.explanation}`).appendTo($explanations);
    }
  });
  $('
').text('playlist: a playlist whose contents will be included or excluded.' +
                ' Hidden if no other playlists are available.').appendTo($explanations);
  $('
').html('random sort: a random total ordering, different on each refresh of the Music tab.' +
                ' See ' +
                ' the wiki for more details.').appendTo($explanations);

  if (playlistId) {
    Storage.getPlaylist(userId, playlistId, loadedPlaylist => {
      initialPlaylist = loadedPlaylist;
      $('#playlist-title').val(loadedPlaylist.title);

      console.log('loading playlist', loadedPlaylist);
      initConditions.data = loadedPlaylist.rules;
      if (isLocked) {
        initConditions.disabled = true;
      }
      $conditions.conditionsBuilder(initConditions);

      $('#limit-to').val(loadedPlaylist.limit);

      for (let i = 0; i < loadedPlaylist.sorts.length; i++) {
        const sort = loadedPlaylist.sorts[i];
        const $sort = createSort(sortedFields, isLocked);
        $sort.children('.sort-by').val(sort.sortBy);
        $sort.children('.sort-by-order').val(sort.sortByOrder);
        $sorts.append($sort);
      }
    });
  } else {
    console.log('creating empty form');
    $conditions.conditionsBuilder(initConditions);
    $('#playlist-title').val('[auto] new playlist').focus();
    $sorts.append(createSort(sortedFields, isLocked));
    $('#delete').hide();
  }

  function readForm() {
    const playlist = initialPlaylist || {};
    const playlistRules = $('#conditions').conditionsBuilder('data');

    if (!('localId' in playlist)) {
      playlist.localId = `${new Date().getTime()}`;
    }

    playlist.title = $('#playlist-title').val() || '[untitled autoplaylist]';
    playlist.rules = playlistRules;
    playlist.userId = userId;
    playlist.limit = Math.min(1000, parseInt($('#limit-to').val(), 10));
    playlist.sorts = parseSorts($('#sorts'));

    return playlist;
  }

  $('#add-sort').click(e => {
    e.preventDefault();
    $('#sorts').append(createSort(sortedFields, isLocked));
  });

  $('#submit').click(e => {
    e.preventDefault();
    const playlist = readForm();

    console.log('writing', playlist);

    Storage.savePlaylist(playlist, () => {
      utils.goToManager(userId);
    });
  });

  $('#test').click(e => {
    e.preventDefault();

    const playlist = readForm();
    console.log('testing', playlist);

    // When testing, show the total number of matched tracks.
    playlist.limit = null;

    chrome.runtime.sendMessage({action: 'query', playlist}, response => {
      const columnNames = new Set();
      columnNames.add('title');
      columnNames.add('artist');
      columnNames.add('album');

      for (const fieldName in Playlist.involvedFields(playlist)) {
        if (fieldName !== 'random') {
          columnNames.add(fieldName);
        }
      }

      const columns = [];
      const datetimeRender = val => new Date(val / 1000).toLocaleString();
      for (const name of columnNames) {
        const field = Track.fieldsByName[name];
        const column = {data: name, title: field.label};
        if (field.is_datetime) {
          column.render = datetimeRender;
        }
        columns.push(column);
      }

      if ($.fn.DataTable.isDataTable('#query-result')) {
        // option destroy: true does not work when changing the number of columns,
        // so clear the table and the dom manually.
        $('#query-result').dataTable().fnDestroy();
        $('#query-result').empty();
      }

      $('#query-result').DataTable({ // eslint-disable-line new-cap
        data: response.tracks,
        columns,
        aaSorting: [],
      });
      $('#query-modal').modal();
    });
  });

  $('#delete').click(e => {
    e.preventDefault();
    Storage.deletePlaylist(userId, playlistId, () => {
      utils.goToManager(userId);
    });
  });

  if (isLocked) {
    $('#drag-explanation').remove();
    $('#limit-explanation').remove();
    $('input').prop('disabled', true);
    $('select').prop('disabled', true);
    $('a').remove();

    $('#submit, #test')
    .addClass('locked')
    .addClass('disabled')
    .wrap('
');
  }
}

function main() {
  Reporting.reportHit('playlist_editor.js');
  const qstring = Qs.parse(location.search.substring(1));

  Storage.getPlaylistsForUser(qstring.userId, playlists => {
    chrome.runtime.sendMessage({action: 'getSplaylistcache', userId: qstring.userId}, splaylistcache => {
      initializeForm(qstring.userId, qstring.id, qstring.locked === 'true', playlists, splaylistcache);
    });
  });
}

$(main);
<|endoftext|>


<commit_before>'use strict';
import _ from 'lodash';
import delay from 'delay';
import ProgressBar from 'progress';
import clc from 'cli-color';

const log_error = clc.xterm(9);
const log_warn = clc.xterm(221);
const log_notice = clc.xterm(33);
const log_remove = clc.xterm(196);
const log_edit = clc.xterm(40);
const log_white = clc.xterm(254);

class TaskManager {
  constructor(options){
    this.options = this.patchOption(options);

    this.tasks = {};
    this.taskList = [];
    this.lock = false;
  }

  patchOption(option){
    let newOption = option || {};
    let optionDefault = {
      delay: 100
    }

    return _.merge(optionDefault, newOption)
  }

  createTask(name, task){
    const self = this;
    self.tasks[name] = task;
  }

  /**
   * [
     {
       task: 'watch',
       param: {
         name: 'test'
       }
     },
     {
       task: 'watch',
       param: {
         name: 'test'
       }
     }
    ]
   */
  addTaskList(listTask){
    const self = this;
    return new Promise((resolve, reject) => {
    if (!self.lock) {
      self.taskList = listTask;
      self.taskSequencer().then(() => {
        resolve();
      });
    }else{
      delay(500).then(()=>{
        self.addTaskList(listTask);
      })
    }
    });
  }

  runTask(name, param, filter){
    var self = this;
    if (filter) {
      // адская сотона сравнивает вложенные объекты
      // TODO: Причесать вытащить в отдельный метод
      let keys = _.keys(filter);
      _.remove(self.taskList, (task, index) => {
        let listSize = _.size(self.taskList) - 1;
        let isRemove = true;
        let keysSize = _.size(keys);
        let deepKeysSize = 0;
        let countCompare = 0;
        let countDeepKeys = 0;
        let onRemove = false;

        if (index === 0 || index === listSize) {
          isRemove = false;
        }

        for (var i = 0; i < keys.length; i++) {
          let _key = keys[i];
          if (task.param[_key]) {
            if (_.keys(filter[_key]).length > 0 && _.isObject(filter[_key])) {
              keysSize = keysSize + _.keys(filter[_key]).length;
              let deepKeys = _.keys(filter[_key]);
              deepKeysSize = _.size(deepKeys);
              for (var i = 0; i < deepKeys.length; i++) {
                let deepKey = deepKeys[i];
                if (task.param[_key] && task.param[_key][deepKey]) {
                  if ( _.isEqual(task.param[_key][deepKey], filter[_key][deepKey]) ) {
                    countDeepKeys++
                  }
                }
              }
            }else{
              if ( _.isEqual(task.param[_key], filter[_key]) ) {
                countCompare++
              }
            }
          }
        }

        onRemove = (_.isEqual(keysSize, countCompare) && isRemove || _.isEqual(deepKeysSize, countDeepKeys) && isRemove);

        return onRemove;
      });
    }

    self.taskList.push({
      task: name,
      param
    })

    if (!self.lock) {
      self.taskSequencer();
    }
  }

  taskSequencer(){
    const self = this;
    return new Promise((resolve, reject) => {
    if (_.size(self.taskList) === 0){
      resolve();
      return;
    }

    if (self.options.statusBar) {
      var statusBar = new ProgressBar(self.options.statusBar.template, {
        complete: '█',
        incomplete: '.',
        width: 20,
        clear: true,
        total: self.taskList.length
      });
    }

    taskRunner();
    function taskRunner(){
      self.lock = true;
      delay(self.options.delay).then(() => {
        self.tasks[self.taskList[0].task](self.taskList[0].param).then(content => {
          let _content = content || {};

          self.taskList.splice(0, 1);
          self.lock = false;
          if (self.options.statusBar && statusBar) {
            statusBar.tick(_content);
          }

          if (_.size(self.taskList) > 0){
            return taskRunner();
          }else{
            resolve()
            return;
          }
        })
      })
    }
    });
  }

  getTaskList(){
    const self = this;
    console.log(self.tasks);
  }
}
export default options => new TaskManager(options);
<commit_msg>remove cli-color<commit_after>'use strict';
import _ from 'lodash';
import delay from 'delay';
import ProgressBar from 'progress';

class TaskManager {
  constructor(options){
    this.options = this.patchOption(options);

    this.tasks = {};
    this.taskList = [];
    this.lock = false;
  }

  patchOption(option){
    let newOption = option || {};
    let optionDefault = {
      delay: 100
    }

    return _.merge(optionDefault, newOption)
  }

  createTask(name, task){
    const self = this;
    self.tasks[name] = task;
  }

  /**
   * [
     {
       task: 'watch',
       param: {
         name: 'test'
       }
     },
     {
       task: 'watch',
       param: {
         name: 'test'
       }
     }
    ]
   */
  addTaskList(listTask){
    const self = this;
    return new Promise((resolve, reject) => {
    if (!self.lock) {
      self.taskList = listTask;
      self.taskSequencer().then(() => {
        resolve();
      });
    }else{
      delay(500).then(()=>{
        self.addTaskList(listTask);
      })
    }
    });
  }

  runTask(name, param, filter){
    var self = this;
    if (filter) {
      // адская сотона сравнивает вложенные объекты
      // TODO: Причесать вытащить в отдельный метод
      let keys = _.keys(filter);
      _.remove(self.taskList, (task, index) => {
        let listSize = _.size(self.taskList) - 1;
        let isRemove = true;
        let keysSize = _.size(keys);
        let deepKeysSize = 0;
        let countCompare = 0;
        let countDeepKeys = 0;
        let onRemove = false;

        if (index === 0 || index === listSize) {
          isRemove = false;
        }

        for (var i = 0; i < keys.length; i++) {
          let _key = keys[i];
          if (task.param[_key]) {
            if (_.keys(filter[_key]).length > 0 && _.isObject(filter[_key])) {
              keysSize = keysSize + _.keys(filter[_key]).length;
              let deepKeys = _.keys(filter[_key]);
              deepKeysSize = _.size(deepKeys);
              for (var i = 0; i < deepKeys.length; i++) {
                let deepKey = deepKeys[i];
                if (task.param[_key] && task.param[_key][deepKey]) {
                  if ( _.isEqual(task.param[_key][deepKey], filter[_key][deepKey]) ) {
                    countDeepKeys++
                  }
                }
              }
            }else{
              if ( _.isEqual(task.param[_key], filter[_key]) ) {
                countCompare++
              }
            }
          }
        }

        onRemove = (_.isEqual(keysSize, countCompare) && isRemove || _.isEqual(deepKeysSize, countDeepKeys) && isRemove);

        return onRemove;
      });
    }

    self.taskList.push({
      task: name,
      param
    })

    if (!self.lock) {
      self.taskSequencer();
    }
  }

  taskSequencer(){
    const self = this;
    return new Promise((resolve, reject) => {
    if (_.size(self.taskList) === 0){
      resolve();
      return;
    }

    if (self.options.statusBar) {
      var statusBar = new ProgressBar(self.options.statusBar.template, {
        complete: '█',
        incomplete: '.',
        width: 20,
        clear: true,
        total: self.taskList.length
      });
    }

    taskRunner();
    function taskRunner(){
      self.lock = true;
      delay(self.options.delay).then(() => {
        self.tasks[self.taskList[0].task](self.taskList[0].param).then(content => {
          let _content = content || {};

          self.taskList.splice(0, 1);
          self.lock = false;
          if (self.options.statusBar && statusBar) {
            statusBar.tick(_content);
          }

          if (_.size(self.taskList) > 0){
            return taskRunner();
          }else{
            resolve()
            return;
          }
        })
      })
    }
    });
  }

  getTaskList(){
    const self = this;
    console.log(self.tasks);
  }
}
export default options => new TaskManager(options);
<|endoftext|>


<commit_before>/**
 * Holds shared logic for minigames.
 * How to use:
 * Number amount:        this.amount
 * Representation:       this.representation
 * The number to answer: this.currentNumber
 *
 * Add game objects to:     this.gameGroup
 * Add buttons and HUD to:  this.hudGroup (default visibility = false)
 * Add background music to: this.music
 * Use agent with:          this.agent, (default visibility = false)
 *
 *
 * Functions:
 * Add event subscriptions: this.addEvent
 * Start game:              this.startGame
 * Go to next mode:         this.nextMode (only use this in intro-mode, the others change automatically)
 * Run next round:          this.nextRound (this will call the appropriate mode function)
 * Try a number:            this.tryNumber
 *
 * These function should be overshadowed by the game:
 * modeIntro      // Introduce the game, call nextMode and nextRound to start next mode.
 * modePlayerDo   // Player only
 * modePlayerShow // Player is showing the TA
 * modeAgentTry   // TA is guessing and the player is helping out
 * modeAgentDo    // TA only
 * modeOutro      // The game is finished, celebrate!
 *
 *
 * Typical game flow:
 * this.startGame();  // the first mode, this.modeIntro, will be called
 * this.nextMode();   // change mode, now this.modePlayerDo will be called
 * this.nextRound();  // start the mode
 * this.tryNumber(x); // try a number against the current one, returns true or false
 * this.nextRound();  // do this regardless if right or wrong,
 *                    // it takes care of mode switching and function calls for you
 * // Do until game is done, then quit by using: this.state.start(GLOBAL.STATE.garden);
 */
function Minigame () {}

/* Phaser state function */
Minigame.prototype.init = function (options) {
	/* "Private" variables */
	var _this = this; // Event subscriptions does not have access to this
	this._modes = options.mode || [
		GLOBAL.MODE.intro,
		GLOBAL.MODE.playerDo,
		GLOBAL.MODE.playerShow,
		GLOBAL.MODE.agentTry,
		GLOBAL.MODE.agentDo,
		GLOBAL.MODE.outro
	];
	this._mode = null;
	this._pendingMode = null;
	this._first = true;
	/* Keep track of how many rounds that have been played */
	this._counter = new Counter(options.roundsPerMode || 3, true);
	/* When enough rounds have been played, trigger a mode change */
	this._counter.onMax = function () {
		_this.nextMode();
	};
	this._currentTries = 0;
	this._totalTries = 0;
	this._events = [];

	/* Public variables */
	this.representation = options.representation;
	this.amount = options.amount;
	/* The current number to answer */
	this.currentNumber = null;
	/* The current mode running */
	this.currentMode = null;
	/* Stores the offset of the last try, can be used to judge last try */
	/* Ex: -1 means that last try was one less than currentNumber */
	this.lastTry = 0;

	/* Setup game objects */
	this.gameGroup = game.add.group();
	this.agent = user.agent;
	this.agent.visible = false;
	this.gameGroup.add(this.agent);

	this.hudGroup = game.add.group();
	this.hudGroup.visible = false;

	this.menuGroup = game.add.group();
	this.waterCan = new WaterCan(this.game.width - 100, 10);
	this.menuGroup.add(this.waterCan);
	this.menuGroup.add(new Menu());

	this.music = null;

	//  TODO: Is there an easier way to disable all input, except the menu?
	var bmd = game.add.bitmapData(game.world.width, game.world.height);
	bmd.ctx.fillRect(0, 0, game.world.width, game.world.height);
	var disabler = game.add.sprite(0, 0, bmd);
	disabler.alpha = 0;
	disabler.inputEnabled = true;
	this.disable = function (value) { disabler.visible = value; };
};

/* Phaser state function */
Minigame.prototype.shutdown = function () {
	if (this.music) {
		this.music.stop();
	}

	for (var i = 0; i < this._events.length; i++) {
		unsubscribe(this._events[i]);
	}
};

/* Always add event subscriptions with these functions, they are then removed when shutting down */
/**
 * Subscribe to an event.
 * @param {*} The name of the event
 * @param {Function} The function to run when the event is published
 */
Minigame.prototype.addEvent = function (ev, func) {
	this._events.push(subscribe(ev, func));
};

/**
 * Unsubscribe to an event.
 * @param {*} The name of the event
 */
Minigame.prototype.removeEvent = function (ev) {
	for (var i = 0; i < this._events.length; i++) {
		if (this._events[i] === ev) {
			unsubscribe(this._events[i]);
			break;
		}
	}
};

/**
 * Calls the current mode function.
 * It will be called with two parameters:
 * 1) If it is the first time on this mode.
 * 2) How many tries that have been made on the current number.
 */
Minigame.prototype.nextRound = function () {
	this._mode(this._first, this._currentTries);
	this.currentMode = this._pendingMode;
	this._first = false;
};

/**
 * Translate from integer to mode function
 * @param {Number}
 */
Minigame.prototype.decideMode = function (mode) {
	if (mode === GLOBAL.MODE.intro) {
		this._mode = this.modeIntro;
	} else if (mode === GLOBAL.MODE.playerDo) {
		this._mode = this.modePlayerDo;
	} else if (mode === GLOBAL.MODE.playerShow) {
		this._mode = this.modePlayerShow;
	} else if (mode === GLOBAL.MODE.agentTry) {
		this._mode = this.modeAgentTry;
	} else if (mode === GLOBAL.MODE.agentDo) {
		this._mode = this.modeAgentDo;
	} else { // mode === GLOBAL.MODE.outro
		this._mode = this.modeOutro;
	}
};

/** Change to the next mode in the queue (publishes modeChange event). */
Minigame.prototype.nextMode = function () {
	var newMode = this._modes.shift();
	this.decideMode(newMode);
	this._pendingMode = newMode;
	this._first = true;
	publish(GLOBAL.EVENT.modeChange, [newMode]);
};

/** Change this.currentNumber to a new one (resets the tries). */
Minigame.prototype.nextNumber = function () {
	// Should we allow the same number again?
	this._totalTries += this._currentTries;
	this._currentTries = 0;
	this.currentNumber = parseInt(1+Math.random()*this.amount);
};

/**
 * Try a number against this.currentNumber (publishes tryNumber event).
 * The offset of the last try is stored in this.lastTry.
 *
 * @param {Number} The number to try.
 * @returns {Boolean} The offset of the last try (0 is correct, -x is too low, +x is too high).
 */
Minigame.prototype.tryNumber = function (number) {
	this._currentTries++;
	this.lastTry = number - this.currentNumber;

	if (!this.lastTry) {
		this._counter.value++; // This will trigger next mode if we loop.
		this.nextNumber();
	}
	publish(GLOBAL.EVENT.tryNumber, [this.currentNumber, number]);
	return this.lastTry;
};

Minigame.prototype.addWater = function (x, y, onComplete, force) {
	if (this.currentMode === GLOBAL.MODE.playerShow ||
		this.currentMode === GLOBAL.MODE.agentTry ||
		this.currentMode === GLOBAL.MODE.agentDo ||
		force) {
		var drop = this.add.sprite(x, y, 'drop', 0, this.menuGroup);
		drop.anchor.setTo(0.5);
		drop.scale.y = 0;
		var t = this.add.tween(drop.scale).to({ y: 1 }, 1500, Phaser.Easing.Elastic.Out).start()
			.then(this.add.tween(drop).to({ x: this.waterCan.x + 30, y: this.waterCan.y }, 1500, Phaser.Easing.Quadratic.Out))
			.then(this.add.tween(drop).to({ height: 0 }, 500));
		t.onStart.add(function () {
			user.water++;
		});
		t.onComplete.add(function () {
			drop.destroy();
			onComplete();
		});
	} else {
		onComplete();
	}
};

/** Start the game! */
Minigame.prototype.startGame = function () {
	this.nextMode();
	this.nextNumber();
	this.nextRound();
};

/* Overshadowing mode functions */
/* These functions should be overshadowed in the game object */
Minigame.prototype.modeIntro      = function () { this.nextMode(); };
Minigame.prototype.modePlayerDo   = function () { this.nextMode(); };
Minigame.prototype.modePlayerShow = function () { this.nextMode(); };
Minigame.prototype.modeAgentTry   = function () { this.nextMode(); };
Minigame.prototype.modeAgentDo    = function () { this.nextMode(); };
Minigame.prototype.modeOutro      = function () { this.nextMode(); };<commit_msg>Correct order for minigame disabler<commit_after>/**
 * Holds shared logic for minigames.
 * How to use:
 * Number amount:        this.amount
 * Representation:       this.representation
 * The number to answer: this.currentNumber
 *
 * Add game objects to:     this.gameGroup
 * Add buttons and HUD to:  this.hudGroup (default visibility = false)
 * Add background music to: this.music
 * Use agent with:          this.agent, (default visibility = false)
 *
 *
 * Functions:
 * Add event subscriptions: this.addEvent
 * Start game:              this.startGame
 * Go to next mode:         this.nextMode (only use this in intro-mode, the others change automatically)
 * Run next round:          this.nextRound (this will call the appropriate mode function)
 * Try a number:            this.tryNumber
 *
 * These function should be overshadowed by the game:
 * modeIntro      // Introduce the game, call nextMode and nextRound to start next mode.
 * modePlayerDo   // Player only
 * modePlayerShow // Player is showing the TA
 * modeAgentTry   // TA is guessing and the player is helping out
 * modeAgentDo    // TA only
 * modeOutro      // The game is finished, celebrate!
 *
 *
 * Typical game flow:
 * this.startGame();  // the first mode, this.modeIntro, will be called
 * this.nextMode();   // change mode, now this.modePlayerDo will be called
 * this.nextRound();  // start the mode
 * this.tryNumber(x); // try a number against the current one, returns true or false
 * this.nextRound();  // do this regardless if right or wrong,
 *                    // it takes care of mode switching and function calls for you
 * // Do until game is done, then quit by using: this.state.start(GLOBAL.STATE.garden);
 */
function Minigame () {}

/* Phaser state function */
Minigame.prototype.init = function (options) {
	/* "Private" variables */
	var _this = this; // Event subscriptions does not have access to this
	this._modes = options.mode || [
		GLOBAL.MODE.intro,
		GLOBAL.MODE.playerDo,
		GLOBAL.MODE.playerShow,
		GLOBAL.MODE.agentTry,
		GLOBAL.MODE.agentDo,
		GLOBAL.MODE.outro
	];
	this._mode = null;
	this._pendingMode = null;
	this._first = true;
	/* Keep track of how many rounds that have been played */
	this._counter = new Counter(options.roundsPerMode || 3, true);
	/* When enough rounds have been played, trigger a mode change */
	this._counter.onMax = function () {
		_this.nextMode();
	};
	this._currentTries = 0;
	this._totalTries = 0;
	this._events = [];

	/* Public variables */
	this.representation = options.representation;
	this.amount = options.amount;
	/* The current number to answer */
	this.currentNumber = null;
	/* The current mode running */
	this.currentMode = null;
	/* Stores the offset of the last try, can be used to judge last try */
	/* Ex: -1 means that last try was one less than currentNumber */
	this.lastTry = 0;

	/* Setup game objects */
	this.gameGroup = game.add.group();
	this.agent = user.agent;
	this.agent.visible = false;
	this.gameGroup.add(this.agent);

	this.hudGroup = game.add.group();
	this.hudGroup.visible = false;

	//  TODO: Is there an easier way to disable all input, except the menu?
	var bmd = game.add.bitmapData(game.world.width, game.world.height);
	bmd.ctx.fillRect(0, 0, game.world.width, game.world.height);
	var disabler = game.add.sprite(0, 0, bmd);
	disabler.alpha = 0;
	disabler.inputEnabled = true;
	this.disable = function (value) { disabler.visible = value; };

	this.menuGroup = game.add.group();
	this.waterCan = new WaterCan(this.game.width - 100, 10);
	this.menuGroup.add(this.waterCan);
	this.menuGroup.add(new Menu());

	this.music = null;
};

/* Phaser state function */
Minigame.prototype.shutdown = function () {
	if (this.music) {
		this.music.stop();
	}

	for (var i = 0; i < this._events.length; i++) {
		unsubscribe(this._events[i]);
	}
};

/* Always add event subscriptions with these functions, they are then removed when shutting down */
/**
 * Subscribe to an event.
 * @param {*} The name of the event
 * @param {Function} The function to run when the event is published
 */
Minigame.prototype.addEvent = function (ev, func) {
	this._events.push(subscribe(ev, func));
};

/**
 * Unsubscribe to an event.
 * @param {*} The name of the event
 */
Minigame.prototype.removeEvent = function (ev) {
	for (var i = 0; i < this._events.length; i++) {
		if (this._events[i] === ev) {
			unsubscribe(this._events[i]);
			break;
		}
	}
};

/**
 * Calls the current mode function.
 * It will be called with two parameters:
 * 1) If it is the first time on this mode.
 * 2) How many tries that have been made on the current number.
 */
Minigame.prototype.nextRound = function () {
	this._mode(this._first, this._currentTries);
	this.currentMode = this._pendingMode;
	this._first = false;
};

/**
 * Translate from integer to mode function
 * @param {Number}
 */
Minigame.prototype.decideMode = function (mode) {
	if (mode === GLOBAL.MODE.intro) {
		this._mode = this.modeIntro;
	} else if (mode === GLOBAL.MODE.playerDo) {
		this._mode = this.modePlayerDo;
	} else if (mode === GLOBAL.MODE.playerShow) {
		this._mode = this.modePlayerShow;
	} else if (mode === GLOBAL.MODE.agentTry) {
		this._mode = this.modeAgentTry;
	} else if (mode === GLOBAL.MODE.agentDo) {
		this._mode = this.modeAgentDo;
	} else { // mode === GLOBAL.MODE.outro
		this._mode = this.modeOutro;
	}
};

/** Change to the next mode in the queue (publishes modeChange event). */
Minigame.prototype.nextMode = function () {
	var newMode = this._modes.shift();
	this.decideMode(newMode);
	this._pendingMode = newMode;
	this._first = true;
	publish(GLOBAL.EVENT.modeChange, [newMode]);
};

/** Change this.currentNumber to a new one (resets the tries). */
Minigame.prototype.nextNumber = function () {
	// Should we allow the same number again?
	this._totalTries += this._currentTries;
	this._currentTries = 0;
	this.currentNumber = parseInt(1+Math.random()*this.amount);
};

/**
 * Try a number against this.currentNumber (publishes tryNumber event).
 * The offset of the last try is stored in this.lastTry.
 *
 * @param {Number} The number to try.
 * @returns {Boolean} The offset of the last try (0 is correct, -x is too low, +x is too high).
 */
Minigame.prototype.tryNumber = function (number) {
	this._currentTries++;
	this.lastTry = number - this.currentNumber;

	if (!this.lastTry) {
		this._counter.value++; // This will trigger next mode if we loop.
		this.nextNumber();
	}
	publish(GLOBAL.EVENT.tryNumber, [this.currentNumber, number]);
	return this.lastTry;
};

Minigame.prototype.addWater = function (x, y, onComplete, force) {
	if (this.currentMode === GLOBAL.MODE.playerShow ||
		this.currentMode === GLOBAL.MODE.agentTry ||
		this.currentMode === GLOBAL.MODE.agentDo ||
		force) {
		var drop = this.add.sprite(x, y, 'drop', 0, this.menuGroup);
		drop.anchor.setTo(0.5);
		drop.scale.y = 0;
		var t = this.add.tween(drop.scale).to({ y: 1 }, 1500, Phaser.Easing.Elastic.Out).start()
			.then(this.add.tween(drop).to({ x: this.waterCan.x + 30, y: this.waterCan.y }, 1500, Phaser.Easing.Quadratic.Out))
			.then(this.add.tween(drop).to({ height: 0 }, 500));
		t.onStart.add(function () {
			user.water++;
		});
		t.onComplete.add(function () {
			drop.destroy();
			onComplete();
		});
	} else {
		onComplete();
	}
};

/** Start the game! */
Minigame.prototype.startGame = function () {
	this.nextMode();
	this.nextNumber();
	this.nextRound();
};

/* Overshadowing mode functions */
/* These functions should be overshadowed in the game object */
Minigame.prototype.modeIntro      = function () { this.nextMode(); };
Minigame.prototype.modePlayerDo   = function () { this.nextMode(); };
Minigame.prototype.modePlayerShow = function () { this.nextMode(); };
Minigame.prototype.modeAgentTry   = function () { this.nextMode(); };
Minigame.prototype.modeAgentDo    = function () { this.nextMode(); };
Minigame.prototype.modeOutro      = function () { this.nextMode(); };<|endoftext|>


<commit_before>/*jshint node:true*/
'use strict';

var Promise = require('../ext/promise');
var fs = require('fs-extra');
var path = require('path');
var copy = Promise.denodeify(fs.copy);
var readFile = Promise.denodeify(fs.readFile);

function determineMicroAddonType(packageJsonContent) {
  var packageInfo = JSON.parse(packageJsonContent);
  var addonType;

  if (packageInfo.keywords) {
    if (packageInfo.keywords.indexOf('ember-micro-component') >= 0) {
      addonType = 'component';
    } else if (packageInfo.keywords.indexOf('ember-micro-helper') >= 0) {
      addonType = 'helper';
    } else if (packageInfo.keywords.indexOf('ember-micro-library') >= 0) {
      addonType = 'library';
    }
  }

  return addonType;
}

function writeIndexJs(microAddon) {

  var indexFile = fs.createOutputStream(path.join(microAddon.path, 'dist', 'index.js'));
  indexFile.write('\/* jshint node: true */\n');
  indexFile.write('\n');
  indexFile.write('module.exports = {\n');
  indexFile.write('  name: \'' + microAddon.name + '\'\n');
  indexFile.write('};\n');
  indexFile.end();

  return Promise.resolve(microAddon);
}

function copyPackageJson(microAddon) {
  var oldPackageJson = path.join(microAddon.path, 'package.json');
  var newPackageJson = path.join(microAddon.path, 'dist', 'package.json');
  return copy(oldPackageJson, newPackageJson, { clobber: true }).then(function() {
    console.log(microAddon.path, microAddon.name);
    return Promise.resolve(microAddon);
  });
}

function copyHelperJs(microAddon) {
  var oldHelper = path.join(microAddon.path, 'helper.js');
  var newHelper = path.join(microAddon.path, 'dist', 'app', 'helpers', microAddon.name + '.js');
  return copy(oldHelper, newHelper, { clobber: true });
}

function copyLibraryJs(microAddon) {
  var oldLibrary = path.join(microAddon.path, 'library.js');
  var newLibrary = path.join(microAddon.path, 'dist', 'app', 'lib', microAddon.name + '.js');
  return copy(oldLibrary, newLibrary, { clobber: true });
}

function buildMicroHelper(microAddon) {
  console.log('Packaging helper...', microAddon.name);
  return copyPackageJson(microAddon).then(writeIndexJs).then(copyHelperJs);
}

function buildMicroLibrary(microAddon) {
  console.log('Packaging library...', microAddon.name);
  return copyPackageJson(microAddon).then(writeIndexJs).then(copyLibraryJs);
}

function buildMicroComponent(microAddon) {
  console.log('Packaging component...', microAddon.name);
}

function buildMicroAddon(microAddon, type) {
  if (type === 'helper')  {
    return buildMicroHelper(microAddon);
  } else if (type === 'library') {
    return buildMicroLibrary(microAddon);
  } else if (type === 'component') {
    return buildMicroComponent(microAddon);
  }
}

module.exports = function(rawArgs) {

  var microAddon = {
    name: rawArgs[0],
    path: path.join(process.cwd(), rawArgs[0])
  };

  if (microAddon.name) {
    return readFile(path.join(microAddon.path, 'package.json'))
    .then(determineMicroAddonType)
    .then(function(type) {
      return buildMicroAddon(microAddon, type);
  	});

  } else {
    return Promise.reject(new Error('A "name" parameter is required to build a micro-addon'));
  }
};
<commit_msg>ember micro:build support for components<commit_after>/*jshint node:true*/
'use strict';

var Promise = require('../ext/promise');
var fs = require('fs-extra');
var path = require('path');
var copy = Promise.denodeify(fs.copy);
var readFile = Promise.denodeify(fs.readFile);

function determineMicroAddonType(packageJsonContent) {
  var packageInfo = JSON.parse(packageJsonContent);
  var addonType;

  if (packageInfo.keywords) {
    if (packageInfo.keywords.indexOf('ember-micro-component') >= 0) {
      addonType = 'component';
    } else if (packageInfo.keywords.indexOf('ember-micro-helper') >= 0) {
      addonType = 'helper';
    } else if (packageInfo.keywords.indexOf('ember-micro-library') >= 0) {
      addonType = 'library';
    }
  }

  return addonType;
}

function writeIndexJs(microAddon) {

  var indexFile = fs.createOutputStream(path.join(microAddon.path, 'dist', 'index.js'));
  indexFile.write('\/* jshint node: true */\n');
  indexFile.write('\n');
  indexFile.write('module.exports = {\n');
  indexFile.write('  name: \'' + microAddon.name + '\'\n');
  indexFile.write('};\n');
  indexFile.end();

  return Promise.resolve(microAddon);
}

function copyPackageJson(microAddon) {
  var oldPackageJson = path.join(microAddon.path, 'package.json');
  var newPackageJson = path.join(microAddon.path, 'dist', 'package.json');
  return copy(oldPackageJson, newPackageJson, { clobber: true }).then(function() {
    return Promise.resolve(microAddon);
  });
}

function copyHelperJs(microAddon) {
  var oldHelper = path.join(microAddon.path, 'helper.js');
  var newHelper = path.join(microAddon.path, 'dist', 'app', 'helpers', microAddon.name + '.js');
  return copy(oldHelper, newHelper, { clobber: true }).then(function() {
    return Promise.resolve(microAddon);
  });
}

function copyLibraryJs(microAddon) {
  var oldLibrary = path.join(microAddon.path, 'library.js');
  var newLibrary = path.join(microAddon.path, 'dist', 'app', 'lib', microAddon.name + '.js');
  return copy(oldLibrary, newLibrary, { clobber: true }).then(function() {
    return Promise.resolve(microAddon);
  });
}

function copyComponentJs(microAddon) {
  var oldComponent = path.join(microAddon.path, 'component.js');
  var newComponent = path.join(microAddon.path, 'dist', 'app', 'components', microAddon.name + '.js');
  return copy(oldComponent, newComponent, { clobber: true }).then(function() {
    return Promise.resolve(microAddon);
  });
}

function copyComponentTemplate(microAddon) {
  var oldTemplate = path.join(microAddon.path, 'template.hbs');
  var newTemplate = path.join(microAddon.path, 'dist', 'app', 'templates', 'components', microAddon.name + '.hbs');
  return copy(oldTemplate, newTemplate, { clobber: true}).then(function() {
    return Promise.resolve(microAddon);
  });
}

function copyComponentStyle(microAddon) {
  var oldStyle = path.join(microAddon.path, 'style.css');
  var newStyle = path.join(microAddon.path, 'dist', 'addon', 'styles', microAddon.name + '.css');
  return copy(oldStyle, newStyle, { clobber: true }).then(function() {
    return Promise.resolve(microAddon);
  });
}

function buildMicroHelper(microAddon) {
  console.log('Packaging helper...', microAddon.name);
  return copyPackageJson(microAddon)
    .then(writeIndexJs)
    .then(copyHelperJs);
}

function buildMicroLibrary(microAddon) {
  console.log('Packaging library...', microAddon.name);
  return copyPackageJson(microAddon)
    .then(writeIndexJs)
    .then(copyLibraryJs);
}

function buildMicroComponent(microAddon) {
  console.log('Packaging component...', microAddon.name);
  return copyPackageJson(microAddon)
    .then(writeIndexJs)
    .then(copyComponentJs)
    .then(copyComponentTemplate)
    .then(copyComponentStyle);
}

function buildMicroAddon(microAddon, type) {
  if (type === 'helper')  {
    return buildMicroHelper(microAddon);
  } else if (type === 'library') {
    return buildMicroLibrary(microAddon);
  } else if (type === 'component') {
    return buildMicroComponent(microAddon);
  }
}

module.exports = function(rawArgs) {

  var microAddon = {
    name: rawArgs[0],
    path: path.join(process.cwd(), rawArgs[0])
  };

  if (microAddon.name) {
    return readFile(path.join(microAddon.path, 'package.json'))
    .then(determineMicroAddonType)
    .then(function(type) {
      return buildMicroAddon(microAddon, type);
  	});

  } else {
    return Promise.reject(new Error('A "name" parameter is required to build a micro-addon'));
  }
};
<|endoftext|>


<commit_before>// Load wetfish basic
var $ = require('wetfish-basic');

// Load custom modules
var storage = require('../app/storage');
var helper = require('../app/helper');
var pool = require('../plugins/pool');

// A map of currently pressed keys
var pressed = {};

// Global timeout cache
var timeout =
{
    // The durations of the different explosion animations
    explosion:
    {
        1: 1600,
        2: 1200,
        3: 860,
        4: 1060,
        5: 1600,
        6: 900,
        7: 1090
    }
};

// Key codes for control characters
var keys =
{
    8: 'backspace',
    9: 'tab',
    16: 'shift',
    17: 'control',
    18: 'alt',
    20: 'caps',
    27: 'escape',
    46: 'delete',
    91: 'system',
};

// UI interactions
var interactions =
{
    init: function()
    {
        var explosion = $('.preload .explosion').el[0];
        var hitmarker = $('.preload .hitmarker').el[0];

        pool.init(explosion, 'explosion', 16);
        pool.init(hitmarker, 'hitmarker', 7);

        // Hitmarkers
        $('body').on('mousedown touchstart', '.workspace, .workspace *', function(event)
        {
            // Prevent default behavior when in tool mode
            if($('body').hasClass('tool-mode'))
            {
                return;
            }

            // If left click was pressed, or this is a touch event
            if(!event.buttons || event.buttons == 1)
            {
                // If the click timeout is defined, this must be a double click!
                if(timeout.click)
                {
                    // If this is an audio or video element
                    if(this.nodeName && this.nodeName.toLowerCase() == 'audio' || this.nodeName.toLowerCase() == 'video')
                    {
                        // Toggle playing state
                        if(this.paused)
                        {
                            this.play();
                        }
                        else
                        {
                            this.pause();
                        }
                    }

                    timeout.click = null;
                }
                else
                {
                    // Create a timeout to determine if the user is double clicking
                    timeout.click = setTimeout(function()
                    {
                        timeout.click = null;
                    }, 250);
                }

                // If the user is holding control while clicking
                if(pressed.control)
                {
                    var image = document.createElement('img');
                    $(image).style({'top': event.clientY + 'px', 'left': event.clientX + 'px', 'z-index': helper.layers + 1});
                    $(image).addClass('hitmarker');
                    $(image).attr('src', 'img/hitmarker.png');
                    $('.workspace').el[0].appendChild(image);

                    pool.play('hitmarker');

                    // Remove the image after a bit
                    setTimeout(function()
                    {
                        $(image).remove();
                    }, 500);
                }
            }
        });

        // Deleting things
        $('body').on('mousedown', '.workspace *', function(event)
        {
            // Prevent default behavior when in tool mode
            if($('body').hasClass('tool-mode'))
            {
                return;
            }

            // If right click was pressed
            if(event.buttons == 2)
            {
                // If the user is holding control while deleting an image
                if(pressed.control)
                {
                    // Save the size and position of the current element
                    var size = $(this).size();
                    var position = $(this).position();

                    // Increase the size of the explosion a little bit
                    size.width += 50;
                    size.height += 50;
                    position.top -= 25;
                    position.left -= 25;

                    // Create an image to overlay the explosion over the current element
                    var image = document.createElement('img');
                    var options =
                    {
                        'position': 'absolute',
                        'top': position.top + 'px',
                        'left': position.left + 'px',
                        'width': size.width + 'px',
                        'height': size.height + 'px',
                        'z-index': $(this).style('z-index')
                    };

                    $(image).style(options);
                    $(image).addClass('explosion');

                    // Generate a random explosion image
                    var random = helper.randomInt(1, 7);
                    $(image).attr('src', 'img/explosion-' + random + '.gif');

                    // EXPLODE!
                    $('.workspace').el[0].appendChild(image);

                    pool.play('explosion');

                    // Remove the original element, if it's not an explosion
                    if(!$(this).hasClass('explosion'))
                    {
                        $(this).remove();
                    }

                    // Remove the explosion after the animation is finished
                    setTimeout(function()
                    {
                        $(image).remove();
                    }, timeout.explosion[random])
                }
                else
                {
                    // Otherwise just remove it
                    $(this).remove();
                }

                storage.remove(this);
            }
        });

        // When something is moved around
        $('.workspace').on('dragend', function(event)
        {
            // Update storage
            storage.update(event.detail);
        });

        $('body').on('contextmenu', function(event)
        {
            // Is the current element the workspace or a child of it?
            if($(event.target).hasClass('workspace') || $(event.target).parents('.workspace').el.length)
            {
                // Prevent right-click menu from appearing
                event.preventDefault();
            }

            // Otherwise, let menus appear normally
        });

        $('body').on('keydown', function(event)
        {
            var key = (event.key) ? event.key.toLowerCase() : keys[event.which];
            pressed[key] = true;

            // Keyboard shortcuts that trigger when a user presses escape
            if(key == 'escape')
            {
                // Is an overlay open?
                if($('body').hasClass('overlay-open'))
                {
                    overlay.close();
                }

                // Are we in tool mode?
                else if($('body').hasClass('tool-mode'))
                {
                    tools.stop();
                }

                // Otherwise, toggle the menu when pressing escape
                else
                {
                    $('.menu').toggle('hidden');
                }
            }
        });

        $('body').on('keyup', function(event)
        {
            var key = (event.key) ? event.key.toLowerCase() : keys[event.which];
            delete pressed[key];
        });
    },
};

module.exports = interactions;
<commit_msg>Fixed hitmarker positioning after scrolling<commit_after>// Load wetfish basic
var $ = require('wetfish-basic');

// Load custom modules
var storage = require('../app/storage');
var helper = require('../app/helper');
var pool = require('../plugins/pool');

// A map of currently pressed keys
var pressed = {};

// Global timeout cache
var timeout =
{
    // The durations of the different explosion animations
    explosion:
    {
        1: 1600,
        2: 1200,
        3: 860,
        4: 1060,
        5: 1600,
        6: 900,
        7: 1090
    }
};

// Key codes for control characters
var keys =
{
    8: 'backspace',
    9: 'tab',
    16: 'shift',
    17: 'control',
    18: 'alt',
    20: 'caps',
    27: 'escape',
    46: 'delete',
    91: 'system',
};

// UI interactions
var interactions =
{
    init: function()
    {
        var explosion = $('.preload .explosion').el[0];
        var hitmarker = $('.preload .hitmarker').el[0];

        pool.init(explosion, 'explosion', 16);
        pool.init(hitmarker, 'hitmarker', 7);

        // Hitmarkers
        $('body').on('mousedown touchstart', '.workspace, .workspace *', function(event)
        {
            // Prevent default behavior when in tool mode
            if($('body').hasClass('tool-mode'))
            {
                return;
            }

            // If left click was pressed, or this is a touch event
            if(!event.buttons || event.buttons == 1)
            {
                // If the click timeout is defined, this must be a double click!
                if(timeout.click)
                {
                    // If this is an audio or video element
                    if(this.nodeName && this.nodeName.toLowerCase() == 'audio' || this.nodeName.toLowerCase() == 'video')
                    {
                        // Toggle playing state
                        if(this.paused)
                        {
                            this.play();
                        }
                        else
                        {
                            this.pause();
                        }
                    }

                    timeout.click = null;
                }
                else
                {
                    // Create a timeout to determine if the user is double clicking
                    timeout.click = setTimeout(function()
                    {
                        timeout.click = null;
                    }, 250);
                }

                // If the user is holding control while clicking
                if(pressed.control)
                {
                    var position =
                    {
                        top: event.clientY + window.scrollY,
                        left: event.clientX + window.scrollX
                    };

                    var image = document.createElement('img');
                    $(image).style({'top': position.top + 'px', 'left': position.left + 'px', 'z-index': helper.layers + 1});
                    $(image).addClass('hitmarker');
                    $(image).attr('src', 'img/hitmarker.png');
                    $('.workspace').el[0].appendChild(image);

                    pool.play('hitmarker');

                    // Remove the image after a bit
                    setTimeout(function()
                    {
                        $(image).remove();
                    }, 500);
                }
            }
        });

        // Deleting things
        $('body').on('mousedown', '.workspace *', function(event)
        {
            // Prevent default behavior when in tool mode
            if($('body').hasClass('tool-mode'))
            {
                return;
            }

            // If right click was pressed
            if(event.buttons == 2)
            {
                // If the user is holding control while deleting an image
                if(pressed.control)
                {
                    // Save the size and position of the current element
                    var size = $(this).size();
                    var position = $(this).position();

                    // Increase the size of the explosion a little bit
                    size.width += 50;
                    size.height += 50;
                    position.top -= 25;
                    position.left -= 25;

                    // Create an image to overlay the explosion over the current element
                    var image = document.createElement('img');
                    var options =
                    {
                        'position': 'absolute',
                        'top': position.top + 'px',
                        'left': position.left + 'px',
                        'width': size.width + 'px',
                        'height': size.height + 'px',
                        'z-index': $(this).style('z-index')
                    };

                    $(image).style(options);
                    $(image).addClass('explosion');

                    // Generate a random explosion image
                    var random = helper.randomInt(1, 7);
                    $(image).attr('src', 'img/explosion-' + random + '.gif');

                    // EXPLODE!
                    $('.workspace').el[0].appendChild(image);

                    pool.play('explosion');

                    // Remove the original element, if it's not an explosion
                    if(!$(this).hasClass('explosion'))
                    {
                        $(this).remove();
                    }

                    // Remove the explosion after the animation is finished
                    setTimeout(function()
                    {
                        $(image).remove();
                    }, timeout.explosion[random])
                }
                else
                {
                    // Otherwise just remove it
                    $(this).remove();
                }

                storage.remove(this);
            }
        });

        // When something is moved around
        $('.workspace').on('dragend', function(event)
        {
            // Update storage
            storage.update(event.detail);
        });

        $('body').on('contextmenu', function(event)
        {
            // Is the current element the workspace or a child of it?
            if($(event.target).hasClass('workspace') || $(event.target).parents('.workspace').el.length)
            {
                // Prevent right-click menu from appearing
                event.preventDefault();
            }

            // Otherwise, let menus appear normally
        });

        $('body').on('keydown', function(event)
        {
            var key = (event.key) ? event.key.toLowerCase() : keys[event.which];
            pressed[key] = true;

            // Keyboard shortcuts that trigger when a user presses escape
            if(key == 'escape')
            {
                // Is an overlay open?
                if($('body').hasClass('overlay-open'))
                {
                    overlay.close();
                }

                // Are we in tool mode?
                else if($('body').hasClass('tool-mode'))
                {
                    tools.stop();
                }

                // Otherwise, toggle the menu when pressing escape
                else
                {
                    $('.menu').toggle('hidden');
                }
            }
        });

        $('body').on('keyup', function(event)
        {
            var key = (event.key) ? event.key.toLowerCase() : keys[event.which];
            delete pressed[key];
        });
    },
};

module.exports = interactions;
<|endoftext|>


<commit_before>import config from 'config';
import reusePromise from 'reuse-promise';

import { send as sendMessage } from './messaging';
import Log from './logger';
import * as Providers from './providers/index';
import * as BHelper from './browserHelper';

const endpoints = {
  embedly: 'https://api.embed.ly/1/oembed?',
  '500px': 'https://500px.com/oembed?format=json&url=',
  dailymotion: 'https://api.dailymotion.com/video/',
  deviantart: 'https://backend.deviantart.com/oembed?',
  dribbble: 'https://api.dribbble.com/v1/shots/',
  noembed: 'https://noembed.com/embed?nowrap=on&url=',
  imgur: 'https://api.imgur.com/3/',
  instagram: 'https://api.instagram.com/oembed?omitscript=true&url=',
  twitch: 'https://api.twitch.tv/kraken/',
  giphy: 'https://giphy.com/services/oembed?url=',
  pixiv: 'http://embed.pixiv.net/embed_json.php?callback=callback&size=medium&id=',
  tinami: 'https://www.tinami.com/api/content/info?',
  nicoseiga: 'http://ext.seiga.nicovideo.jp/thumb/',
  photozou: 'https://api.photozou.jp/rest/',
  googleplus: 'https://picasaweb.google.com/data/feed/api/',
  gyazo: 'https://api.gyazo.com/api/oembed?url=',
  gfycat: 'https://api.gfycat.com/v1/oembed?url=',
};

let providersSettings;

sendMessage({ action: 'get', key: 'thumbnails' }, (response) => {
  providersSettings = response.val;
});

/**
 * Returns a 'safe' URL for images to use as background-image/src value
 * @param  {String} url url of image
 * @return {String}     safe url
 */
export const getSafeURL = (url) => {
  if (!url) {
    return '';
  }

  if (url.startsWith('//')) {
    url = `https:${url}`;
  }

  return `https://images4-focus-opensocial.googleusercontent.com/gadgets/proxy?url=${encodeURIComponent(url)}&container=focus&resize_w=720&refresh=86400`;
};

/**
 * Returns the endpoint for the asked service
 * @param  {String} service key of service from `endpoints` object
 * @return {String}         url to fetch
 */
const getEnpointFor = service => endpoints[service];

/**
 * Returns API key from config for a given servic3e
 * @param  {String} service Name of service
 * @return {String}         API key of service
 */
const getKeyFor = service => config.Client.APIs[service];

/**
 * Function to use in promise that will return the json output of a request
 */
const statusAndJson = (res) => {
  if (res.status >= 200 && res.status < 300) {
    return res.json();
  }

  return Promise.reject(new Error(res.statusText));
};

/**
 * Function to use in promise that will return the text output of a request
 */
const statusAndText = (res) => {
  if (res.status >= 200 && res.status < 300) {
    return res.text();
  }
  return Promise.reject(new Error(res.statusText));
};

/**
 * Returns a promise with image data from noembed
 */
const noEmbedImgCB = (url) => {
  return fetch(`${getEnpointFor('noembed')}${url}`)
    .then(statusAndJson)
    .then((data) => {
      const obj = {
        type: 'image',
        thumbnail_url: getSafeURL(data.media_url),
        url: getSafeURL(data.media_url),
      };

      return obj;
    });
};

/**
 * Returns a promise with video data from noembed
 */
const noEmbedVideoCB = (url) => {
  return fetch(`${getEnpointFor('noembed')}${url}`)
    .then(statusAndJson)
    .then((data) => {
      const obj = {
        type: 'video',
        thumbnail_url: getSafeURL(data.thumbnail_url),
        html: data.html,
        url,
      };

      return obj;
    });
};

// We export a few useful functions for providers
const util = {
  getKeyFor,
  statusAndJson,
  statusAndText,
  getEnpointFor,
  getSafeURL,
  noEmbedVideoCB,
  noEmbedImgCB,
};

export const schemeWhitelist = [
  Providers.fivehundredpx(util),
  Providers.bandcamp(util),
  Providers.cloudapp(util),
  Providers.dailymotion(util),
  Providers.deviantart(util),
  Providers.dribbble(util),
  Providers.droplr(util),
  Providers.flickr(util),
  Providers.gyazo(util),
  Providers.gfycat(util),
  Providers.giphy(util),
  Providers.googleplus(util),
  Providers.imgur(util),
  Providers.miil(util),
  Providers.mixcloud(util),
  Providers.mobyTo(util),
  Providers.nicoseiga(util),
  Providers.photozou(util),
  Providers.skitch(util),
  Providers.soundcloud(util),
  Providers.spotify(util),
  Providers.streamable(util),
  Providers.ted(util),
  Providers.tinami(util),
  Providers.tumblr(util),
  Providers.twitch(util),
  Providers.vimeo(util),
  Providers.worldcosplay(util),
  Providers.youtube(util),
  Providers.yfrog(util),
  Providers.universal(util),
];

if (!BHelper.isFirefox) {
  schemeWhitelist.push(Providers.instagram(util));
}

export const validateUrl = (url) => {
  let provider = '';
  let cb;

  // We test every scheme to see if the url matches one of them
  const some = schemeWhitelist.some((scheme) => {
    if (scheme.re.test(url)) {
      if (providersSettings[scheme.setting] === false) {
        return false;
      } else if (providersSettings[scheme.setting] === true || scheme.default) {
        // We an url matches, we stop and
        // get the corresponding provider/callback func
        provider = scheme.name || scheme.setting;
        cb = scheme.callback;
        return true;
      }
    }

    return false;
  });

  // We return whether the url matches and some additional info
  return { matches: some, provider, cb };
};

function thumbnailForFetch(url) {
  const validationObj = validateUrl(url);
  const cleanUrl = new URL(url);
  // Pixiv requires url parameters
  if (cleanUrl && !cleanUrl.hostname.includes('pixiv.')) {
    cleanUrl.search = '';
  }
  url = cleanUrl.toString();

  if (!validationObj.cb) {
    return Promise.reject();
  }

  Log(`[${validationObj.provider.toUpperCase()}] Fetching ${url}`);
  return validationObj.cb(url);
}

// We use reuse-promise so we don't have to fetch the same URL twice
export const thumbnailFor = reusePromise(thumbnailForFetch);
<commit_msg>:scissors: remove embedly endpoint<commit_after>import config from 'config';
import reusePromise from 'reuse-promise';

import { send as sendMessage } from './messaging';
import Log from './logger';
import * as Providers from './providers/index';
import * as BHelper from './browserHelper';

const endpoints = {
  '500px': 'https://500px.com/oembed?format=json&url=',
  dailymotion: 'https://api.dailymotion.com/video/',
  deviantart: 'https://backend.deviantart.com/oembed?',
  dribbble: 'https://api.dribbble.com/v1/shots/',
  noembed: 'https://noembed.com/embed?nowrap=on&url=',
  imgur: 'https://api.imgur.com/3/',
  instagram: 'https://api.instagram.com/oembed?omitscript=true&url=',
  twitch: 'https://api.twitch.tv/kraken/',
  giphy: 'https://giphy.com/services/oembed?url=',
  pixiv: 'http://embed.pixiv.net/embed_json.php?callback=callback&size=medium&id=',
  tinami: 'https://www.tinami.com/api/content/info?',
  nicoseiga: 'http://ext.seiga.nicovideo.jp/thumb/',
  photozou: 'https://api.photozou.jp/rest/',
  googleplus: 'https://picasaweb.google.com/data/feed/api/',
  gyazo: 'https://api.gyazo.com/api/oembed?url=',
  gfycat: 'https://api.gfycat.com/v1/oembed?url=',
};

let providersSettings;

sendMessage({ action: 'get', key: 'thumbnails' }, (response) => {
  providersSettings = response.val;
});

/**
 * Returns a 'safe' URL for images to use as background-image/src value
 * @param  {String} url url of image
 * @return {String}     safe url
 */
export const getSafeURL = (url) => {
  if (!url) {
    return '';
  }

  if (url.startsWith('//')) {
    url = `https:${url}`;
  }

  return `https://images4-focus-opensocial.googleusercontent.com/gadgets/proxy?url=${encodeURIComponent(url)}&container=focus&resize_w=720&refresh=86400`;
};

/**
 * Returns the endpoint for the asked service
 * @param  {String} service key of service from `endpoints` object
 * @return {String}         url to fetch
 */
const getEnpointFor = service => endpoints[service];

/**
 * Returns API key from config for a given servic3e
 * @param  {String} service Name of service
 * @return {String}         API key of service
 */
const getKeyFor = service => config.Client.APIs[service];

/**
 * Function to use in promise that will return the json output of a request
 */
const statusAndJson = (res) => {
  if (res.status >= 200 && res.status < 300) {
    return res.json();
  }

  return Promise.reject(new Error(res.statusText));
};

/**
 * Function to use in promise that will return the text output of a request
 */
const statusAndText = (res) => {
  if (res.status >= 200 && res.status < 300) {
    return res.text();
  }
  return Promise.reject(new Error(res.statusText));
};

/**
 * Returns a promise with image data from noembed
 */
const noEmbedImgCB = (url) => {
  return fetch(`${getEnpointFor('noembed')}${url}`)
    .then(statusAndJson)
    .then((data) => {
      const obj = {
        type: 'image',
        thumbnail_url: getSafeURL(data.media_url),
        url: getSafeURL(data.media_url),
      };

      return obj;
    });
};

/**
 * Returns a promise with video data from noembed
 */
const noEmbedVideoCB = (url) => {
  return fetch(`${getEnpointFor('noembed')}${url}`)
    .then(statusAndJson)
    .then((data) => {
      const obj = {
        type: 'video',
        thumbnail_url: getSafeURL(data.thumbnail_url),
        html: data.html,
        url,
      };

      return obj;
    });
};

// We export a few useful functions for providers
const util = {
  getKeyFor,
  statusAndJson,
  statusAndText,
  getEnpointFor,
  getSafeURL,
  noEmbedVideoCB,
  noEmbedImgCB,
};

export const schemeWhitelist = [
  Providers.fivehundredpx(util),
  Providers.bandcamp(util),
  Providers.cloudapp(util),
  Providers.dailymotion(util),
  Providers.deviantart(util),
  Providers.dribbble(util),
  Providers.droplr(util),
  Providers.flickr(util),
  Providers.gyazo(util),
  Providers.gfycat(util),
  Providers.giphy(util),
  Providers.googleplus(util),
  Providers.imgur(util),
  Providers.miil(util),
  Providers.mixcloud(util),
  Providers.mobyTo(util),
  Providers.nicoseiga(util),
  Providers.photozou(util),
  Providers.skitch(util),
  Providers.soundcloud(util),
  Providers.spotify(util),
  Providers.streamable(util),
  Providers.ted(util),
  Providers.tinami(util),
  Providers.tumblr(util),
  Providers.twitch(util),
  Providers.vimeo(util),
  Providers.worldcosplay(util),
  Providers.youtube(util),
  Providers.yfrog(util),
  Providers.universal(util),
];

if (!BHelper.isFirefox) {
  schemeWhitelist.push(Providers.instagram(util));
}

export const validateUrl = (url) => {
  let provider = '';
  let cb;

  // We test every scheme to see if the url matches one of them
  const some = schemeWhitelist.some((scheme) => {
    if (scheme.re.test(url)) {
      if (providersSettings[scheme.setting] === false) {
        return false;
      } else if (providersSettings[scheme.setting] === true || scheme.default) {
        // We an url matches, we stop and
        // get the corresponding provider/callback func
        provider = scheme.name || scheme.setting;
        cb = scheme.callback;
        return true;
      }
    }

    return false;
  });

  // We return whether the url matches and some additional info
  return { matches: some, provider, cb };
};

function thumbnailForFetch(url) {
  const validationObj = validateUrl(url);
  const cleanUrl = new URL(url);
  // Pixiv requires url parameters
  if (cleanUrl && !cleanUrl.hostname.includes('pixiv.')) {
    cleanUrl.search = '';
  }
  url = cleanUrl.toString();

  if (!validationObj.cb) {
    return Promise.reject();
  }

  Log(`[${validationObj.provider.toUpperCase()}] Fetching ${url}`);
  return validationObj.cb(url);
}

// We use reuse-promise so we don't have to fetch the same URL twice
export const thumbnailFor = reusePromise(thumbnailForFetch);
<|endoftext|>