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: