/** * Perf optimization: Hook into require() to modify how certain modules load: * * - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not * actually used because auto-prefixing is disabled with * `darkBaseTheme.userAgent = false`. Return a fake object. */ let Module = require('module') const _require = Module.prototype.require Module.prototype.require = function (id) { if (id === 'inline-style-prefixer') return {} return _require.apply(this, arguments) } console.time('init') const crashReporter = require('../crash-reporter') crashReporter.init() // Perf optimization: Start asynchronously read on config file before all the // blocking require() calls below. const State = require('./lib/state') State.load(onState) const createGetter = require('fn-getter') const debounce = require('debounce') 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 TorrentPlayer = require('./lib/torrent-player') // Perf optimization: Needed immediately, so do not lazy load it below const TorrentListController = require('./controllers/torrent-list-controller') // Required by Material UI -- adds `onTouchTap` event require('react-tap-event-plugin')() const App = require('./pages/app') // 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 // 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) // Make available for easier debugging state = window.state = _state window.dispatch = dispatch telemetry.init(state) // Log uncaught JS errors window.addEventListener( 'error', (e) => telemetry.logUncaughtError('window', e), true /* capture */ ) // Create controllers controllers = { media: createGetter(() => { const MediaController = require('./controllers/media-controller') return new MediaController(state) }), playback: createGetter(() => { const PlaybackController = require('./controllers/playback-controller') return new PlaybackController(state, config, update) }), prefs: createGetter(() => { const PrefsController = require('./controllers/prefs-controller') return new PrefsController(state, config) }), subtitles: createGetter(() => { const SubtitlesController = require('./controllers/subtitles-controller') return new SubtitlesController(state) }), torrent: createGetter(() => { const TorrentController = require('./controllers/torrent-controller') return new TorrentController(state) }), torrentList: createGetter(() => { return new TorrentListController(state) }), update: createGetter(() => { const UpdateController = require('./controllers/update-controller') return new UpdateController(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() // Initialize ReactDOM app = ReactDOM.render(, document.querySelector('#body')) // 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) // Listen for messages from the main process setupIpc() // 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) const debouncedFullscreenToggle = debounce(function () { dispatch('toggleFullScreen') }, 1000, true) document.addEventListener('wheel', function (event) { // ctrlKey detects pinch to zoom, http://crbug.com/289887 if (event.ctrlKey) { event.preventDefault() debouncedFullscreenToggle() } }) // ...focus and blur. Needed to show correct dock icon text ('badge') in OSX window.addEventListener('focus', onFocus) window.addEventListener('blur', onBlur) if (electron.remote.getCurrentWindow().isVisible()) { sound.play('STARTUP') } // To keep app startup fast, some code is delayed. window.setTimeout(delayedInit, config.DELAYED_INIT) // Done! Ideally we want to get here < 500ms after the user clicks the app console.timeEnd('init') } // Runs a few seconds after the app loads, to avoid slowing down startup time function delayedInit () { telemetry.send(state) // Send telemetry data every 12 hours, for users who keep the app running // for extended periods of time setInterval(() => telemetry.send(state), 12 * 3600 * 1000) // Warn if the download dir is gone, eg b/c an external drive is unplugged checkDownloadPath() // ...window visibility state. document.addEventListener('webkitvisibilitychange', onVisibilityChange) onVisibilityChange() lazyLoadCast() } // 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), 'pauseAllTorrents': () => controllers.torrentList().pauseAllTorrents(), 'resumeAllTorrents': () => controllers.torrentList().resumeAllTorrents(), '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), 'prioritizeTorrent': (infoHash) => controllers.torrentList().prioritizeTorrent(infoHash), 'resumePausedTorrents': () => controllers.torrentList().resumePausedTorrents(), // 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: