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

/*
 * Generate gulliver.js from this file via `npm prestart`. (`npm start` will run
 * `prestart` automatically.)
 */

/* eslint-env browser */

// A Promise polyfill, as used by
// https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/Promise/config.json
import 'yaku/dist/yaku.browser.global.min.js';
// A fetch polyfill, as used by
// https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/fetch/config.json
import 'whatwg-fetch/fetch';

import {authInit} from './gapi.es6.js';
import './loader.js';
import Messaging from './messaging';
import NotificationCheckbox from './notification-checkbox';

/**
 * Translate generic "system" event like 'online', 'offline' and 'userchange'
 * into Gulliver-specific events. (e.g. as indicated by classes.)
 *
 * What this function does:
 *
 *   * all elements with class .gulliver-online-aware will:
 *     * have an 'online' dataset property that reflects the current online state.
 *     * receive a 'change' event whenever the state changes.
 *
 *   * all elements with class .gulliver-signedin-aware will:
 *     * have a 'signedin' dataset property that reflects the current signed in state.
 *     * receive a 'change' event whenever the state changes.
 *
 *   * the element #pwaForm also gets some special behaviour--see code. (This probably
 *     shouldn't happen here.)
 */
function setupEventHandlers() {
  window.addEventListener('online', () => {
    console.log('ONLINE');
    const onlineAware = document.querySelectorAll('.gulliver-online-aware');
    for (const e of onlineAware) {
      e.dataset.online = JSON.stringify(true);
      e.dispatchEvent(new CustomEvent('change'));
    }
  });

  window.addEventListener('offline', () => {
    console.log('OFFLINE');
    const onlineAware = document.querySelectorAll('.gulliver-online-aware');
    for (const e of onlineAware) {
      e.dataset.online = JSON.stringify(false);
      e.dispatchEvent(new CustomEvent('change'));
    }
  });

  window.addEventListener('userchange', e => {
    const user = e.detail;
    if (user.isSignedIn()) {
      console.log('id_token', user.getAuthResponse().id_token);
      const pwaForm = document.getElementById('pwaForm');
      if (pwaForm) {
        const idTokenInput = document.getElementById('idToken');
        idTokenInput.setAttribute('value', user.getAuthResponse().id_token);
      }
    } else {
      console.log('user signed out/never signed in');
      const pwaForm = document.getElementById('pwaForm');
      if (pwaForm) {
        const idTokenInput = document.getElementById('idToken');
        idTokenInput.setAttribute('value', '');
      }
    }
    const signedinAware = document.querySelectorAll('.gulliver-signedin-aware');
    for (const e of signedinAware) {
      e.dataset.signedin = JSON.stringify(user.isSignedIn());
      e.dispatchEvent(new CustomEvent('change'));
    }
  });
}

/**
 * Configures elements with class `gulliver-signed-aware` and
 * `gulliver-online-aware` to respond to 'change' events.
 */
function setupSignedinAware() {
  const list = document.querySelectorAll('.gulliver-signedin-aware.gulliver-online-aware');
  for (const e of list) {
    e.dataset.online = JSON.stringify(false);
    e.dataset.signedin = JSON.stringify(false);
    e.addEventListener('change', function() {
      const online = JSON.parse(this.dataset.online);
      const signedin = JSON.parse(this.dataset.signedin);
      switch (e.tagName.toLowerCase()) {
        case 'button':
          if (e.id === 'auth-button') {
            // auth-button state depends only on online state
            this.disabled = !online;
          } else {
            this.disabled = !online || !signedin;
          }
          break;
        case 'div':
          if (online && signedin) {
            this.style.opacity = 1;
            this.onclick = null;
          } else {
            this.style.opacity = 0.5;
            this.onclick = f => f.preventDefault();
          }
          break;
        default:
      }
    });
  }
}

/**
 * Configures elements with class `gulliver-online-aware` to respond to 'change'
 * events.
 */
function setupOnlineAware() {
  const l1 = document.querySelectorAll('div.button.gulliver-online-aware');
  for (const e of l1) {
    e.addEventListener('change', function() {
      if (JSON.parse(this.dataset.online)) {
        this.style.transition = 'opacity .5s ease-in-out';
        this.style.opacity = 1;
        this.onclick = null;
      } else {
        this.style.opacity = 0.5;
        this.onclick = f => f.preventDefault();
      }
    });
  }
  const l2 = document.querySelectorAll('a.card-pwa.gulliver-online-aware');
  for (const e of l2) {
    e.addEventListener('change', function() {
      if (JSON.parse(this.dataset.online)) {
        // Online, make element active
        this.style.transition = 'opacity .5s ease-in-out';
        this.style.opacity = 1;
        this.onclick = null;
        return;
      }
      const href = e.getAttribute('href');
      if (href) {
        fetch(href, {method: 'HEAD'}).then(r => {
          if (r.status === 200) {
            // Available in cache, allow click
            this.style.transition = 'opacity .5s ease-in-out';
            this.style.opacity = 1;
            this.onclick = null;
          } else {
            // Not cached, prevent click
            this.style.transition = 'opacity .5s ease-in-out';
            this.style.opacity = 0.5;
            this.onclick = f => f.preventDefault();
          }
        });
      }
    });
  }
  const l3 = document.querySelectorAll('div.offline-status.gulliver-online-aware');
  for (const e of l3) {
    e.innerHTML = 'Offline';
    e.addEventListener('change', function() {
      this.style.opacity = 1;
      this.style.display = 'block';
      if (JSON.parse(this.dataset.online)) {
        this.style.transition = 'opacity .5s ease-in-out';
        this.style.opacity = 0;
      } else {
        this.style.transition = 'opacity .5s ease-in-out';
        this.style.opacity = 1;
      }
    });
  }
}

/**
 * Setup/configure Google signin itself. This translates GSI events into 'userchange'
 * events on the window object.
 */
function setupSignin() {
  /* eslint-disable camelcase */
  const params = {
    scope: 'profile',
    client_id: window.__config.client_id,
    fetch_basic_profile: false
  };
  /* eslint-enable camelcase */

  return authInit(params).then(auth => {
    // Fire 'userchange' event on page load (not just when status changes)
    window.dispatchEvent(new CustomEvent('userchange', {
      detail: auth.currentUser.get()
    }));

    // Fire 'userchange' event when status changes
    auth.currentUser.listen(user => {
      window.dispatchEvent(new CustomEvent('userchange', {
        detail: user
      }));
    });

    const authButton = document.getElementById('auth-button');

    function updateAuthButtonLabel() {
      authButton.innerText = authButton.dataset.signedin === 'true' ?
        'Logout' :
        'Login';
    }

    authButton.addEventListener('change', updateAuthButtonLabel);
    updateAuthButtonLabel();

    authButton.addEventListener(
      'click',
      () => {
        if (authButton.dataset.signedin === 'true') {
          auth.signOut();
        } else {
          auth.signIn();
        }
      }
    );

    return auth;
  });
}

/**
 * Disable the save button after been clicked to avoid double submission.
 */
function setupSaveButton() {
  const submitButton = document.getElementById('pwaSubmit');
  if (submitButton) {
    submitButton.addEventListener('click', _ => {
      submitButton.disabled = true;
      document.getElementById('pwaForm').submit();
    });
  }
}

/**
 * Register service worker.
 */
function setupServiceWorker() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js').then(r => {
      console.log('REGISTRATION', r);
    });
  } else {
    console.log('SW not registered; navigator.serviceWorker is not available');
  }
}

function setupConfig() {
  const config = document.getElementById('config');
  if (config) {
    window.__config = JSON.parse(config.innerHTML);
  } else {
    console.log('CONFIG NOT FOUND');
  }
}

function setupMessaging() {
  const NEW_APPS_TOPIC = 'new-apps';
  const firebaseMsgSenderId = window.__config.firebase_msg_sender_id;
  const checkbox = document.getElementById('notifications');
  const messaging = new Messaging(firebaseMsgSenderId);
  // eslint-disable-next-line no-unused-vars
  const notificationCheckbox = new NotificationCheckbox(messaging, checkbox, NEW_APPS_TOPIC);
}

setupConfig();
setupOnlineAware();
setupSignedinAware();
setupSignin();
setupSaveButton();
setupEventHandlers();
setupServiceWorker();
setupMessaging();

// Fire 'online' or 'offline' event on page load. (Without this, would only
// fire on change.)
window.dispatchEvent(new CustomEvent(navigator.onLine ? 'online' : 'offline'));

// GA embed code
/* eslint-disable */
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', window.__config.ga_id, 'auto');
ga('set', 'dimension1', navigator.onLine);
ga('send', 'pageview');

// Setup a listener to track Add to Homescreen events.
window.addEventListener('beforeinstallprompt', e => {
  e.userChoice.then(choiceResult => {
    ga('send', 'event', 'A2H', choiceResult.outcome);
  });
});
/* eslint-enable */
<commit_msg>Start gulliver.js refactor<commit_after>/**
 * Copyright 2015-2016, Google, Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Generate gulliver.js from this file via `npm prestart`. (`npm start` will run
 * `prestart` automatically.)
 */

/* eslint-env browser */

// A Promise polyfill, as used by
// https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/Promise/config.json
import 'yaku/dist/yaku.browser.global.min.js';
// A fetch polyfill, as used by
// https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/fetch/config.json
import 'whatwg-fetch/fetch';

import {authInit} from './gapi.es6.js';
import './loader.js';
import Messaging from './messaging';
import NotificationCheckbox from './notification-checkbox';

class Gulliver {
  /**
   * Translate generic "system" event like 'online', 'offline' and 'userchange'
   * into Gulliver-specific events. (e.g. as indicated by classes.)
   *
   * What this function does:
   *
   *   * all elements with class .gulliver-online-aware will:
   *     * have an 'online' dataset property that reflects the current online state.
   *     * receive a 'change' event whenever the state changes.
   *
   *   * all elements with class .gulliver-signedin-aware will:
   *     * have a 'signedin' dataset property that reflects the current signed in state.
   *     * receive a 'change' event whenever the state changes.
   *
   *   * the element #pwaForm also gets some special behaviour--see code. (This probably
   *     shouldn't happen here.)
   */
  setupEventHandlers() {
    window.addEventListener('online', () => {
      console.log('ONLINE');
      const onlineAware = document.querySelectorAll('.gulliver-online-aware');
      for (const e of onlineAware) {
        e.dataset.online = JSON.stringify(true);
        e.dispatchEvent(new CustomEvent('change'));
      }
    });

    window.addEventListener('offline', () => {
      console.log('OFFLINE');
      const onlineAware = document.querySelectorAll('.gulliver-online-aware');
      for (const e of onlineAware) {
        e.dataset.online = JSON.stringify(false);
        e.dispatchEvent(new CustomEvent('change'));
      }
    });

    window.addEventListener('userchange', e => {
      const user = e.detail;
      if (user.isSignedIn()) {
        console.log('id_token', user.getAuthResponse().id_token);
        const pwaForm = document.getElementById('pwaForm');
        if (pwaForm) {
          const idTokenInput = document.getElementById('idToken');
          idTokenInput.setAttribute('value', user.getAuthResponse().id_token);
        }
      } else {
        console.log('user signed out/never signed in');
        const pwaForm = document.getElementById('pwaForm');
        if (pwaForm) {
          const idTokenInput = document.getElementById('idToken');
          idTokenInput.setAttribute('value', '');
        }
      }
      const signedinAware = document.querySelectorAll('.gulliver-signedin-aware');
      for (const e of signedinAware) {
        e.dataset.signedin = JSON.stringify(user.isSignedIn());
        e.dispatchEvent(new CustomEvent('change'));
      }
    });
  }

  /**
   * Configures elements with class `gulliver-signed-aware` and
   * `gulliver-online-aware` to respond to 'change' events.
   */
  setupSignedinAware() {
    const list = document.querySelectorAll('.gulliver-signedin-aware.gulliver-online-aware');
    for (const e of list) {
      e.dataset.online = JSON.stringify(false);
      e.dataset.signedin = JSON.stringify(false);
      e.addEventListener('change', function() {
        const online = JSON.parse(this.dataset.online);
        const signedin = JSON.parse(this.dataset.signedin);
        switch (e.tagName.toLowerCase()) {
          case 'button':
            if (e.id === 'auth-button') {
              // auth-button state depends only on online state
              this.disabled = !online;
            } else {
              this.disabled = !online || !signedin;
            }
            break;
          case 'div':
            if (online && signedin) {
              this.style.opacity = 1;
              this.onclick = null;
            } else {
              this.style.opacity = 0.5;
              this.onclick = f => f.preventDefault();
            }
            break;
          default:
        }
      });
    }
  }

  /**
   * Configures elements with class `gulliver-online-aware` to respond to 'change'
   * events.
   */
  setupOnlineAware() {
    const l1 = document.querySelectorAll('div.button.gulliver-online-aware');
    for (const e of l1) {
      e.addEventListener('change', function() {
        if (JSON.parse(this.dataset.online)) {
          this.style.transition = 'opacity .5s ease-in-out';
          this.style.opacity = 1;
          this.onclick = null;
        } else {
          this.style.opacity = 0.5;
          this.onclick = f => f.preventDefault();
        }
      });
    }
    const l2 = document.querySelectorAll('a.card-pwa.gulliver-online-aware');
    for (const e of l2) {
      e.addEventListener('change', function() {
        if (JSON.parse(this.dataset.online)) {
          // Online, make element active
          this.style.transition = 'opacity .5s ease-in-out';
          this.style.opacity = 1;
          this.onclick = null;
          return;
        }
        const href = e.getAttribute('href');
        if (href) {
          fetch(href, {method: 'HEAD'}).then(r => {
            if (r.status === 200) {
              // Available in cache, allow click
              this.style.transition = 'opacity .5s ease-in-out';
              this.style.opacity = 1;
              this.onclick = null;
            } else {
              // Not cached, prevent click
              this.style.transition = 'opacity .5s ease-in-out';
              this.style.opacity = 0.5;
              this.onclick = f => f.preventDefault();
            }
          });
        }
      });
    }
    const l3 = document.querySelectorAll('div.offline-status.gulliver-online-aware');
    for (const e of l3) {
      e.innerHTML = 'Offline';
      e.addEventListener('change', function() {
        this.style.opacity = 1;
        this.style.display = 'block';
        if (JSON.parse(this.dataset.online)) {
          this.style.transition = 'opacity .5s ease-in-out';
          this.style.opacity = 0;
        } else {
          this.style.transition = 'opacity .5s ease-in-out';
          this.style.opacity = 1;
        }
      });
    }
  }

  /**
   * Setup/configure Google signin itself. This translates GSI events into 'userchange'
   * events on the window object.
   */
  setupSignin() {
    /* eslint-disable camelcase */
    const params = {
      scope: 'profile',
      client_id: window.__config.client_id,
      fetch_basic_profile: false
    };
    /* eslint-enable camelcase */

    return authInit(params).then(auth => {
      // Fire 'userchange' event on page load (not just when status changes)
      window.dispatchEvent(new CustomEvent('userchange', {
        detail: auth.currentUser.get()
      }));

      // Fire 'userchange' event when status changes
      auth.currentUser.listen(user => {
        window.dispatchEvent(new CustomEvent('userchange', {
          detail: user
        }));
      });

      const authButton = document.getElementById('auth-button');

      function updateAuthButtonLabel() {
        authButton.innerText = authButton.dataset.signedin === 'true' ?
          'Logout' :
          'Login';
      }

      authButton.addEventListener('change', updateAuthButtonLabel);
      updateAuthButtonLabel();

      authButton.addEventListener(
        'click',
        () => {
          if (authButton.dataset.signedin === 'true') {
            auth.signOut();
          } else {
            auth.signIn();
          }
        }
      );

      return auth;
    });
  }

  /**
   * Disable the save button after been clicked to avoid double submission.
   */
  setupSaveButton() {
    const submitButton = document.getElementById('pwaSubmit');
    if (submitButton) {
      submitButton.addEventListener('click', _ => {
        submitButton.disabled = true;
        document.getElementById('pwaForm').submit();
      });
    }
  }

  /**
   * Register service worker.
   */
  setupServiceWorker() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(r => {
        console.log('REGISTRATION', r);
      });
    } else {
      console.log('SW not registered; navigator.serviceWorker is not available');
    }
  }

  setupConfig() {
    const config = document.getElementById('config');
    if (config) {
      window.__config = JSON.parse(config.innerHTML);
    } else {
      console.log('CONFIG NOT FOUND');
    }
  }

  setupMessaging() {
    const NEW_APPS_TOPIC = 'new-apps';
    const firebaseMsgSenderId = window.__config.firebase_msg_sender_id;
    const checkbox = document.getElementById('notifications');
    const messaging = new Messaging(firebaseMsgSenderId);
    // eslint-disable-next-line no-unused-vars
    const notificationCheckbox = new NotificationCheckbox(messaging, checkbox, NEW_APPS_TOPIC);
  }
}

const gulliver = new Gulliver();
gulliver.setupConfig();
gulliver.setupOnlineAware();
gulliver.setupSignedinAware();
gulliver.setupSignin();
gulliver.setupSaveButton();
gulliver.setupEventHandlers();
gulliver.setupServiceWorker();
gulliver.setupMessaging();

// Fire 'online' or 'offline' event on page load. (Without this, would only
// fire on change.)
window.dispatchEvent(new CustomEvent(navigator.onLine ? 'online' : 'offline'));

// GA embed code
/* eslint-disable */
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', window.__config.ga_id, 'auto');
ga('set', 'dimension1', navigator.onLine);
ga('send', 'pageview');

// Setup a listener to track Add to Homescreen events.
window.addEventListener('beforeinstallprompt', e => {
  e.userChoice.then(choiceResult => {
    ga('send', 'event', 'A2H', choiceResult.outcome);
  });
});
/* eslint-enable */
<|endoftext|>


<commit_before>const DrawCard = require('../../drawcard');

class RhllorInfiltrator extends DrawCard {
    setupCardAbilities(ability) {
        this.action({
            title: 'Give control and gain 1 power',
            phase: 'dominance',
            condition: () => this.controller.canGainFactionPower(),
            chooseOpponent: true,
            costs: ability.costs.kneelSelf(),
            message: {
                format: '{player} kneels {source} to give control of {source} to {opponent} and gain 1 power',
                args: { opponent: context => context.opponent }
            },
            handler: context => {
                this.game.takeControl(context.opponent, this);
                this.game.addPower(context.player, 1);
            }
        });
    }
}

RhllorInfiltrator.code = '13007';

module.exports = RhllorInfiltrator;
<commit_msg>Fix kneel cost for R'hllor Infiltrator (#2456)<commit_after>const DrawCard = require('../../drawcard');

class RhllorInfiltrator extends DrawCard {
    setupCardAbilities(ability) {
        this.action({
            title: 'Give control and gain 1 power',
            phase: 'dominance',
            condition: () => this.controller.canGainFactionPower(),
            chooseOpponent: true,
            cost: ability.costs.kneelSelf(),
            message: {
                format: '{player} kneels {source} to give control of {source} to {opponent} and gain 1 power',
                args: { opponent: context => context.opponent }
            },
            handler: context => {
                this.game.takeControl(context.opponent, this);
                this.game.addPower(context.player, 1);
            }
        });
    }
}

RhllorInfiltrator.code = '13007';

module.exports = RhllorInfiltrator;
<|endoftext|>


<commit_before>const runSequence = require('run-sequence').use(protoss.gulp); // TODO: remove on Gulp 4

const isWebpack = protoss.config.scripts.workflow === 'webpack';

protoss.gulp.task('protoss/watch-and-sync', (cb) => {
  runSequence(
    'protoss/watch',
    'protoss/serve',
    cb,
  );
});

const watchTasks = [
  'protoss/dev',
  'protoss/styles:watch',
  'protoss/images:watch',
  'protoss/templates:watch',
  'protoss/sprites:watch',
  'protoss/sprites-svg:watch',
  'protoss/icons:watch',
];

if (!isWebpack) {
  watchTasks.push('protoss/scripts:watch');
}

protoss.gulp.task('protoss/watch', (cb) => {
  watchTasks.push(() => {
    protoss.isWatch = true;
    if (isWebpack) {
      protoss.notifier.warning('Run `protoss/webpack:watch` for start webpack!');
    }
    cb();
  });
  runSequence.apply(...watchTasks);
});

protoss.gulp.task('protoss/build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/del',
    'protoss/dev',
    'protoss/images:optimize',
    () => {
      protoss.notifier.success('Production version was built successfully!');
      cb();
    },
  );
});

protoss.gulp.task('protoss/dev', (cb) => {
  runSequence(
    [
      'protoss/images',
      'protoss/sprites',
      'protoss/sprites-svg',
      'protoss/icons',
      'protoss/favicons',
    ],
    [
      'protoss/copy',
      isWebpack ? 'protoss/webpack' : 'protoss/scripts',
      'protoss/templates',
      'protoss/styles',
    ],
    () => {
      protoss.notifier.success('Development version was built successfully!');
      cb();
    },
  );
});

protoss.gulp.task('protoss/styles:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    [
      'protoss/sprites',
      'protoss/sprites-svg',
    ],
    'protoss/styles',
    cb,
  );
});

if (isWebpack) {
  protoss.gulp.task('protoss/webpack:build', (cb) => {
    process.env.NODE_ENV = 'production';
    runSequence(
      'protoss/webpack',
      cb,
    );
  });
} else {
  protoss.gulp.task('protoss/scripts:build', (cb) => {
    process.env.NODE_ENV = 'production';
    runSequence(
      'protoss/scripts',
      cb,
    );
  });
}

protoss.gulp.task('protoss/templates:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/templates',
    cb,
  );
});

protoss.gulp.task('protoss/images:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/images',
    'protoss/images:optimize',
    cb,
  );
});
<commit_msg>fix runSequence<commit_after>const runSequence = require('run-sequence').use(protoss.gulp); // TODO: remove on Gulp 4

const isWebpack = protoss.config.scripts.workflow === 'webpack';

protoss.gulp.task('protoss/watch-and-sync', (cb) => {
  runSequence(
    'protoss/watch',
    'protoss/serve',
    cb,
  );
});

const watchTasks = [
  'protoss/dev',
  'protoss/styles:watch',
  'protoss/images:watch',
  'protoss/templates:watch',
  'protoss/sprites:watch',
  'protoss/sprites-svg:watch',
  'protoss/icons:watch',
];

if (!isWebpack) {
  watchTasks.push('protoss/scripts:watch');
}

protoss.gulp.task('protoss/watch', (cb) => {
  watchTasks.push(() => {
    protoss.isWatch = true;
    if (isWebpack) {
      protoss.notifier.warning('Run `protoss/webpack:watch` for start webpack!');
    }
    cb();
  });
  runSequence.apply(null, watchTasks); // eslint-disable-line prefer-spread
});

protoss.gulp.task('protoss/build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/del',
    'protoss/dev',
    'protoss/images:optimize',
    () => {
      protoss.notifier.success('Production version was built successfully!');
      cb();
    },
  );
});

protoss.gulp.task('protoss/dev', (cb) => {
  runSequence(
    [
      'protoss/images',
      'protoss/sprites',
      'protoss/sprites-svg',
      'protoss/icons',
      'protoss/favicons',
    ],
    [
      'protoss/copy',
      isWebpack ? 'protoss/webpack' : 'protoss/scripts',
      'protoss/templates',
      'protoss/styles',
    ],
    () => {
      protoss.notifier.success('Development version was built successfully!');
      cb();
    },
  );
});

protoss.gulp.task('protoss/styles:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    [
      'protoss/sprites',
      'protoss/sprites-svg',
    ],
    'protoss/styles',
    cb,
  );
});

if (isWebpack) {
  protoss.gulp.task('protoss/webpack:build', (cb) => {
    process.env.NODE_ENV = 'production';
    runSequence(
      'protoss/webpack',
      cb,
    );
  });
} else {
  protoss.gulp.task('protoss/scripts:build', (cb) => {
    process.env.NODE_ENV = 'production';
    runSequence(
      'protoss/scripts',
      cb,
    );
  });
}

protoss.gulp.task('protoss/templates:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/templates',
    cb,
  );
});

protoss.gulp.task('protoss/images:build', (cb) => {
  process.env.NODE_ENV = 'production';
  runSequence(
    'protoss/images',
    'protoss/images:optimize',
    cb,
  );
});
<|endoftext|>


<commit_before>// How works TTS
// 0. If now tts is playing, just add new text to queue and finished. If TTS is not playing now =>
// 1. Store current state
// 2. Start fade out.
// 2. Pause/Stop playing after fadeout finished
// 3. Add track to list
// 4. Set Volume to 0
// 5. Start play
// 6. Start fade in or Set volume to TTS volume
// 7. Wait till the track played
// 8. If Play paused and it was TTS,
// 9. Remove track from list
// 10. check if we have the next text, if yes go to 4, but without fade in/fade out
// 11. Restore the state stored on step 1

const audioExtensions = ['mp3', 'aiff', 'flac', 'less', 'wav'];

const FADE_STEP_MS = 100;

function TTS(adapter, player) {
    let fadeInMs  = parseInt(adapter.config.fadeIn,  10) || 0;
    let fadeOutMs = parseInt(adapter.config.fadeOut, 10) || 0;

    this._player = player;
    this._queue = [];
    this._storedState = null;
    this._ttsPlaying = false;
    this._restoring = false;
    this._lastAddedTrack = null;

    this._restoreState = function () {
        adapter.log.debug('Restore after ' + JSON.stringify(this._player.state));
        this._ttsPlaying = false;

        if (this._storedState && !this._restoring) {
            this._restoring = true;

            // restore mute state
            return new Promise(resolve => {
                if (this._player._isMuted !== this._storedState.mute) {
                    return (this._storedState.mute ? this._player.mute() : this._player.unMute())
                        .then(() => resolve());
                } else {
                    return resolve();
                }
            })
                // required for fadeIn
                .then(() => fadeInMs && this._player.setVolume(0))
                .then(() => {
                    let promise;
                    // if was radio playing
                    if (this._storedState.radio) {
                        promise = this._player.setAVTransport(this._storedState.currentTrack.uri, this._storedState.avTransportUriMetadata)
                            .catch(error =>
                                adapter.log.error('Cannot setAVTransport: ' + error));
                    } else {
                        // if not radio
                        // Set old track number
                        promise = this._player.trackSeek(this._storedState.trackNo)
                            .then(() => this._wait(200))
                            // Set elapsed time
                            .then(() => this._player.timeSeek(this._storedState.elapsedTime))
                            .then(() => this._wait(200));
                    }

                    return promise
                        .then(() => this._storedState.playbackState === 'PLAYING' ? this._player.play() : Promise.resolve())
                        .then(() => {
                            if (fadeInMs && this._storedState.playbackState === 'PLAYING') {
                                return new Promise(resolve => {
                                    this._waitPlayerStarted = {
                                        resolve,
                                        timer: setTimeout(() => {
                                            this._waitPlayerStarted.timer = null;
                                            resolve();
                                        }, 3000),
                                    };
                                });
                            } else {
                                return Promise.resolve();
                            }
                        })
                        .then(() => {
                            if (this._waitPlayerStarted) {
                                this._waitPlayerStarted.timer && clearTimeout(this._waitPlayerStarted);
                                this._waitPlayerStarted.timer = null;
                                this._waitPlayerStarted = null;
                            }

                            return this._storedState.playbackState === 'PLAYING' ? this._fadeIn(this._storedState.volume) : this._player.setVolume(this._storedState.volume);
                        })
                })
                .catch(e => adapter.log.error('Cannot restore state: ' + e))
                .then(() => {
                    this._restoring = false;
                    this._storedState = null;

                    // If during state restoring new file added => start TTS anew
                    this._queue.length && this._startTTS();
                });
        }
    };

    this._storeState = function () {
        this._storedState = JSON.parse(JSON.stringify(this._player.state));
        this._storedState.avTransportUriMetadata = JSON.parse(JSON.stringify(this._player.avTransportUriMetadata));
        this._storedState.time = Date.now();
        this._storedState.radio = this._isRadio(this._storedState);
        adapter.log.debug(`[TTS / _storeState] volume=${this._storedState.volume} currentTrack.uri=${this._storedState.currentTrack && this._storedState.currentTrack.uri} tts.playbackState=${this._storedState.playbackState}`);
    };

    this._isRadio = function (state) {
        const extension = state.currentTrack && state.currentTrack.uri ? state.currentTrack.uri.split('.').pop() : 'none';

        const uri = state.currentTrack && state.currentTrack.uri;

        if (!uri) {
            return true;
        }

        // If not radio
        const isFile =
            uri.includes('x-file-cifs:') ||
            uri.includes('x-sonos-spotify:') ||
            uri.includes('x-sonosapi-hls-static:') ||
            audioExtensions.includes(extension);

        if (isFile) {
            return false;
        } else {
            return state.currentTrack.uri.split(':')[0];
        }
    };

    this._startTTS = function () {
        if (!this._queue.length) {
            if (this._ttsPlaying) {
                this._removeAddedTrack()
                    .then(() => this._restoreState());
            }
        } else {
            if (!this._ttsPlaying && !this._restoring) {
                this._ttsPlaying = true;
                let wasPlaying;

                const task = this._queue.shift();

                this._storeState();

                this._fadeOut()
                    .then(() => {
                        if (this._storedState.radio) {
                            wasPlaying = false;
                            this._lastAddedTrack = null;
                            return this._player.setAVTransport(task.uri);
                        } else {
                            wasPlaying = this._storedState.playbackState === 'PLAYING';

                            return this._player.addURIToQueue(task.uri)
                                .then(res => {
                                    // Find out added track
                                    this._lastAddedTrack = parseInt(res.firsttracknumberenqueued, 10);
                                    return this._player.trackSeek(this._lastAddedTrack);
                                });
                        }
                    })
                    .then(() => {
                        // remember start of playing
                        this._playStopped = Date.now();
                        this._playStoppedTimeout = setTimeout(() => {
                            this._playStoppedTimeout = null;
                            this._afterPlayingStopped();
                        }, 40000);

                        return !wasPlaying ? this._player.play() : Promise.resolve();
                    })
                    .then(() => {
                        // wait till playing started to start fadeIn process
                        if (fadeInMs) {
                            return new Promise(resolve => {
                                this._waitPlayerStarted = {
                                    resolve,
                                    timer: setTimeout(() => {
                                        this._waitPlayerStarted.timer = null;
                                        resolve();
                                    }, 3000),
                                };
                            });
                        } else {
                            return Promise.resolve();
                        }
                    })
                    .then(() => {
                        if (this._waitPlayerStarted && this._waitPlayerStarted.timer) {
                            clearTimeout(this._waitPlayerStarted);
                            this._waitPlayerStarted.timer = null;
                        }
                        this._waitPlayerStarted = null;
                        return this._fadeIn(task.volume);
                    })
                    .catch(error => {
                        adapter.log.errors('Cannot execute TTS: ' + error.toString());
                    })
                    // wait till ended in playingEnded
            } else {
                // do nothing
            }
        }
    };

    this.immediatelyStopTTS = function () {
        if (this._ttsPlaying) {
            this._restoring = true;
            this._player.pause()
                .then(() => this._restoreState());
        }
    };

    this.add = function (uri, volume) {
        this._queue.push({uri, volume});
        this._startTTS();
    };

    this._removeAddedTrack = function () {
        if (this._lastAddedTrack !== null) {
            // remove track
            return this._player.removeTrackFromQueue(this._lastAddedTrack)
                .catch(error => adapter.log.error(`Cannot removeTrackFromQueue: ${error}`))
                .then(() => this._lastAddedTrack = null);
        } else {
            return Promise.resolve();
        }
    };

    this._afterPlayingStopped = function () {
        // ignore stop state immediately after play. Sonos Bug?
        if (Date.now() - this._playStopped < 1000) {
            return adapter.log.debug('Stop state ignored, right after the play start');
        }

        // stop wait for "speech end timeout"
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }

        this._removeAddedTrack()
            .then(() => {
                // process next task
                if (!this._queue.length) {
                    return this._restoreState();
                } else {
                    const task = this._queue.shift();

                    let promise;

                    if (this._storedState.radio) {
                        promise = this._player.setAVTransport(task.uri);
                    } else {
                        promise = player.addURIToQueue(task.uri)
                            .then(res => {
                                // Find out added track
                                this._lastAddedTrack = parseInt(res.firsttracknumberenqueued, 10);
                                return this._player.trackSeek(this._lastAddedTrack);
                            });
                    }
                    return promise
                        .then(() => this._player.setVolume(task.volume))
                        .then(() => this._player.play());
                }
            })
            .catch(error => adapter.log.error('Cannot remove track: ' + error.toString()));
    };

    this.playingStarted = function() {
        console.log('PLAYING STARTED!!!');
        if ((this._restoring || this._ttsPlaying) && this._waitPlayerStarted) {
            clearTimeout(this._waitPlayerStarted.timer);
            this._waitPlayerStarted.timer = null;
            setImmediate(() =>
                this._waitPlayerStarted.resolve());
        }
    };

    this.playingEnded = function() {
        console.log('PLAYING ENDED!!!');
        setImmediate(() =>
            this._afterPlayingStopped());
    };

    this._wait = function (ms) {
        return new Promise(resolve => setTimeout(() => resolve(), ms));
    };

    this._fadeIn = function (targetVolume, options) {
        if (!fadeInMs) {
            return this._player.setVolume(targetVolume);
        }

        if (options === undefined) {
            adapter.log.debug(`[TTS / _fadeIn] << fadeIn start to ${targetVolume}`);
            targetVolume = parseInt(targetVolume, 10);

            options = {
                step: Math.round(targetVolume / Math.max(fadeInMs / FADE_STEP_MS, 1)) || 1,
                actual: 0
            };
        }

        adapter.log.debug(`[TTS / _fadeIn] >> fadeIn to ${options.actual} of ${targetVolume}`);

        options.actual += options.step;

        if (options.actual >= targetVolume) {
            adapter.log.debug(`[TTS / _fadeIn] << fadeIn end to ${targetVolume}`);
            return player.setVolume(targetVolume);
        } else {
            return player.setVolume(options.actual)
                .then(() => this._wait(FADE_STEP_MS))
                .then(() => this._fadeIn(targetVolume, options));
        }
    };

    this._fadeOut = function (options) {
        // player was in mute state, so no fadeout required
        if (!fadeOutMs || this._player._isMute || this._player.state.playbackState !== 'PLAYING') {
            return this._player.setVolume(0)
                .then(() => this._player._isMute && this._player.unMute());
        }

        if (options === undefined) {
            const actual = parseInt(this._player._volume, 10);

            options = {
                actual,
                step: Math.round(actual / Math.max(fadeOutMs / 100, 1)) || 1
            };
        }

        options.actual -= options.step;

        if (options.actual > 0) {
            return player.setVolume(options.actual)
                .then(() => {
                    adapter.log.debug(`[TTS / _fadeOut] >> fadeOut: setVolume: ${options.actual}`);

                    return this._wait(FADE_STEP_MS)
                        .then(() => this._fadeOut(options));
                });
        } else {
            return player.setVolume(0)
                .then(() => adapter.log.debug('[TTS / _fadeOut] << fadeOut '));
        }
    };

    this.destroy = function () {
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }
    }
}

module.exports = TTS;
<commit_msg>typo corrected<commit_after>// How works TTS
// 0. If now tts is playing, just add new text to queue and finished. If TTS is not playing now =>
// 1. Store current state
// 2. Start fade out.
// 2. Pause/Stop playing after fadeout finished
// 3. Add track to list
// 4. Set Volume to 0
// 5. Start play
// 6. Start fade in or Set volume to TTS volume
// 7. Wait till the track played
// 8. If Play paused and it was TTS,
// 9. Remove track from list
// 10. check if we have the next text, if yes go to 4, but without fade in/fade out
// 11. Restore the state stored on step 1

const audioExtensions = ['mp3', 'aiff', 'flac', 'less', 'wav'];

const FADE_STEP_MS = 100;

function TTS(adapter, player) {
    let fadeInMs  = parseInt(adapter.config.fadeIn,  10) || 0;
    let fadeOutMs = parseInt(adapter.config.fadeOut, 10) || 0;

    this._player = player;
    this._queue = [];
    this._storedState = null;
    this._ttsPlaying = false;
    this._restoring = false;
    this._lastAddedTrack = null;

    this._restoreState = function () {
        adapter.log.debug('Restore after ' + JSON.stringify(this._player.state));
        this._ttsPlaying = false;

        if (this._storedState && !this._restoring) {
            this._restoring = true;

            // restore mute state
            return new Promise(resolve => {
                if (this._player._isMuted !== this._storedState.mute) {
                    return (this._storedState.mute ? this._player.mute() : this._player.unMute())
                        .then(() => resolve());
                } else {
                    return resolve();
                }
            })
                // required for fadeIn
                .then(() => fadeInMs && this._player.setVolume(0))
                .then(() => {
                    let promise;
                    // if was radio playing
                    if (this._storedState.radio) {
                        promise = this._player.setAVTransport(this._storedState.currentTrack.uri, this._storedState.avTransportUriMetadata)
                            .catch(error =>
                                adapter.log.error('Cannot setAVTransport: ' + error));
                    } else {
                        // if not radio
                        // Set old track number
                        promise = this._player.trackSeek(this._storedState.trackNo)
                            .then(() => this._wait(200))
                            // Set elapsed time
                            .then(() => this._player.timeSeek(this._storedState.elapsedTime))
                            .then(() => this._wait(200));
                    }

                    return promise
                        .then(() => this._storedState.playbackState === 'PLAYING' ? this._player.play() : Promise.resolve())
                        .then(() => {
                            if (fadeInMs && this._storedState.playbackState === 'PLAYING') {
                                return new Promise(resolve => {
                                    this._waitPlayerStarted = {
                                        resolve,
                                        timer: setTimeout(() => {
                                            this._waitPlayerStarted.timer = null;
                                            resolve();
                                        }, 3000),
                                    };
                                });
                            } else {
                                return Promise.resolve();
                            }
                        })
                        .then(() => {
                            if (this._waitPlayerStarted) {
                                this._waitPlayerStarted.timer && clearTimeout(this._waitPlayerStarted);
                                this._waitPlayerStarted.timer = null;
                                this._waitPlayerStarted = null;
                            }

                            return this._storedState.playbackState === 'PLAYING' ? this._fadeIn(this._storedState.volume) : this._player.setVolume(this._storedState.volume);
                        })
                })
                .catch(e => adapter.log.error('Cannot restore state: ' + e))
                .then(() => {
                    this._restoring = false;
                    this._storedState = null;

                    // If during state restoring new file added => start TTS anew
                    this._queue.length && this._startTTS();
                });
        }
    };

    this._storeState = function () {
        this._storedState = JSON.parse(JSON.stringify(this._player.state));
        this._storedState.avTransportUriMetadata = JSON.parse(JSON.stringify(this._player.avTransportUriMetadata));
        this._storedState.time = Date.now();
        this._storedState.radio = this._isRadio(this._storedState);
        adapter.log.debug(`[TTS / _storeState] volume=${this._storedState.volume} currentTrack.uri=${this._storedState.currentTrack && this._storedState.currentTrack.uri} tts.playbackState=${this._storedState.playbackState}`);
    };

    this._isRadio = function (state) {
        const extension = state.currentTrack && state.currentTrack.uri ? state.currentTrack.uri.split('.').pop() : 'none';

        const uri = state.currentTrack && state.currentTrack.uri;

        if (!uri) {
            return true;
        }

        // If not radio
        const isFile =
            uri.includes('x-file-cifs:') ||
            uri.includes('x-sonos-spotify:') ||
            uri.includes('x-sonosapi-hls-static:') ||
            audioExtensions.includes(extension);

        if (isFile) {
            return false;
        } else {
            return state.currentTrack.uri.split(':')[0];
        }
    };

    this._startTTS = function () {
        if (!this._queue.length) {
            if (this._ttsPlaying) {
                this._removeAddedTrack()
                    .then(() => this._restoreState());
            }
        } else {
            if (!this._ttsPlaying && !this._restoring) {
                this._ttsPlaying = true;
                let wasPlaying;

                const task = this._queue.shift();

                this._storeState();

                this._fadeOut()
                    .then(() => {
                        if (this._storedState.radio) {
                            wasPlaying = false;
                            this._lastAddedTrack = null;
                            return this._player.setAVTransport(task.uri);
                        } else {
                            wasPlaying = this._storedState.playbackState === 'PLAYING';

                            return this._player.addURIToQueue(task.uri)
                                .then(res => {
                                    // Find out added track
                                    this._lastAddedTrack = parseInt(res.firsttracknumberenqueued, 10);
                                    return this._player.trackSeek(this._lastAddedTrack);
                                });
                        }
                    })
                    .then(() => {
                        // remember start of playing
                        this._playStopped = Date.now();
                        this._playStoppedTimeout = setTimeout(() => {
                            this._playStoppedTimeout = null;
                            this._afterPlayingStopped();
                        }, 40000);

                        return !wasPlaying ? this._player.play() : Promise.resolve();
                    })
                    .then(() => {
                        // wait till playing started to start fadeIn process
                        if (fadeInMs) {
                            return new Promise(resolve => {
                                this._waitPlayerStarted = {
                                    resolve,
                                    timer: setTimeout(() => {
                                        this._waitPlayerStarted.timer = null;
                                        resolve();
                                    }, 3000),
                                };
                            });
                        } else {
                            return Promise.resolve();
                        }
                    })
                    .then(() => {
                        if (this._waitPlayerStarted && this._waitPlayerStarted.timer) {
                            clearTimeout(this._waitPlayerStarted);
                            this._waitPlayerStarted.timer = null;
                        }
                        this._waitPlayerStarted = null;
                        return this._fadeIn(task.volume);
                    })
                    .catch(error => {
                        adapter.log.error('Cannot execute TTS: ' + error.toString());
                    })
                    // wait till ended in playingEnded
            } else {
                // do nothing
            }
        }
    };

    this.immediatelyStopTTS = function () {
        if (this._ttsPlaying) {
            this._restoring = true;
            this._player.pause()
                .then(() => this._restoreState());
        }
    };

    this.add = function (uri, volume) {
        this._queue.push({uri, volume});
        this._startTTS();
    };

    this._removeAddedTrack = function () {
        if (this._lastAddedTrack !== null) {
            // remove track
            return this._player.removeTrackFromQueue(this._lastAddedTrack)
                .catch(error => adapter.log.error(`Cannot removeTrackFromQueue: ${error}`))
                .then(() => this._lastAddedTrack = null);
        } else {
            return Promise.resolve();
        }
    };

    this._afterPlayingStopped = function () {
        // ignore stop state immediately after play. Sonos Bug?
        if (Date.now() - this._playStopped < 1000) {
            return adapter.log.debug('Stop state ignored, right after the play start');
        }

        // stop wait for "speech end timeout"
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }

        this._removeAddedTrack()
            .then(() => {
                // process next task
                if (!this._queue.length) {
                    return this._restoreState();
                } else {
                    const task = this._queue.shift();

                    let promise;

                    if (this._storedState.radio) {
                        promise = this._player.setAVTransport(task.uri);
                    } else {
                        promise = player.addURIToQueue(task.uri)
                            .then(res => {
                                // Find out added track
                                this._lastAddedTrack = parseInt(res.firsttracknumberenqueued, 10);
                                return this._player.trackSeek(this._lastAddedTrack);
                            });
                    }
                    return promise
                        .then(() => this._player.setVolume(task.volume))
                        .then(() => this._player.play());
                }
            })
            .catch(error => adapter.log.error('Cannot remove track: ' + error.toString()));
    };

    this.playingStarted = function() {
        console.log('PLAYING STARTED!!!');
        if ((this._restoring || this._ttsPlaying) && this._waitPlayerStarted) {
            clearTimeout(this._waitPlayerStarted.timer);
            this._waitPlayerStarted.timer = null;
            setImmediate(() =>
                this._waitPlayerStarted.resolve());
        }
    };

    this.playingEnded = function() {
        console.log('PLAYING ENDED!!!');
        setImmediate(() =>
            this._afterPlayingStopped());
    };

    this._wait = function (ms) {
        return new Promise(resolve => setTimeout(() => resolve(), ms));
    };

    this._fadeIn = function (targetVolume, options) {
        if (!fadeInMs) {
            return this._player.setVolume(targetVolume);
        }

        if (options === undefined) {
            adapter.log.debug(`[TTS / _fadeIn] << fadeIn start to ${targetVolume}`);
            targetVolume = parseInt(targetVolume, 10);

            options = {
                step: Math.round(targetVolume / Math.max(fadeInMs / FADE_STEP_MS, 1)) || 1,
                actual: 0
            };
        }

        adapter.log.debug(`[TTS / _fadeIn] >> fadeIn to ${options.actual} of ${targetVolume}`);

        options.actual += options.step;

        if (options.actual >= targetVolume) {
            adapter.log.debug(`[TTS / _fadeIn] << fadeIn end to ${targetVolume}`);
            return player.setVolume(targetVolume);
        } else {
            return player.setVolume(options.actual)
                .then(() => this._wait(FADE_STEP_MS))
                .then(() => this._fadeIn(targetVolume, options));
        }
    };

    this._fadeOut = function (options) {
        // player was in mute state, so no fadeout required
        if (!fadeOutMs || this._player._isMute || this._player.state.playbackState !== 'PLAYING') {
            return this._player.setVolume(0)
                .then(() => this._player._isMute && this._player.unMute());
        }

        if (options === undefined) {
            const actual = parseInt(this._player._volume, 10);

            options = {
                actual,
                step: Math.round(actual / Math.max(fadeOutMs / 100, 1)) || 1
            };
        }

        options.actual -= options.step;

        if (options.actual > 0) {
            return player.setVolume(options.actual)
                .then(() => {
                    adapter.log.debug(`[TTS / _fadeOut] >> fadeOut: setVolume: ${options.actual}`);

                    return this._wait(FADE_STEP_MS)
                        .then(() => this._fadeOut(options));
                });
        } else {
            return player.setVolume(0)
                .then(() => adapter.log.debug('[TTS / _fadeOut] << fadeOut '));
        }
    };

    this.destroy = function () {
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }
        if (this._playStoppedTimeout) {
            clearTimeout(this._playStoppedTimeout);
            this._playStoppedTimeout = null;
        }
    }
}

