/**
* 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 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)
// ...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),
// 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: