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), 'toggleCreateTorrentAdvanced': () => controllers.torrentList.toggleCreateTorrentAdvanced(), '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': (torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary), // Playback 'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playPause': () => controllers.playback.playPause(), 'skip': (time) => controllers.playback.skip(time), '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: