console.time('init') var cfg = require('application-config')('WebTorrent') var concat = require('concat-stream') var dragDrop = require('drag-drop') var electron = require('electron') var EventEmitter = require('events') var fs = require('fs') var mainLoop = require('main-loop') var path = require('path') var srtToVtt = require('srt-to-vtt') var createElement = require('virtual-dom/create-element') var diff = require('virtual-dom/diff') var patch = require('virtual-dom/patch') var App = require('./views/app') var config = require('../config') var crashReporter = require('../crash-reporter') var errors = require('./lib/errors') var sound = require('./lib/sound') var State = require('./state') var TorrentPlayer = require('./lib/torrent-player') var util = require('./util') var {setDispatch} = require('./lib/dispatcher') setDispatch(dispatch) // 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 var clipboard = electron.clipboard var dialog = electron.remote.dialog var Menu = electron.remote.Menu var MenuItem = electron.remote.MenuItem var remote = electron.remote // This dependency is the slowest-loading, so we lazy load it var Cast = null // For easy debugging in Developer Tools var state = global.state = State.getInitialState() var vdomLoop // Report crashes back to our server. // Not global JS exceptions, not like Rollbar, handles segfaults/core dumps only crashReporter.init() // 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. loadState(init) /** * 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 init () { // Push the first page into the location history state.location.go({ url: 'home' }) initWebTorrent() window.setTimeout(delayedInit, 5000) // 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) // OS integrations: // ...drag and drop a torrent or video file to play or seed dragDrop('body', (files) => dispatch('onOpen', files)) // ...same thing if you paste a torrent document.addEventListener('paste', onPaste) // ...keyboard shortcuts document.addEventListener('keydown', function (e) { if (e.which === 27) { /* ESC means either exit fullscreen or go back */ if (state.modal) { dispatch('exitModal') } else if (state.window.isFullScreen) { dispatch('toggleFullScreen') } else { dispatch('back') } } else if (e.which === 32) { /* spacebar pauses or plays the video */ dispatch('playPause') } }) // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX window.addEventListener('focus', function () { state.window.isFocused = true state.dock.badge = 0 update() }) window.addEventListener('blur', function () { state.window.isFocused = false update() }) // Listen for messages from the main process setupIpc() // Done! Ideally we want to get here <100ms after the user clicks the app sound.play('STARTUP') console.timeEnd('init') } 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 } // Talk to WebTorrent process, resume torrents, start monitoring torrent progress function initWebTorrent () { // 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) } // 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 () { showOrHidePlayerControls() vdomLoop.update(state) updateElectron() } 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) } if (action === 'onOpen') { onOpen(args[0] /* files */) } if (action === 'addTorrent') { addTorrent(args[0] /* torrent */) } if (action === 'showCreateTorrent') { ipcRenderer.send('showCreateTorrent') /* open file or folder to seed */ } if (action === 'showOpenTorrentFile') { ipcRenderer.send('showOpenTorrentFile') /* open torrent file */ } if (action === 'createTorrent') { createTorrent(args[0] /* options */) } if (action === 'openFile') { openFile(args[0] /* infoHash */, args[1] /* index */) } if (action === 'openFolder') { openFolder(args[0] /* infoHash */) } if (action === 'toggleTorrent') { toggleTorrent(args[0] /* infoHash */) } if (action === 'deleteTorrent') { deleteTorrent(args[0] /* infoHash */) } if (action === 'toggleSelectTorrent') { toggleSelectTorrent(args[0] /* infoHash */) } if (action === 'openTorrentContextMenu') { openTorrentContextMenu(args[0] /* infoHash */) } if (action === 'openDevice') { lazyLoadCast().open(args[0] /* deviceType */) } if (action === 'closeDevice') { lazyLoadCast().close() } if (action === 'setDimensions') { setDimensions(args[0] /* dimensions */) } if (action === 'backToList') { while (state.location.hasBack()) state.location.back() // Work around virtual-dom issue: it doesn't expose its redraw function, // and only redraws on requestAnimationFrame(). That means when the user // closes the window (hide window / minimize to tray) and we want to pause // the video, we update the vdom but it keeps playing until you reopen! var mediaTag = document.querySelector('video,audio') if (mediaTag) mediaTag.pause() } if (action === 'back') { state.location.back() } if (action === 'forward') { state.location.forward() } if (action === 'playPause') { playPause() } if (action === 'play') { if (state.location.pending()) return state.location.go({ url: 'player', onbeforeload: function (cb) { openPlayer(args[0] /* infoHash */, args[1] /* index */, cb) }, onbeforeunload: closePlayer }) playPause(false) } if (action === 'pause') { playPause(true) } if (action === 'playbackJump') { jumpToTime(args[0] /* seconds */) } if (action === 'changeVolume') { changeVolume(args[0] /* increase */) } if (action === 'setVolume') { setVolume(args[0] /* increase */) } if (action === 'openSubtitles') { openSubtitles() } if (action === 'mediaPlaying') { state.playing.isPaused = false ipcRenderer.send('blockPowerSave') } if (action === 'mediaPaused') { // When removing the