<commit_before>var browserUI = require('api-wrapper.js')
const previewCache = require('previewCache.js')
var getView = remote.getGlobal('getView')

/* implements selecting webviews, switching between them, and creating new ones. */

var placeholderImg = document.getElementById('webview-placeholder')

var windowIsFullscreen = false // TODO track this for each individual webContents

if (window.platformType === 'windows') {
  var navbarHeight = 46 // used to set the bounds of the view
} else {
  var navbarHeight = 36
}

function lazyRemoteObject (getObject) {
  var cachedItem = null
  return new Proxy({}, {
    get: function (obj, prop) {
      if (!cachedItem) {
        cachedItem = getObject()
      }
      return cachedItem[prop]
    }
  })
}

// the permissionRequestHandler used for webviews
function pagePermissionRequestHandler (webContents, permission, callback) {
  if (permission === 'notifications' || permission === 'fullscreen') {
    callback(true)
  } else {
    callback(false)
  }
}

function getViewBounds () {
  if (windowIsFullscreen) {
    return {
      x: 0,
      y: 0,
      width: window.innerWidth,
      height: window.innerHeight
    }
  }
  return {
    x: 0,
    y: navbarHeight,
    width: window.innerWidth,
    height: window.innerHeight - 36
  }
}

function captureCurrentTab () {
  if (webviews.placeholderRequests.length > 0) {
    // capturePage doesn't work while the view is hidden
    return
  }

  ipc.send('getCapture', {
    id: webviews.selectedId,
    width: Math.round(window.innerWidth / 10),
    height: Math.round(window.innerHeight / 10)
  })
}

function updateBackButton () {
  webviews.callAsync(tabs.getSelected(), 'canGoBack', null, function (canGoBack) {
    goBackButton.disabled = !canGoBack
  })
}

// set the permissionRequestHandler for non-private tabs

remote.session.defaultSession.setPermissionRequestHandler(pagePermissionRequestHandler)

// called whenever the page url changes

function onPageLoad (e) {
  var _this = this
  setTimeout(function () { // TODO convert to arrow function
    /* add a small delay before getting these attributes, because they don't seem to update until a short time after the did-finish-load event is fired. Fixes #320 */

    var tab = webviews.getTabFromContents(_this)
    var url = _this.getURL()

    // capture a preview image if a new page has been loaded
    if (tab === tabs.getSelected() && tabs.get(tab).url !== url) {
      setTimeout(function () {
        // sometimes the page isn't visible until a short time after the did-finish-load event occurs
        captureCurrentTab()
      }, 100)
    }

    // if the page is an error page, the URL is really the value of the "url" query parameter
    if (url.startsWith(webviews.internalPages.error) || url.startsWith(webviews.internalPages.crash)) {
      url = new URLSearchParams(new URL(url).search).get('url')
    }

    if (url.indexOf('https://') === 0 || url.indexOf('about:') === 0 || url.indexOf('chrome:') === 0 || url.indexOf('file://') === 0) {
      tabs.update(tab, {
        secure: true,
        url: url
      })
    } else {
      tabs.update(tab, {
        secure: false,
        url: url
      })
    }

    tabBar.rerenderTab(tab)
  }, 0)

  updateBackButton()
}

// called whenever a navigation finishes

function onNavigate () {
  updateBackButton()
}

window.webviews = {
  tabViewMap: {}, // tabId: browserView
  tabContentsMap: {}, // tabId: webContents
  selectedId: null,
  placeholderRequests: [],
  asyncCallbacks: {},
  internalPages: {
    crash: 'file://' + __dirname + '/pages/crash/index.html',
    error: 'file://' + __dirname + '/pages/error/index.html'
  },
  events: [],
  IPCEvents: [],
  bindEvent: function (event, fn, options) {
    webviews.events.push({
      event: event,
      fn: fn,
      options: options
    })
  },
  bindIPC: function (name, fn) {
    webviews.IPCEvents.push({
      name: name,
      fn: fn
    })
  },
  add: function (tabId) {
    var tabData = tabs.get(tabId)

    // if the tab is private, we want to partition it. See http://electron.atom.io/docs/v0.34.0/api/web-view-tag/#partition
    // since tab IDs are unique, we can use them as partition names
    if (tabs.get(tabId).private === true) {
      var partition = tabId.toString() // options.tabId is a number, which remote.session.fromPartition won't accept. It must be converted to a string first

      // register permissionRequestHandler for this tab
      // private tabs use a different session, so the default permissionRequestHandler won't apply

      remote.session.fromPartition(partition).setPermissionRequestHandler(pagePermissionRequestHandler)

      // enable ad/tracker/contentType blocking in this tab if needed

      registerFiltering(partition)
    }

    ipc.send('createView', {
      id: tabId,
      webPreferencesString: JSON.stringify({
        webPreferences: {
          nodeIntegration: false,
          scrollBounce: true,
          preload: __dirname + '/dist/preload.js',
          allowPopups: false,
          partition: partition
        }
      }),
      boundsString: JSON.stringify(getViewBounds()),
      events: webviews.events
    })

    let view = lazyRemoteObject(function () {
      return getView(tabId)
    })

    let contents = lazyRemoteObject(function () {
      return getView(tabId).webContents
    })

    webviews.callAsync(tabData.id, 'loadURL', tabData.url)

    webviews.tabViewMap[tabId] = view
    webviews.tabContentsMap[tabId] = contents
    return view
  },
  setSelected: function (id, options) { // options.focus - whether to focus the view. Defaults to true.
    webviews.selectedId = id

    // create the view if it doesn't already exist
    if (!webviews.getView(id)) {
      webviews.add(id)
    }

    updateBackButton()

    if (webviews.placeholderRequests.length > 0) {
      return
    }

    ipc.send('setView', {
      id: id,
      bounds: getViewBounds(),
      focus: !options || options.focus !== false
    })
  },
  update: function (id, url) {
    webviews.callAsync(id, 'loadURL', urlParser.parse(url))
  },
  destroy: function (id) {
    var w = webviews.tabViewMap[id]
    if (w) {
      ipc.send('destroyView', id)
    }
    delete webviews.tabViewMap[id]
    delete webviews.tabContentsMap[id]
  },
  getView: function (id) {
    return webviews.tabViewMap[id]
  },
  get: function (id) {
    return webviews.tabContentsMap[id]
  },
  requestPlaceholder: function (reason) {
    if (!webviews.placeholderRequests.includes(reason)) {
      webviews.placeholderRequests.push(reason)
    }
    if (webviews.placeholderRequests.length === 1) {
      // create a new placeholder

      var img = previewCache.get(webviews.selectedId)
      var url = tabs.get(webviews.selectedId).url
      if (img) {
        placeholderImg.src = img
        placeholderImg.hidden = false
      } else if (url && url !== 'about:blank') {
        captureCurrentTab()
      }
    }
    setTimeout(function () {
      ipc.send('hideView', webviews.selectedId)
    }, 0)
  },
  hidePlaceholder: function (reason) {
    webviews.placeholderRequests.splice(webviews.placeholderRequests.indexOf(reason), 1)

    if (webviews.placeholderRequests.length === 0) {
      // multiple things can request a placeholder at the same time, but we should only show the view again if nothing requires a placeholder anymore
      if (webviews.tabViewMap[webviews.selectedId]) {
        ipc.send('setView', {
          id: webviews.selectedId,
          bounds: getViewBounds(),
          focus: true
        })
      }
      placeholderImg.hidden = true
    }
  },
  getTabFromContents: function (contents) {
    for (let tabId in webviews.tabContentsMap) {
      if (webviews.tabContentsMap[tabId] === contents) {
        return tabId
      }
    }
    return null
  },
  releaseFocus: function () {
    ipc.send('focusMainWebContents')
  },
  focus: function (id) {
    ipc.send('focusView', id)
  },
  callAsync: function (id, method, args, callback) {
    if (!(args instanceof Array)) {
      args = [args]
    }
    if (callback) {
      var callId = Math.random()
      webviews.asyncCallbacks[callId] = callback
    }
    ipc.send('callViewMethod', {id: id, callId: callId, method: method, args: args})
  }
}

// called when js/preload/textExtractor.js returns the page's text content
webviews.bindIPC('pageData', function (webview, tabId, args) {
  var tab = tabs.get(tabId),
    data = args[0]

  var isInternalPage = tab.url.indexOf(__dirname) !== -1 && tab.url.indexOf(readerView.readerURL) === -1

  // don't save to history if in private mode, or the page is a browser page
  if (tab.private === false && !isInternalPage) {
    bookmarks.updateHistory(tabId, data.extractedText, data.metadata)
  }
})

webviews.bindEvent('new-window', function (e, url, frameName, disposition) {
  var tab = webviews.getTabFromContents(this)
  var currentIndex = tabs.getIndex(tabs.getSelected())

  var newTab = tabs.add({
    url: url,
    private: tabs.get(tab).private // inherit private status from the current tab
  }, currentIndex + 1)
  browserUI.addTab(newTab, {
    enterEditMode: false,
    openInBackground: disposition === 'background-tab' // possibly open in background based on disposition
  })
}, {preventDefault: true})

window.addEventListener('resize', throttle(function () {
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
}, 100))

ipc.on('enter-html-full-screen', function () {
  windowIsFullscreen = true
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
})

ipc.on('leave-html-full-screen', function () {
  windowIsFullscreen = false
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
})

webviews.bindEvent('did-finish-load', onPageLoad)
webviews.bindEvent('did-navigate-in-page', onPageLoad)
webviews.bindEvent('did-navigate', onNavigate)

webviews.bindEvent('page-favicon-updated', function (e, favicons) {
  var id = webviews.getTabFromContents(this)
  updateTabColor(favicons, id)
})

webviews.bindEvent('page-title-updated', function (e, title, explicitSet) {
  var tab = webviews.getTabFromContents(this)
  tabs.update(tab, {
    title: title
  })
  tabBar.rerenderTab(tab)
})

webviews.bindEvent('did-start-loading', function () {
  tabBar.handleProgressBar(webviews.getTabFromContents(this), 'start')
})

webviews.bindEvent('did-stop-loading', function () {
  tabBar.handleProgressBar(webviews.getTabFromContents(this), 'finish')
})

webviews.bindEvent('did-fail-load', function (e, errorCode, errorDesc, validatedURL, isMainFrame) {
  if (errorCode && errorCode !== -3 && isMainFrame && validatedURL) {
    browserUI.navigate(webviews.getTabFromContents(this), webviews.internalPages.error + '?ec=' + encodeURIComponent(errorCode) + '&url=' + encodeURIComponent(validatedURL))
  }
})

webviews.bindEvent('crashed', function (e, isKilled) {
  var tabId = webviews.getTabFromContents(this)
  var url = tabs.get(tabId).url

  tabs.update(tabId, {
    url: webviews.internalPages.crash + '?url=' + encodeURIComponent(url)
  })

  // the existing process has crashed, so we can't reuse it
  webviews.destroy(tabId)
  webviews.add(tabId)

  if (tabId === tabs.getSelected()) {
    webviews.setSelected(tabId)
  }
})

webviews.bindIPC('close-window', function (webview, tabId, args) {
  browserUI.closeTab(tabId)
})

ipc.on('view-event', function (e, args) {
  webviews.events.forEach(function (ev) {
    if (ev.event === args.name) {
      ev.fn.apply(webviews.tabContentsMap[args.id], [e].concat(args.args))
    }
  })
})

ipc.on('async-call-result', function (e, args) {
  webviews.asyncCallbacks[args.callId](args.data)
  delete webviews.asyncCallbacks[args.callId]
})

ipc.on('view-ipc', function (e, data) {
  webviews.IPCEvents.forEach(function (item) {
    if (item.name === data.name) {
      item.fn(webviews.tabContentsMap[data.id], data.id, [data.data])
    }
  })
})

setInterval(function () {
  captureCurrentTab()
}, 30000)

ipc.on('captureData', function (e, data) {
  previewCache.set(data.id, data.url)
  if (data.id === webviews.selectedId && webviews.placeholderRequests.length > 0) {
    placeholderImg.src = data.url
    placeholderImg.hidden = false
  }
})
<commit_msg>reduce flickering when hiding placeholder<commit_after>var browserUI = require('api-wrapper.js')
const previewCache = require('previewCache.js')
var getView = remote.getGlobal('getView')

/* implements selecting webviews, switching between them, and creating new ones. */

var placeholderImg = document.getElementById('webview-placeholder')

var windowIsFullscreen = false // TODO track this for each individual webContents

if (window.platformType === 'windows') {
  var navbarHeight = 46 // used to set the bounds of the view
} else {
  var navbarHeight = 36
}

function lazyRemoteObject (getObject) {
  var cachedItem = null
  return new Proxy({}, {
    get: function (obj, prop) {
      if (!cachedItem) {
        cachedItem = getObject()
      }
      return cachedItem[prop]
    }
  })
}

// the permissionRequestHandler used for webviews
function pagePermissionRequestHandler (webContents, permission, callback) {
  if (permission === 'notifications' || permission === 'fullscreen') {
    callback(true)
  } else {
    callback(false)
  }
}

function getViewBounds () {
  if (windowIsFullscreen) {
    return {
      x: 0,
      y: 0,
      width: window.innerWidth,
      height: window.innerHeight
    }
  }
  return {
    x: 0,
    y: navbarHeight,
    width: window.innerWidth,
    height: window.innerHeight - 36
  }
}

function captureCurrentTab () {
  if (webviews.placeholderRequests.length > 0) {
    // capturePage doesn't work while the view is hidden
    return
  }

  ipc.send('getCapture', {
    id: webviews.selectedId,
    width: Math.round(window.innerWidth / 10),
    height: Math.round(window.innerHeight / 10)
  })
}

function updateBackButton () {
  webviews.callAsync(tabs.getSelected(), 'canGoBack', null, function (canGoBack) {
    goBackButton.disabled = !canGoBack
  })
}

// set the permissionRequestHandler for non-private tabs

remote.session.defaultSession.setPermissionRequestHandler(pagePermissionRequestHandler)

// called whenever the page url changes

function onPageLoad (e) {
  var _this = this
  setTimeout(function () { // TODO convert to arrow function
    /* add a small delay before getting these attributes, because they don't seem to update until a short time after the did-finish-load event is fired. Fixes #320 */

    var tab = webviews.getTabFromContents(_this)
    var url = _this.getURL()

    // capture a preview image if a new page has been loaded
    if (tab === tabs.getSelected() && tabs.get(tab).url !== url) {
      setTimeout(function () {
        // sometimes the page isn't visible until a short time after the did-finish-load event occurs
        captureCurrentTab()
      }, 100)
    }

    // if the page is an error page, the URL is really the value of the "url" query parameter
    if (url.startsWith(webviews.internalPages.error) || url.startsWith(webviews.internalPages.crash)) {
      url = new URLSearchParams(new URL(url).search).get('url')
    }

    if (url.indexOf('https://') === 0 || url.indexOf('about:') === 0 || url.indexOf('chrome:') === 0 || url.indexOf('file://') === 0) {
      tabs.update(tab, {
        secure: true,
        url: url
      })
    } else {
      tabs.update(tab, {
        secure: false,
        url: url
      })
    }

    tabBar.rerenderTab(tab)
  }, 0)

  updateBackButton()
}

// called whenever a navigation finishes

function onNavigate () {
  updateBackButton()
}

window.webviews = {
  tabViewMap: {}, // tabId: browserView
  tabContentsMap: {}, // tabId: webContents
  selectedId: null,
  placeholderRequests: [],
  asyncCallbacks: {},
  internalPages: {
    crash: 'file://' + __dirname + '/pages/crash/index.html',
    error: 'file://' + __dirname + '/pages/error/index.html'
  },
  events: [],
  IPCEvents: [],
  bindEvent: function (event, fn, options) {
    webviews.events.push({
      event: event,
      fn: fn,
      options: options
    })
  },
  bindIPC: function (name, fn) {
    webviews.IPCEvents.push({
      name: name,
      fn: fn
    })
  },
  add: function (tabId) {
    var tabData = tabs.get(tabId)

    // if the tab is private, we want to partition it. See http://electron.atom.io/docs/v0.34.0/api/web-view-tag/#partition
    // since tab IDs are unique, we can use them as partition names
    if (tabs.get(tabId).private === true) {
      var partition = tabId.toString() // options.tabId is a number, which remote.session.fromPartition won't accept. It must be converted to a string first

      // register permissionRequestHandler for this tab
      // private tabs use a different session, so the default permissionRequestHandler won't apply

      remote.session.fromPartition(partition).setPermissionRequestHandler(pagePermissionRequestHandler)

      // enable ad/tracker/contentType blocking in this tab if needed

      registerFiltering(partition)
    }

    ipc.send('createView', {
      id: tabId,
      webPreferencesString: JSON.stringify({
        webPreferences: {
          nodeIntegration: false,
          scrollBounce: true,
          preload: __dirname + '/dist/preload.js',
          allowPopups: false,
          partition: partition
        }
      }),
      boundsString: JSON.stringify(getViewBounds()),
      events: webviews.events
    })

    let view = lazyRemoteObject(function () {
      return getView(tabId)
    })

    let contents = lazyRemoteObject(function () {
      return getView(tabId).webContents
    })

    webviews.callAsync(tabData.id, 'loadURL', tabData.url)

    webviews.tabViewMap[tabId] = view
    webviews.tabContentsMap[tabId] = contents
    return view
  },
  setSelected: function (id, options) { // options.focus - whether to focus the view. Defaults to true.
    webviews.selectedId = id

    // create the view if it doesn't already exist
    if (!webviews.getView(id)) {
      webviews.add(id)
    }

    updateBackButton()

    if (webviews.placeholderRequests.length > 0) {
      return
    }

    ipc.send('setView', {
      id: id,
      bounds: getViewBounds(),
      focus: !options || options.focus !== false
    })
  },
  update: function (id, url) {
    webviews.callAsync(id, 'loadURL', urlParser.parse(url))
  },
  destroy: function (id) {
    var w = webviews.tabViewMap[id]
    if (w) {
      ipc.send('destroyView', id)
    }
    delete webviews.tabViewMap[id]
    delete webviews.tabContentsMap[id]
  },
  getView: function (id) {
    return webviews.tabViewMap[id]
  },
  get: function (id) {
    return webviews.tabContentsMap[id]
  },
  requestPlaceholder: function (reason) {
    if (!webviews.placeholderRequests.includes(reason)) {
      webviews.placeholderRequests.push(reason)
    }
    if (webviews.placeholderRequests.length === 1) {
      // create a new placeholder

      var img = previewCache.get(webviews.selectedId)
      var url = tabs.get(webviews.selectedId).url
      if (img) {
        placeholderImg.src = img
        placeholderImg.hidden = false
      } else if (url && url !== 'about:blank') {
        captureCurrentTab()
      }
    }
    setTimeout(function () {
      ipc.send('hideView', webviews.selectedId)
    }, 0)
  },
  hidePlaceholder: function (reason) {
    webviews.placeholderRequests.splice(webviews.placeholderRequests.indexOf(reason), 1)

    if (webviews.placeholderRequests.length === 0) {
      // multiple things can request a placeholder at the same time, but we should only show the view again if nothing requires a placeholder anymore
      if (webviews.tabViewMap[webviews.selectedId]) {
        ipc.send('setView', {
          id: webviews.selectedId,
          bounds: getViewBounds(),
          focus: true
        })
      }
      // wait for the view to be visible before removing the placeholder
      setTimeout(function () {
        if (webviews.placeholderRequests.length === 0) { // make sure the placeholder hasn't been re-enabled
          placeholderImg.hidden = true
        }
      }, 32)
    }
  },
  getTabFromContents: function (contents) {
    for (let tabId in webviews.tabContentsMap) {
      if (webviews.tabContentsMap[tabId] === contents) {
        return tabId
      }
    }
    return null
  },
  releaseFocus: function () {
    ipc.send('focusMainWebContents')
  },
  focus: function (id) {
    ipc.send('focusView', id)
  },
  callAsync: function (id, method, args, callback) {
    if (!(args instanceof Array)) {
      args = [args]
    }
    if (callback) {
      var callId = Math.random()
      webviews.asyncCallbacks[callId] = callback
    }
    ipc.send('callViewMethod', {id: id, callId: callId, method: method, args: args})
  }
}

// called when js/preload/textExtractor.js returns the page's text content
webviews.bindIPC('pageData', function (webview, tabId, args) {
  var tab = tabs.get(tabId),
    data = args[0]

  var isInternalPage = tab.url.indexOf(__dirname) !== -1 && tab.url.indexOf(readerView.readerURL) === -1

  // don't save to history if in private mode, or the page is a browser page
  if (tab.private === false && !isInternalPage) {
    bookmarks.updateHistory(tabId, data.extractedText, data.metadata)
  }
})

webviews.bindEvent('new-window', function (e, url, frameName, disposition) {
  var tab = webviews.getTabFromContents(this)
  var currentIndex = tabs.getIndex(tabs.getSelected())

  var newTab = tabs.add({
    url: url,
    private: tabs.get(tab).private // inherit private status from the current tab
  }, currentIndex + 1)
  browserUI.addTab(newTab, {
    enterEditMode: false,
    openInBackground: disposition === 'background-tab' // possibly open in background based on disposition
  })
}, {preventDefault: true})

window.addEventListener('resize', throttle(function () {
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
}, 100))

ipc.on('enter-html-full-screen', function () {
  windowIsFullscreen = true
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
})

ipc.on('leave-html-full-screen', function () {
  windowIsFullscreen = false
  ipc.send('setBounds', {id: webviews.selectedId, bounds: getViewBounds()})
})

webviews.bindEvent('did-finish-load', onPageLoad)
webviews.bindEvent('did-navigate-in-page', onPageLoad)
webviews.bindEvent('did-navigate', onNavigate)

webviews.bindEvent('page-favicon-updated', function (e, favicons) {
  var id = webviews.getTabFromContents(this)
  updateTabColor(favicons, id)
})

webviews.bindEvent('page-title-updated', function (e, title, explicitSet) {
  var tab = webviews.getTabFromContents(this)
  tabs.update(tab, {
    title: title
  })
  tabBar.rerenderTab(tab)
})

webviews.bindEvent('did-start-loading', function () {
  tabBar.handleProgressBar(webviews.getTabFromContents(this), 'start')
})

webviews.bindEvent('did-stop-loading', function () {
  tabBar.handleProgressBar(webviews.getTabFromContents(this), 'finish')
})

webviews.bindEvent('did-fail-load', function (e, errorCode, errorDesc, validatedURL, isMainFrame) {
  if (errorCode && errorCode !== -3 && isMainFrame && validatedURL) {
    browserUI.navigate(webviews.getTabFromContents(this), webviews.internalPages.error + '?ec=' + encodeURIComponent(errorCode) + '&url=' + encodeURIComponent(validatedURL))
  }
})

webviews.bindEvent('crashed', function (e, isKilled) {
  var tabId = webviews.getTabFromContents(this)
  var url = tabs.get(tabId).url

  tabs.update(tabId, {
    url: webviews.internalPages.crash + '?url=' + encodeURIComponent(url)
  })

  // the existing process has crashed, so we can't reuse it
  webviews.destroy(tabId)
  webviews.add(tabId)

  if (tabId === tabs.getSelected()) {
    webviews.setSelected(tabId)
  }
})

webviews.bindIPC('close-window', function (webview, tabId, args) {
  browserUI.closeTab(tabId)
})

ipc.on('view-event', function (e, args) {
  webviews.events.forEach(function (ev) {
    if (ev.event === args.name) {
      ev.fn.apply(webviews.tabContentsMap[args.id], [e].concat(args.args))
    }
  })
})

ipc.on('async-call-result', function (e, args) {
  webviews.asyncCallbacks[args.callId](args.data)
  delete webviews.asyncCallbacks[args.callId]
})

ipc.on('view-ipc', function (e, data) {
  webviews.IPCEvents.forEach(function (item) {
    if (item.name === data.name) {
      item.fn(webviews.tabContentsMap[data.id], data.id, [data.data])
    }
  })
})

setInterval(function () {
  captureCurrentTab()
}, 30000)

ipc.on('captureData', function (e, data) {
  previewCache.set(data.id, data.url)
  if (data.id === webviews.selectedId && webviews.placeholderRequests.length > 0) {
    placeholderImg.src = data.url
    placeholderImg.hidden = false
  }
})
<|endoftext|>


<commit_before>SO_DEBUGGING = false;            // for blather(), below.

function translate(text) {
    /* Stub translate function for eventual i8n. */
    return text
};

$(document).ready(function() {
    /* Prevent "event.layerX and event.layerY are broken and deprecated ..."
       WebKit warning message, per:
      http://stackoverflow.com/questions/7825448/webkit-issues-with-event-layerx-and-event-layery
      */
    $.event.props = $.event.props.join('|').replace('layerX|layerY|', '').split('|');
})

/* from http://forthescience.org/blog/2010/11/30/base32-encoding-in-javascript/
   Unrestrained via "WTFPL": http://sam.zoy.org/wtfpl/
*/
function b32encode_trim(s) {
    return b32encode(s).replace(/=+$/, '');
}
function b32encode(s) {
    /* encodes a string s to base32 and returns the encoded string */
    var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    var parts = [];
    var quanta= Math.floor((s.length / 5));
    var leftover = s.length % 5;

    if (leftover != 0) {
        for (var i = 0; i < (5-leftover); i++) { s += '\x00'; }
        quanta += 1;
    }
    for (i = 0; i < quanta; i++) {
        parts.push(alphabet.charAt(s.charCodeAt(i*5) >> 3));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5) & 0x07) << 2)
                                    | (s.charCodeAt(i*5+1) >> 6)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+1) & 0x3F) >> 1) ));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+1) & 0x01) << 4)
                                    | (s.charCodeAt(i*5+2) >> 4)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+2) & 0x0F) << 1)
                                    | (s.charCodeAt(i*5+3) >> 7)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+3) & 0x7F) >> 2)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+3) & 0x03) << 3)
                                    | (s.charCodeAt(i*5+4) >> 5)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+4) & 0x1F) )));
    }
    var replace = 0;
    if (leftover == 1) replace = 6;
    else if (leftover == 2) replace = 4;
    else if (leftover == 3) replace = 3;
    else if (leftover == 4) replace = 1;
    for (i = 0; i < replace; i++) parts.pop();
    for (i = 0; i < replace; i++) parts.push("=");
    return parts.join("");
}

function error_alert(purpose, status_code) {
    /* Post and alert and throw an error for 'purpose' with 'status_code'.
       'purpose' should name the problem, and 'status_code' can be one
       of a few SpiderOak web status codes, or text elaborating the problem. */
    var msg;
    if (typeof status_code === "string") {
        msg = status_code; }
    else if (status_code === 401) {
        msg = 'Unauthorized.'; }
    else if (status_code === 403) {
        msg = 'Incorrect username or password.'; }
    else if (status_code === 404) {
        msg = 'Incorrect ShareID or RoomKey.'; }
    else {
        msg = ('Temporary server failure. Please try again later.'); }
    msg = translate(purpose) + ": " + translate(msg);
    if (typeof status_code !== "string") {
        msg += " (" + status_code + ")"; }
    $(document).trigger("error");
    alert(msg);
    throw new Error(msg);
}

function fragment_quote(id) {
    /* Escape fragment selector chars to avoid jQuery mistaking for CSS
       selector *or other* non-fragment constructs. */
    // The '/' does need to be quoted, surprisingly.
    return id.replace(/(:|\.|\/)/g,'\\$1'); }

function blather(msg, do_alert) {
    if (SO_DEBUGGING) {
        if (do_alert) { alert(msg); }
        else { console.log(msg); }
    }
}

FILE_TYPE_BY_SUFFIX = {txt: "Text",
                       pdf: "Adobe PDF",
                       doc: "MS Word", docx: "MS Word (Open XML)",
                       xls: "MS Excel",
                       ppt: "MS Powerpoint",
                       png: "Image", jpg: "Image", jpeg: "Image", gif: "Image",
                       ico: "MS Icon",
                       svg: "Structured Vector Graphics",
                       ps: "PostScript", eps: "Extended PostScript",
                       avi: "Video", mpg: "Video", mpeg: "Video",
                       mov: "Video",
                       mp3: "MPEG Audio", ogg: "Ogg Vorbis Audio",
                       wav: "Waveform Audio",
                       exe: "Executable",
                       o: "Linkable Object Code",
                       c: "C Source Code",
                       sh: "Shell Script",
                       py: "Python Script",
                       pl: "Perl Script",
                       tcl: "TCL Script",
                       js: "Javascript",
                       bat: "MS Batch Script",
                       zip: "Compressed Archive (zip)",
                       gz: "Compressed (gzip)",
                       tgz: "Compressed Archive (gzip)",
                       jar: "Java Archive",
                       htm: "HyperText", html: "HyperText",
                       php: "PHP HyperText",
                       xml: "Extensible Markup Language",
                      }
function classify_file_by_name(name) {
    /* Return a string inferred from a file's name.
       Return an empty string if fail to infer anything.
     */
    var splat = name.split('.');
    var extension = splat[splat.length-1];
    var is_backup = false;
    var classification = FILE_TYPE_BY_SUFFIX[extension.toLowerCase()];
    if (! classification) {
        if (splat.length > 2 && (extension.match(/[0-9]/)
                                 || extension.match("~")
                                 || extension.match("#"))) {
            is_backup = true;
            var extension = splat[splat.length-2];
            classification = FILE_TYPE_BY_SUFFIX[extension];
        }}
    return classification || "";
}
function bytesToSize(bytes) {
    /* Return description of number of 'bytes' */
    /* Adapted from an entry found on:
       http://codeaid.net/javascript/convert-size-in-bytes-to-human-readable-format-(javascript) */
    if (! bytes) { return "empty"; }
    var sizes = [ 'n/a', 'bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
    var i = +Math.floor(Math.log(bytes) / Math.log(1024));
    return  ((bytes / Math.pow(1024, i)).toFixed(i ? 1 : 0)
             + ' ' + sizes[isNaN(bytes) ? 0 : i+1]);
}

function add_query_param(url, parameter, value, dither) {
    /* Include on 'url' query 'parameter' with 'value'.
       Optional 'dither', if true, adds 'dither=<9-digit random integer>' to
       the query string, for a URL very likely to be distinct.
     */
    var parsed = $.mobile.path.parseUrl(url);
    var got = url + (parsed.search ? "&" : "?") + parameter + "=" + value;
    if (dither) { got += '&dither=' + Math.floor(Math.random() * 1e9); }
    return got;
}
function query_params(url) {
    /* Return an object with settings indicated by 'url' search parameters. */
    var search = $.mobile.path.parseUrl(url).search;
    var got = {};
    if (search) {
        search.slice(1,search.length).split('&').map(
            function (combo) {
                var pair = combo.split('='); got[pair[0]] = pair[1]; })};
    return got;
}

function replace_button_text($slot, label_text) {
    /* Set a button contained in jquery '$slot' element to have 'label_text'.
       We look for .ui-btn-txt within the slot, else replace the slot's html.
     */
    var $into = $slot.find('.ui-btn-text');
    if (! $into.length) { $into = $slot; }
    $into.html(label_text); }

function elide(text, limit) {
    /* Return 'text', with any characters beyond 'limit' elided with "..."
       elipsis. */
    if (text.length <= limit) { return text; }
    else { return text.slice(0, limit) + "..."; }}

<commit_msg>Make this provision more readable.<commit_after>SO_DEBUGGING = false;            // for blather(), below.

function translate(text) {
    /* Stub translate function for eventual i8n. */
    return text
};

$(document).ready(function() {
    /* Prevent "event.layerX and event.layerY are broken and deprecated ..."
       WebKit warning message, per:
      http://stackoverflow.com/questions/7825448/webkit-issues-with-event-layerx-and-event-layery
      */
    $.event.props = ($.event.props.join('|')
                     .replace('layerX|layerY|', '')
                     .split('|'));
})

/* from http://forthescience.org/blog/2010/11/30/base32-encoding-in-javascript/
   Unrestrained via "WTFPL": http://sam.zoy.org/wtfpl/
*/
function b32encode_trim(s) {
    return b32encode(s).replace(/=+$/, '');
}
function b32encode(s) {
    /* encodes a string s to base32 and returns the encoded string */
    var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    var parts = [];
    var quanta= Math.floor((s.length / 5));
    var leftover = s.length % 5;

    if (leftover != 0) {
        for (var i = 0; i < (5-leftover); i++) { s += '\x00'; }
        quanta += 1;
    }
    for (i = 0; i < quanta; i++) {
        parts.push(alphabet.charAt(s.charCodeAt(i*5) >> 3));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5) & 0x07) << 2)
                                    | (s.charCodeAt(i*5+1) >> 6)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+1) & 0x3F) >> 1) ));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+1) & 0x01) << 4)
                                    | (s.charCodeAt(i*5+2) >> 4)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+2) & 0x0F) << 1)
                                    | (s.charCodeAt(i*5+3) >> 7)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+3) & 0x7F) >> 2)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+3) & 0x03) << 3)
                                    | (s.charCodeAt(i*5+4) >> 5)));
        parts.push(alphabet.charAt( ((s.charCodeAt(i*5+4) & 0x1F) )));
    }
    var replace = 0;
    if (leftover == 1) replace = 6;
    else if (leftover == 2) replace = 4;
    else if (leftover == 3) replace = 3;
    else if (leftover == 4) replace = 1;
    for (i = 0; i < replace; i++) parts.pop();
    for (i = 0; i < replace; i++) parts.push("=");
    return parts.join("");
}

function error_alert(purpose, status_code) {
    /* Post and alert and throw an error for 'purpose' with 'status_code'.
       'purpose' should name the problem, and 'status_code' can be one
       of a few SpiderOak web status codes, or text elaborating the problem. */
    var msg;
    if (typeof status_code === "string") {
        msg = status_code; }
    else if (status_code === 401) {
        msg = 'Unauthorized.'; }
    else if (status_code === 403) {
        msg = 'Incorrect username or password.'; }
    else if (status_code === 404) {
        msg = 'Incorrect ShareID or RoomKey.'; }
    else {
        msg = ('Temporary server failure. Please try again later.'); }
    msg = translate(purpose) + ": " + translate(msg);
    if (typeof status_code !== "string") {
        msg += " (" + status_code + ")"; }
    $(document).trigger("error");
    alert(msg);
    throw new Error(msg);
}

function fragment_quote(id) {
    /* Escape fragment selector chars to avoid jQuery mistaking for CSS
       selector *or other* non-fragment constructs. */
    // The '/' does need to be quoted, surprisingly.
    return id.replace(/(:|\.|\/)/g,'\\$1'); }

function blather(msg, do_alert) {
    if (SO_DEBUGGING) {
        if (do_alert) { alert(msg); }
        else { console.log(msg); }
    }
}

FILE_TYPE_BY_SUFFIX = {txt: "Text",
                       pdf: "Adobe PDF",
                       doc: "MS Word", docx: "MS Word (Open XML)",
                       xls: "MS Excel",
                       ppt: "MS Powerpoint",
                       png: "Image", jpg: "Image", jpeg: "Image", gif: "Image",
                       ico: "MS Icon",
                       svg: "Structured Vector Graphics",
                       ps: "PostScript", eps: "Extended PostScript",
                       avi: "Video", mpg: "Video", mpeg: "Video",
                       mov: "Video",
                       mp3: "MPEG Audio", ogg: "Ogg Vorbis Audio",
                       wav: "Waveform Audio",
                       exe: "Executable",
                       o: "Linkable Object Code",
                       c: "C Source Code",
                       sh: "Shell Script",
                       py: "Python Script",
                       pl: "Perl Script",
                       tcl: "TCL Script",
                       js: "Javascript",
                       bat: "MS Batch Script",
                       zip: "Compressed Archive (zip)",
                       gz: "Compressed (gzip)",
                       tgz: "Compressed Archive (gzip)",
                       jar: "Java Archive",
                       htm: "HyperText", html: "HyperText",
                       php: "PHP HyperText",
                       xml: "Extensible Markup Language",
                      }
function classify_file_by_name(name) {
    /* Return a string inferred from a file's name.
       Return an empty string if fail to infer anything.
     */
    var splat = name.split('.');
    var extension = splat[splat.length-1];
    var is_backup = false;
    var classification = FILE_TYPE_BY_SUFFIX[extension.toLowerCase()];
    if (! classification) {
        if (splat.length > 2 && (extension.match(/[0-9]/)
                                 || extension.match("~")
                                 || extension.match("#"))) {
            is_backup = true;
            var extension = splat[splat.length-2];
            classification = FILE_TYPE_BY_SUFFIX[extension];
        }}
    return classification || "";
}
function bytesToSize(bytes) {
    /* Return description of number of 'bytes' */
    /* Adapted from an entry found on:
       http://codeaid.net/javascript/convert-size-in-bytes-to-human-readable-format-(javascript) */
    if (! bytes) { return "empty"; }
    var sizes = [ 'n/a', 'bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
    var i = +Math.floor(Math.log(bytes) / Math.log(1024));
    return  ((bytes / Math.pow(1024, i)).toFixed(i ? 1 : 0)
             + ' ' + sizes[isNaN(bytes) ? 0 : i+1]);
}

function add_query_param(url, parameter, value, dither) {
    /* Include on 'url' query 'parameter' with 'value'.
       Optional 'dither', if true, adds 'dither=<9-digit random integer>' to
       the query string, for a URL very likely to be distinct.
     */
    var parsed = $.mobile.path.parseUrl(url);
    var got = url + (parsed.search ? "&" : "?") + parameter + "=" + value;
    if (dither) { got += '&dither=' + Math.floor(Math.random() * 1e9); }
    return got;
}
function query_params(url) {
    /* Return an object with settings indicated by 'url' search parameters. */
    var search = $.mobile.path.parseUrl(url).search;
    var got = {};
    if (search) {
        search.slice(1,search.length).split('&').map(
            function (combo) {
                var pair = combo.split('='); got[pair[0]] = pair[1]; })};
    return got;
}

function replace_button_text($slot, label_text) {
    /* Set a button contained in jquery '$slot' element to have 'label_text'.
       We look for .ui-btn-txt within the slot, else replace the slot's html.
     */
    var $into = $slot.find('.ui-btn-text');
    if (! $into.length) { $into = $slot; }
    $into.html(label_text); }

function elide(text, limit) {
    /* Return 'text', with any characters beyond 'limit' elided with "..."
       elipsis. */
    if (text.length <= limit) { return text; }
    else { return text.slice(0, limit) + "..."; }}

<|endoftext|>


<commit_before>/* jshint immed:true, latedef:true, newcap:true, browser:true, node:true */

var _ = require("lodash");
var Rx  = require("rx");
var clutility = require("clutility");

//
// Set up namespaces
//

var Fume = Fume || {};

//
// Utilties
//
Fume.util = {};

var fail = Fume.util.fail = function(msg) {
    msg = msg || "This code shouldn't be triggered";
    throw new Error("IllegalStateException: " + msg);
};

//
// Events
//

var Event = Fume.Event = clutility({
    initialize : function(type, props) {
        this.type = type;
        if (props)
            _.extend(this, props);
    },
    isStop : function() {
        return this.type === "STOP";
    },
    isDirty : function() {
        return this.type === "DIRTY";
    },
    isReady : function() {
        return this.type === "READY";
    },
    isValue : function() {
        return this.type === "VALUE";
    },
    isError : function() {
        return this.type === "ERROR";
    },
    isClear : function() {
        return this.type === "CLEAR";
    },
    isListInsert : function() {
        return this.type === "INSERT";
    },
    isItemRemove : function() {
        return this.type === "REMOVE";
    },
    isItemUpdate : function() {
        return this.type === "UPDATE";
    },
    toString : function() {
        return JSON.stringify(this);
    }
});
Event.Stop = function() {
    return new Event("STOP"); //Optimize: use single event instance
};
Event.Dirty = function() {
    return new Event("DIRTY");
};
Event.Ready = function() {
    return new Event("READY");
};
Event.Value = function(value) {
    return new Event("VALUE", { value : value });
};
Event.Error = function(code, error) {
    return new Event("ERROR", { code : code, error : error});
};
Event.Clear = function() {
    return new Event("CLEAR");
};
Event.ListInsert = function(index, value) {
    return new Event("INSERT", { index : index, value : value});
};
Event.ItemRemove = function(index, value) {
    return new Event("REMOVE", { index : index, value : value});
};
Event.ItemUpdate = function(index, value, oldvalue) {
    return new Event("UPDATE", { index : index, value : value, oldvalue : oldvalue });
};

/**
    Observable is a stream that might push values to all its subscribers until the Fume.Event.Stop is propagated.

*/
var Observable = Fume.Observable = clutility({

    /*
        Create a new observable.
        @param {Boolean} autoClose: automatically stop this observer if the last observable has left
    */
    initialize : function(autoClose) {
        this.autoClose = autoClose;
        this.isStopped = false;
        this.observersIdx = 0;
        this.observers = {};
    },

    /*
        Subscribes an observer to this observable
        observables should implement the 'onNext(value)' method.
        On subscribing, the current state of this (to be determined by @see getState), will be pushed to the observable

        @param {Observer} observer. Observer object that will receive the events. It is accepted to pass in a function as well.
        @returns {Disposable} disposable object. Call the @see Fume.Disposable#dispose function to stop listening to this observer
    */
    subscribe : function(observer) {
        if (this.isStopped)
            fail(this + ": cannot perform 'subscribe'; already stopped.");

        if (typeof observer === "function")
            observer = { onNext : observer };

        this.replay(observer);

        this.observersIdx += 1;
        this.observers[this.observersIdx] = observer;

        var observing = this;
        return {
            observerId : this.observersIdx,
            isDisposed : false,
            dispose : function(){
                if (this.isDisposed)
                    fail();
                this.isDisposed = true;
                observing.unsubcribe(this);
            }
        };
    },

    /*
        @private
    */
    unsubcribe : function(disposable) {
         delete this.observers[disposable.observerId];
         if (this.autoClose && !this.hasObservers())
            this.stop();
    },

    /*
        Returns whether any observers are listening to this observable
    */
    hasObservers : function() {
        if (this.observers) for(var key in this.observers)
            return true;
        return false;
    },

    /*
        Returns the current state of this observer in as few events as possible.
        When a new observer subscribes to this observable, the current state will be pushed
        to the observer, to get it in sync with all other observers.

        @param {Observer} observer Observer that should receives the initial state.
    */
    replay : function(observer) {
        //sub
    },

    /*
        @private
        Use this method to push a value to all observers of this observable
    */
    next : function(value) {
        if (this.isStopped)
            fail(this + ": cannot perform 'next'; already stopped");
        for(var key in this.observers)
            this.observers[key].onNext(value);
    },

    /*
        Stops this observable from emitting events.
        Will distribute the Stop event to all its subscribers
    */
    stop : function() {
        if (this.isStopped)
            return;
        if (this.hasObservers())
            this.next(Event.Stop());
        this.isStopped = true;
        this.observers = null;
    }
});

var Disposable = Fume.Disposable = clutility({
    dispose : function() {
        //stub
    }
});

var Observer = Fume.Observer = clutility({
    onNext : function(value) {
        //Just a stub
    }
});

var Pipe = Fume.Pipe = clutility(Observable, {

    initialize : function($super, observable, autoClose){
        $super(autoClose === false ? false : true);

        observable = Observable.fromValue(observable);
        this.dirtyCount = 0;
        this.observing = observable;
        this.subscription = observable.subscribe(this);
        this.isReplaying = false; //for cycle detection
    },

    onNext : function(event) {
        if (!this.hasObservers()) //Optimization
            return;

        if (event.isStop())
            this.stop();
        else if (event.isDirty()) {
            if (this.dirtyCount === 0)
                this.next(event); //push dirty
            this.dirtyCount += 1;
        }
        else if (event.isReady()) {
            this.dirtyCount -= 1;
            if (this.dirtyCount === 0)
                this.next(event); //push ready
        }
        else {
            this.next(event);
        }
    },

    stop : function($super) {
        $super();
        if (this.subscription) {
            this.subscription.dispose();
            this.subscription = null;
        }
    },

    replay : function(observer) {
        if (this.isReplaying) {
            //replay() is called before the observer is registered using subscribe,
            //so we can push the error without introducing unendless recursion
            observer.next(Event.Error("cycle_detected", "Detected cycle in " + this));
            return;
        }

        this.isReplaying = true;
        this.observing.replay(observer);
        this.isReplaying = false;
    },

    observe : function(observable) {
        if (this.isStopped)
            fail(this + ": cannot perform 'observe', already stopped");

        observable = Observable.fromValue(observable);
        if (observable != this.observing && !Constant.equals(observable, this.observing)) {
            this.subscription.dispose();

            this.observing = observable;
            this.subscription = observable.subscribe(this);
        }
    }
});

var Constant = Fume.Constant = clutility(Fume.Observable, {
    initialize : function($super, value) {
        $super(false);
        this.value = value;
    },
    replay : function(observer) {
        observer.onNext(Event.Dirty());
        observer.onNext(Event.Value(this.value));
        observer.onNext(Event.Ready());
    }
});

Constant.equals = function(left, right) {
    return left instanceof Constant && right instanceof Constant && left.value === right.value;
};

var AnonymousObserver = Fume.AnonymousObserver = clutility(Observer, {
    initialize : function(onNext) {
        if (onNext)
            this.onNext = onNext;
    }
});

var DisposableObserver = Fume.DisposableObserver = clutility(AnonymousObserver, {
    initialize : function($super, observable, onNext) {
        $super(onNext);
        this.subscription = observable.subscribe(this);
    },
    dispose : function() {
        this.subscription.dispose();
    }
});

var Transformer = Fume.Transformer = clutility(Observable, {
    initialize : function($super, observables) {
        $super(true);
        this.inputObservables = _.map(observables, Observable.fromValue);
        this.inputDirtyCount = 0;
        this.inputStates = []; //last event per input

        //observer that pushes new events to our own observers
        var self = this;
        this.sink = new AnonymousObserver(function(value) {
            self.next(value);
        });

        this.inputObservers = _.map(this.inputObservables, function(observable, idx) {
            return new DisposableObserver(observable,  _.bind(this.onNext, this, idx));
        }, this);
    },
    onNext : function(inputIndex, event) {
        //todo: event should only be dirty, clean, error or value? what about stop?
        if (event.isDirty()) {
            if (this.inputDirtyCount === 0)
                this.next(event);
            this.inputDirtyCount += 1;
        }
        else if (event.isReady()) {
            this.inputDirtyCount -= 1;

            /*
                if all inputs are satisfied, apply the process function
            */
            if (this.inputDirtyCount === 0) {
                this.process(this.sink, this.inputStates);
                this.next(event); //ready
            }
        }
        else {
            //TODO: is it correct that only the last event is processed
            this.inputStates[inputIndex] = event;
        }
    },
    process : function(observer, inputs){

    },
    replay : function(observer) {
        observer.onNext(Event.Dirty());
        /*
            replay all inputs, save the state
        */
        var states = [];
        _.forEach(this.inputObservables, function(observable, idx) {
            states[idx] = [];
            observable.replay(new AnonymousObserver(function(event){
                if (!event.isDirty() && !event.isReady())
                    states[idx] = event;
            }));
        });

        /*
            apply process on the inputs
        */
        this.process(observer, states);

        observer.onNext(Event.Ready());
    },
    stop : function($super) {
        _.forEach(this.inputObservers, function(observer) {
            observer.dispose();
        });
        $super();
    }
});

var PrimitiveTransformer = Fume.PrimitiveTransformer = clutility(Transformer, {
    initialize : function($super, func, observables) {
        this.simpleFunction = func;
        $super(observables);
    },
    process : function(observer, inputs) {
        var hasError = false;
        args = [];
        for(var i = 0; i < inputs.length; i++) {
            var event = inputs[i];
            if (event.isError()) {
                observer.onNext(event);
                hasError = true;
                break;
            }
            else if (!event.isValue())
                fail("PrimitiveTransformer only supports primitive values as value " + this + ", got: " + event);
            args[i] = event.value;
        }
        if (!hasError)
            observer.onNext(Event.Value(this.simpleFunction.apply(this, args)));
    }
});

var ChildItem = clutility(Pipe, {
    initialize : function($super, parent, idx, initialValue) {
        this.parent = parent;
        this.index = idx;
        $super(initialValue, false);
    },
    observe : function($super, newValue) {
        var oldValue = this.get();
        newValue = Observable.fromValue(newValue);

        if (newValue === oldValue || Constant.equals(newValue, oldValue))
            return;

        this.parent.markDirty(false);

        $super(newValue);
        this.parent.next(Event.ItemUpdate(this.index, newValue, oldValue));

        this.parent.markReady(false);
    },
    set : function(newValue) {
        this.observe(newValue);
    },
    get : function() {
        return this.observing;
    }
});

var List = Fume.List = clutility(Observable, {
    initialize : function($super) {
        $super(false);
        this.items = [];
        this.lengthPipe = new Observable(false);
    },

    insert : function(idx, value) {
        this.markDirty(true);

        var item = new ChildItem(this, idx, value);
        this.items.splice(idx, 0, item);
        for (var i = idx + 1; i < this.items.length; i++)
            this.items[i].index += 1;

        this.next(Event.ListInsert(idx, item.get()));
        this.lengthPipe.next(Event.Value(this.items.length));

        this.markReady(true);
    },

    set : function(idx, value) {
        this.items[idx].set(value);
    },

    remove : function(idx) {
        this.markDirty(true);

        var item = this.items[idx];
        this.items.splice(idx, 1);
        for (var i = idx ; i < this.items.length; i++)
            this.items[i].index -= 1;

        this.next(Event.ItemRemove(idx));
        this.lengthPipe.next(Event.Value(this.items.length));
        item.stop();

        this.markReady(true);
    },

    clear : function() {
        this.markDirty(true);

        for (var i = this.items.length -1; i >= 0; i--)
            this.items[i].stop();
        this.items = [];

        this.next(Event.Clear());
        this.lengthPipe.next(Event.Value(0));

        this.markReady(true);
    },

    length : function() {
        return this.lengthPipe;
    },

    replay : function(observer) {
        observer.onNext(Event.Dirty());
        observer.onNext(Event.Clear());
        for(var i = 0, l = this.items.length; i < l; i++)
            observer.onNext(Event.ListInsert(i, this.items[i].get()));
        observer.onNext(Event.Ready());
    },

    add : function(value) {
        this.insert(this.items.length, value);
    },

    get : function(index) {
        return this.items[index];
    },

    toArray : function() {
        var res = [];
        _.forEach(this.items, function(x){
            res.push(x.get().value);
        });
        return res;
    },

    stop : function($super) {
        this.clear();
        this.lengthPipe.clear();
        _.forEach(this.items, function(item) {
            item.stop();
        });
        $super();
    },

    markDirty : function(includeLength) {
        this.next(Event.Dirty());
        if (includeLength)
            this.lengthPipe.next(Event.Dirty());
    },

    markReady : function(includeLength) {
        this.next(Event.Ready());
        if (includeLength)
            this.lengthPipe.next(Event.Ready());
    },

    toString : function() {
        return "[" + this.toArray().join(",") + "]";
    }
});

Observable.fromValue = function(value) {
    if (value instanceof Observable)
        return value;
    return new Constant(value); //TODO: list, record, error, function...
};

Fume.multiply = function(left, right) {
    return new PrimitiveTransformer(function(x, y) {
        return x * y;
    }, [left, right]);
};

Fume.ValueBuffer = clutility({
    initialize : function() {
        this.reset();
    },
    onNext : function(x) {
        if (x.isValue())
            this.buffer.push(x.value);
    },
    reset : function() {
        this.buffer = [];
        return this;
    }
});

Fume.EventTypeBuffer = clutility(Fume.ValueBuffer, {
    onNext : function(x) {
        this.buffer.push(x.type);
    }
});

module.exports = Fume;<commit_msg>small optimization<commit_after>/* jshint immed:true, latedef:true, newcap:true, browser:true, node:true */

var _ = require("lodash");
var Rx  = require("rx");
var clutility = require("clutility");

//
// Set up namespaces
//

var Fume = Fume || {};

//
// Utilties
//
Fume.util = {};

var fail = Fume.util.fail = function(msg) {
    msg = msg || "This code shouldn't be triggered";
    throw new Error("IllegalStateException: " + msg);
};

//
// Events
//

var Event = Fume.Event = clutility({
    initialize : function(type, props) {
        this.type = type;
        if (props)
            _.extend(this, props);
    },
    isStop : function() {
        return this.type === "STOP";
    },
    isDirty : function() {
        return this.type === "DIRTY";
    },
    isReady : function() {
        return this.type === "READY";
    },
    isValue : function() {
        return this.type === "VALUE";
    },
    isError : function() {
        return this.type === "ERROR";
    },
    isClear : function() {
        return this.type === "CLEAR";
    },
    isListInsert : function() {
        return this.type === "INSERT";
    },
    isItemRemove : function() {
        return this.type === "REMOVE";
    },
    isItemUpdate : function() {
        return this.type === "UPDATE";
    },
    toString : function() {
        return JSON.stringify(this);
    }
});
Event.Stop = function() {
    return new Event("STOP"); //Optimize: use single event instance
};
Event.Dirty = function() {
    return new Event("DIRTY");
};
Event.Ready = function() {
    return new Event("READY");
};
Event.Value = function(value) {
    return new Event("VALUE", { value : value });
};
Event.Error = function(code, error) {
    return new Event("ERROR", { code : code, error : error});
};
Event.Clear = function() {
    return new Event("CLEAR");
};
Event.ListInsert = function(index, value) {
    return new Event("INSERT", { index : index, value : value});
};
Event.ItemRemove = function(index, value) {
    return new Event("REMOVE", { index : index, value : value});
};
Event.ItemUpdate = function(index, value, oldvalue) {
    return new Event("UPDATE", { index : index, value : value, oldvalue : oldvalue });
};

/**
    Observable is a stream that might push values to all its subscribers until the Fume.Event.Stop is propagated.

*/
var Observable = Fume.Observable = clutility({

    /*
        Create a new observable.
        @param {Boolean} autoClose: automatically stop this observer if the last observable has left
    */
    initialize : function(autoClose) {
        this.autoClose = autoClose;
        this.isStopped = false;
        this.observersIdx = 0;
        this.observers = {};
    },

    /*
        Subscribes an observer to this observable
        observables should implement the 'onNext(value)' method.
        On subscribing, the current state of this (to be determined by @see getState), will be pushed to the observable

        @param {Observer} observer. Observer object that will receive the events. It is accepted to pass in a function as well.
        @returns {Disposable} disposable object. Call the @see Fume.Disposable#dispose function to stop listening to this observer
    */
    subscribe : function(observer) {
        if (this.isStopped)
            fail(this + ": cannot perform 'subscribe'; already stopped.");

        if (typeof observer === "function")
            observer = { onNext : observer };

        this.replay(observer);

        this.observersIdx += 1;
        this.observers[this.observersIdx] = observer;

        var observing = this;
        return {
            observerId : this.observersIdx,
            isDisposed : false,
            dispose : function(){
                if (this.isDisposed)
                    fail();
                this.isDisposed = true;
                observing.unsubcribe(this);
            }
        };
    },

    /*
        @private
    */
    unsubcribe : function(disposable) {
         delete this.observers[disposable.observerId];
         if (this.autoClose && !this.hasObservers())
            this.stop();
    },

    /*
        Returns whether any observers are listening to this observable
    */
    hasObservers : function() {
        if (this.observers) for(var key in this.observers)
            return true;
        return false;
    },

    /*
        Returns the current state of this observer in as few events as possible.
        When a new observer subscribes to this observable, the current state will be pushed
        to the observer, to get it in sync with all other observers.

        @param {Observer} observer Observer that should receives the initial state.
    */
    replay : function(observer) {
        //sub
    },

    /*
        @private
        Use this method to push a value to all observers of this observable
    */
    next : function(value) {
        if (this.isStopped)
            fail(this + ": cannot perform 'next'; already stopped");
        for(var key in this.observers)
            this.observers[key].onNext(value);
    },

    /*
        Stops this observable from emitting events.
        Will distribute the Stop event to all its subscribers
    */
    stop : function() {
        if (this.isStopped)
            return;
        if (this.hasObservers())
            this.next(Event.Stop());
        this.isStopped = true;
        this.observers = null;
    }
});

var Disposable = Fume.Disposable = clutility({
    dispose : function() {
        //stub
    }
});

var Observer = Fume.Observer = clutility({
    onNext : function(value) {
        //Just a stub
    }
});

var Pipe = Fume.Pipe = clutility(Observable, {

    initialize : function($super, observable, autoClose){
        $super(autoClose === false ? false : true);

        observable = Observable.fromValue(observable);
        this.dirtyCount = 0;
        this.observing = observable;
        this.subscription = observable.subscribe(this);
        this.isReplaying = false; //for cycle detection
    },

    onNext : function(event) {
        if (!this.hasObservers()) //Optimization
            return;

        if (event.isStop())
            this.stop();
        else if (event.isDirty()) {
            if (this.dirtyCount === 0)
                this.next(event); //push dirty
            this.dirtyCount += 1;
        }
        else if (event.isReady()) {
            this.dirtyCount -= 1;
            if (this.dirtyCount === 0)
                this.next(event); //push ready
        }
        else {
            this.next(event);
        }
    },

    stop : function($super) {
        $super();
        if (this.subscription) {
            this.subscription.dispose();
            this.subscription = null;
        }
    },

    replay : function(observer) {
        if (this.isReplaying) {
            //replay() is called before the observer is registered using subscribe,
            //so we can push the error without introducing unendless recursion
            observer.next(Event.Error("cycle_detected", "Detected cycle in " + this));
            return;
        }

        this.isReplaying = true;
        this.observing.replay(observer);
        this.isReplaying = false;
    },

    observe : function(observable) {
        if (this.isStopped)
            fail(this + ": cannot perform 'observe', already stopped");

        observable = Observable.fromValue(observable);
        if (observable != this.observing && !Constant.equals(observable, this.observing)) {
            this.subscription.dispose();

            this.observing = observable;
            this.subscription = observable.subscribe(this);
        }
    }
});

var Constant = Fume.Constant = clutility(Fume.Observable, {
    initialize : function($super, value) {
        $super(false);
        this.value = value;
    },
    replay : function(observer) {
        observer.onNext(Event.Dirty());
        observer.onNext(Event.Value(this.value));
        observer.onNext(Event.Ready());
    },
    toString : function() {
        return "(" + this.value + ")";
    }
});

Constant.equals = function(left, right) {
    return left instanceof Constant && right instanceof Constant && left.value === right.value;
};

var AnonymousObserver = Fume.AnonymousObserver = clutility(Observer, {
    initialize : function(onNext) {
        if (onNext)
            this.onNext = onNext;
    }
});

var DisposableObserver = Fume.DisposableObserver = clutility(AnonymousObserver, {
    initialize : function($super, observable, onNext) {
        $super(onNext);
        this.subscription = observable.subscribe(this);
    },
    dispose : function() {
        this.subscription.dispose();
    }
});

var Transformer = Fume.Transformer = clutility(Observable, {
    initialize : function($super, observables) {
        $super(true);
        this.inputObservables = _.map(observables, Observable.fromValue);
        this.inputDirtyCount = 0;
        this.inputStates = []; //last event per input

        //observer that pushes new events to our own observers
        var self = this;
        this.sink = new AnonymousObserver(function(value) {
            self.next(value);
        });

        this.inputObservers = _.map(this.inputObservables, function(observable, idx) {
            return new DisposableObserver(observable,  _.bind(this.onNext, this, idx));
        }, this);
    },
    onNext : function(inputIndex, event) {
        //todo: event should only be dirty, clean, error or value? what about stop?
        if (event.isDirty()) {
            if (this.inputDirtyCount === 0)
                this.next(event);
            this.inputDirtyCount += 1;
        }
        else if (event.isReady()) {
            this.inputDirtyCount -= 1;

            /*
                if all inputs are satisfied, apply the process function
            */
            if (this.inputDirtyCount === 0) {
                this.process(this.sink, this.inputStates);
                this.next(event); //ready
            }
        }
        else {
            //TODO: is it correct that only the last event is processed
            this.inputStates[inputIndex] = event;
        }
    },
    process : function(observer, inputs){

    },
    replay : function(observer) {
        observer.onNext(Event.Dirty());
        /*
            replay all inputs, save the state
        */
        var states = [];
        _.forEach(this.inputObservables, function(observable, idx) {
            states[idx] = [];
            observable.replay(new AnonymousObserver(function(event){
                if (!event.isDirty() && !event.isReady())
                    states[idx] = event;
            }));
        });

        /*
            apply process on the inputs
        */
        this.process(observer, states);

        observer.onNext(Event.Ready());
    },
    stop : function($super) {
        _.forEach(this.inputObservers, function(observer) {
            observer.dispose();
        });
        $super();
    }
});

var PrimitiveTransformer = Fume.PrimitiveTransformer = clutility(Transformer, {
    initialize : function($super, func, observables) {
        this.simpleFunction = func;
        $super(observables);
    },
    process : function(observer, inputs) {
        var hasError = false;
        args = [];
        for(var i = 0; i < inputs.length; i++) {
            var event = inputs[i];
            if (event.isError()) {
                observer.onNext(event);
                hasError = true;
                break;
            }
            else if (!event.isValue())
                fail("PrimitiveTransformer only supports primitive values as value " + this + ", got: " + event);
            args[i] = event.value;
        }
        if (!hasError)
            observer.onNext(Event.Value(this.simpleFunction.apply(this, args)));
    }
});

var ChildItem = clutility(Pipe, {
    initialize : function($super, parent, idx, initialValue) {
        this.parent = parent;
        this.index = idx;
        $super(initialValue, false);
    },
    observe : function($super, newValue) {
        var oldValue = this.get();
        newValue = Observable.fromValue(newValue);

        if (newValue === oldValue || Constant.equals(newValue, oldValue))
            return;

        this.parent.markDirty(false);

        $super(newValue);
        this.parent.next(Event.ItemUpdate(this.index, newValue, oldValue));

        this.parent.markReady(false);
    },
    set : function(newValue) {
        this.observe(newValue);
    },
    get : function() {
        return this.observing;
    },
    toString : function() {
        return this.index + ":" + this.observing.toString();
    }
});

var List = Fume.List = clutility(Observable, {
    initialize : function($super) {
        $super(false);
        this.items = [];
        this.lengthPipe = new Observable(false);
    },

    insert : function(idx, value) {
        this.markDirty(true);

        var item = new ChildItem(this, idx, value);
        this.items.splice(idx, 0, item);
        for (var i = idx + 1; i < this.items.length; i++)
            this.items[i].index += 1;

        this.next(Event.ListInsert(idx, item.get()));
        this.lengthPipe.next(Event.Value(this.items.length));

        this.markReady(true);
    },

    set : function(idx, value) {
        this.items[idx].set(value);
    },

    remove : function(idx) {
        this.markDirty(true);

        var item = this.items[idx];
        this.items.splice(idx, 1);
        for (var i = idx ; i < this.items.length; i++)
            this.items[i].index -= 1;

        this.next(Event.ItemRemove(idx));
        this.lengthPipe.next(Event.Value(this.items.length));
        item.stop();

        this.markReady(true);
    },

    clear : function() {
        if (this.length === 0)
            return;

        this.markDirty(true);

        for (var i = this.items.length -1; i >= 0; i--)
            this.items[i].stop();
        this.items = [];

        this.next(Event.Clear());
        this.lengthPipe.next(Event.Value(0));

        this.markReady(true);
    },

    length : function() {
        return this.lengthPipe;
    },

    replay : function(observer) {
        observer.onNext(Event.Dirty());
        observer.onNext(Event.Clear());
        for(var i = 0, l = this.items.length; i < l; i++)
            observer.onNext(Event.ListInsert(i, this.items[i].get()));
        observer.onNext(Event.Ready());
    },

    add : function(value) {
        this.insert(this.items.length, value);
    },

    get : function(index) {
        return this.items[index];
    },

    toArray : function() {
        var res = [];
        _.forEach(this.items, function(x){
            res.push(x.get().value);
        });
        return res;
    },

    stop : function($super) {
        this.clear();
        this.lengthPipe.clear();
        _.forEach(this.items, function(item) {
            item.stop();
        });
        $super();
    },

    markDirty : function(includeLength) {
        this.next(Event.Dirty());
        if (includeLength)
            this.lengthPipe.next(Event.Dirty());
    },

    markReady : function(includeLength) {
        this.next(Event.Ready());
        if (includeLength)
            this.lengthPipe.next(Event.Ready());
    },

    toString : function() {
        return "[" + this.toArray().join(",") + "]";
    }
});

Observable.fromValue = function(value) {
    if (value instanceof Observable)
        return value;
    return new Constant(value); //TODO: list, record, error, function...
};

Fume.multiply = function(left, right) {
    return new PrimitiveTransformer(function(x, y) {
        return x * y;
    }, [left, right]);
};

Fume.ValueBuffer = clutility({
    initialize : function() {
        this.reset();
    },
    onNext : function(x) {
        if (x.isValue())
            this.buffer.push(x.value);
    },
    reset : function() {
        this.buffer = [];
        return this;
    }
});

Fume.EventTypeBuffer = clutility(Fume.ValueBuffer, {
    onNext : function(x) {
        this.buffer.push(x.type);
    }
});

module.exports = Fume;<|endoftext|>


<commit_before>// Require the libraries.
var async =   require('async'),
    $ =       require('jquerygo'),
    prompt =  require('prompt'),
    config =  require('nconf');

// Function to easily get a configuration or prompt for it...
var get = function(param, value, done) {
  var self = this;

  // Allow them to provide a done function.
  if (typeof value === 'function') {
    done = value;
    value = null;
  }

  // Get the parameter name.
  var paramName = (typeof param === 'string') ? param : param.name;
  if (value) {
    config.set(paramName, value);
  }
  else {
    value = config.get(paramName);
  }

  if (!value) {
    prompt.get([param], function (err, result) {
      value = result[paramName];
      config.set(paramName, value);
      done(null, value);
    });
  }
  else if(done) {
    done(null, value);
  }
  else {
    return value;
  }
};

// Load the configuration.
config.argv().env();
var configFile = config.get('o');
if (configFile) {
  config.file({file:configFile});
}

// Start the prompt.
prompt.start();

// Jimmy Johns already has jquery.
$.config.addJQuery = false;

/**
 * Input a value.
 *
 * @param {type} selector
 * @param {type} value
 * @param {type} done
 * @returns {undefined}
 */
var input = function(selector, value, done) {
  $(selector).val(value, function() {
    this.blur(done);
  });
};

/**
 * Helper to set an input within an async series.
 * @param {type} selector
 * @param {type} config
 * @returns {unresolved}
 */
var setInput = function(selector, param, direct) {
  direct = !!direct;
  return function(done) {
    if (direct) {
      input(selector, config.get(param), done);
    }
    else {
      get(param, function(err, value) {
        input(selector, value, done);
      });
    }
  };
};

/**
 * Select a value.
 *
 * @param {type} selector
 * @param {type} value
 * @param {type} done
 * @returns {undefined}
 */
var select = function(selector, value, done) {
  if (value) {
    $(selector + ' li:contains("' + value + '") a:eq(0)').click(function() {
      this.blur(done);
    });
  }
  else {
    done();
  }
};

/**
 * Helper to select a value within an async series.
 * @param {type} selector
 * @param {type} value
 * @returns {unresolved}
 */
var selectVal = function(selector, param, direct) {
  direct = !!direct;
  return function(done) {
    selector += 'SelectBoxItOptions';
    if (direct) {
      var value = config.get(param);
      if (!value) {
        value = param;
      }
      select(selector, value, done);
    }
    else {
      get(param, function(err, value) {
        select(selector, value, done);
      });
    }
  };
};

/**
 * Selects an ingredient.
 *
 * @param {type} param
 * @returns {unresolved}
 */
var selectIngredient = function(param) {
  return function(done) {
    get(param, function(err, value) {
      if (value) {
        var query = 'div.includedIngredientsWrap ';
        query += 'h4:contains("' + param + '") ';
        query += '~ span:contains("' + value + '")';
        $(query).click(function() {
          this.blur(done);
        });
      }
      else {
        done();
      }
    });
  };
};

/**
 * Helper to print something when it is executed.
 *
 * @param {type} text
 * @returns {unresolved}
 */
var print = function(text) {
  return function(done) {
    console.log(text);
    done();
  };
};

/**
 * Helper function to take a pause...
 *
 * @param {type} time
 * @returns {unresolved}
 */
var sleep = function(time) {
  return function(done) {
    setTimeout(done, time);
  };
};

/**
 * Login to Jimmy Johns.
 */
var login = function(done) {
  async.series([
    print('Logging in.'),
    $.go(false, 'visit', 'https://online.jimmyjohns.com/#/login'),
    $.go(false, 'waitForElement', '#email'),
    setInput('#email', 'email'),
    setInput('#loginPassword', {name: 'pass', hidden: true}),
    $('#loginButton').go(false, 'click'),
    $.go(false, 'waitForElement', 'p.welcomeMsg'),
    print('Successfully logged in.')
  ], done);
};

/**
 * Fill out the new delivery stuff..
 * @param {type} done
 * @returns {undefined}
 */
var newDelivery = function(done) {
  async.series([
    print('Filling out delivery information.'),
    $.go(false, 'visit', 'https://online.jimmyjohns.com/#/deliveryaddress'),
    $.go(false, 'waitForElement', '#companyInput'),
    setInput('#companyInput', 'company', true),
    setInput('#addressInput', 'address'),
    setInput('#aptNoInput', 'apt/suite', true),
    setInput('#cityInput', 'city'),
    selectVal('#stateSelect', 'state'),
    setInput('#zip', 'zip'),
    sleep(1000),
    $('#verifyAddressBtn').go(false, 'click'),
    $.go(false, 'waitForElement', '#confirmDeliveryAddressBtn'),
    sleep(1000),
    $('#confirmDeliveryAddressBtn').go(false, 'click'),
    $.go(false, 'waitForElement', 'a.menuTab'),
    print('Sucessfully filled out delivery information.'),
  ], done);
};

/**
 * Select your sandwich on the page.
 *
 * @param {type} done
 * @returns {selectSandwich}
 */
var selectSandwich = function(done) {
  console.log('Selecting sandwich.');
  var sandwich = config.get('sandwich');
  if (!sandwich) {
    console.log('No sandwich specified in your config file.');
    done(true);
  }
  else {
    sandwich = sandwich.toLowerCase();
    var found = false, selection = 0, query = 'a.menuItem span.displayName';

    // Iterate over each sandwich.
    $(query).each(function(index, item, done) {
      item.text(function(text) {
        if (text.toLowerCase().indexOf(sandwich) != -1) {
          selection = index;
          found = true;
        }
        done();
      });
    }, function() {
      if (!found) {
        console.log('Could not find your sandwich.');
        done();
      }
      else {

        // Select this sandwich.
        query += ':eq(' + selection + ')';
        console.log('Sandwich found...');
        $(query).click(function() {
          $.waitForElement('*:contains("Customize Your Order")', done);
        });
      }
    });
  }
};

/**
 * Customize your order.
 *
 * @param {type} done
 * @returns {customizeOrder}
 */
var customizeOrder = function(done) {
  async.series([
    print('Customizing order...'),
    sleep(2000),
    $.go(false, 'waitForElement', '#textInput'),
    setInput('#textInput', 'who'),
    selectVal('#select_2944', 'bread'),
    $.go(false, 'waitForElement', 'span:contains("Cut in half")'),
    function(done) {
      if (config.get('cut')) {
        $('#chk_3894').click(done);
      }
    },
    selectVal('#select_3963', 'drink'),
    selectVal('#select_3882', 'chips'),
    selectVal('#select_3992', 'cookie'),
    selectVal('#select_3943', 'pickle'),
    selectIngredient('Tomato'),
    $('*:contains("Add To Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Your Order For Delivery")'),
    print('Done customizing order.'),
  ], done);
};

/**
 * Select a credit card.
 *
 * @param {type} done
 * @returns {undefined}
 */
var selectCard = function(done) {
  // Have them chose their credit card.
  console.log("Please select your card type:\n\
  1.) American Express\n\
  2.) Visa\n\
  3.) Mastercard\n\
  4.) Discover Card");
  prompt.get(['card'], function (err, result) {
    switch (result.card) {
      case '1':
        $('a#radio_Amex').click(done);
        break;
      case '2':
        $('a#radio_Visa').click(done);
        break;
      case '3':
        $('a#radio_Mastercard').click(done);
        break;
      case '4':
        $('a#radio_Discover').click(done);
        break;
      default:
        done(true);
        break;
    }
  });
};

/**
 * Enter their card number.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardNumber = function(done) {
  console.log("Enter your card number:");
  prompt.get(['cardnum'], function(err, result) {
    input('#cardNumber', result.cardnum, done);
  });
};

/**
 * Enter the card expiration date.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardExpire = function(done) {
  console.log("Enter the card expiration: Format MM-YY:");
  prompt.get(['expire'], function(err, result) {
    var parts = result.expire.split('-');
    select('#expMonthSelectBoxItOptions', parts[0], function() {
      select('#expYearSelectBoxItOptions', '20' + parts[1], done);
    });
  });
};

/**
 * Enter the card code.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardCode = function(done) {
  console.log("Enter the card security code (on back):");
  prompt.get(['code'], function(err, result) {
    input('#securityCode', result.code, done);
  });
};

/**
 * Enter the name on the card.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardName = function(done) {
  console.log("Enter the name on the card:");
  prompt.get(['name'], function(err, result) {
    input('#nameOnCard', result.name, done);
  });
};

/**
 * Now checkout...
 * @param {type} done
 * @returns {undefined}
 */
var checkout = function(done) {
  async.series([
    print('Checking out...'),
    $.go(false, 'waitForElement', 'a#gotoCheckoutBtn'),
    $('a#gotoCheckoutBtn').go(false, 'click'),
    $.go(false, 'waitForElement', '#selectPaymentType'),
    selectVal('#selectPaymentType', 'Credit Card', true),
    $.go(false, 'waitForElement', 'div#cards'),
    selectCard,
    selectVal('#selectPaymentType', 'Credit Card', true),
    enterCardNumber,
    enterCardExpire,
    enterCardCode,
    enterCardName,
    setInput('#tipAmount', 'tip'),
    setInput('#billingAddress', 'billing_address'),
    setInput('#billingCity', 'billing_city'),
    selectVal('#stateSelect', 'billing_state'),
    setInput('#billingZip', 'billing_zip'),
    $('*:contains("Review Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Review & Place Order")'),
    setInput('#companyInput', 'company', true),
    setInput('#aptNoInput', 'apt/suite', true),
    setInput('#gateCode', 'gate_code', true),
    setInput('#deliveryInstructions', 'delivery_instructions', true),
    sleep(1000),
    print('Placing order...'),
    $('*:contains("Place Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Thank you for your order")'),
    $.go('capture', __dirname + '/complete.png'),
    print('Delivery on its way! Order details at ' + __dirname + '/complete.png')
  ], done);
};

// Order a sandwich!!!
async.series([
  login,
  newDelivery,
  selectSandwich,
  customizeOrder,
  checkout
], function() {
  $.close();
});
<commit_msg>Require sudo on unix platforms<commit_after>// Require sudo on unix platforms
if(process.platform !== 'win32'){
  if(process.env.USER !== "root"){
    console.log("What? Make it yourself.");
    process.exit();
  }
}

// Require the libraries.
var async =   require('async'),
    $ =       require('jquerygo'),
    prompt =  require('prompt'),
    config =  require('nconf');

// Function to easily get a configuration or prompt for it...
var get = function(param, value, done) {
  var self = this;

  // Allow them to provide a done function.
  if (typeof value === 'function') {
    done = value;
    value = null;
  }

  // Get the parameter name.
  var paramName = (typeof param === 'string') ? param : param.name;
  if (value) {
    config.set(paramName, value);
  }
  else {
    value = config.get(paramName);
  }

  if (!value) {
    prompt.get([param], function (err, result) {
      value = result[paramName];
      config.set(paramName, value);
      done(null, value);
    });
  }
  else if(done) {
    done(null, value);
  }
  else {
    return value;
  }
};

// Load the configuration.
config.argv().env();
var configFile = config.get('o');
if (configFile) {
  config.file({file:configFile});
}

// Start the prompt.
prompt.start();

// Jimmy Johns already has jquery.
$.config.addJQuery = false;

/**
 * Input a value.
 *
 * @param {type} selector
 * @param {type} value
 * @param {type} done
 * @returns {undefined}
 */
var input = function(selector, value, done) {
  $(selector).val(value, function() {
    this.blur(done);
  });
};

/**
 * Helper to set an input within an async series.
 * @param {type} selector
 * @param {type} config
 * @returns {unresolved}
 */
var setInput = function(selector, param, direct) {
  direct = !!direct;
  return function(done) {
    if (direct) {
      input(selector, config.get(param), done);
    }
    else {
      get(param, function(err, value) {
        input(selector, value, done);
      });
    }
  };
};

/**
 * Select a value.
 *
 * @param {type} selector
 * @param {type} value
 * @param {type} done
 * @returns {undefined}
 */
var select = function(selector, value, done) {
  if (value) {
    $(selector + ' li:contains("' + value + '") a:eq(0)').click(function() {
      this.blur(done);
    });
  }
  else {
    done();
  }
};

/**
 * Helper to select a value within an async series.
 * @param {type} selector
 * @param {type} value
 * @returns {unresolved}
 */
var selectVal = function(selector, param, direct) {
  direct = !!direct;
  return function(done) {
    selector += 'SelectBoxItOptions';
    if (direct) {
      var value = config.get(param);
      if (!value) {
        value = param;
      }
      select(selector, value, done);
    }
    else {
      get(param, function(err, value) {
        select(selector, value, done);
      });
    }
  };
};

/**
 * Selects an ingredient.
 *
 * @param {type} param
 * @returns {unresolved}
 */
var selectIngredient = function(param) {
  return function(done) {
    get(param, function(err, value) {
      if (value) {
        var query = 'div.includedIngredientsWrap ';
        query += 'h4:contains("' + param + '") ';
        query += '~ span:contains("' + value + '")';
        $(query).click(function() {
          this.blur(done);
        });
      }
      else {
        done();
      }
    });
  };
};

/**
 * Helper to print something when it is executed.
 *
 * @param {type} text
 * @returns {unresolved}
 */
var print = function(text) {
  return function(done) {
    console.log(text);
    done();
  };
};

/**
 * Helper function to take a pause...
 *
 * @param {type} time
 * @returns {unresolved}
 */
var sleep = function(time) {
  return function(done) {
    setTimeout(done, time);
  };
};

/**
 * Login to Jimmy Johns.
 */
var login = function(done) {
  async.series([
    print('Logging in.'),
    $.go(false, 'visit', 'https://online.jimmyjohns.com/#/login'),
    $.go(false, 'waitForElement', '#email'),
    setInput('#email', 'email'),
    setInput('#loginPassword', {name: 'pass', hidden: true}),
    $('#loginButton').go(false, 'click'),
    $.go(false, 'waitForElement', 'p.welcomeMsg'),
    print('Successfully logged in.')
  ], done);
};

/**
 * Fill out the new delivery stuff..
 * @param {type} done
 * @returns {undefined}
 */
var newDelivery = function(done) {
  async.series([
    print('Filling out delivery information.'),
    $.go(false, 'visit', 'https://online.jimmyjohns.com/#/deliveryaddress'),
    $.go(false, 'waitForElement', '#companyInput'),
    setInput('#companyInput', 'company', true),
    setInput('#addressInput', 'address'),
    setInput('#aptNoInput', 'apt/suite', true),
    setInput('#cityInput', 'city'),
    selectVal('#stateSelect', 'state'),
    setInput('#zip', 'zip'),
    sleep(1000),
    $('#verifyAddressBtn').go(false, 'click'),
    $.go(false, 'waitForElement', '#confirmDeliveryAddressBtn'),
    sleep(1000),
    $('#confirmDeliveryAddressBtn').go(false, 'click'),
    $.go(false, 'waitForElement', 'a.menuTab'),
    print('Sucessfully filled out delivery information.'),
  ], done);
};

/**
 * Select your sandwich on the page.
 *
 * @param {type} done
 * @returns {selectSandwich}
 */
var selectSandwich = function(done) {
  console.log('Selecting sandwich.');
  var sandwich = config.get('sandwich');
  if (!sandwich) {
    console.log('No sandwich specified in your config file.');
    done(true);
  }
  else {
    sandwich = sandwich.toLowerCase();
    var found = false, selection = 0, query = 'a.menuItem span.displayName';

    // Iterate over each sandwich.
    $(query).each(function(index, item, done) {
      item.text(function(text) {
        if (text.toLowerCase().indexOf(sandwich) != -1) {
          selection = index;
          found = true;
        }
        done();
      });
    }, function() {
      if (!found) {
        console.log('Could not find your sandwich.');
        done();
      }
      else {

        // Select this sandwich.
        query += ':eq(' + selection + ')';
        console.log('Sandwich found...');
        $(query).click(function() {
          $.waitForElement('*:contains("Customize Your Order")', done);
        });
      }
    });
  }
};

/**
 * Customize your order.
 *
 * @param {type} done
 * @returns {customizeOrder}
 */
var customizeOrder = function(done) {
  async.series([
    print('Customizing order...'),
    sleep(2000),
    $.go(false, 'waitForElement', '#textInput'),
    setInput('#textInput', 'who'),
    selectVal('#select_2944', 'bread'),
    $.go(false, 'waitForElement', 'span:contains("Cut in half")'),
    function(done) {
      if (config.get('cut')) {
        $('#chk_3894').click(done);
      }
    },
    selectVal('#select_3963', 'drink'),
    selectVal('#select_3882', 'chips'),
    selectVal('#select_3992', 'cookie'),
    selectVal('#select_3943', 'pickle'),
    selectIngredient('Tomato'),
    $('*:contains("Add To Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Your Order For Delivery")'),
    print('Done customizing order.'),
  ], done);
};

/**
 * Select a credit card.
 *
 * @param {type} done
 * @returns {undefined}
 */
var selectCard = function(done) {
  // Have them chose their credit card.
  console.log("Please select your card type:\n\
  1.) American Express\n\
  2.) Visa\n\
  3.) Mastercard\n\
  4.) Discover Card");
  prompt.get(['card'], function (err, result) {
    switch (result.card) {
      case '1':
        $('a#radio_Amex').click(done);
        break;
      case '2':
        $('a#radio_Visa').click(done);
        break;
      case '3':
        $('a#radio_Mastercard').click(done);
        break;
      case '4':
        $('a#radio_Discover').click(done);
        break;
      default:
        done(true);
        break;
    }
  });
};

/**
 * Enter their card number.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardNumber = function(done) {
  console.log("Enter your card number:");
  prompt.get(['cardnum'], function(err, result) {
    input('#cardNumber', result.cardnum, done);
  });
};

/**
 * Enter the card expiration date.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardExpire = function(done) {
  console.log("Enter the card expiration: Format MM-YY:");
  prompt.get(['expire'], function(err, result) {
    var parts = result.expire.split('-');
    select('#expMonthSelectBoxItOptions', parts[0], function() {
      select('#expYearSelectBoxItOptions', '20' + parts[1], done);
    });
  });
};

/**
 * Enter the card code.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardCode = function(done) {
  console.log("Enter the card security code (on back):");
  prompt.get(['code'], function(err, result) {
    input('#securityCode', result.code, done);
  });
};

/**
 * Enter the name on the card.
 *
 * @param {type} done
 * @returns {undefined}
 */
var enterCardName = function(done) {
  console.log("Enter the name on the card:");
  prompt.get(['name'], function(err, result) {
    input('#nameOnCard', result.name, done);
  });
};

/**
 * Now checkout...
 * @param {type} done
 * @returns {undefined}
 */
var checkout = function(done) {
  async.series([
    print('Checking out...'),
    $.go(false, 'waitForElement', 'a#gotoCheckoutBtn'),
    $('a#gotoCheckoutBtn').go(false, 'click'),
    $.go(false, 'waitForElement', '#selectPaymentType'),
    selectVal('#selectPaymentType', 'Credit Card', true),
    $.go(false, 'waitForElement', 'div#cards'),
    selectCard,
    selectVal('#selectPaymentType', 'Credit Card', true),
    enterCardNumber,
    enterCardExpire,
    enterCardCode,
    enterCardName,
    setInput('#tipAmount', 'tip'),
    setInput('#billingAddress', 'billing_address'),
    setInput('#billingCity', 'billing_city'),
    selectVal('#stateSelect', 'billing_state'),
    setInput('#billingZip', 'billing_zip'),
    $('*:contains("Review Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Review & Place Order")'),
    setInput('#companyInput', 'company', true),
    setInput('#aptNoInput', 'apt/suite', true),
    setInput('#gateCode', 'gate_code', true),
    setInput('#deliveryInstructions', 'delivery_instructions', true),
    sleep(1000),
    print('Placing order...'),
    $('*:contains("Place Order")').go(false, 'click'),
    $.go(false, 'waitForElement', '*:contains("Thank you for your order")'),
    $.go('capture', __dirname + '/complete.png'),
    print('Delivery on its way! Order details at ' + __dirname + '/complete.png')
  ], done);
};

// Order a sandwich!!!
async.series([
  login,
  newDelivery,
  selectSandwich,
  customizeOrder,
  checkout
], function() {
  $.close();
});
<|endoftext|>


<commit_before>var Video = require('./video');
var Input = require('./input');
var Assets = require('./assets');
var isRetina = require('./retina');

/**
 * Game class that is subclassed by actual game code
 * @constructor
 * @param {HTMLCanvasElement} canvas - canvas DOM element
 */
var Game = function(canvas) {
  /**
   * Canvas DOM element
   * @type {HTMLCanvasElement}
   */
  this.canvas = canvas;

  /**
   * Video instance for rendering into canvas
   * @type {Video}
   */
  this.video = new Video(canvas);

  /**
   * Game width in pixels
   * @type {number}
   */
  this.width = 300;

  /**
   * Game highs in pixels
   * @type {number}
   */
  this.height = 300;

  /**
   * Instance of Assets for loading assets for the game
   * @type {Assets}
   */
  this.assets = new Assets();

  /**
   * True if you are using retina screen
   * @type {boolean}
   */
  this.isRetina = isRetina();

  /**
   * Input instance for mouse and keyboard events
   * @type {Input}
   */
  this.input = new Input(this);

  this.configure();
};

/**
 * Is called when all assets are loaded
 * @abstract
 */
Game.prototype.init = function() {};

/**
 * Configure the game
 * @abstract
 */
Game.prototype.configure = function() {};

/**
 * Window resize event
 * @abstract
 */
Game.prototype.resize = function() {};

/**
 * Renders the game
 * @abstract
 */
Game.prototype.render = function() {};

/**
 * Updates the game
 * @param {number} time - time in seconds since last frame
 * @abstract
 */
Game.prototype.update = function(time) {};

/**
 * Mousemove event
 * @param {number} x - x position
 * @param {number} y - y position
 * @abstract
 */
Game.prototype.mousemove = function(x, y) {};

Game.prototype.mouseup = function(x, y) {};

/**
 * Window Focus event
 * @abstract
 */
Game.prototype.focus = function() {};

Game.prototype.click = function() {};

Game.prototype.keydown = function() {};

Game.prototype.keyup = function() {};

/**
 * Window Blur event
 * @abstract
 */
Game.prototype.blur = function() {};

Game.prototype.preloading = function(time) {
  if (!this.video.ctx) { return; }
  if (this._preloaderWidth === undefined) { this._preloaderWidth = 0; }

  var ratio = Math.max(0, Math.min(1, (this.assets.loadedItemsCount)/this.assets.itemsCount));
  var width = Math.min(this.width * 2/3, 300);
  var height = 20;

  var y = (this.height - height) / 2;
  var x = (this.width - width) / 2;

  var currentWidth = width * ratio;
  this._preloaderWidth = this._preloaderWidth + (currentWidth - this._preloaderWidth) * time * 10;

  this.video.ctx.save();

  this.video.ctx.fillStyle = '#a9c848';
  this.video.ctx.fillRect(0, 0, this.width, this.height);

  this.video.ctx.fillStyle = '#88a237';
  this.video.ctx.fillRect(x, y, width, height);

  this.video.ctx.fillStyle = '#f6ffda';
  this.video.ctx.fillRect(x, y, this._preloaderWidth, height);

  this.video.ctx.restore();
},

module.exports = Game;
<commit_msg>fix small mistake in syntax<commit_after>var Video = require('./video');
var Input = require('./input');
var Assets = require('./assets');
var isRetina = require('./retina');

/**
 * Game class that is subclassed by actual game code
 * @constructor
 * @param {HTMLCanvasElement} canvas - canvas DOM element
 */
var Game = function(canvas) {
  /**
   * Canvas DOM element
   * @type {HTMLCanvasElement}
   */
  this.canvas = canvas;

  /**
   * Video instance for rendering into canvas
   * @type {Video}
   */
  this.video = new Video(canvas);

  /**
   * Game width in pixels
   * @type {number}
   */
  this.width = 300;

  /**
   * Game highs in pixels
   * @type {number}
   */
  this.height = 300;

  /**
   * Instance of Assets for loading assets for the game
   * @type {Assets}
   */
  this.assets = new Assets();

  /**
   * True if you are using retina screen
   * @type {boolean}
   */
  this.isRetina = isRetina();

  /**
   * Input instance for mouse and keyboard events
   * @type {Input}
   */
  this.input = new Input(this);

  this.configure();
};

/**
 * Is called when all assets are loaded
 * @abstract
 */
Game.prototype.init = function() {};

/**
 * Configure the game
 * @abstract
 */
Game.prototype.configure = function() {};

/**
 * Window resize event
 * @abstract
 */
Game.prototype.resize = function() {};

/**
 * Renders the game
 * @abstract
 */
Game.prototype.render = function() {};

/**
 * Updates the game
 * @param {number} time - time in seconds since last frame
 * @abstract
 */
Game.prototype.update = function(time) {};

/**
 * Mousemove event
 * @param {number} x - x position
 * @param {number} y - y position
 * @abstract
 */
Game.prototype.mousemove = function(x, y) {};

Game.prototype.mouseup = function(x, y) {};

/**
 * Window Focus event
 * @abstract
 */
Game.prototype.focus = function() {};

Game.prototype.click = function() {};

Game.prototype.keydown = function() {};

Game.prototype.keyup = function() {};

/**
 * Window Blur event
 * @abstract
 */
Game.prototype.blur = function() {};

Game.prototype.preloading = function(time) {
  if (!this.video.ctx) { return; }
  if (this._preloaderWidth === undefined) { this._preloaderWidth = 0; }

  var ratio = Math.max(0, Math.min(1, (this.assets.loadedItemsCount)/this.assets.itemsCount));
  var width = Math.min(this.width * 2/3, 300);
  var height = 20;

  var y = (this.height - height) / 2;
  var x = (this.width - width) / 2;

  var currentWidth = width * ratio;
  this._preloaderWidth = this._preloaderWidth + (currentWidth - this._preloaderWidth) * time * 10;

  this.video.ctx.save();

  this.video.ctx.fillStyle = '#a9c848';
  this.video.ctx.fillRect(0, 0, this.width, this.height);

  this.video.ctx.fillStyle = '#88a237';
  this.video.ctx.fillRect(x, y, width, height);

  this.video.ctx.fillStyle = '#f6ffda';
  this.video.ctx.fillRect(x, y, this._preloaderWidth, height);

  this.video.ctx.restore();
};

module.exports = Game;
<|endoftext|>