module.exports = TTS;
<|endoftext|>


<commit_before>/*global define*/

define(['angular'], function (angular) {
    "use strict";

    var directive = function ($ionicScrollDelegate, $interval, Client, $sce, $ionicLoading, $timeout, $ionicGesture, $ionicPosition  ) {
	 	return {
       		restrict: 'AE',
			link: function(scope, el, attrs) {

            	scope.showVideo = false;
            	var playing = false;

            	attrs.$observe('entity', function(entity){
					entity = JSON.parse(entity);
					scope.srcFull = $sce.trustAsResourceUrl(entity.src['360.mp4']);
				});

				/**
				 * Initialises the playback.
				 */
				scope.play = function(){
					scope.$digest();
					$ionicLoading.show({
						template: 'Loading...'
					});
					$timeout(function(){
						$ionicLoading.hide();
					}, 3000);

					if(!scope.srcFull){
						console.log('downloading src info');
						Client.get('api/v1/archive/'+attrs.guid, {},
	    					function(success){

	    						scope.showVideo = true;
	    						scope.srcFull = $sce.trustAsResourceUrl(success.transcodes['360.mp4']);
	    						console.log(scope.srcFull);
	    						scope.$digest();

	    						var video = el[0].querySelector('#video');
	    						console.log(video);
	    						video.play();
	    						video.onended = function(){
	    	 						scope.showVideo = false;
	    	 					}
	    					},
	    					function(error){
	    						console.log(error);
	    					});
    				} else {

    					scope.showVideo = true;
    					if ( device.platform == 'android' || device.platform == 'Android' ){
    						scope.showVideo = false;
    						scope.$apply();
    					}

    					var video = el[0].querySelector('#video');
    					//console.log('video is..');
    					//console.log(el);
    					//return true;
	 					video.play();
	 					video.webkitEnterFullscreen();
	 					video.onplaying  = function(){
	 						$ionicLoading.hide();
	 					};
	 					video.onerror = function(){
	 						alert('error in playing');
	 					};
	 					video.onended = function(){
	 						scope.showVideo = false;
	 					}

	 					video.addEventListener('webkitendfullscreen', function (e) {
	 						scope.showVideo = false;
	 						scope.$apply();
	 						console.log('ended full screen');
	 					});
	 					video.addEventListener('ended', function(e){
	 						scope.showVideo = false;
	 						scope.$apply();
	 					}, false);

    				}
				};

            	el.on('click', scope.play);

            	var element = angular.element(document.querySelector('ion-content'));

				$ionicGesture.on('dragup', function(){

					var scroll = $ionicScrollDelegate.getScrollPosition();
            		var position = el.parent().prop('offsetTop');
            		var diff = (scroll.top - position);
            		var src = "";
            		if(diff > -250 && diff < 250 && !scope.srcFull){

        				Client.get('api/v1/archive/'+attrs.guid, {},
        					function(success){
        						scope.showVideo = false;
        						scope.srcFull = $sce.trustAsResourceUrl(success.transcodes['360.mp4']);
            				},
            				function(error){
            					console.log(error);
            				});

            		}
				}, element);

			}
       	 };
    };

    directive.$inject = ['$ionicScrollDelegate', '$interval', 'Client', '$sce', '$ionicLoading', '$timeout', '$ionicGesture', '$ionicPosition'];
    return directive;
});<commit_msg>android close/backbutotn now stops video<commit_after>/*global define*/

define(['angular'], function (angular) {
    "use strict";

    var directive = function ($ionicScrollDelegate, $interval, Client, $sce, $ionicLoading, $timeout, $ionicGesture, $ionicPosition, $ionicPlatform  ) {
	 	return {
       		restrict: 'AE',
			link: function(scope, el, attrs) {

            	scope.showVideo = false;
            	var playing = false;

            	attrs.$observe('entity', function(entity){
					entity = JSON.parse(entity);
					scope.srcFull = $sce.trustAsResourceUrl(entity.src['360.mp4']);
				});

				/**
				 * Initialises the playback.
				 */
				scope.play = function(){
					scope.$digest();
					$ionicLoading.show({
						template: 'Loading...'
					});
					$timeout(function(){
						$ionicLoading.hide();
					}, 3000);

					if(!scope.srcFull){
						console.log('downloading src info');
						Client.get('api/v1/archive/'+attrs.guid, {},
	    					function(success){

	    						scope.showVideo = true;
	    						scope.srcFull = $sce.trustAsResourceUrl(success.transcodes['360.mp4']);
	    						console.log(scope.srcFull);
	    						scope.$digest();

	    						var video = el[0].querySelector('#video');
	    						console.log(video);
	    						video.play();
	    						video.onended = function(){
	    	 						scope.showVideo = false;
	    	 					}
	    					},
	    					function(error){
	    						console.log(error);
	    					});
    				} else {

    					scope.showVideo = true;
    					if ( device.platform == 'android' || device.platform == 'Android' ){
    						scope.showVideo = false;
    						scope.$apply();
    					}

    					var video = el[0].querySelector('#video');
    					//console.log('video is..');
    					//console.log(el);
    					//return true;
	 					video.play();
	 					video.webkitEnterFullscreen();
	 					video.onplaying  = function(){
	 						$ionicLoading.hide();
	 					};
	 					video.onerror = function(){
	 						alert('error in playing');
	 					};
	 					video.onended = function(){
	 						scope.showVideo = false;
	 					}

	 					video.addEventListener('webkitendfullscreen', function (e) {
	 						scope.showVideo = false;
	 						scope.$apply();
	 						console.log('ended full screen');
	 					});
	 					video.addEventListener('webkitfullscreenchange', function (e) {
	 						scope.showVideo = false;
	 						scope.$apply();
	 						if(!document.webkitIsFullScreen){
	 							video.pause();
	 						}
	 					});
	 					video.addEventListener('ended', function(e){
	 						scope.showVideo = false;
	 						scope.$apply();
	 					}, false);

    				}
				};

            	el.on('click', scope.play);

            	var element = angular.element(document.querySelector('ion-content'));

				$ionicGesture.on('dragup', function(){

					var scroll = $ionicScrollDelegate.getScrollPosition();
            		var position = el.parent().prop('offsetTop');
            		var diff = (scroll.top - position);
            		var src = "";
            		if(diff > -250 && diff < 250 && !scope.srcFull){

        				Client.get('api/v1/archive/'+attrs.guid, {},
        					function(success){
        						scope.showVideo = false;
        						scope.srcFull = $sce.trustAsResourceUrl(success.transcodes['360.mp4']);
            				},
            				function(error){
            					console.log(error);
            				});

            		}
				}, element);

			}
       	 };
    };

    directive.$inject = ['$ionicScrollDelegate', '$interval', 'Client', '$sce', '$ionicLoading', '$timeout', '$ionicGesture', '$ionicPosition', '$ionicPlatform'];
    return directive;
});<|endoftext|>


<commit_before>
"use strict";

angular.module('myapp')
  .controller("WeatherController", function($scope, $http){

    $scope.loading = true;
    if (navigator.geolocation) navigator.geolocation.getCurrentPosition(onPositionUpdate);

    function onPositionUpdate(position) {
      var lati = position.coords.latitude;
      var longi = position.coords.longitude;
      \\var url = 'https://api.wunderground.com/api/4a82154c39d11213/conditions/geolocation/q/' + lati + ',' + longi + '.json';
      var url = 'https://api.wunderground.com/api/4a82154c39d11213/forecast/geolocation/q/' + lati + ',' + longi + '.json';
      $http.get(url)
        .then(function(info) {
        console.log(info);

          /*$scope.weatherNow = {
            city: info.data.current_observation.display_location.city,
            description: info.data.current_observation.weather,
            temp_f: Math.round(info.data.current_observation.temp_f),
            temp_c: Math.round(info.data.current_observation.temp_c),
            temp: null,
            humidity: info.data.current_observation.relative_humidity,
            wind_mph: Math.round(info.data.current_observation.wind_mph) + "mph",
            wind_kph: Math.round(info.data.current_observation.wind_kph) + "kph",
            wind: null,
            imageSrc: info.data.current_observation.icon_url
          };*/

          /*var currentTime = new Date();
          var hours = currentTime.getHours();
          var minutes = currentTime.getMinutes();
          var ending = "AM";

          if (minutes < 10) {
            minutes = "0" + minutes;
          }
          if (hours >= 12) {
            hours = hours - 12;
            ending = "PM";
          }

          if (hours === 0) {
            hours = 12;
          }
          var currentDate = new Date();
          var timeNow = hours + ":" + minutes + " " + ending;
          $scope.weatherNow.timeDate = timeNow + " " + currentDate.toDateString();
          console.log($scope.weatherNow);

          $scope.imperial = true;
          document.getElementById("fahrenheit").style.backgroundColor = "#99e6ff";
          document.getElementById("celsius").style.backgroundColor = "#f2f2f2";
          $scope.weatherNow.temp = $scope.weatherNow.temp_f;
          $scope.weatherNow.wind = $scope.weatherNow.wind_mph;

          $scope.unitChange = function(elem){
            $scope.imperial = elem;
            console.log(Boolean($scope.imperial));
            if($scope.imperial === true){
            document.getElementById("fahrenheit").style.backgroundColor = "#99e6ff";
            document.getElementById("celsius").style.backgroundColor = "#f2f2f2";
            $scope.weatherNow.temp = $scope.weatherNow.temp_f;
            $scope.weatherNow.wind = $scope.weatherNow.wind_mph;
          } else if($scope.imperial === false) {
            document.getElementById("fahrenheit").style.backgroundColor = "#f2f2f2";
            document.getElementById("celsius").style.backgroundColor = "#99e6ff";
            $scope.weatherNow.temp = $scope.weatherNow.temp_c;
            $scope.weatherNow.wind = $scope.weatherNow.wind_kph;
          }*/
        };

      });
    }
  });

<commit_msg>experiment 2<commit_after>
"use strict";

angular.module('myapp')
  .controller("WeatherController", function($scope, $http){

    $scope.loading = true;
    if (navigator.geolocation) navigator.geolocation.getCurrentPosition(onPositionUpdate);

    function onPositionUpdate(position) {
      var lati = position.coords.latitude;
      var longi = position.coords.longitude;
      \\var url = 'https://api.wunderground.com/api/4a82154c39d11213/conditions/geolocation/q/' + lati + ',' + longi + '.json';
      var url = 'https://api.wunderground.com/api/4a82154c39d11213/forecast/geolocation/q/' + lati + ',' + longi + '.json';
      $http.get(url)
        .then(function(info) {
        console.log(info);

          $scope.weatherNow = {
            city: info.data.current_observation.display_location.city,
            description: info.data.current_observation.weather,
            temp_f: Math.round(info.data.current_observation.temp_f),
            temp_c: Math.round(info.data.current_observation.temp_c),
            temp: null,
            humidity: info.data.current_observation.relative_humidity,
            wind_mph: Math.round(info.data.current_observation.wind_mph) + "mph",
            wind_kph: Math.round(info.data.current_observation.wind_kph) + "kph",
            wind: null,
            imageSrc: info.data.current_observation.icon_url
          };

          var currentTime = new Date();
          var hours = currentTime.getHours();
          var minutes = currentTime.getMinutes();
          var ending = "AM";

          if (minutes < 10) {
            minutes = "0" + minutes;
          }
          if (hours >= 12) {
            hours = hours - 12;
            ending = "PM";
          }

          if (hours === 0) {
            hours = 12;
          }
          var currentDate = new Date();
          var timeNow = hours + ":" + minutes + " " + ending;
          $scope.weatherNow.timeDate = timeNow + " " + currentDate.toDateString();
          console.log($scope.weatherNow);

          $scope.imperial = true;
          document.getElementById("fahrenheit").style.backgroundColor = "#99e6ff";
          document.getElementById("celsius").style.backgroundColor = "#f2f2f2";
          $scope.weatherNow.temp = $scope.weatherNow.temp_f;
          $scope.weatherNow.wind = $scope.weatherNow.wind_mph;

          $scope.unitChange = function(elem){
            $scope.imperial = elem;
            console.log(Boolean($scope.imperial));
            if($scope.imperial === true){
            document.getElementById("fahrenheit").style.backgroundColor = "#99e6ff";
            document.getElementById("celsius").style.backgroundColor = "#f2f2f2";
            $scope.weatherNow.temp = $scope.weatherNow.temp_f;
            $scope.weatherNow.wind = $scope.weatherNow.wind_mph;
          } else if($scope.imperial === false) {
            document.getElementById("fahrenheit").style.backgroundColor = "#f2f2f2";
            document.getElementById("celsius").style.backgroundColor = "#99e6ff";
            $scope.weatherNow.temp = $scope.weatherNow.temp_c;
            $scope.weatherNow.wind = $scope.weatherNow.wind_kph;
          }
        };

      });
    }
  });

<|endoftext|>


<commit_before>// d3 margin convention
var margin = {
  top: 50,
  left: 50,
  bottom: 50,
  right: 50,
}

var width = 1060 - margin.left - margin.right; // 960
var height = 800 - margin.top - margin.bottom; // 700
var centered;

var formatVal = d3.format('s');

var bbVis = {
  x: 100,
  y: 10,
  w: width - 100, h: 300
}

var detailVis = d3.select('#detailVis').append('svg')
  .attr('width', 350)
  .attr('height', 200)

var svg = d3.select('#vis').append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  //.on('click', clicked)

var g = svg.append('g')
  .attr('class', 'country')
  .attr('transform', 'translate(' + margin.left + ','
    + margin.right + ')')

var projection = d3.geo.albersUsa()
  .translate([width / 2, height / 2])
  .precision(0.1)

var path = d3.geo.path()
  .projection(projection)

var dataset = [];
var completeDataset = [];

var tip = d3.tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(function(d) {
    var station = d['USAF'];
    var sum = completeDataset[station] !== undefined ?
      completeDataset[d['USAF']]['sum'] : 'No data';
    //return '' + d['STATION'] + '
'
    return '' + d['STATION'] + '
'
      + ''
      + (isNaN(sum) ? sum : formatVal(sum)) + '';
  });

svg
  .call(tip);

d3.json('../data/us-named.json', function(error, data) {

  // convert topojson to geojson
  var usMap = topojson.feature(data, data.objects.states);

  g
    .selectAll('.state')
    .data(usMap.features)
    .enter()
      .append('path')
      .attr('class', 'state')
      .attr('d', path)
      .on('click', clicked)
      //.on('mouseover', tip.show)
      //.on('mouseout', tip.hide)

  // needed?
//  g
//    .append('path')
//    .datum(topojson.mesh(data, data.objects.states, function(a, b) {
//      return a !== b;
//    }));

  loadStats();
}); // end d3.json -- us-named

function clicked(d) {
  var x, y, k;

  if (d && centered !== d) {
    var centroid = path.centroid(d);
    x = centroid[0];
    y = centroid[1];
    k = 4;
    centered = d;
  } else {
    x = width / 2;
    y = height / 2;
    k = 1;
    centered = null;
  }

  //console.log(x, y);
  g
    .selectAll('path')
    .classed('active', centered && function(d) {
      return d === centered;
    })

  g
    .transition()
    .duration(750)
    .attr('transform', 'translate(' + (width / 2) + ','
      + (height / 2) + ')scale(' + k + ')translate('
      + -x + ',' + -y + ')')
    .style('stroke-width', 1.5 / k + 'px')

}

function loadStations() {
  d3.csv('../data/NSRDB_StationsMeta.csv', function(error, data) {
  //console.log(data);

// for calcuating a good radius
//  var sums = [];
//  for (var s in completeDataset) {
//    sums.push(completeDataset[s].sum)
//  }
//  var max = d3.max(sums);
//  var min = d3.min(sums);
//  console.log(min, max);

  g
    .selectAll('.station')
    .data(data)
    .enter()
      .append('circle')
      .attr('class', 'station')
      .attr('cx', function(d) {
        var x = projection(
          [
            parseFloat(d['NSRDB_LON(dd)']),
            parseFloat(d['NSRDB_LAT (dd)'])
          ]
        );
        // make sure we have an array before returning part of it
        if (x) return x[0];
      })
      .attr('cy', function(d) {
        var y = projection(
          [
            parseFloat(d['NSRDB_LON(dd)']),
            parseFloat(d['NSRDB_LAT (dd)'])
          ]
        );
        // make sure we have an array before returning part of it
        if (y) return y[1];
      })
      .attr('r', function(d) {
        var usaf = d['USAF'];
        if (completeDataset[usaf]) {
          return completeDataset[usaf].sum / 10000000;
        } else {
          return 2;
        }
      })
      .style('fill', function(d) {
        var usaf = d['USAF'];
        if (!completeDataset[usaf]) {
          return '#aaa';
        }
      })
      .style('stroke', function(d) {
        var usaf = d['USAF'];
        if (!completeDataset[usaf]) {
          return '#aaa';
        }
      })
      .on('mouseover', function(d) {
          tip.show(d)
      })
      .on('mouseout', tip.hide)

    //console.log(completeDataset['690150'].sum);

  }); // end d3.csv() -- stationsmeta
} // end loadStations()

function loadStats() {
  d3.json('../data/reduceMonthStationHour2003_2004.json',
    function(error, data) {

    completeDataset = data;

    loadStations();

  }); // end d3.json()
} // end loadStats()

function zoomIn(d) {
  console.log(d.properties.name);
}

function zoomOut() {

}
<commit_msg>gets xAxis in detailVis to appear<commit_after>// d3 margin convention
var margin = {
  top: 50,
  left: 50,
  bottom: 50,
  right: 50,
}

var width = 960 - margin.left - margin.right; // 860
var height = 800 - margin.top - margin.bottom; // 700
var centered;

var formatVal = d3.format('s');

var bbVis = {
  x: 100,
  y: 10,
  w: width - 100, h: 300
}

var detailVis = d3.select('#detailVis').append('svg')
  .attr('width', 350)
  .attr('height', 200)

var xScaleDetail = d3.time.scale()
  .domain([new Date, new Date])
  .nice(d3.time.hour, 24)
  .range([0, parseInt(detailVis.attr('width'))]);

//console.log(xScaleDetail.domain());
//console.log(xScaleDetail.range());

var yScaleDetail = d3.scale.linear()
  .range([detailVis.attr('height'), 0]);

var xAxisDetail = d3.svg.axis()
  .scale(xScaleDetail)
  .ticks(24)
  .orient('bottom');

var yAxisDetail = d3.svg.axis()
  .scale(yScaleDetail)
  .orient('top');

detailVis
  .append('g')
  .attr('class', 'x axis')
  //.attr('transform', 'translate(0,' +
    //detailVis.attr('height') + ')')
  .call(xAxisDetail)

var svg = d3.select('#vis').append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  //.on('click', clicked)

var g = svg.append('g')
  .attr('class', 'country')
  .attr('transform', 'translate(' + margin.left + ','
    + margin.right + ')')

var projection = d3.geo.albersUsa()
  .translate([width / 2, height / 2])
  .precision(0.1)

var path = d3.geo.path()
  .projection(projection)

var dataset = [];
var completeDataset = [];

/*
 * Inititialize our tooltip
 */
var tip = d3.tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(function(d) {
    var station = d['USAF'];
    var sum = completeDataset[station] !== undefined ?
      completeDataset[d['USAF']]['sum'] : 'No data';
    //return '' + d['STATION'] + '
'
    return '' + d['STATION'] + '
'
      + ''
      + (isNaN(sum) ? sum : formatVal(sum)) + '';
  });

/*
 * Activate our tooltip
 */
svg
  .call(tip);

/*
 * Load US map
 */
d3.json('../data/us-named.json', function(error, data) {

  // convert topojson to geojson
  var usMap = topojson.feature(data, data.objects.states);

  g
    .selectAll('.state')
    .data(usMap.features)
    .enter()
      .append('path')
      .attr('class', 'state')
      .attr('d', path)
      .on('click', stateClicked)

  loadStats();
}); // end d3.json -- us-named

function stateClicked(d) {
  var x, y, k;

  if (d && centered !== d) {
    var centroid = path.centroid(d);
    x = centroid[0];
    y = centroid[1];
    k = 4;
    centered = d;
  } else {
    x = width / 2;
    y = height / 2;
    k = 1;
    centered = null;
  }

  //console.log(x, y);
  g
    .selectAll('path')
    .classed('active', centered && function(d) {
      return d === centered;
    })

  g
    .transition()
    .duration(750)
    .attr('transform', 'translate(' + (width / 2) + ','
      + (height / 2) + ')scale(' + k + ')translate('
      + -x + ',' + -y + ')')
    .style('stroke-width', 1.5 / k + 'px')
} // end stateClicked()

function loadStations() {
  d3.csv('../data/NSRDB_StationsMeta.csv', function(error, data) {
  //console.log(data);

// for calcuating a good radius
//  var sums = [];
//  for (var s in completeDataset) {
//    sums.push(completeDataset[s].sum)
//  }
//  var max = d3.max(sums);
//  var min = d3.min(sums);
//  console.log(min, max);

  g
    .selectAll('.station')
    .data(data)
    .enter()
      .append('circle')
      .attr('class', 'station')
      .attr('cx', function(d) {
        var x = projection(
          [
            parseFloat(d['NSRDB_LON(dd)']),
            parseFloat(d['NSRDB_LAT (dd)'])
          ]
        );
        // make sure we have an array before returning part of it
        if (x) return x[0];
      })
      .attr('cy', function(d) {
        var y = projection(
          [
            parseFloat(d['NSRDB_LON(dd)']),
            parseFloat(d['NSRDB_LAT (dd)'])
          ]
        );
        // make sure we have an array before returning part of it
        if (y) return y[1];
      })
      .attr('r', function(d) {
        var usaf = d['USAF'];
        if (completeDataset[usaf]) {
          return completeDataset[usaf].sum / 10000000;
        } else {
          return 2;
        }
      })
      .style('fill', function(d) {
        var usaf = d['USAF'];
        if (!completeDataset[usaf]) {
          return '#aaa';
        }
      })
      .style('stroke', function(d) {
        var usaf = d['USAF'];
        if (!completeDataset[usaf]) {
          return '#aaa';
        }
      })
      .on('mouseover', function(d) {
          tip.show(d) // just experimenting with function calls
      })
      .on('mouseout', tip.hide)
      .on('click', stationClicked)

    //console.log(completeDataset['690150'].sum);

  }); // end d3.csv() -- stationsmeta
} // end loadStations()

function stationClicked(d) {
  d3.select('#detailVis svg')
    .attr('class', function() {
      console.log(d3.select(this));
    })
  //console.log(d);
} // end stationClicked()

function loadStats() {
  d3.json('../data/reduceMonthStationHour2003_2004.json',
    function(error, data) {

    completeDataset = data;

    loadStations();

  }); // end d3.json()
} // end loadStats()

function zoomIn(d) {
  console.log(d.properties.name);
}

function zoomOut() {

}
<|endoftext|>


<commit_before>/**
 * This is free and unencumbered software released into the public domain.
 * See UNLICENSE.txt
 */

var starter = {
    numRows: 15,
    numCols: 21,
    blocks: [
        {
            row: 3,
            col: 9
        },
        {
            row: 3,
            col: 10
        },
        {
            row: 3,
            col: 11
        },
        {
            row: 11,
            col: 9
        },
        {
            row: 11,
            col: 10
        },
        {
            row: 11,
            col: 11
        },
        {
            row: 6,
            col: 6
        },
        {
            row: 7,
            col: 6
        },
        {
            row: 8,
            col: 6
        },
        {
            row: 6,
            col: 14
        },
        {
            row: 7,
            col: 14
        },
        {
            row: 8,
            col: 14
        },
    ],
    goals: [
        {
            row: 2,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
    ],
    ball: {
        row: 7,
        col: 10
    }
}

var boardConfig1 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 2,
            col: 0
        },
        {
            row: 2,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
        {
            row: 2,
            col: 3
        },
        {
            row: 2,
            col: 8
        },
        {
            row: 2,
            col: 4
        },
        {
            row: 2,
            col: 9
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        },
        {
            row: 5,
            col: 8,
            dir: hundo.DirectionEnum.LEFT
        },
        {
            row: 10,
            col: 5,
            dir: hundo.DirectionEnum.RIGHT
        }

    ],
    ball: {
        row: 2,
        col: 7,
    },
}

var boardConfig2 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 2,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        }

    ],
    ball: {
        row: 2,
        col: 7,
    }
}

var boardConfig3 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 5,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        }

    ],
    ball: {
        row: 2,
        col: 7,
    }
}

var foo = {"numRows":15,"numCols":21,"blocks":[{"row":1,"col":1},{"row":1,"col":2},{"row":1,"col":3},{"row":1,"col":14},{"row":2,"col":18},{"row":3,"col":1},{"row":3,"col":10},{"row":3,"col":11},{"row":3,"col":12},{"row":3,"col":13},{"row":3,"col":14},{"row":3,"col":18},{"row":4,"col":1},{"row":4,"col":18},{"row":5,"col":1},{"row":5,"col":18},{"row":6,"col":1},{"row":6,"col":18},{"row":7,"col":1},{"row":7,"col":18},{"row":8,"col":1},{"row":8,"col":12},{"row":9,"col":1},{"row":9,"col":12},{"row":10,"col":1},{"row":10,"col":12},{"row":10,"col":16},{"row":10,"col":17},{"row":10,"col":18},{"row":11,"col":12},{"row":12,"col":1},{"row":12,"col":2},{"row":12,"col":3},{"row":12,"col":12},{"row":14,"col":12},{"row":14,"col":13},{"row":14,"col":14}],"goals":[{"row":13,"col":6,"dir":"RIGHT"}],"ball":{"row":6,"col":9}}
var diagonal = {"numRows":15,"numCols":21,"blocks":[{"row":2,"col":5},{"row":2,"col":6},{"row":2,"col":7},{"row":4,"col":3},{"row":4,"col":7},{"row":4,"col":8},{"row":4,"col":9},{"row":4,"col":10},{"row":4,"col":11},{"row":5,"col":3},{"row":6,"col":3},{"row":6,"col":5},{"row":6,"col":13},{"row":7,"col":5},{"row":7,"col":13},{"row":8,"col":5},{"row":8,"col":13},{"row":9,"col":5},{"row":9,"col":13},{"row":9,"col":15},{"row":10,"col":15},{"row":11,"col":7},{"row":11,"col":8},{"row":11,"col":9},{"row":11,"col":10},{"row":11,"col":11},{"row":11,"col":15},{"row":13,"col":11},{"row":13,"col":12},{"row":13,"col":13}],"goals":[{"row":3,"col":1,"dir":"RIGHT"},{"row":12,"col":17,"dir":"LEFT"}],"ball":{"row":7,"col":9}}
var iceIntro ={"numRows":15,"numCols":21,"blocks":[{"row":7,"col":17}],"goals":[{"row":3,"col":11,"dir":"DOWN"},{"row":11,"col":11,"dir":"UP"}],"ice":[{"row":7,"col":8},{"row":7,"col":9},{"row":7,"col":10},{"row":7,"col":11},{"row":7,"col":12}],"ball":{"row":7,"col":5}};
var pairTheIce = {"numRows":15,"numCols":21,"blocks":[{"row":1,"col":2},{"row":1,"col":3},{"row":1,"col":4},{"row":2,"col":9},{"row":2,"col":10},{"row":2,"col":11},{"row":2,"col":18},{"row":3,"col":18},{"row":4,"col":18},{"row":5,"col":3},{"row":6,"col":3},{"row":7,"col":3},{"row":7,"col":16},{"row":7,"col":17},{"row":9,"col":2},{"row":9,"col":19},{"row":10,"col":2},{"row":10,"col":19},{"row":11,"col":2},{"row":11,"col":9},{"row":11,"col":10},{"row":11,"col":11},{"row":11,"col":19},{"row":13,"col":7},{"row":13,"col":8},{"row":13,"col":9}],"goals":[{"row":11,"col":6,"dir":"UP"}],"ice":[{"row":6,"col":7},{"row":6,"col":13}],"ball":{"row":6,"col":10}};
var iceDistraction = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":15},{"row":0,"col":16},{"row":1,"col":5},{"row":1,"col":11},{"row":2,"col":5},{"row":2,"col":6},{"row":2,"col":11},{"row":3,"col":1},{"row":3,"col":2},{"row":3,"col":3},{"row":4,"col":16},{"row":5,"col":2},{"row":5,"col":16},{"row":6,"col":2},{"row":6,"col":16},{"row":7,"col":2},{"row":8,"col":15},{"row":8,"col":16},{"row":9,"col":9},{"row":9,"col":10},{"row":9,"col":11},{"row":9,"col":15},{"row":9,"col":16},{"row":10,"col":2},{"row":10,"col":4},{"row":11,"col":2},{"row":11,"col":14},{"row":11,"col":15},{"row":11,"col":16},{"row":11,"col":17},{"row":12,"col":2},{"row":12,"col":11},{"row":12,"col":12},{"row":13,"col":11},{"row":13,"col":12}],"goals":[{"row":1,"col":8,"dir":"DOWN"}],"ice":[{"row":1,"col":17},{"row":4,"col":6},{"row":7,"col":17},{"row":10,"col":18},{"row":11,"col":9},{"row":12,"col":3}],"ball":{"row":4,"col":9}}
var arrowIntro = {"numRows":15,"numCols":21,"blocks":[{"row":4,"col":15},{"row":5,"col":15},{"row":6,"col":15},{"row":7,"col":15},{"row":8,"col":15},{"row":9,"col":15},{"row":10,"col":15}],"goals":[{"row":4,"col":11,"dir":"DOWN"},{"row":10,"col":11,"dir":"UP"}],"ice":[],"arrows":[{"row":7,"col":10,"dir":"RIGHT"}],"ball":{"row":7,"col":5}};
var arrowDistraction = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":15},{"row":1,"col":4},{"row":1,"col":5},{"row":1,"col":6},{"row":1,"col":15},{"row":2,"col":15},{"row":3,"col":17},{"row":3,"col":18},{"row":3,"col":19},{"row":3,"col":20},{"row":4,"col":10},{"row":6,"col":1},{"row":6,"col":9},{"row":7,"col":1},{"row":7,"col":17},{"row":8,"col":17},{"row":9,"col":8},{"row":10,"col":8},{"row":10,"col":19},{"row":11,"col":8},{"row":11,"col":19},{"row":12,"col":1},{"row":12,"col":2},{"row":12,"col":3},{"row":12,"col":16},{"row":12,"col":19}],"goals":[{"row":3,"col":12,"dir":"DOWN"}],"ice":[],"arrows":[{"row":7,"col":11,"dir":"RIGHT"},{"row":10,"col":13,"dir":"LEFT"}],"ball":{"row":6,"col":5}};
var arrowIceMachine = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":12},{"row":1,"col":19},{"row":2,"col":4},{"row":3,"col":8},{"row":4,"col":2},{"row":4,"col":11},{"row":5,"col":9},{"row":6,"col":7},{"row":6,"col":9},{"row":7,"col":6},{"row":7,"col":7},{"row":7,"col":9},{"row":9,"col":1},{"row":9,"col":8},{"row":9,"col":18},{"row":10,"col":8},{"row":12,"col":8},{"row":12,"col":20},{"row":13,"col":2},{"row":13,"col":3},{"row":13,"col":4},{"row":13,"col":5},{"row":13,"col":9},{"row":13,"col":10}],"goals":[{"row":5,"col":15,"dir":"DOWN"},{"row":11,"col":15,"dir":"UP"}],"ice":[{"row":7,"col":8},{"row":8,"col":5},{"row":9,"col":7},{"row":9,"col":9}],"arrows":[{"row":7,"col":5,"dir":"DOWN"},{"row":8,"col":11,"dir":"RIGHT"},{"row":8,"col":16,"dir":"LEFT"}],"ball":{"row":8,"col":3}};
var gblockIntro = {"numRows":15,"numCols":21,"blocks":[{"row":13,"col":7},{"row":13,"col":8},{"row":13,"col":9},{"row":13,"col":10},{"row":13,"col":11}],"goals":[{"row":1,"col":17,"dir":"DOWN"},{"row":11,"col":18,"dir":"LEFT"},{"row":12,"col":2,"dir":"UP"}],"ice":[],"arrows":[],"gblocks":[{"row":4,"col":15,"groupNum":1},{"row":5,"col":3,"groupNum":2},{"row":5,"col":15,"groupNum":1},{"row":6,"col":3,"groupNum":2},{"row":6,"col":13,"groupNum":1},{"row":6,"col":14,"groupNum":1},{"row":6,"col":15,"groupNum":1},{"row":7,"col":3,"groupNum":2},{"row":7,"col":15,"groupNum":1},{"row":8,"col":3,"groupNum":2},{"row":8,"col":15,"groupNum":1},{"row":9,"col":8,"groupNum":0},{"row":9,"col":9,"groupNum":0},{"row":9,"col":10,"groupNum":0},{"row":10,"col":8,"groupNum":0},{"row":10,"col":9,"groupNum":0},{"row":10,"col":10,"groupNum":0}],"ball":{"row":6,"col":9}}

levels = [starter, diagonal, iceIntro, pairTheIce, arrowIntro, arrowIceMachine, gblockIntro];

new Hundo({
    levels: levels,
    id: 1,
    viz: {
        playButton: false,
        levelSelect: true
    },
    maker: false
});<commit_msg>new level intro to gblocks<commit_after>/**
 * This is free and unencumbered software released into the public domain.
 * See UNLICENSE.txt
 */

var starter = {
    numRows: 15,
    numCols: 21,
    blocks: [
        {
            row: 3,
            col: 9
        },
        {
            row: 3,
            col: 10
        },
        {
            row: 3,
            col: 11
        },
        {
            row: 11,
            col: 9
        },
        {
            row: 11,
            col: 10
        },
        {
            row: 11,
            col: 11
        },
        {
            row: 6,
            col: 6
        },
        {
            row: 7,
            col: 6
        },
        {
            row: 8,
            col: 6
        },
        {
            row: 6,
            col: 14
        },
        {
            row: 7,
            col: 14
        },
        {
            row: 8,
            col: 14
        },
    ],
    goals: [
        {
            row: 2,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
    ],
    ball: {
        row: 7,
        col: 10
    }
}

var boardConfig1 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 2,
            col: 0
        },
        {
            row: 2,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
        {
            row: 2,
            col: 3
        },
        {
            row: 2,
            col: 8
        },
        {
            row: 2,
            col: 4
        },
        {
            row: 2,
            col: 9
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        },
        {
            row: 5,
            col: 8,
            dir: hundo.DirectionEnum.LEFT
        },
        {
            row: 10,
            col: 5,
            dir: hundo.DirectionEnum.RIGHT
        }

    ],
    ball: {
        row: 2,
        col: 7,
    },
}

var boardConfig2 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 2,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        }

    ],
    ball: {
        row: 2,
        col: 7,
    }
}

var boardConfig3 = {
    numRows: 15,
    numCols: 20,
    blocks : [
        {
            row: 5,
            col: 1
        },
        {
            row: 2,
            col: 2
        },
    ],
    goals: [
        {
            row: 1,
            col: 7,
            dir: hundo.DirectionEnum.DOWN
        },
        {
            row: 3,
            col: 7,
            dir: hundo.DirectionEnum.UP
        }

    ],
    ball: {
        row: 2,
        col: 7,
    }
}

var foo = {"numRows":15,"numCols":21,"blocks":[{"row":1,"col":1},{"row":1,"col":2},{"row":1,"col":3},{"row":1,"col":14},{"row":2,"col":18},{"row":3,"col":1},{"row":3,"col":10},{"row":3,"col":11},{"row":3,"col":12},{"row":3,"col":13},{"row":3,"col":14},{"row":3,"col":18},{"row":4,"col":1},{"row":4,"col":18},{"row":5,"col":1},{"row":5,"col":18},{"row":6,"col":1},{"row":6,"col":18},{"row":7,"col":1},{"row":7,"col":18},{"row":8,"col":1},{"row":8,"col":12},{"row":9,"col":1},{"row":9,"col":12},{"row":10,"col":1},{"row":10,"col":12},{"row":10,"col":16},{"row":10,"col":17},{"row":10,"col":18},{"row":11,"col":12},{"row":12,"col":1},{"row":12,"col":2},{"row":12,"col":3},{"row":12,"col":12},{"row":14,"col":12},{"row":14,"col":13},{"row":14,"col":14}],"goals":[{"row":13,"col":6,"dir":"RIGHT"}],"ball":{"row":6,"col":9}}
var diagonal = {"numRows":15,"numCols":21,"blocks":[{"row":2,"col":5},{"row":2,"col":6},{"row":2,"col":7},{"row":4,"col":3},{"row":4,"col":7},{"row":4,"col":8},{"row":4,"col":9},{"row":4,"col":10},{"row":4,"col":11},{"row":5,"col":3},{"row":6,"col":3},{"row":6,"col":5},{"row":6,"col":13},{"row":7,"col":5},{"row":7,"col":13},{"row":8,"col":5},{"row":8,"col":13},{"row":9,"col":5},{"row":9,"col":13},{"row":9,"col":15},{"row":10,"col":15},{"row":11,"col":7},{"row":11,"col":8},{"row":11,"col":9},{"row":11,"col":10},{"row":11,"col":11},{"row":11,"col":15},{"row":13,"col":11},{"row":13,"col":12},{"row":13,"col":13}],"goals":[{"row":3,"col":1,"dir":"RIGHT"},{"row":12,"col":17,"dir":"LEFT"}],"ball":{"row":7,"col":9}}
var iceIntro ={"numRows":15,"numCols":21,"blocks":[{"row":7,"col":17}],"goals":[{"row":3,"col":11,"dir":"DOWN"},{"row":11,"col":11,"dir":"UP"}],"ice":[{"row":7,"col":8},{"row":7,"col":9},{"row":7,"col":10},{"row":7,"col":11},{"row":7,"col":12}],"ball":{"row":7,"col":5}};
var pairTheIce = {"numRows":15,"numCols":21,"blocks":[{"row":1,"col":2},{"row":1,"col":3},{"row":1,"col":4},{"row":2,"col":9},{"row":2,"col":10},{"row":2,"col":11},{"row":2,"col":18},{"row":3,"col":18},{"row":4,"col":18},{"row":5,"col":3},{"row":6,"col":3},{"row":7,"col":3},{"row":7,"col":16},{"row":7,"col":17},{"row":9,"col":2},{"row":9,"col":19},{"row":10,"col":2},{"row":10,"col":19},{"row":11,"col":2},{"row":11,"col":9},{"row":11,"col":10},{"row":11,"col":11},{"row":11,"col":19},{"row":13,"col":7},{"row":13,"col":8},{"row":13,"col":9}],"goals":[{"row":11,"col":6,"dir":"UP"}],"ice":[{"row":6,"col":7},{"row":6,"col":13}],"ball":{"row":6,"col":10}};
var iceDistraction = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":15},{"row":0,"col":16},{"row":1,"col":5},{"row":1,"col":11},{"row":2,"col":5},{"row":2,"col":6},{"row":2,"col":11},{"row":3,"col":1},{"row":3,"col":2},{"row":3,"col":3},{"row":4,"col":16},{"row":5,"col":2},{"row":5,"col":16},{"row":6,"col":2},{"row":6,"col":16},{"row":7,"col":2},{"row":8,"col":15},{"row":8,"col":16},{"row":9,"col":9},{"row":9,"col":10},{"row":9,"col":11},{"row":9,"col":15},{"row":9,"col":16},{"row":10,"col":2},{"row":10,"col":4},{"row":11,"col":2},{"row":11,"col":14},{"row":11,"col":15},{"row":11,"col":16},{"row":11,"col":17},{"row":12,"col":2},{"row":12,"col":11},{"row":12,"col":12},{"row":13,"col":11},{"row":13,"col":12}],"goals":[{"row":1,"col":8,"dir":"DOWN"}],"ice":[{"row":1,"col":17},{"row":4,"col":6},{"row":7,"col":17},{"row":10,"col":18},{"row":11,"col":9},{"row":12,"col":3}],"ball":{"row":4,"col":9}}
var arrowIntro = {"numRows":15,"numCols":21,"blocks":[{"row":4,"col":15},{"row":5,"col":15},{"row":6,"col":15},{"row":7,"col":15},{"row":8,"col":15},{"row":9,"col":15},{"row":10,"col":15}],"goals":[{"row":4,"col":11,"dir":"DOWN"},{"row":10,"col":11,"dir":"UP"}],"ice":[],"arrows":[{"row":7,"col":10,"dir":"RIGHT"}],"ball":{"row":7,"col":5}};
var arrowDistraction = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":15},{"row":1,"col":4},{"row":1,"col":5},{"row":1,"col":6},{"row":1,"col":15},{"row":2,"col":15},{"row":3,"col":17},{"row":3,"col":18},{"row":3,"col":19},{"row":3,"col":20},{"row":4,"col":10},{"row":6,"col":1},{"row":6,"col":9},{"row":7,"col":1},{"row":7,"col":17},{"row":8,"col":17},{"row":9,"col":8},{"row":10,"col":8},{"row":10,"col":19},{"row":11,"col":8},{"row":11,"col":19},{"row":12,"col":1},{"row":12,"col":2},{"row":12,"col":3},{"row":12,"col":16},{"row":12,"col":19}],"goals":[{"row":3,"col":12,"dir":"DOWN"}],"ice":[],"arrows":[{"row":7,"col":11,"dir":"RIGHT"},{"row":10,"col":13,"dir":"LEFT"}],"ball":{"row":6,"col":5}};
var arrowIceMachine = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":12},{"row":1,"col":19},{"row":2,"col":4},{"row":3,"col":8},{"row":4,"col":2},{"row":4,"col":11},{"row":5,"col":9},{"row":6,"col":7},{"row":6,"col":9},{"row":7,"col":6},{"row":7,"col":7},{"row":7,"col":9},{"row":9,"col":1},{"row":9,"col":8},{"row":9,"col":18},{"row":10,"col":8},{"row":12,"col":8},{"row":12,"col":20},{"row":13,"col":2},{"row":13,"col":3},{"row":13,"col":4},{"row":13,"col":5},{"row":13,"col":9},{"row":13,"col":10}],"goals":[{"row":5,"col":15,"dir":"DOWN"},{"row":11,"col":15,"dir":"UP"}],"ice":[{"row":7,"col":8},{"row":8,"col":5},{"row":9,"col":7},{"row":9,"col":9}],"arrows":[{"row":7,"col":5,"dir":"DOWN"},{"row":8,"col":11,"dir":"RIGHT"},{"row":8,"col":16,"dir":"LEFT"}],"ball":{"row":8,"col":3}};
var gblockIntro = {"numRows":15,"numCols":21,"blocks":[{"row":0,"col":9},{"row":0,"col":10},{"row":0,"col":11},{"row":6,"col":1},{"row":6,"col":20},{"row":7,"col":1},{"row":7,"col":20},{"row":8,"col":1},{"row":8,"col":20},{"row":14,"col":9},{"row":14,"col":10},{"row":14,"col":11}],"goals":[{"row":0,"col":16,"dir":"DOWN"},{"row":3,"col":0,"dir":"RIGHT"},{"row":11,"col":20,"dir":"LEFT"},{"row":14,"col":4,"dir":"UP"}],"ice":[],"arrows":[],"gblocks":[{"row":3,"col":10,"groupNum":0},{"row":4,"col":9,"groupNum":0},{"row":4,"col":10,"groupNum":0},{"row":4,"col":11,"groupNum":0},{"row":5,"col":10,"groupNum":0},{"row":6,"col":7,"groupNum":3},{"row":6,"col":13,"groupNum":2},{"row":7,"col":6,"groupNum":3},{"row":7,"col":7,"groupNum":3},{"row":7,"col":8,"groupNum":3},{"row":7,"col":12,"groupNum":2},{"row":7,"col":13,"groupNum":2},{"row":7,"col":14,"groupNum":2},{"row":8,"col":7,"groupNum":3},{"row":8,"col":13,"groupNum":2},{"row":9,"col":10,"groupNum":1},{"row":10,"col":9,"groupNum":1},{"row":10,"col":10,"groupNum":1},{"row":10,"col":11,"groupNum":1},{"row":11,"col":10,"groupNum":1}],"ball":{"row":7,"col":10}};

levels = [starter, diagonal, iceIntro, pairTheIce, arrowIntro, arrowIceMachine, gblockIntro, ];

new Hundo({
    levels: levels,
    id: 1,
    viz: {
        playButton: false,
        levelSelect: true
    },
    maker: false
});<|endoftext|>