<commit_before>console.time('init')

const crashReporter = require('../crash-reporter')
crashReporter.init()

const dragDrop = require('drag-drop')
const electron = require('electron')
const mainLoop = require('main-loop')
const path = require('path')

const createElement = require('virtual-dom/create-element')
const diff = require('virtual-dom/diff')
const patch = require('virtual-dom/patch')

const config = require('../config')
const App = require('./views/app')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player')
const TorrentSummary = require('./lib/torrent-summary')

const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')

// Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch)

// From dispatch(...), events are sent to one of the controllers
var controllers = null

// This dependency is the slowest-loading, so we lazy load it
var Cast = null

// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer

// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
var state, vdomLoop

State.load(onState)

// Called once when the application loads. (Not once per window.)
// Connects to the torrent networks, sets up the UI and OS integrations like
// the dock icon and drag+drop.
function onState (err, _state) {
  if (err) return onError(err)
  state = _state

  // Create controllers
  controllers = {
    media: new MediaController(state),
    update: new UpdateController(state),
    prefs: new PrefsController(state, config),
    torrentList: new TorrentListController(state),
    playback: new PlaybackController(state, config, update),
    subtitles: new SubtitlesController(state)
  }

  // Add first page to location history
  state.location.go({ url: 'home' })

  // Restart everything we were torrenting last time the app ran
  resumeTorrents()

  // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
  window.setTimeout(delayedInit, config.DELAYED_INIT)

  // The UI is built with virtual-dom, a minimalist library extracted from React
  // The concepts--one way data flow, a pure function that renders state to a
  // virtual DOM tree, and a diff that applies changes in the vdom to the real
  // DOM, are all the same. Learn more: https://facebook.github.io/react/
  vdomLoop = mainLoop(state, render, {
    create: createElement,
    diff: diff,
    patch: patch
  })
  document.body.appendChild(vdomLoop.target)

  // Listen for messages from the main process
  setupIpc()

  // Calling update() updates the UI given the current state
  // Do this at least once a second to give every file in every torrentSummary
  // a progress bar and to keep the cursor in sync when playing a video
  setInterval(update, 1000)

  // OS integrations:
  // ...drag and drop a torrent or video file to play or seed
  dragDrop('body', onOpen)

  // ...same thing if you paste a torrent
  document.addEventListener('paste', onPaste)

  // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
  window.addEventListener('focus', onFocus)
  window.addEventListener('blur', onBlur)

  // ...window visibility state.
  document.addEventListener('webkitvisibilitychange', onVisibilityChange)

  // Log uncaught JS errors
  window.addEventListener('error',
    (e) => telemetry.logUncaughtError('window', e.error), true)

  // Done! Ideally we want to get here < 500ms after the user clicks the app
  sound.play('STARTUP')
  console.timeEnd('init')
}

// Runs a few seconds after the app loads, to avoid slowing down startup time
function delayedInit () {
  lazyLoadCast()
  sound.preload()
  telemetry.init(state)
}

// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
  if (!Cast) {
    Cast = require('./lib/cast')
    Cast.init(state, update) // Search the local network for Chromecast and Airplays
  }
  return Cast
}

// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
// tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) {
  try {
    return App(state)
  } catch (e) {
    console.log('rendering error: %s\n\t%s', e.message, e.stack)
  }
}

// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
  controllers.playback.showOrHidePlayerControls()
  vdomLoop.update(state)
  updateElectron()
}

// Some state changes can't be reflected in the DOM, instead we have to
// tell the main process to update the window or OS integrations
function updateElectron () {
  if (state.window.title !== state.prev.title) {
    state.prev.title = state.window.title
    ipcRenderer.send('setTitle', state.window.title)
  }
  if (state.dock.progress !== state.prev.progress) {
    state.prev.progress = state.dock.progress
    ipcRenderer.send('setProgress', state.dock.progress)
  }
  if (state.dock.badge !== state.prev.badge) {
    state.prev.badge = state.dock.badge
    ipcRenderer.send('setBadge', state.dock.badge || '')
  }
}

// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
  // Log dispatch calls, for debugging
  if (!['mediaMouseMoved', 'mediaTimeUpdate'].includes(action)) {
    console.log('dispatch: %s %o', action, args)
  }

  // Torrent list: creating, deleting, selecting torrents
  if (action === 'openTorrentFile') {
    ipcRenderer.send('openTorrentFile') /* open torrent file */
  }
  if (action === 'openFiles') {
    ipcRenderer.send('openFiles') /* add files with dialog */
  }
  if (action === 'openTorrentAddress') {
    state.modal = { id: 'open-torrent-address-modal' }
  }
  if (action === 'addTorrent') {
    controllers.torrentList.addTorrent(args[0] /* torrent */)
  }
  if (action === 'showCreateTorrent') {
    controllers.torrentList.showCreateTorrent(args[0] /* paths */)
  }
  if (action === 'createTorrent') {
    controllers.torrentList.createTorrent(args[0] /* options */)
  }
  if (action === 'toggleTorrent') {
    controllers.torrentList.toggleTorrent(args[0] /* infoHash */)
  }
  if (action === 'toggleTorrentFile') {
    controllers.torrentList.toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
  }
  if (action === 'deleteTorrent') {
    controllers.torrentList.deleteTorrent(args[0] /* infoHash */)
  }
  if (action === 'toggleSelectTorrent') {
    controllers.torrentList.toggleSelectTorrent(args[0] /* infoHash */)
  }
  if (action === 'openTorrentContextMenu') {
    controllers.torrentList.openTorrentContextMenu(args[0] /* infoHash */)
  }
  if (action === 'startTorrentingSummary') {
    startTorrentingSummary(args[0] /* torrentSummary */)
  }

  // Playback
  if (action === 'playFile') {
    controllers.playback.playFile(args[0] /* infoHash */, args[1] /* index */)
  }
  if (action === 'playPause') {
    controllers.playback.playPause()
  }
  if (action === 'skipTo') {
    controllers.playback.skipTo(args[0] /* seconds */)
  }
  if (action === 'changePlaybackRate') {
    controllers.playback.changePlaybackRate(args[0] /* direction */)
  }
  if (action === 'changeVolume') {
    controllers.playback.changeVolume(args[0] /* increase */)
  }
  if (action === 'setVolume') {
    controllers.playback.setVolume(args[0] /* increase */)
  }
  if (action === 'openItem') {
    controllers.playback.openItem(args[0] /* infoHash */, args[1] /* index */)
  }

  // Subtitles
  if (action === 'openSubtitles') {
    controllers.subtitles.openSubtitles()
  }
  if (action === 'selectSubtitle') {
    controllers.subtitles.selectSubtitle(args[0] /* index */)
  }
  if (action === 'toggleSubtitlesMenu') {
    controllers.subtitles.toggleSubtitlesMenu()
  }
  if (action === 'checkForSubtitles') {
    controllers.subtitles.checkForSubtitles()
  }

  // Local media: <video>, <audio>, VLC
  if (action === 'mediaStalled') {
    controllers.media.mediaStalled()
  }
  if (action === 'mediaError') {
    controllers.media.mediaError(args[0] /* error */)
  }
  if (action === 'mediaSuccess') {
    controllers.media.mediaSuccess()
  }
  if (action === 'mediaTimeUpdate') {
    controllers.media.mediaTimeUpdate()
  }
  if (action === 'mediaMouseMoved') {
    controllers.media.mediaMouseMoved()
  }
  if (action === 'vlcPlay') {
    controllers.media.vlcPlay()
  }
  if (action === 'vlcNotFound') {
    controllers.media.vlcNotFound()
  }

  // Remote casting: Chromecast, Airplay, etc
  if (action === 'toggleCastMenu') {
    lazyLoadCast().toggleMenu(args[0] /* deviceType */)
  }
  if (action === 'selectCastDevice') {
    lazyLoadCast().selectDevice(args[0] /* index */)
  }
  if (action === 'stopCasting') {
    lazyLoadCast().stop()
  }

  // Preferences screen
  if (action === 'preferences') {
    controllers.prefs.show()
  }
  if (action === 'updatePreferences') {
    controllers.prefs.update(args[0] /* key */, args[1] /* value */)
  }

  // Update (check for new versions on Linux, where there's no auto updater)
  if (action === 'updateAvailable') {
    controllers.update.updateAvailable(args[0] /* version */)
  }
  if (action === 'skipVersion') {
    controllers.update.skipVersion(args[0] /* version */)
  }

  // Navigation between screens (back, forward, ESC, etc)
  if (action === 'exitModal') {
    state.modal = null
  }
  if (action === 'backToList') {
    backToList()
  }
  if (action === 'escapeBack') {
    escapeBack()
  }
  if (action === 'back') {
    state.location.back()
  }
  if (action === 'forward') {
    state.location.forward()
  }

  // Controlling the window
  if (action === 'setDimensions') {
    setDimensions(args[0] /* dimensions */)
  }
  if (action === 'toggleFullScreen') {
    ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
  }
  if (action === 'setTitle') {
    state.window.title = args[0] /* title */
  }

  // Everything else
  if (action === 'onOpen') {
    onOpen(args[0] /* files */)
  }
  if (action === 'saveState') {
    State.save(state)
  }
  if (action === 'onError') {
    onError(args[0] /* user-friendly error */)
  }
  if (action === 'uncaughtError') {
    telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */)
  }

  // Update the virtual-dom, unless it's just a mouse move event
  if (action !== 'mediaMouseMoved' ||
      controllers.playback.showOrHidePlayerControls()) {
    update()
  }
}

// Listen to events from the main and webtorrent processes
function setupIpc () {
  ipcRenderer.on('log', (e, ...args) => console.log(...args))
  ipcRenderer.on('error', (e, ...args) => console.error(...args))

  ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))

  ipcRenderer.on('fullscreenChanged', onFullscreenChanged)

  ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
  ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args))
  ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
  ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args))<commit_msg>Refactor main.js: replace dispatch() if-else with hash<commit_after>console.time('init')

const crashReporter = require('../crash-reporter')
crashReporter.init()

const dragDrop = require('drag-drop')
const electron = require('electron')
const mainLoop = require('main-loop')
const path = require('path')

const createElement = require('virtual-dom/create-element')
const diff = require('virtual-dom/diff')
const patch = require('virtual-dom/patch')

const config = require('../config')
const App = require('./views/app')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player')
const TorrentSummary = require('./lib/torrent-summary')

const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')

// Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch)

// From dispatch(...), events are sent to one of the controllers
var controllers = null

// This dependency is the slowest-loading, so we lazy load it
var Cast = null

// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer

// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
var state, vdomLoop

State.load(onState)

// Called once when the application loads. (Not once per window.)
// Connects to the torrent networks, sets up the UI and OS integrations like
// the dock icon and drag+drop.
function onState (err, _state) {
  if (err) return onError(err)
  state = _state

  // Create controllers
  controllers = {
    media: new MediaController(state),
    update: new UpdateController(state),
    prefs: new PrefsController(state, config),
    torrentList: new TorrentListController(state),
    playback: new PlaybackController(state, config, update),
    subtitles: new SubtitlesController(state)
  }

  // Add first page to location history
  state.location.go({ url: 'home' })

  // Restart everything we were torrenting last time the app ran
  resumeTorrents()

  // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
  window.setTimeout(delayedInit, config.DELAYED_INIT)

  // The UI is built with virtual-dom, a minimalist library extracted from React
  // The concepts--one way data flow, a pure function that renders state to a
  // virtual DOM tree, and a diff that applies changes in the vdom to the real
  // DOM, are all the same. Learn more: https://facebook.github.io/react/
  vdomLoop = mainLoop(state, render, {
    create: createElement,
    diff: diff,
    patch: patch
  })
  document.body.appendChild(vdomLoop.target)

  // Listen for messages from the main process
  setupIpc()

  // Calling update() updates the UI given the current state
  // Do this at least once a second to give every file in every torrentSummary
  // a progress bar and to keep the cursor in sync when playing a video
  setInterval(update, 1000)

  // OS integrations:
  // ...drag and drop a torrent or video file to play or seed
  dragDrop('body', onOpen)

  // ...same thing if you paste a torrent
  document.addEventListener('paste', onPaste)

  // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
  window.addEventListener('focus', onFocus)
  window.addEventListener('blur', onBlur)

  // ...window visibility state.
  document.addEventListener('webkitvisibilitychange', onVisibilityChange)

  // Log uncaught JS errors
  window.addEventListener('error',
    (e) => telemetry.logUncaughtError('window', e.error), true)

  // Done! Ideally we want to get here < 500ms after the user clicks the app
  sound.play('STARTUP')
  console.timeEnd('init')
}

// Runs a few seconds after the app loads, to avoid slowing down startup time
function delayedInit () {
  lazyLoadCast()
  sound.preload()
  telemetry.init(state)
}

// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
  if (!Cast) {
    Cast = require('./lib/cast')
    Cast.init(state, update) // Search the local network for Chromecast and Airplays
  }
  return Cast
}

// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
// tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) {
  try {
    return App(state)
  } catch (e) {
    console.log('rendering error: %s\n\t%s', e.message, e.stack)
  }
}

// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
  controllers.playback.showOrHidePlayerControls()
  vdomLoop.update(state)
  updateElectron()
}

// Some state changes can't be reflected in the DOM, instead we have to
// tell the main process to update the window or OS integrations
function updateElectron () {
  if (state.window.title !== state.prev.title) {
    state.prev.title = state.window.title
    ipcRenderer.send('setTitle', state.window.title)
  }
  if (state.dock.progress !== state.prev.progress) {
    state.prev.progress = state.dock.progress
    ipcRenderer.send('setProgress', state.dock.progress)
  }
  if (state.dock.badge !== state.prev.badge) {
    state.prev.badge = state.dock.badge
    ipcRenderer.send('setBadge', state.dock.badge || '')
  }
}

const dispatchHandlers = {
  // Torrent list: creating, deleting, selecting torrents
  'openTorrentFile': () => ipcRenderer.send('openTorrentFile'),
  'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
  'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },

  'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
  'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
  'createTorrent': (options) => controllers.torrentList.createTorrent(options),
  'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
  'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
  'deleteTorrent': (infoHash) => controllers.torrentList.deleteTorrent(infoHash),
  'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
  'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),

  'startTorrentingSummary': startTorrentingSummary,

  // Playback
  'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
  'playPause': () => controllers.playback.playPause(),
  'skipTo': (time) => controllers.playback.skipTo(time),
  'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
  'changeVolume': (delta) => controllers.playback.changeVolume(delta),
  'setVolume': (vol) => controllers.playback.setVolume(vol),
  'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index),

  // Subtitles
  'openSubtitles': () => controllers.subtitles.openSubtitles(),
  'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index),
  'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(),
  'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),

  // Local media: <video>, <audio>, VLC
  'mediaStalled': () => controllers.media.mediaStalled(),
  'mediaError': (err) => controllers.media.mediaError(err),
  'mediaSuccess': () => controllers.media.mediaSuccess(),
  'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
  'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
  'vlcPlay': () => controllers.media.vlcPlay(),
  'vlcNotFound': () => controllers.media.vlcNotFound(),

  // Remote casting: Chromecast, Airplay, etc
  'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
  'selectCastDevice': (index) => lazyLoadCast().selectDevice(index),
  'stopCasting': () => lazyLoadCast().stop(),

  // Preferences screen
  'preferences': () => controllers.prefs.show(),
  'updatePreferences': (key, value) => controllers.prefs.update(key, value),

  // Update (check for new versions on Linux, where there's no auto updater)
  'updateAvailable': (version) => controllers.update.updateAvailable(version),
  'skipVersion': (version) => controllers.update.skipVersion(version),

  // Navigation between screens (back, forward, ESC, etc)
  'exitModal': () => { state.modal = null },
  'backToList': backToList,
  'escapeBack': escapeBack,
  'back': () => state.location.back(),
  'forward': () => state.location.forward(),

  // Controlling the window
  'setDimensions': (dimensions) => setDimensions(dimensions),
  'toggleFullScreen': (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
  'setTitle': (title) => { state.window.title = title },

  // Everything else
  'onOpen': (files) => onOpen(files),
  'saveState': (state) => State.save(state),
  'onError': (err) => onError(err),
  'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err)
}

// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
  // Log dispatch calls, for debugging
  if (!['mediaMouseMoved', 'mediaTimeUpdate'].includes(action)) {
    console.log('dispatch: %s %o', action, args)
  }

  var handler = dispatchHandlers[action]
  if (handler) handler.apply(null, args)
  else console.error('Missing dispatch handler: ' + action)

  // Update the virtual-dom, unless it's just a mouse move event
  if (action !== 'mediaMouseMoved' ||
      controllers.playback.showOrHidePlayerControls()) {
    update()
  }
}

// Listen to events from the main and webtorrent processes
function setupIpc () {
  ipcRenderer.on('log', (e, ...args) => console.log(...args))
  ipcRenderer.on('error', (e, ...args) => console.error(...args))

  ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))

  ipcRenderer.on('fullscreenChanged', onFullscreenChanged)

  ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
  ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args))
  ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
  ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args))
{'fraction_non_alphanumeric': 0.14538049493116117, 'fraction_numerical': 0.0020806587276993227, 'mean_word_length': 3.995577178239717, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 73, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7612151', 'n_tokens_mistral': 6997, 'n_tokens_neox': 6800, 'n_words': 1887}