console.time('init') const crashReporter = require('../crash-reporter') crashReporter.init() const dragDrop = require('drag-drop') const electron = require('electron') const fs = require('fs') const React = require('react') const ReactDOM = require('react-dom') const config = require('../config') const telemetry = require('./lib/telemetry') const sound = require('./lib/sound') const State = require('./lib/state') const TorrentPlayer = require('./lib/torrent-player') // Required by Material UI -- adds `onTouchTap` event require('react-tap-event-plugin')() const App = require('./pages/app') const MediaController = require('./controllers/media-controller') const UpdateController = require('./controllers/update-controller') const PrefsController = require('./controllers/prefs-controller') const PlaybackController = require('./controllers/playback-controller') const SubtitlesController = require('./controllers/subtitles-controller') const TorrentListController = require('./controllers/torrent-list-controller') const TorrentController = require('./controllers/torrent-controller') // 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 const ipcRenderer = electron.ipcRenderer // 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 let controllers = null // This dependency is the slowest-loading, so we lazy load it let Cast = null // 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. let state // Root React component let app 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 = window.state = _state // Make available for easier debugging window.dispatch = dispatch telemetry.init(state) // Log uncaught JS errors window.addEventListener('error', (e) => telemetry.logUncaughtError('window', e), true /* capture */) // Create controllers controllers = { media: new MediaController(state), update: new UpdateController(state), prefs: new PrefsController(state, config), playback: new PlaybackController(state, config, update), subtitles: new SubtitlesController(state), torrentList: new TorrentListController(state), torrent: new TorrentController(state) } // Add first page to location history state.location.go({ url: 'home', setup: (cb) => { state.window.title = config.APP_WINDOW_TITLE cb(null) } }) // Restart everything we were torrenting last time the app ran resumeTorrents() // 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) app = ReactDOM.render(, document.querySelector('#body')) // Lazy-load other stuff, like the AppleTV module, later to keep startup fast window.setTimeout(delayedInit, config.DELAYED_INIT) // Listen for messages from the main process setupIpc() // Warn if the download dir is gone, eg b/c an external drive is unplugged checkDownloadPath() // OS integrations: // ...drag and drop files/text to start torrenting or seeding dragDrop('body', { onDrop: onOpen, onDropText: 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) // Done! Ideally we want to get here < 500ms after the user clicks the app if (electron.remote.getCurrentWindow().isVisible()) { 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() } // 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 } // React loop: // 1. update() - recompute the virtual DOM, diff, apply to the real DOM // 2. event - might be a click or other DOM event, or something external // 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller // 4. controller - the controller handles the event, changing the state object function update () { controllers.playback.showOrHidePlayerControls() app.setState(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.toFixed(2) !== state.prev.progress.toFixed(2)) { 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 || 0) } } 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), 'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData), 'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData), 'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash), 'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash), 'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey), 'saveTorrentFileAs': (torrentKey) => controllers.torrentList.saveTorrentFileAs(torrentKey), // Playback 'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playPause': () => controllers.playback.playPause(), 'nextTrack': () => controllers.playback.nextTrack(), 'previousTrack': () => controllers.playback.previousTrack(), '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(), 'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect), // Local media: