<commit_before>const fs = require('fs')
const path = require('path')
const electron = require('electron')

const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')

const ipcRenderer = electron.ipcRenderer

const instantIoRegex = /^(https:\/\/)?instant\.io\/#/

// Controls the torrent list: creating, adding, deleting, & manipulating torrents
module.exports = class TorrentListController {
  constructor (state) {
    this.state = state
  }

  // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
  // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
  addTorrent (torrentId) {
    if (torrentId.path) {
      // Use path string instead of W3C File object
      torrentId = torrentId.path
    }

    // Allow a instant.io link to be pasted
    if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
      torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
    }

    var torrentKey = this.state.nextTorrentKey++
    var path = this.state.saved.prefs.downloadPath

    ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)

    dispatch('backToList')
  }

  // Shows the Create Torrent page with options to seed a given file or folder
  showCreateTorrent (files) {
    // You can only create torrents from the home screen.
    if (this.state.location.url() !== 'home') {
      return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
    }

    // Files will either be an array of file objects, which we can send directly
    // to the create-torrent screen
    if (files.length === 0 || typeof files[0] !== 'string') {
      this.state.location.go({
        url: 'create-torrent',
        files: files,
        setup: (cb) => {
          this.state.window.title = 'Create New Torrent'
          cb(null)
        }
      })
      return
    }

    // ... or it will be an array of mixed file and folder paths. We have to walk
    // through all the folders and find the files
    findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
  }

  // Creates a new torrent and start seeeding
  createTorrent (options) {
    var state = this.state
    var torrentKey = state.nextTorrentKey++
    ipcRenderer.send('wt-create-torrent', torrentKey, options)
    state.location.cancel()
  }

  // Starts downloading and/or seeding a given torrentSummary.
  startTorrentingSummary (torrentKey) {
    var s = TorrentSummary.getByKey(this.state, torrentKey)
    if (!s) throw new Error('Missing key: ' + torrentKey)

    // New torrent: give it a path
    if (!s.path) {
      // Use Downloads folder by default
      s.path = this.state.saved.prefs.downloadPath
      return start()
    }

    // Existing torrent: check that the path is still there
    fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
      if (err) {
        s.error = 'path-missing'
        return
      }
      start()
    })

    function start () {
      ipcRenderer.send('wt-start-torrenting',
        s.torrentKey,
        TorrentSummary.getTorrentID(s),
        s.path,
        s.fileModtimes,
        s.selections)
    }
  }

  // TODO: use torrentKey, not infoHash
  toggleTorrent (infoHash) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    if (torrentSummary.status === 'paused') {
      torrentSummary.status = 'new'
      this.startTorrentingSummary(torrentSummary.torrentKey)
      sound.play('ENABLE')
    } else {
      torrentSummary.status = 'paused'
      ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
      sound.play('DISABLE')
    }
  }

  toggleTorrentFile (infoHash, index) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    torrentSummary.selections[index] = !torrentSummary.selections[index]

    // Let the WebTorrent process know to start or stop fetching that file
    ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
  }

  confirmDeleteTorrent (infoHash, deleteData) {
    this.state.modal = {
      id: 'remove-torrent-modal',
      infoHash,
      deleteData
    }
  }

  // TODO: use torrentKey, not infoHash
  deleteTorrent (infoHash, deleteData) {
    ipcRenderer.send('wt-stop-torrenting', infoHash)

    var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)

    if (index > -1) {
      var summary = this.state.saved.torrents[index]

      // remove torrent and poster file
      deleteFile(TorrentSummary.getTorrentPath(summary))
      deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?

      // optionally delete the torrent data
      if (deleteData) moveItemToTrash(summary)

      // remove torrent from saved list
      this.state.saved.torrents.splice(index, 1)
      State.saveThrottled(this.state)
    }

    this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
    sound.play('DELETE')
  }

  toggleSelectTorrent (infoHash) {
    if (this.state.selectedInfoHash === infoHash) {
      this.state.selectedInfoHash = null
    } else {
      this.state.selectedInfoHash = infoHash
    }
  }

  openTorrentContextMenu (infoHash) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    var menu = new electron.remote.Menu()

    menu.append(new electron.remote.MenuItem({
      label: 'Remove From List',
      click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Remove Data File',
      click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
    }))

    menu.append(new electron.remote.MenuItem({
      type: 'separator'
    }))

    if (torrentSummary.files) {
      menu.append(new electron.remote.MenuItem({
        label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
        click: () => showItemInFolder(torrentSummary)
      }))
      menu.append(new electron.remote.MenuItem({
        type: 'separator'
      }))
    }

    menu.append(new electron.remote.MenuItem({
      label: 'Copy Magnet Link to Clipboard',
      click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Copy Instant.io Link to Clipboard',
      click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Save Torrent File As...',
      click: () => saveTorrentFileAs(torrentSummary)
    }))

    menu.popup(electron.remote.getCurrentWindow())
  }
}

// Recursively finds {name, path, size} for all files in a folder
// Calls `cb` on success, calls `onError` on failure
function findFilesRecursive (paths, cb) {
  if (paths.length > 1) {
    var numComplete = 0
    var ret = []
    paths.forEach(function (path) {
      findFilesRecursive([path], function (fileObjs) {
        ret = ret.concat(fileObjs)
        if (++numComplete === paths.length) {
          ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
          cb(ret)
        }
      })
    })
    return
  }

  var fileOrFolder = paths[0]
  fs.stat(fileOrFolder, function (err, stat) {
    if (err) return dispatch('error', err)

    // Files: return name, path, and size
    if (!stat.isDirectory()) {
      var filePath = fileOrFolder
      return cb([{
        name: path.basename(filePath),
        path: filePath,
        size: stat.size
      }])
    }

    // Folders: recurse, make a list of all the files
    var folderPath = fileOrFolder
    fs.readdir(folderPath, function (err, fileNames) {
      if (err) return dispatch('error', err)
      var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
      findFilesRecursive(paths, cb)
    })
  })
}

function deleteFile (path) {
  if (!path) return
  fs.unlink(path, function (err) {
    if (err) dispatch('error', err)
  })
}

// Delete all files in a torrent
function moveItemToTrash (torrentSummary) {
  var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
  if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
}

function showItemInFolder (torrentSummary) {
  ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}

function saveTorrentFileAs (torrentSummary) {
  var downloadPath = this.state.saved.prefs.downloadPath
  var newFileName = path.parse(torrentSummary.name).name + '.torrent'
  var opts = {
    title: 'Save Torrent File',
    defaultPath: path.join(downloadPath, newFileName),
    filters: [
      { name: 'Torrent Files', extensions: ['torrent'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  }
  electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
    if (!savePath) return // They clicked Cancel
    var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
    fs.readFile(torrentPath, function (err, torrentFile) {
      if (err) return dispatch('error', err)
      fs.writeFile(savePath, torrentFile, function (err) {
        if (err) return dispatch('error', err)
      })
    })
  })
}
<commit_msg>ensure that torrent file metadata is present (#869)<commit_after>const fs = require('fs')
const path = require('path')
const electron = require('electron')

const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')

const ipcRenderer = electron.ipcRenderer

const instantIoRegex = /^(https:\/\/)?instant\.io\/#/

// Controls the torrent list: creating, adding, deleting, & manipulating torrents
module.exports = class TorrentListController {
  constructor (state) {
    this.state = state
  }

  // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
  // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
  addTorrent (torrentId) {
    if (torrentId.path) {
      // Use path string instead of W3C File object
      torrentId = torrentId.path
    }

    // Allow a instant.io link to be pasted
    if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
      torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
    }

    var torrentKey = this.state.nextTorrentKey++
    var path = this.state.saved.prefs.downloadPath

    ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)

    dispatch('backToList')
  }

  // Shows the Create Torrent page with options to seed a given file or folder
  showCreateTorrent (files) {
    // You can only create torrents from the home screen.
    if (this.state.location.url() !== 'home') {
      return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
    }

    // Files will either be an array of file objects, which we can send directly
    // to the create-torrent screen
    if (files.length === 0 || typeof files[0] !== 'string') {
      this.state.location.go({
        url: 'create-torrent',
        files: files,
        setup: (cb) => {
          this.state.window.title = 'Create New Torrent'
          cb(null)
        }
      })
      return
    }

    // ... or it will be an array of mixed file and folder paths. We have to walk
    // through all the folders and find the files
    findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
  }

  // Creates a new torrent and start seeeding
  createTorrent (options) {
    var state = this.state
    var torrentKey = state.nextTorrentKey++
    ipcRenderer.send('wt-create-torrent', torrentKey, options)
    state.location.cancel()
  }

  // Starts downloading and/or seeding a given torrentSummary.
  startTorrentingSummary (torrentKey) {
    var s = TorrentSummary.getByKey(this.state, torrentKey)
    if (!s) throw new Error('Missing key: ' + torrentKey)

    // New torrent: give it a path
    if (!s.path) {
      // Use Downloads folder by default
      s.path = this.state.saved.prefs.downloadPath
      return start()
    }

    var fileOrFolder = TorrentSummary.getFileOrFolder(s)

    // New torrent: metadata not yet received
    if (!fileOrFolder) return start()

    // Existing torrent: check that the path is still there
    fs.stat(fileOrFolder, function (err) {
      if (err) {
        s.error = 'path-missing'
        return
      }
      start()
    })

    function start () {
      ipcRenderer.send('wt-start-torrenting',
        s.torrentKey,
        TorrentSummary.getTorrentID(s),
        s.path,
        s.fileModtimes,
        s.selections)
    }
  }

  // TODO: use torrentKey, not infoHash
  toggleTorrent (infoHash) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    if (torrentSummary.status === 'paused') {
      torrentSummary.status = 'new'
      this.startTorrentingSummary(torrentSummary.torrentKey)
      sound.play('ENABLE')
    } else {
      torrentSummary.status = 'paused'
      ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
      sound.play('DISABLE')
    }
  }

  toggleTorrentFile (infoHash, index) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    torrentSummary.selections[index] = !torrentSummary.selections[index]

    // Let the WebTorrent process know to start or stop fetching that file
    ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
  }

  confirmDeleteTorrent (infoHash, deleteData) {
    this.state.modal = {
      id: 'remove-torrent-modal',
      infoHash,
      deleteData
    }
  }

  // TODO: use torrentKey, not infoHash
  deleteTorrent (infoHash, deleteData) {
    ipcRenderer.send('wt-stop-torrenting', infoHash)

    var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)

    if (index > -1) {
      var summary = this.state.saved.torrents[index]

      // remove torrent and poster file
      deleteFile(TorrentSummary.getTorrentPath(summary))
      deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?

      // optionally delete the torrent data
      if (deleteData) moveItemToTrash(summary)

      // remove torrent from saved list
      this.state.saved.torrents.splice(index, 1)
      State.saveThrottled(this.state)
    }

    this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
    sound.play('DELETE')
  }

  toggleSelectTorrent (infoHash) {
    if (this.state.selectedInfoHash === infoHash) {
      this.state.selectedInfoHash = null
    } else {
      this.state.selectedInfoHash = infoHash
    }
  }

  openTorrentContextMenu (infoHash) {
    var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
    var menu = new electron.remote.Menu()

    menu.append(new electron.remote.MenuItem({
      label: 'Remove From List',
      click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Remove Data File',
      click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
    }))

    menu.append(new electron.remote.MenuItem({
      type: 'separator'
    }))

    if (torrentSummary.files) {
      menu.append(new electron.remote.MenuItem({
        label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
        click: () => showItemInFolder(torrentSummary)
      }))
      menu.append(new electron.remote.MenuItem({
        type: 'separator'
      }))
    }

    menu.append(new electron.remote.MenuItem({
      label: 'Copy Magnet Link to Clipboard',
      click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Copy Instant.io Link to Clipboard',
      click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
    }))

    menu.append(new electron.remote.MenuItem({
      label: 'Save Torrent File As...',
      click: () => saveTorrentFileAs(torrentSummary)
    }))

    menu.popup(electron.remote.getCurrentWindow())
  }
}

// Recursively finds {name, path, size} for all files in a folder
// Calls `cb` on success, calls `onError` on failure
function findFilesRecursive (paths, cb) {
  if (paths.length > 1) {
    var numComplete = 0
    var ret = []
    paths.forEach(function (path) {
      findFilesRecursive([path], function (fileObjs) {
        ret = ret.concat(fileObjs)
        if (++numComplete === paths.length) {
          ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
          cb(ret)
        }
      })
    })
    return
  }

  var fileOrFolder = paths[0]
  fs.stat(fileOrFolder, function (err, stat) {
    if (err) return dispatch('error', err)

    // Files: return name, path, and size
    if (!stat.isDirectory()) {
      var filePath = fileOrFolder
      return cb([{
        name: path.basename(filePath),
        path: filePath,
        size: stat.size
      }])
    }

    // Folders: recurse, make a list of all the files
    var folderPath = fileOrFolder
    fs.readdir(folderPath, function (err, fileNames) {
      if (err) return dispatch('error', err)
      var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
      findFilesRecursive(paths, cb)
    })
  })
}

function deleteFile (path) {
  if (!path) return
  fs.unlink(path, function (err) {
    if (err) dispatch('error', err)
  })
}

// Delete all files in a torrent
function moveItemToTrash (torrentSummary) {
  var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
  if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
}

function showItemInFolder (torrentSummary) {
  ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}

function saveTorrentFileAs (torrentSummary) {
  var downloadPath = this.state.saved.prefs.downloadPath
  var newFileName = path.parse(torrentSummary.name).name + '.torrent'
  var opts = {
    title: 'Save Torrent File',
    defaultPath: path.join(downloadPath, newFileName),
    filters: [
      { name: 'Torrent Files', extensions: ['torrent'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  }
  electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
    if (!savePath) return // They clicked Cancel
    var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
    fs.readFile(torrentPath, function (err, torrentFile) {
      if (err) return dispatch('error', err)
      fs.writeFile(savePath, torrentFile, function (err) {
        if (err) return dispatch('error', err)
      })
    })
  })
}
<|endoftext|>


<commit_before>import config from 'travis/config/environment';
import ActiveModelAdapter from 'active-model-adapter';
import { service } from 'ember-decorators/service';

export default ActiveModelAdapter.extend({
  @service auth: null,

  host: config.apiEndpoint,
  coalesceFindRequests: true,

  // Before Ember Data 2.0 the default behaviour of running `findAll` was to get
  // new records only when there're no records in the store. This will change
  // to a different strategy in 2.0: when you run `findAll` it will not get any
  // new data initially, but it will try loading new data in the background.
  //
  // I'm disabling the new behaviour for now.
  shouldBackgroundReloadRecord() {
    return false;
  },

  ajaxOptions() {
    let hash = this._super(...arguments);
    hash.headers = hash.headers || {};
    hash.headers['accept'] = 'application/json; version=2';

    let token = this.get('auth').token();
    if (token) {
      if (!hash.headers['Authorization']) {
        hash.headers['Authorization'] = `token ${token}`;
      }
    }

    if (window.localStorage['apiTrace']) {
      hash.headers['Trace'] = 'true';
    }

    return hash;
  },

  findMany(store, type, ids) {
    return this.ajax(this.buildURL(type.modelName), 'GET', {
      data: {
        ids: ids
      }
    });
  },

  handleResponse(status, headers, payload, request) {
    if (status > 299) {
      if (this.get('features.debugLogging')) {
        // eslint-disable-next-line
        console.log("[ERROR] API responded with an error (" + status + "): " + (JSON.stringify(payload)));
      }
    }

    if (window.localStorage['apiTrace']) {
      console.log(`${request.method} ${request.url} ${status} ${headers['x-request-id']}`);
    }

    return this._super(...arguments);
  },
});
<commit_msg>add console.log exception for eslint<commit_after>import config from 'travis/config/environment';
import ActiveModelAdapter from 'active-model-adapter';
import { service } from 'ember-decorators/service';

export default ActiveModelAdapter.extend({
  @service auth: null,

  host: config.apiEndpoint,
  coalesceFindRequests: true,

  // Before Ember Data 2.0 the default behaviour of running `findAll` was to get
  // new records only when there're no records in the store. This will change
  // to a different strategy in 2.0: when you run `findAll` it will not get any
  // new data initially, but it will try loading new data in the background.
  //
  // I'm disabling the new behaviour for now.
  shouldBackgroundReloadRecord() {
    return false;
  },

  ajaxOptions() {
    let hash = this._super(...arguments);
    hash.headers = hash.headers || {};
    hash.headers['accept'] = 'application/json; version=2';

    let token = this.get('auth').token();
    if (token) {
      if (!hash.headers['Authorization']) {
        hash.headers['Authorization'] = `token ${token}`;
      }
    }

    if (window.localStorage['apiTrace']) {
      hash.headers['Trace'] = 'true';
    }

    return hash;
  },

  findMany(store, type, ids) {
    return this.ajax(this.buildURL(type.modelName), 'GET', {
      data: {
        ids: ids
      }
    });
  },

  handleResponse(status, headers, payload, request) {
    if (status > 299) {
      if (this.get('features.debugLogging')) {
        // eslint-disable-next-line
        console.log("[ERROR] API responded with an error (" + status + "): " + (JSON.stringify(payload)));
      }
    }

    if (window.localStorage['apiTrace']) {
      // eslint-disable-next-line
      console.log(`${request.method} ${request.url} ${status} ${headers['x-request-id']}`);
    }

    return this._super(...arguments);
  },
});
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
import LoadMore from './load_more';
import LoadPending from './load_pending';
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
import LoadingIndicator from './loading_indicator';
import { connect } from 'react-redux';

const MOUSE_IDLE_DELAY = 300;

const mapStateToProps = (state, { scrollKey }) => {
  return {
    preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
  };
};

export default @connect(mapStateToProps)
class ScrollableList extends PureComponent {

  static contextTypes = {
    router: PropTypes.object,
  };

  static propTypes = {
    scrollKey: PropTypes.string.isRequired,
    onLoadMore: PropTypes.func,
    onLoadPending: PropTypes.func,
    onScrollToTop: PropTypes.func,
    onScroll: PropTypes.func,
    trackScroll: PropTypes.bool,
    shouldUpdateScroll: PropTypes.func,
    isLoading: PropTypes.bool,
    showLoading: PropTypes.bool,
    hasMore: PropTypes.bool,
    numPending: PropTypes.number,
    prepend: PropTypes.node,
    append: PropTypes.node,
    alwaysPrepend: PropTypes.bool,
    emptyMessage: PropTypes.node,
    children: PropTypes.node,
    bindToDocument: PropTypes.bool,
    preventScroll: PropTypes.bool,
  };

  static defaultProps = {
    trackScroll: true,
  };

  state = {
    fullscreen: null,
    cachedMediaWidth: 300,
  };

  intersectionObserverWrapper = new IntersectionObserverWrapper();

  handleScroll = throttle(() => {
    if (this.node) {
      const scrollTop = this.getScrollTop();
      const scrollHeight = this.getScrollHeight();
      const clientHeight = this.getClientHeight();
      const offset = scrollHeight - scrollTop - clientHeight;

      if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
        this.props.onLoadMore();
      }

      if (scrollTop < 100 && this.props.onScrollToTop) {
        this.props.onScrollToTop();
      } else if (this.props.onScroll) {
        this.props.onScroll();
      }

      if (!this.lastScrollWasSynthetic) {
        // If the last scroll wasn't caused by setScrollTop(), assume it was
        // intentional and cancel any pending scroll reset on mouse idle
        this.scrollToTopOnMouseIdle = false;
      }
      this.lastScrollWasSynthetic = false;
    }
  }, 150, {
    trailing: true,
  });

  mouseIdleTimer = null;
  mouseMovedRecently = false;
  lastScrollWasSynthetic = false;
  scrollToTopOnMouseIdle = false;

  setScrollTop = newScrollTop => {
    if (this.getScrollTop() !== newScrollTop) {
      this.lastScrollWasSynthetic = true;

      if (this.props.bindToDocument) {
        document.scrollingElement.scrollTop = newScrollTop;
      } else {
        this.node.scrollTop = newScrollTop;
      }
    }
  };

  clearMouseIdleTimer = () => {
    if (this.mouseIdleTimer === null) {
      return;
    }
    clearTimeout(this.mouseIdleTimer);
    this.mouseIdleTimer = null;
  };

  handleMouseMove = throttle(() => {
    // As long as the mouse keeps moving, clear and restart the idle timer.
    this.clearMouseIdleTimer();
    this.mouseIdleTimer =
      setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);

    if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
      // Only set if we just started moving and are scrolled to the top.
      this.scrollToTopOnMouseIdle = true;
    }
    // Save setting this flag for last, so we can do the comparison above.
    this.mouseMovedRecently = true;
  }, MOUSE_IDLE_DELAY / 2);

  handleWheel = throttle(() => {
    this.scrollToTopOnMouseIdle = false;
  }, 150, {
    trailing: true,
  });

  handleMouseIdle = () => {
    if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
      this.setScrollTop(0);
    }
    this.mouseMovedRecently = false;
    this.scrollToTopOnMouseIdle = false;
  }

  componentDidMount () {
    this.attachScrollListener();
    this.attachIntersectionObserver();
    attachFullscreenListener(this.onFullScreenChange);

    // Handle initial scroll posiiton
    this.handleScroll();
  }

  getScrollPosition = () => {
    if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
      return { height: this.getScrollHeight(), top: this.getScrollTop() };
    } else {
      return null;
    }
  }

  getScrollTop = () => {
    return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
  }

  getScrollHeight = () => {
    return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
  }

  getClientHeight = () => {
    return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
  }

  updateScrollBottom = (snapshot) => {
    const newScrollTop = this.getScrollHeight() - snapshot;

    this.setScrollTop(newScrollTop);
  }

  cacheMediaWidth = (width) => {
    if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width });
  }

  getSnapshotBeforeUpdate (prevProps, prevState) {
    const someItemInserted = React.Children.count(prevProps.children) > 0 &&
      React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
      this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
    const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);

    if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
      return this.getScrollHeight() - this.getScrollTop();
    } else {
      return null;
    }
  }

  componentDidUpdate (prevProps, prevState, snapshot) {
    // Reset the scroll position when a new child comes in in order not to
    // jerk the scrollbar around if you're already scrolled down the page.
    if (snapshot !== null) {
      this.updateScrollBottom(snapshot);
    }
  }

  componentWillUnmount () {
    this.clearMouseIdleTimer();
    this.detachScrollListener();
    this.detachIntersectionObserver();
    detachFullscreenListener(this.onFullScreenChange);
  }

  onFullScreenChange = () => {
    this.setState({ fullscreen: isFullscreen() });
  }

  attachIntersectionObserver () {
    this.intersectionObserverWrapper.connect({
      root: this.node,
      rootMargin: '300% 0px',
    });
  }

  detachIntersectionObserver () {
    this.intersectionObserverWrapper.disconnect();
  }

  attachScrollListener () {
    if (this.props.bindToDocument) {
      document.addEventListener('scroll', this.handleScroll);
      document.addEventListener('wheel', this.handleWheel);
    } else {
      this.node.addEventListener('scroll', this.handleScroll);
      this.node.addEventListener('wheel', this.handleWheel);
    }
  }

  detachScrollListener () {
    if (this.props.bindToDocument) {
      document.removeEventListener('scroll', this.handleScroll);
      document.removeEventListener('wheel', this.handleWheel);
    } else {
      this.node.removeEventListener('scroll', this.handleScroll);
      this.node.removeEventListener('wheel', this.handleWheel);
    }
  }

  getFirstChildKey (props) {
    const { children } = props;
    let firstChild     = children;

    if (children instanceof ImmutableList) {
      firstChild = children.get(0);
    } else if (Array.isArray(children)) {
      firstChild = children[0];
    }

    return firstChild && firstChild.key;
  }

  setRef = (c) => {
    this.node = c;
  }

  handleLoadMore = e => {
    e.preventDefault();
    this.props.onLoadMore();
  }

  defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
    if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false;
    return !(location.state && location.state.mastodonModalOpen);
  }

  handleLoadPending = e => {
    e.preventDefault();
    this.props.onLoadPending();
    // Prevent the weird scroll-jumping behavior, as we explicitly don't want to
    // scroll to top, and we know the scroll height is going to change
    this.scrollToTopOnMouseIdle = false;
    this.lastScrollWasSynthetic = false;
    this.clearMouseIdleTimer();
    this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
    this.mouseMovedRecently = true;
  }

  render () {
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
    const { fullscreen } = this.state;
    const childrenCount = React.Children.count(children);

    const loadMore     = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
    const loadPending  = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
    let scrollableArea = null;

    if (showLoading) {
      scrollableArea = (

            {prepend}

            <LoadingIndicator />

      );
    } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
      scrollableArea = (

            {prepend}

            {loadPending}

            {React.Children.map(this.props.children, (child, index) => (
              <IntersectionObserverArticleContainer
                key={child.key}
                id={child.key}
                index={index}
                listLength={childrenCount}
                intersectionObserverWrapper={this.intersectionObserverWrapper}
                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
              >
                {React.cloneElement(child, {
                  getScrollPosition: this.getScrollPosition,
                  updateScrollBottom: this.updateScrollBottom,
                  cachedMediaWidth: this.state.cachedMediaWidth,
                  cacheMediaWidth: this.cacheMediaWidth,
                })}
              </IntersectionObserverArticleContainer>
            ))}

            {loadMore}

            {!hasMore && append}

      );
    } else {
      scrollableArea = (

          {alwaysPrepend && prepend}

            {emptyMessage}

      );
    }

    if (trackScroll) {
      return (
        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}>
          {scrollableArea}
        </ScrollContainer>
      );
    } else {
      return scrollableArea;
    }
  }

}
<commit_msg>[Glitch] Fix keyboard navigation in scrollable lists<commit_after>import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
import LoadMore from './load_more';
import LoadPending from './load_pending';
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
import LoadingIndicator from './loading_indicator';
import { connect } from 'react-redux';

const MOUSE_IDLE_DELAY = 300;

const mapStateToProps = (state, { scrollKey }) => {
  return {
    preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
  };
};

export default @connect(mapStateToProps, null, null, { forwardRef: true })
class ScrollableList extends PureComponent {

  static contextTypes = {
    router: PropTypes.object,
  };

  static propTypes = {
    scrollKey: PropTypes.string.isRequired,
    onLoadMore: PropTypes.func,
    onLoadPending: PropTypes.func,
    onScrollToTop: PropTypes.func,
    onScroll: PropTypes.func,
    trackScroll: PropTypes.bool,
    shouldUpdateScroll: PropTypes.func,
    isLoading: PropTypes.bool,
    showLoading: PropTypes.bool,
    hasMore: PropTypes.bool,
    numPending: PropTypes.number,
    prepend: PropTypes.node,
    append: PropTypes.node,
    alwaysPrepend: PropTypes.bool,
    emptyMessage: PropTypes.node,
    children: PropTypes.node,
    bindToDocument: PropTypes.bool,
    preventScroll: PropTypes.bool,
  };

  static defaultProps = {
    trackScroll: true,
  };

  state = {
    fullscreen: null,
    cachedMediaWidth: 300,
  };

  intersectionObserverWrapper = new IntersectionObserverWrapper();

  handleScroll = throttle(() => {
    if (this.node) {
      const scrollTop = this.getScrollTop();
      const scrollHeight = this.getScrollHeight();
      const clientHeight = this.getClientHeight();
      const offset = scrollHeight - scrollTop - clientHeight;

      if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
        this.props.onLoadMore();
      }

      if (scrollTop < 100 && this.props.onScrollToTop) {
        this.props.onScrollToTop();
      } else if (this.props.onScroll) {
        this.props.onScroll();
      }

      if (!this.lastScrollWasSynthetic) {
        // If the last scroll wasn't caused by setScrollTop(), assume it was
        // intentional and cancel any pending scroll reset on mouse idle
        this.scrollToTopOnMouseIdle = false;
      }
      this.lastScrollWasSynthetic = false;
    }
  }, 150, {
    trailing: true,
  });

  mouseIdleTimer = null;
  mouseMovedRecently = false;
  lastScrollWasSynthetic = false;
  scrollToTopOnMouseIdle = false;

  setScrollTop = newScrollTop => {
    if (this.getScrollTop() !== newScrollTop) {
      this.lastScrollWasSynthetic = true;

      if (this.props.bindToDocument) {
        document.scrollingElement.scrollTop = newScrollTop;
      } else {
        this.node.scrollTop = newScrollTop;
      }
    }
  };

  clearMouseIdleTimer = () => {
    if (this.mouseIdleTimer === null) {
      return;
    }
    clearTimeout(this.mouseIdleTimer);
    this.mouseIdleTimer = null;
  };

  handleMouseMove = throttle(() => {
    // As long as the mouse keeps moving, clear and restart the idle timer.
    this.clearMouseIdleTimer();
    this.mouseIdleTimer =
      setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);

    if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
      // Only set if we just started moving and are scrolled to the top.
      this.scrollToTopOnMouseIdle = true;
    }
    // Save setting this flag for last, so we can do the comparison above.
    this.mouseMovedRecently = true;
  }, MOUSE_IDLE_DELAY / 2);

  handleWheel = throttle(() => {
    this.scrollToTopOnMouseIdle = false;
  }, 150, {
    trailing: true,
  });

  handleMouseIdle = () => {
    if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
      this.setScrollTop(0);
    }
    this.mouseMovedRecently = false;
    this.scrollToTopOnMouseIdle = false;
  }

  componentDidMount () {
    this.attachScrollListener();
    this.attachIntersectionObserver();
    attachFullscreenListener(this.onFullScreenChange);

    // Handle initial scroll posiiton
    this.handleScroll();
  }

  getScrollPosition = () => {
    if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
      return { height: this.getScrollHeight(), top: this.getScrollTop() };
    } else {
      return null;
    }
  }

  getScrollTop = () => {
    return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
  }

  getScrollHeight = () => {
    return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
  }

  getClientHeight = () => {
    return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
  }

  updateScrollBottom = (snapshot) => {
    const newScrollTop = this.getScrollHeight() - snapshot;

    this.setScrollTop(newScrollTop);
  }

  cacheMediaWidth = (width) => {
    if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width });
  }

  getSnapshotBeforeUpdate (prevProps, prevState) {
    const someItemInserted = React.Children.count(prevProps.children) > 0 &&
      React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
      this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
    const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);

    if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
      return this.getScrollHeight() - this.getScrollTop();
    } else {
      return null;
    }
  }

  componentDidUpdate (prevProps, prevState, snapshot) {
    // Reset the scroll position when a new child comes in in order not to
    // jerk the scrollbar around if you're already scrolled down the page.
    if (snapshot !== null) {
      this.updateScrollBottom(snapshot);
    }
  }

  componentWillUnmount () {
    this.clearMouseIdleTimer();
    this.detachScrollListener();
    this.detachIntersectionObserver();
    detachFullscreenListener(this.onFullScreenChange);
  }

  onFullScreenChange = () => {
    this.setState({ fullscreen: isFullscreen() });
  }

  attachIntersectionObserver () {
    this.intersectionObserverWrapper.connect({
      root: this.node,
      rootMargin: '300% 0px',
    });
  }

  detachIntersectionObserver () {
    this.intersectionObserverWrapper.disconnect();
  }

  attachScrollListener () {
    if (this.props.bindToDocument) {
      document.addEventListener('scroll', this.handleScroll);
      document.addEventListener('wheel', this.handleWheel);
    } else {
      this.node.addEventListener('scroll', this.handleScroll);
      this.node.addEventListener('wheel', this.handleWheel);
    }
  }

  detachScrollListener () {
    if (this.props.bindToDocument) {
      document.removeEventListener('scroll', this.handleScroll);
      document.removeEventListener('wheel', this.handleWheel);
    } else {
      this.node.removeEventListener('scroll', this.handleScroll);
      this.node.removeEventListener('wheel', this.handleWheel);
    }
  }

  getFirstChildKey (props) {
    const { children } = props;
    let firstChild     = children;

    if (children instanceof ImmutableList) {
      firstChild = children.get(0);
    } else if (Array.isArray(children)) {
      firstChild = children[0];
    }

    return firstChild && firstChild.key;
  }

  setRef = (c) => {
    this.node = c;
  }

  handleLoadMore = e => {
    e.preventDefault();
    this.props.onLoadMore();
  }

  defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
    if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false;
    return !(location.state && location.state.mastodonModalOpen);
  }

  handleLoadPending = e => {
    e.preventDefault();
    this.props.onLoadPending();
    // Prevent the weird scroll-jumping behavior, as we explicitly don't want to
    // scroll to top, and we know the scroll height is going to change
    this.scrollToTopOnMouseIdle = false;
    this.lastScrollWasSynthetic = false;
    this.clearMouseIdleTimer();
    this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
    this.mouseMovedRecently = true;
  }

  render () {
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
    const { fullscreen } = this.state;
    const childrenCount = React.Children.count(children);

    const loadMore     = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
    const loadPending  = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
    let scrollableArea = null;

    if (showLoading) {
      scrollableArea = (

            {prepend}

            <LoadingIndicator />

      );
    } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
      scrollableArea = (

            {prepend}

            {loadPending}

            {React.Children.map(this.props.children, (child, index) => (
              <IntersectionObserverArticleContainer
                key={child.key}
                id={child.key}
                index={index}
                listLength={childrenCount}
                intersectionObserverWrapper={this.intersectionObserverWrapper}
                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
              >
                {React.cloneElement(child, {
                  getScrollPosition: this.getScrollPosition,
                  updateScrollBottom: this.updateScrollBottom,
                  cachedMediaWidth: this.state.cachedMediaWidth,
                  cacheMediaWidth: this.cacheMediaWidth,
                })}
              </IntersectionObserverArticleContainer>
            ))}

            {loadMore}

            {!hasMore && append}

      );
    } else {
      scrollableArea = (

          {alwaysPrepend && prepend}

            {emptyMessage}

      );
    }

    if (trackScroll) {
      return (
        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}>
          {scrollableArea}
        </ScrollContainer>
      );
    } else {
      return scrollableArea;
    }
  }

}
<|endoftext|>


<commit_before>Games = new Mongo.Collection("Games");

if (Meteor.isServer) {

  Meteor.startup(function() {

    return Meteor.methods({
      init: function() {
		Games.remove({});
		Games.insert({Turn:{isPlayerOne: true, round:1}, Players:[], Gifs:[]})
      },

      changeTurn: function() {
        var turn = Games.findOne({}).Turn;
		var id = Games.findOne({})._id;
		Games.update(id, {$set:{"Turn.isPlayerOne": !turn.isPlayerOne}});
      },

      openVoting: function() {
		var game = Games.findOne({});
		var id = game._id;
		Games.update(id, {$set:{"Turn.isVotingOpen": true}});

        Meteor.setTimeout(function(){
          Games.update(id, {$set: {"Turn.isVotingOpen": false, "Turn.isPlayerOne": true, "Turn.round": game.Turn.round + 1}});

        },15000);
      },

      voteOne: function() {
        Games.findOne({}).Players.update(
            { number: 1 },
            { $inc: { votes: 1} }
        )
      },

      voteTwo: function() {
        Games.findOne({}).Players.update(
            { number: 2 },
            { $inc: { votes: 1} }
        )
      }
    });
  });
}

if (Meteor.isClient) {
  // This code only runs on the client
  Template.body.helpers({
    players: function () {
      return Games.findOne({}).Players;
    },
    currentGifs: function() {
		var game = Games.findOne({});
		if(game === undefined) return [];
      var currentRound = game.Turn.round;

		var gifs = [];
		for(i = 0; i < game.Gifs.length; i++){
			if(game.Gifs[i].round === game.Turn.round)
			{
				gifs.push(game.Gifs[i]);
			}
		}

      return gifs;
    }
  });

  Template.body.events({
    "click #clearPlayers": function () {
      Meteor.call('init');
    }
  });

  Template.joinForm.helpers({
    gameOver: function() {
	if(Games.findOne({}) === undefined) return false;

      return Games.findOne({}).Turn.round == 4; //3 rounds
    },

    noPlayers: function() {
      if(Games.findOne({}) === undefined || Games.findOne({}).Players.length == 0)
      {
        Session.set("playerOne", false);
        Session.set("playerTwo", false);
        Session.set("voted", false);
        return true;
      }

      return false;
    },

    hasntVoted: function() {
      return Session.equals("voted", false);
    },

    onePlayer: function() {
		return Games.findOne({}) !== undefined && Games.findOne({}).Players.length == 1;
    },

    twoPlayers: function() {
      return Games.findOne({}) !== undefined && Games.findOne({}).Players.length == 2;
    },

    playerOne: function() {
      return Games.findOne({}).Players[0].name;
    },

    playerTwo: function() {
      return Games.findOne({}).Players[1].name;
    },

    imPlayerOne: function() {
      return Session.equals("playerOne", true);
    },

    imPlayerTwo: function() {
      return Session.equals("playerTwo", true);
    },

    notAPlayer: function() {
      return !(Session.get("playerOne") || Session.get("playerTwo"));
    },

    myTurn: function() {
      return ((Session.get("playerOne") &&  Games.findOne({}).Turn.isPlayerOne) ||
      (Session.get("playerTwo") &&  !Games.findOne({}).Turn.isPlayerOne));
    },

    currentRound: function() {
      return Games.findOne({}).Turn.round;
    },

    finalRound: function() {
      return currentRound() === 3;
    },

    playerOneVotes: function() {
      return Games.findOne({}).Players[0].votes
    },

    playerTwoVotes: function() {
      return Games.findOne({}).Players[1].votes
    },

    votingOpen: function() {
	var game = Games.findOne({});
      var open = game.Turn.isVotingOpen;

      if(!open)
      {
        Session.set("voted", false);
      }
      return open;
    },

    winnerName: function() {
      var p1 = Games.findOne({}).Players[0];
      var p2 = Games.findOne({}).Players[1];

      if(p1.votes > p2.votes)
      {
        return p1.name;
      }

      if(p2.votes > p1.votes)
      {
        return p2.name;
      }

      return "NO ONE";
    }
  });

  Template.joinForm.events({

    "click #newGame": function (event) {
      event.preventDefault();
      Meteor.call('init');
    },

    "submit .player-one": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
	var id = Games.findOne({})._id;
      Games.update(id, {$push:{Players:{name: event.target.text.value, votes: 0}}});
      Session.set("playerOne", true);
    },

    "submit .player-two": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
		var id = Games.findOne({})._id;
		Games.update(id, {$push:{Players:{name: event.target.text.value, votes: 0}}});
      Session.set("playerTwo", true);
    },

    "submit .add-gif": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
		var url = event.target.text.value;
		var gifRe = /^http[s]?:\/\/.+\.gif$|http:\/\/[^.]+.gfycat.com\/.*/g;
		if(url === "") return;
		//if(url.match(gifRe).length === 0) return;

      var currentTurn = Games.findOne({}).Turn;
      var currentPlayer = currentTurn.isPlayerOne ? 0 : 1;
      var playerName = Games.findOne({}).Players[currentPlayer].name;
		var id = Games.findOne({})._id;
      Games.update(id, {$push:{Gifs:{user: playerName, href: url, round: currentTurn.round}}});
      if(currentPlayer == 1)
      {
        Meteor.call('openVoting');
        Session.Set("voted", false);
      }

      Meteor.call('changeTurn');
    },

    "click #voteOne": function (event) {
      event.preventDefault();
      Session.set("voted",true);
      Meteor.call('voteOne');
    },

    "click #voteTwo": function (event) {
      event.preventDefault();
      Session.set("voted",true);
      Meteor.call('voteTwo');
    }
  });

  Template.gif.helpers({
    gifId: function()
    {
      return this.round + "gif" + this.user;
    },

    imgurGifV: function() {
      if(this.href.substring(this.href.length-5) == ".gifv")
      {
        return true;
      }
      return false;
    },

    imgurGifVRoot: function() {
      return this.href.substring(0, this.href.length-5);
    },

    gfycat: function () {
      if (this.href.indexOf("gfycat") !== -1) {

        var last = this.href.lastIndexOf("/");

        if(last == this.href.length)
        {
          this.href = this.href.substring(0, this.href.length - 1);
          last = this.href.lastIndexOf("/");
        }
        this.gfylink = this.href.substring(last+1);

        return true;
      }

      return false;
    },

    finish: function() {
      var gfy = document.getElementById(this.round + "gif" + this.user);
		if(gfy === null) return;
      var a = new gfyObject(gfy);
      a.init();
    }
  });
}
<commit_msg>Added multiple game storage<commit_after>Games = new Mongo.Collection("Games");

if (Meteor.isServer) {

  Meteor.startup(function() {

    return Meteor.methods({
      init: function() {
		//Games.remove({});
		Games.insert({Created: new Date(), Turn:{isPlayerOne: true, round:1}, Players:[], Gifs:[]})
      },

      changeTurn: function() {
        var turn = Games.findOne({},{sort:{Created:-1}}).Turn;
		var id = Games.findOne({},{sort:{Created:-1}})._id;
		Games.update(id, {$set:{"Turn.isPlayerOne": !turn.isPlayerOne}});
      },

      openVoting: function() {
		var game = Games.findOne({},{sort:{Created:-1}});
		var id = game._id;
		Games.update(id, {$set:{"Turn.isVotingOpen": true}});

        Meteor.setTimeout(function(){
          Games.update(id, {$set: {"Turn.isVotingOpen": false, "Turn.isPlayerOne": true, "Turn.round": game.Turn.round + 1}});

        },15000);
      },

      voteOne: function() {
        Games.findOne({},{sort:{Created:-1}}).Players.update(
            { number: 1 },
            { $inc: { votes: 1} }
        )
      },

      voteTwo: function() {
        Games.findOne({},{sort:{Created:-1}}).Players.update(
            { number: 2 },
            { $inc: { votes: 1} }
        )
      }
    });
  });
}

if (Meteor.isClient) {
  // This code only runs on the client
  Template.body.helpers({
    players: function () {
      return Games.findOne({},{sort:{Created:-1}}).Players;
    },
    currentGifs: function() {
		var game = Games.findOne({},{sort:{Created:-1}});
		if(game === undefined) return [];
      var currentRound = game.Turn.round;

		var gifs = [];
		for(i = 0; i < game.Gifs.length; i++){
			if(game.Gifs[i].round === game.Turn.round)
			{
				gifs.push(game.Gifs[i]);
			}
		}

      return gifs;
    }
  });

  Template.body.events({
    "click #clearPlayers": function () {
      Meteor.call('init');
    }
  });

  Template.joinForm.helpers({
    gameOver: function() {
	if(Games.findOne({},{sort:{Created:-1}}) === undefined) return false;

      return Games.findOne({},{sort:{Created:-1}}).Turn.round == 4; //3 rounds
    },

    noPlayers: function() {
      if(Games.findOne({},{sort:{Created:-1}}) === undefined || Games.findOne({},{sort:{Created:-1}}).Players.length == 0)
      {
        Session.set("playerOne", false);
        Session.set("playerTwo", false);
        Session.set("voted", false);
        return true;
      }

      return false;
    },

    hasntVoted: function() {
      return Session.equals("voted", false);
    },

    onePlayer: function() {
		return Games.findOne({},{sort:{Created:-1}}) !== undefined && Games.findOne({},{sort:{Created:-1}}).Players.length == 1;
    },

    twoPlayers: function() {
      return Games.findOne({},{sort:{Created:-1}}) !== undefined && Games.findOne({},{sort:{Created:-1}}).Players.length == 2;
    },

    playerOne: function() {
      return Games.findOne({},{sort:{Created:-1}}).Players[0].name;
    },

    playerTwo: function() {
      return Games.findOne({},{sort:{Created:-1}}).Players[1].name;
    },

    imPlayerOne: function() {
      return Session.equals("playerOne", true);
    },

    imPlayerTwo: function() {
      return Session.equals("playerTwo", true);
    },

    notAPlayer: function() {
      return !(Session.get("playerOne") || Session.get("playerTwo"));
    },

    myTurn: function() {
      return ((Session.get("playerOne") &&  Games.findOne({},{sort:{Created:-1}}).Turn.isPlayerOne) ||
      (Session.get("playerTwo") &&  !Games.findOne({},{sort:{Created:-1}}).Turn.isPlayerOne));
    },

    currentRound: function() {
      return Games.findOne({},{sort:{Created:-1}}).Turn.round;
    },

    finalRound: function() {
      return currentRound() === 3;
    },

    playerOneVotes: function() {
      return Games.findOne({},{sort:{Created:-1}}).Players[0].votes
    },

    playerTwoVotes: function() {
      return Games.findOne({},{sort:{Created:-1}}).Players[1].votes
    },

    votingOpen: function() {
	var game = Games.findOne({},{sort:{Created:-1}});
      var open = game.Turn.isVotingOpen;

      if(!open)
      {
        Session.set("voted", false);
      }
      return open;
    },

    winnerName: function() {
      var p1 = Games.findOne({},{sort:{Created:-1}}).Players[0];
      var p2 = Games.findOne({},{sort:{Created:-1}}).Players[1];

      if(p1.votes > p2.votes)
      {
        return p1.name;
      }

      if(p2.votes > p1.votes)
      {
        return p2.name;
      }

      return "NO ONE";
    }
  });

  Template.joinForm.events({

    "click #newGame": function (event) {
      event.preventDefault();
      Meteor.call('init');
    },

    "submit .player-one": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
	var id = Games.findOne({},{sort:{Created:-1}})._id;
      Games.update(id, {$push:{Players:{name: event.target.text.value, votes: 0}}});
      Session.set("playerOne", true);
    },

    "submit .player-two": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
		var id = Games.findOne({},{sort:{Created:-1}})._id;
		Games.update(id, {$push:{Players:{name: event.target.text.value, votes: 0}}});
      Session.set("playerTwo", true);
    },

    "submit .add-gif": function (event) {
      // Set the checked property to the opposite of its current value
      event.preventDefault();
		var url = event.target.text.value;
		var gifRe = /^http[s]?:\/\/.+\.gif$|http:\/\/[^.]+.gfycat.com\/.*/g;
		if(url === "") return;
		//if(url.match(gifRe).length === 0) return;

      var currentTurn = Games.findOne({},{sort:{Created:-1}}).Turn;
      var currentPlayer = currentTurn.isPlayerOne ? 0 : 1;
      var playerName = Games.findOne({},{sort:{Created:-1}}).Players[currentPlayer].name;
		var id = Games.findOne({},{sort:{Created:-1}})._id;
      Games.update(id, {$push:{Gifs:{user: playerName, href: url, round: currentTurn.round}}});
      if(currentPlayer == 1)
      {
        Meteor.call('openVoting');
        Session.Set("voted", false);
      }

      Meteor.call('changeTurn');
    },

    "click #voteOne": function (event) {
      event.preventDefault();
      Session.set("voted",true);
      Meteor.call('voteOne');
    },

    "click #voteTwo": function (event) {
      event.preventDefault();
      Session.set("voted",true);
      Meteor.call('voteTwo');
    }
  });

  Template.gif.helpers({
    gifId: function()
    {
      return this.round + "gif" + this.user;
    },

    imgurGifV: function() {
      if(this.href.substring(this.href.length-5) == ".gifv")
      {
        return true;
      }
      return false;
    },

    imgurGifVRoot: function() {
      return this.href.substring(0, this.href.length-5);
    },

    gfycat: function () {
      if (this.href.indexOf("gfycat") !== -1) {

        var last = this.href.lastIndexOf("/");

        if(last == this.href.length)
        {
          this.href = this.href.substring(0, this.href.length - 1);
          last = this.href.lastIndexOf("/");
        }
        this.gfylink = this.href.substring(last+1);

        return true;
      }

      return false;
    },

    finish: function() {
      var gfy = document.getElementById(this.round + "gif" + this.user);
		if(gfy === null) return;
      var a = new gfyObject(gfy);
      a.init();
    }
  });
}
<|endoftext|>


<commit_before>/*
 Yarno Boelens
 Soundboard index.js
 created on: 25 06 2017
 */

var audio = new Audio(),
    container = document.getElementById('soundItemContainer');

draw();

function draw() {
    files.forEach(function(file){
        var p = document.createElement("p"),
            text = document.createTextNode(file.title);
        p.appendChild(text);
        var div = document.createElement("div");
        div.appendChild(p);
        div.className += "soundItem " + file.author;
        div.onclick = function(){
            play(file.name);
        };
        container.appendChild(div);
    });
}

function play(file) {
    audio.pause()
    audio = new Audio('../audio/mp3/'+file+'.mp3');
    audio.play();
}

function sortByAuthor() {
    alert("sorted");
}
<commit_msg>[WEB] Made method for sorting by author with compare helper..<commit_after>/*
 Yarno Boelens
 Soundboard index.js
 created on: 25 06 2017
 */

var audio = new Audio(),
    container = document.getElementById('soundItemContainer');

draw();

function draw() {
    container.innerHTML = "";
    files.forEach(function(file){
        var p = document.createElement("p"),
            text = document.createTextNode(file.title);
        p.appendChild(text);
        var div = document.createElement("div");
        div.appendChild(p);
        div.className += "soundItem " + file.author;
        div.onclick = function(){
            play(file.name);
        };
        container.appendChild(div);
    });
}

function play(file) {
    audio.pause()
    audio = new Audio('../audio/mp3/'+file+'.mp3');
    audio.play();
}

// Sorting

var order = -1;

function fileCompare(a, b) {
    var returnVal = 0
    if(a.author < b.author) {
        returnVal = -1;
    } else if(a.author > b.author) {
        returnVal = 1;
    }
    return returnVal * order;
}

function sortByAuthor() {
    order = order * -1;
    files.sort(fileCompare);
    draw();
}
<|endoftext|>


<commit_before>/*
Author: James Cryer
Company: Huddle
Last updated date: 03 Mar 2014
URL: https://github.com/Huddle/PhantomCSS
More: http://tldr.huddle.com/blog/css-testing/
*/

var fs = require('fs');

var _src = '.'+fs.separator+'screenshots';
var _results; // for backwards compatibility results and src are the same - but you can change it!
var _failures = '.'+fs.separator+'failures';

var _count = 0;
var _realPath;
var _diffsToProcess = [];
var _libraryRoot = '.';
var exitStatus;
var _hideElements;
var _addLabelToFailedImage = true;
var _test_match;
var _test_exclude;
var _mismatchTolerance = 0.05;
var diffsCreated = [];

exports.screenshot = screenshot;
exports.compareAll = compareAll;
exports.compareMatched = compareMatched;
exports.compareExplicit = compareExplicit;
exports.compareSession = compareSession;
exports.compareFiles = compareFiles;
exports.waitForTests = waitForTests;
exports.init = init;
exports.update = update;
exports.turnOffAnimations = turnOffAnimations;
exports.getExitStatus = getExitStatus;
exports.getCreatedDiffFiles = getCreatedDiffFiles;

function update(options){

	function stripslash ( str ){
		return str.replace(/\/\//g,'/').replace(/\\/g,'\\');
	}

	options = options || {};

	casper = options.casper || casper;
	_libraryRoot = options.libraryRoot || _libraryRoot;

	_src = stripslash(options.screenshotRoot || _src);
	_results = stripslash(options.comparisonResultRoot || _results || _src);
	_failures = stripslash(options.failedComparisonsRoot || _failures);

	_fileNameGetter = options.fileNameGetter || _fileNameGetter;

	_onPass = options.onPass || _onPass;
	_onFail = options.onFail || _onFail;
	_onTimeout = options.onTimeout || _onTimeout;
	_onComplete = options.onComplete || options.report || _onComplete;

	_hideElements = options.hideElements;

	_mismatchTolerance = options.mismatchTolerance || _mismatchTolerance;

	if(options.addLabelToFailedImage !== undefined){
		_addLabelToFailedImage = options.addLabelToFailedImage;
	}
}

function init(options){
	update(options);
}

function turnOffAnimations(){
	console.log('[PhantomCSS] Turning off animations');
	casper.evaluate(function turnOffAnimations(){
		window.addEventListener('load', function(){

			var css = document.createElement("style");
			css.type = "text/css";
			css.innerHTML = "* { -webkit-transition: none !important; transition: none !important; }";
			document.body.appendChild(css);

			if(jQuery){
				jQuery.fx.off = true;
			}
		},false);
	});
}

function _fileNameGetter(root, fileName){
	var name;

	fileName = fileName || "screenshot";
	name = root + fs.separator + fileName + "_" + _count++;

	if(fs.isFile(name+'.png')){
		return name+'.diff.png';
	} else {
		return name+'.png';
	}
}

function screenshot(selector, timeToWait, hideSelector, fileName){

	if(isNaN(Number(timeToWait)) && typeof timeToWait === 'string'){
		fileName = timeToWait;
		timeToWait = void 0;
	}

	casper.captureBase64('png'); // force pre-render
	casper.wait(timeToWait || 250, function(){

		var srcPath = _fileNameGetter(_src, fileName);
		var resultPath =  srcPath.replace(_src, _results);

		if(hideSelector || _hideElements){
			casper.evaluate(function(s1, s2){
				if(s1){
					$(s1).css('visibility', 'hidden');
				}
				$(s2).css('visibility', 'hidden');
			}, {
				s1: _hideElements,
				s2: hideSelector
			});
		}

		capture(srcPath, resultPath, selector);

	}); // give a bit of time for all the images appear
}

function capture(srcPath, resultPath, selector){
	var originalForResult = resultPath.replace('.diff', '');
	var originalFromSource = srcPath.replace('.diff', '');

	try {

		if( isThisImageADiff(resultPath) ){

			casper.captureSelector( resultPath , selector );

			diffsCreated.push(resultPath);

			if(srcPath !== resultPath){
				// also copy the original over to the result directory
				copyAndReplaceFile(originalFromSource, originalForResult);
			}

		} else {

			casper.captureSelector( srcPath , selector );

			if(srcPath !== resultPath){
				copyAndReplaceFile(srcPath, resultPath);
			}
		}

	}
	catch(ex){
		console.log("[PhantomCSS] Screenshot capture failed: ", ex.message);
	}
}

function isThisImageADiff(path){
	return /\.diff\.png/.test(path);
}

function copyAndReplaceFile(src, dest){
	if(fs.isFile(dest)){
		fs.remove(dest);
	}
	fs.copy(src, dest);
}

function asyncCompare(one, two, func){

	if(!casper.evaluate(function(){ return window._imagediff_;})){
		initClient();
	}

	casper.fill('form#image-diff', {
		'one': one,
		'two': two
	});

	casper.evaluate(function(filename){
		window._imagediff_.run( filename );
	}, {
		label: _addLabelToFailedImage ? one : false
	});

	casper.waitFor(
		function check() {
			return this.evaluate(function(){
				return window._imagediff_.hasResult;
			});
		},
		function () {

			var mismatch = casper.evaluate(function(){
				return window._imagediff_.getResult();
			});

			if(Number(mismatch)){
				func(false, mismatch);
			} else {
				func(true);
			}

		}, function(){
			func(false);
		},
		10000
	);
}

function getDiffs (path){

	var filePath;

	if(({'..':1,'.':1})[path]){ return true; }

	if(_realPath){
		_realPath += fs.separator + path;
	} else {
		_realPath = path;
	}

	filePath = _realPath;

	if(fs.isDirectory(_realPath) ){
		fs.list(_realPath).forEach(getDiffs);
	} else {
		if( /\.diff\./.test(path.toLowerCase()) ){
			if(_test_match){
				if( _test_match.test(_realPath.toLowerCase()) ){
					if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
						console.log('[PhantomCSS] Analysing', _realPath);
						_diffsToProcess.push(filePath);
					}
				}
			} else {
				if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
					_diffsToProcess.push(filePath);
				}
			}
		}
	}

	_realPath = _realPath.replace(fs.separator + path, '');
}

function getCreatedDiffFiles(){
	var d = diffsCreated;
	diffsCreated = [];
	return d;
}

function compareMatched(match, exclude){
	// Search for diff images, but only compare matched filenames
	_test_match = typeof match === 'string' ? new RegExp(match) : match;
	compareAll(exclude);
}

function compareExplicit(list){
	// An explicit list of diff images to compare ['/dialog.diff.png', '/header.diff.png']
	compareAll(void 0, list);
}

function compareSession(list){
	// compare the diffs created in this session
	compareAll(void 0, getCreatedDiffFiles() );
}

function compareFiles(baseFile, file) {
	var html = _libraryRoot+fs.separator+"ResembleJs"+fs.separator+"resemblejscontainer.html";
	var test = {
		filename: baseFile
	};

	if(!fs.isFile(baseFile)) {
		test.error = true;
	} else {

		if( !fs.isFile(html) ){
			console.log('[PhantomCSS] Can\'t find Resemble container. Perhaps the library root is mis configured. ('+html+')');
			test.error = true;
			return;
		}

		casper.thenOpen ( html , function (){

			asyncCompare(baseFile, file, function(isSame, mismatch){

				if(!isSame){

					test.fail = true;

					casper.waitFor(
						function check() {
							return casper.evaluate(function(){
								return window._imagediff_.hasImage;
							});
						},
						function () {
							var failFile, safeFileName, increment;

							if(_failures){
								// flattened structure for failed diffs so that it is easier to preview
								failFile = _failures + fs.separator + file.split(fs.separator).pop().replace('.diff.png', '').replace('.png', '');
								safeFileName = failFile;
								increment = 0;

								while ( fs.isFile(safeFileName+'.fail.png') ){
									increment++;
									safeFileName = failFile+'.'+increment;
								}

								failFile = safeFileName + '.fail.png';

								casper.captureSelector(failFile, 'img');
								console.log('Failure! Saved to', failFile);
							}

							casper.evaluate(function(){
								window._imagediff_.hasImage = false;
							});

							if(mismatch){
								test.mismatch = mismatch;
								_onFail(test); // casper.test.fail throws and error, this function call is aborted
								return;  // Just to make it clear what is happening
							} else {
								_onTimeout(test);
							}

						}, function(){},
						10000
					);
				} else {
                    test.success = true;
					_onPass(test);
				}

			});
		});
	}
    return test;
}

function compareAll(exclude, list){
	var tests = [];

	_test_exclude = typeof exclude === 'string' ? new RegExp(exclude) : exclude;

	if (list){
		_diffsToProcess = list;
	} else {
		_realPath = undefined;
		getDiffs(_results);
	}

	_diffsToProcess.forEach(function(file){
		var baseFile = file.replace('.diff', '');
		tests.push(compareFiles(baseFile, file));
	});
	waitForTests(tests);
}

function waitForTests(tests){
	casper.then(function(){
		casper.waitFor(function(){
			return tests.length === tests.reduce(function(count, test){
				if (test.success || test.fail || test.error) {
					return count + 1;
				} else {
					return count;
				}
			}, 0);
		}, function(){
			var fails = 0, errors = 0;
			tests.forEach(function(test){
				if (test.fail){
					fails++;
				} else if (test.error){
					errors++;
				}
			});
			_onComplete(tests, fails, errors);
		}, function(){

		},
		10000);
	});
}

function initClient(){

	casper.page.injectJs(_libraryRoot+fs.separator+'ResembleJs'+fs.separator+'resemble.js');

	casper.evaluate(function(mismatchTolerance){

		var result;

		var div = document.createElement('div');

		// this is a bit of hack, need to get images into browser for analysis
		div.style = "display:block;position:absolute;border:0;top:-1px;left:-1px;height:1px;width:1px;overflow:hidden;";
		div.innerHTML = '<form id="image-diff">'+
			'<input type="file" id="image-diff-one" name="one"/>'+
			'<input type="file" id="image-diff-two" name="two"/>'+
		'</form>

';
		document.body.appendChild(div);

		window._imagediff_ = {
			hasResult: false,
			hasImage: false,
			run: run,
			getResult: function(){
				window._imagediff_.hasResult = false;
				return result;
			}
		};

		function run(label){

			function render(data){
				var img = new Image();

				img.onload = function(){
					window._imagediff_.hasImage = true;
				};
				document.getElementById('image-diff').appendChild(img);
				img.src = data.getImageDataUrl(label);
			}

			resemble(document.getElementById('image-diff-one').files[0]).
				compareTo(document.getElementById('image-diff-two').files[0]).
				ignoreAntialiasing(). // <-- muy importante
				onComplete(function(data){
					var diffImage;

					if(Number(data.misMatchPercentage) > mismatchTolerance){
						result = data.misMatchPercentage;
					} else {
						result = false;
					}

					window._imagediff_.hasResult = true;

					if(Number(data.misMatchPercentage) > mismatchTolerance){
						render(data);
					}

				});
		}
	}, {
		mismatchTolerance: _mismatchTolerance
	});
}

function _onPass(test){
	console.log('\n');
	casper.test.pass('No changes found for screenshot ' + test.filename);
}
function _onFail(test){
	console.log('\n');
	casper.test.fail('Visual change found for screenshot ' + test.filename + ' (' + test.mismatch + '% mismatch)');
}
function _onTimeout(test){
	console.log('\n');
	casper.test.info('Could not complete image comparison for ' + test.filename);
}
function _onComplete(tests, noOfFails, noOfErrors){

	if( tests.length === 0){
		console.log("\nMust be your first time?");
		console.log("Some screenshots have been generated in the directory " + _results);
		console.log("This is your 'baseline', check the images manually. If they're wrong, delete the images.");
		console.log("The next time you run these tests, new screenshots will be taken.  These screenshots will be compared to the original.");
		console.log('If they are different, PhantomCSS will report a failure.');
	} else {

		if(noOfFails === 0){
			console.log("\nPhantomCSS found " + tests.length + " tests, None of them failed. Which is good right?");
			console.log("\nIf you want to make them fail, go change some CSS - weirdo.");
		} else {
			console.log("\nPhantomCSS found " + tests.length + " tests, " + noOfFails + ' of them failed.');
			console.log('\nPhantomCSS has created some images that try to show the difference (in the directory '+_failures+'). Fuchsia colored pixels indicate a difference betwen the new and old screenshots.');
		}

		if(noOfErrors !== 0){
			console.log("There were " + noOfErrors + "errors.  Is it possible that a baseline image was deleted but not the diff?");
		}

		exitStatus = noOfErrors+noOfFails;
	}
}

function getExitStatus() {
	return exitStatus;
}<commit_msg>Re-added missing step to add failure image alongside diff.<commit_after>/*
Author: James Cryer
Company: Huddle
Last updated date: 03 Mar 2014
URL: https://github.com/Huddle/PhantomCSS
More: http://tldr.huddle.com/blog/css-testing/
*/

var fs = require('fs');

var _src = '.'+fs.separator+'screenshots';
var _results; // for backwards compatibility results and src are the same - but you can change it!
var _failures = '.'+fs.separator+'failures';

var _count = 0;
var _realPath;
var _diffsToProcess = [];
var _libraryRoot = '.';
var exitStatus;
var _hideElements;
var _addLabelToFailedImage = true;
var _test_match;
var _test_exclude;
var _mismatchTolerance = 0.05;
var diffsCreated = [];

exports.screenshot = screenshot;
exports.compareAll = compareAll;
exports.compareMatched = compareMatched;
exports.compareExplicit = compareExplicit;
exports.compareSession = compareSession;
exports.compareFiles = compareFiles;
exports.waitForTests = waitForTests;
exports.init = init;
exports.update = update;
exports.turnOffAnimations = turnOffAnimations;
exports.getExitStatus = getExitStatus;
exports.getCreatedDiffFiles = getCreatedDiffFiles;

function update(options){

	function stripslash ( str ){
		return str.replace(/\/\//g,'/').replace(/\\/g,'\\');
	}

	options = options || {};

	casper = options.casper || casper;
	_libraryRoot = options.libraryRoot || _libraryRoot;

	_src = stripslash(options.screenshotRoot || _src);
	_results = stripslash(options.comparisonResultRoot || _results || _src);
	_failures = stripslash(options.failedComparisonsRoot || _failures);

	_fileNameGetter = options.fileNameGetter || _fileNameGetter;

	_onPass = options.onPass || _onPass;
	_onFail = options.onFail || _onFail;
	_onTimeout = options.onTimeout || _onTimeout;
	_onComplete = options.onComplete || options.report || _onComplete;

	_hideElements = options.hideElements;

	_mismatchTolerance = options.mismatchTolerance || _mismatchTolerance;

	if(options.addLabelToFailedImage !== undefined){
		_addLabelToFailedImage = options.addLabelToFailedImage;
	}
}

function init(options){
	update(options);
}

function turnOffAnimations(){
	console.log('[PhantomCSS] Turning off animations');
	casper.evaluate(function turnOffAnimations(){
		window.addEventListener('load', function(){

			var css = document.createElement("style");
			css.type = "text/css";
			css.innerHTML = "* { -webkit-transition: none !important; transition: none !important; }";
			document.body.appendChild(css);

			if(jQuery){
				jQuery.fx.off = true;
			}
		},false);
	});
}

function _fileNameGetter(root, fileName){
	var name;

	fileName = fileName || "screenshot";
	name = root + fs.separator + fileName + "_" + _count++;

	if(fs.isFile(name+'.png')){
		return name+'.diff.png';
	} else {
		return name+'.png';
	}
}

function screenshot(selector, timeToWait, hideSelector, fileName){

	if(isNaN(Number(timeToWait)) && typeof timeToWait === 'string'){
		fileName = timeToWait;
		timeToWait = void 0;
	}

	casper.captureBase64('png'); // force pre-render
	casper.wait(timeToWait || 250, function(){

		var srcPath = _fileNameGetter(_src, fileName);
		var resultPath =  srcPath.replace(_src, _results);

		if(hideSelector || _hideElements){
			casper.evaluate(function(s1, s2){
				if(s1){
					$(s1).css('visibility', 'hidden');
				}
				$(s2).css('visibility', 'hidden');
			}, {
				s1: _hideElements,
				s2: hideSelector
			});
		}

		capture(srcPath, resultPath, selector);

	}); // give a bit of time for all the images appear
}

function capture(srcPath, resultPath, selector){
	var originalForResult = resultPath.replace('.diff', '');
	var originalFromSource = srcPath.replace('.diff', '');

	try {

		if( isThisImageADiff(resultPath) ){

			casper.captureSelector( resultPath , selector );

			diffsCreated.push(resultPath);

			if(srcPath !== resultPath){
				// also copy the original over to the result directory
				copyAndReplaceFile(originalFromSource, originalForResult);
			}

		} else {

			casper.captureSelector( srcPath , selector );

			if(srcPath !== resultPath){
				copyAndReplaceFile(srcPath, resultPath);
			}
		}

	}
	catch(ex){
		console.log("[PhantomCSS] Screenshot capture failed: ", ex.message);
	}
}

function isThisImageADiff(path){
	return /\.diff\.png/.test(path);
}

function copyAndReplaceFile(src, dest){
	if(fs.isFile(dest)){
		fs.remove(dest);
	}
	fs.copy(src, dest);
}

function asyncCompare(one, two, func){

	if(!casper.evaluate(function(){ return window._imagediff_;})){
		initClient();
	}

	casper.fill('form#image-diff', {
		'one': one,
		'two': two
	});

	casper.evaluate(function(filename){
		window._imagediff_.run( filename );
	}, {
		label: _addLabelToFailedImage ? one : false
	});

	casper.waitFor(
		function check() {
			return this.evaluate(function(){
				return window._imagediff_.hasResult;
			});
		},
		function () {

			var mismatch = casper.evaluate(function(){
				return window._imagediff_.getResult();
			});

			if(Number(mismatch)){
				func(false, mismatch);
			} else {
				func(true);
			}

		}, function(){
			func(false);
		},
		10000
	);
}

function getDiffs (path){

	var filePath;

	if(({'..':1,'.':1})[path]){ return true; }

	if(_realPath){
		_realPath += fs.separator + path;
	} else {
		_realPath = path;
	}

	filePath = _realPath;

	if(fs.isDirectory(_realPath) ){
		fs.list(_realPath).forEach(getDiffs);
	} else {
		if( /\.diff\./.test(path.toLowerCase()) ){
			if(_test_match){
				if( _test_match.test(_realPath.toLowerCase()) ){
					if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
						console.log('[PhantomCSS] Analysing', _realPath);
						_diffsToProcess.push(filePath);
					}
				}
			} else {
				if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
					_diffsToProcess.push(filePath);
				}
			}
		}
	}

	_realPath = _realPath.replace(fs.separator + path, '');
}

function getCreatedDiffFiles(){
	var d = diffsCreated;
	diffsCreated = [];
	return d;
}

function compareMatched(match, exclude){
	// Search for diff images, but only compare matched filenames
	_test_match = typeof match === 'string' ? new RegExp(match) : match;
	compareAll(exclude);
}

function compareExplicit(list){
	// An explicit list of diff images to compare ['/dialog.diff.png', '/header.diff.png']
	compareAll(void 0, list);
}

function compareSession(list){
	// compare the diffs created in this session
	compareAll(void 0, getCreatedDiffFiles() );
}

function compareFiles(baseFile, file) {
	var html = _libraryRoot+fs.separator+"ResembleJs"+fs.separator+"resemblejscontainer.html";
	var test = {
		filename: baseFile
	};

	if(!fs.isFile(baseFile)) {
		test.error = true;
	} else {

		if( !fs.isFile(html) ){
			console.log('[PhantomCSS] Can\'t find Resemble container. Perhaps the library root is mis configured. ('+html+')');
			test.error = true;
			return;
		}

		casper.thenOpen ( html , function (){

			asyncCompare(baseFile, file, function(isSame, mismatch){

				if(!isSame){

					test.fail = true;

					casper.waitFor(
						function check() {
							return casper.evaluate(function(){
								return window._imagediff_.hasImage;
							});
						},
						function () {
							var failFile, safeFileName, increment;

							if(_failures){
								// flattened structure for failed diffs so that it is easier to preview
								failFile = _failures + fs.separator + file.split(fs.separator).pop().replace('.diff.png', '').replace('.png', '');
								safeFileName = failFile;
								increment = 0;

								while ( fs.isFile(safeFileName+'.fail.png') ){
									increment++;
									safeFileName = failFile+'.'+increment;
								}

								failFile = safeFileName + '.fail.png';

								casper.captureSelector(failFile, 'img');
								console.log('Failure! Saved to', failFile);
							}

							// Always create non-flattened failure images
							casper.captureSelector(file.replace('.diff.png', '.fail.png'), 'img');

							casper.evaluate(function(){
								window._imagediff_.hasImage = false;
							});

							if(mismatch){
								test.mismatch = mismatch;
								_onFail(test); // casper.test.fail throws and error, this function call is aborted
								return;  // Just to make it clear what is happening
							} else {
								_onTimeout(test);
							}

						}, function(){},
						10000
					);
				} else {
                    test.success = true;
					_onPass(test);
				}

			});
		});
	}
    return test;
}

function compareAll(exclude, list){
	var tests = [];

	_test_exclude = typeof exclude === 'string' ? new RegExp(exclude) : exclude;

	if (list){
		_diffsToProcess = list;
	} else {
		_realPath = undefined;
		getDiffs(_results);
	}

	_diffsToProcess.forEach(function(file){
		var baseFile = file.replace('.diff', '');
		tests.push(compareFiles(baseFile, file));
	});
	waitForTests(tests);
}

function waitForTests(tests){
	casper.then(function(){
		casper.waitFor(function(){
			return tests.length === tests.reduce(function(count, test){
				if (test.success || test.fail || test.error) {
					return count + 1;
				} else {
					return count;
				}
			}, 0);
		}, function(){
			var fails = 0, errors = 0;
			tests.forEach(function(test){
				if (test.fail){
					fails++;
				} else if (test.error){
					errors++;
				}
			});
			_onComplete(tests, fails, errors);
		}, function(){

		},
		10000);
	});
}

function initClient(){

	casper.page.injectJs(_libraryRoot+fs.separator+'ResembleJs'+fs.separator+'resemble.js');

	casper.evaluate(function(mismatchTolerance){

		var result;

		var div = document.createElement('div');

		// this is a bit of hack, need to get images into browser for analysis
		div.style = "display:block;position:absolute;border:0;top:-1px;left:-1px;height:1px;width:1px;overflow:hidden;";
		div.innerHTML = '<form id="image-diff">'+
			'<input type="file" id="image-diff-one" name="one"/>'+
			'<input type="file" id="image-diff-two" name="two"/>'+
		'</form>

';
		document.body.appendChild(div);

		window._imagediff_ = {
			hasResult: false,
			hasImage: false,
			run: run,
			getResult: function(){
				window._imagediff_.hasResult = false;
				return result;
			}
		};

		function run(label){

			function render(data){
				var img = new Image();

				img.onload = function(){
					window._imagediff_.hasImage = true;
				};
				document.getElementById('image-diff').appendChild(img);
				img.src = data.getImageDataUrl(label);
			}

			resemble(document.getElementById('image-diff-one').files[0]).
				compareTo(document.getElementById('image-diff-two').files[0]).
				ignoreAntialiasing(). // <-- muy importante
				onComplete(function(data){
					var diffImage;

					if(Number(data.misMatchPercentage) > mismatchTolerance){
						result = data.misMatchPercentage;
					} else {
						result = false;
					}

					window._imagediff_.hasResult = true;

					if(Number(data.misMatchPercentage) > mismatchTolerance){
						render(data);
					}

				});
		}
	}, {
		mismatchTolerance: _mismatchTolerance
	});
}

function _onPass(test){
	console.log('\n');
	casper.test.pass('No changes found for screenshot ' + test.filename);
}
function _onFail(test){
	console.log('\n');
	casper.test.fail('Visual change found for screenshot ' + test.filename + ' (' + test.mismatch + '% mismatch)');
}
function _onTimeout(test){
	console.log('\n');
	casper.test.info('Could not complete image comparison for ' + test.filename);
}
function _onComplete(tests, noOfFails, noOfErrors){

	if( tests.length === 0){
		console.log("\nMust be your first time?");
		console.log("Some screenshots have been generated in the directory " + _results);
		console.log("This is your 'baseline', check the images manually. If they're wrong, delete the images.");
		console.log("The next time you run these tests, new screenshots will be taken.  These screenshots will be compared to the original.");
		console.log('If they are different, PhantomCSS will report a failure.');
	} else {

		if(noOfFails === 0){
			console.log("\nPhantomCSS found " + tests.length + " tests, None of them failed. Which is good right?");
			console.log("\nIf you want to make them fail, go change some CSS - weirdo.");
		} else {
			console.log("\nPhantomCSS found " + tests.length + " tests, " + noOfFails + ' of them failed.');
			console.log('\nPhantomCSS has created some images that try to show the difference (in the directory '+_failures+'). Fuchsia colored pixels indicate a difference betwen the new and old screenshots.');
		}

		if(noOfErrors !== 0){
			console.log("There were " + noOfErrors + "errors.  Is it possible that a baseline image was deleted but not the diff?");
		}

		exitStatus = noOfErrors+noOfFails;
	}
}

function getExitStatus() {
	return exitStatus;
}<|endoftext|>