diff --git a/renderer/index.js b/renderer/index.js index 8e282173..7edb64b3 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -23,6 +23,8 @@ var config = require('../config') var TorrentPlayer = require('./lib/torrent-player') var torrentPoster = require('./lib/torrent-poster') var util = require('./util') +var {setDispatch} = require('./lib/dispatcher') +setDispatch(dispatch) // These two dependencies are the slowest-loading, so we lazy load them // This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop @@ -170,7 +172,7 @@ function initWebtorrent () { // 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) { - return App(state, dispatch) + return App(state) } // Calls render() to go from state -> UI, then applies to vdom to the real DOM. @@ -216,32 +218,23 @@ function dispatch (action, ...args) { if (action === 'seed') { seed(args[0] /* files */) } - if (action === 'play') { - state.location.go({ - url: 'player', - onbeforeload: function (cb) { - openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb) - }, - onbeforeunload: closePlayer - }) - } if (action === 'openFile') { - openFile(args[0] /* torrentSummary */, args[1] /* index */) + openFile(args[0] /* infoHash */, args[1] /* index */) } if (action === 'openFolder') { - openFolder(args[0] /* torrentSummary */) + openFolder(args[0] /* infoHash */) } if (action === 'toggleTorrent') { - toggleTorrent(args[0] /* torrentSummary */) + toggleTorrent(args[0] /* infoHash */) } if (action === 'deleteTorrent') { - deleteTorrent(args[0] /* torrentSummary */) + deleteTorrent(args[0] /* infoHash */) } if (action === 'toggleSelectTorrent') { toggleSelectTorrent(args[0] /* infoHash */) } if (action === 'openTorrentContextMenu') { - openTorrentContextMenu(args[0] /* torrentSummary */) + openTorrentContextMenu(args[0] /* infoHash */) } if (action === 'openChromecast') { lazyLoadCast().openChromecast() @@ -265,6 +258,13 @@ function dispatch (action, ...args) { playPause() } if (action === 'play') { + state.location.go({ + url: 'player', + onbeforeload: function (cb) { + openPlayer(args[0] /* infoHash */, args[1] /* index */, cb) + }, + onbeforeunload: closePlayer + }) playPause(false) } if (action === 'pause') { @@ -285,7 +285,7 @@ function dispatch (action, ...args) { ipcRenderer.send('unblockPowerSave') } if (action === 'toggleFullScreen') { - ipcRenderer.send('toggleFullScreen', args[0]) + ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) } if (action === 'mediaMouseMoved') { state.playing.mouseStationarySince = new Date().getTime() @@ -739,8 +739,9 @@ function stopServer () { } // Opens the video player -function openPlayer (torrentSummary, index, cb) { - var torrent = lazyLoadClient().get(torrentSummary.infoHash) +function openPlayer (infoHash, index, cb) { + var torrentSummary = getTorrentSummary(infoHash) + var torrent = lazyLoadClient().get(infoHash) if (!torrent || !torrent.done) playInterfaceSound('PLAY') torrentSummary.playStatus = 'requested' update() @@ -792,16 +793,16 @@ function closePlayer (cb) { cb() } -function openFile (torrentSummary, index) { - var torrent = lazyLoadClient().get(torrentSummary.infoHash) +function openFile (infoHash, index) { + var torrent = lazyLoadClient().get(infoHash) if (!torrent) return var filePath = path.join(torrent.path, torrent.files[index].path) ipcRenderer.send('openItem', filePath) } -function openFolder (torrentSummary) { - var torrent = lazyLoadClient().get(torrentSummary.infoHash) +function openFolder (infoHash) { + var torrent = lazyLoadClient().get(infoHash) if (!torrent) return var folderPath = path.join(torrent.path, torrent.name) @@ -815,7 +816,8 @@ function openFolder (torrentSummary) { }) } -function toggleTorrent (torrentSummary) { +function toggleTorrent (infoHash) { + var torrentSummary = getTorrentSummary(infoHash) if (torrentSummary.status === 'paused') { torrentSummary.status = 'new' startTorrentingSummary(torrentSummary) @@ -827,8 +829,7 @@ function toggleTorrent (torrentSummary) { } } -function deleteTorrent (torrentSummary) { - var infoHash = torrentSummary.infoHash +function deleteTorrent (infoHash) { var torrent = getTorrent(infoHash) if (torrent) torrent.destroy() @@ -845,7 +846,8 @@ function toggleSelectTorrent (infoHash) { update() } -function openTorrentContextMenu (torrentSummary) { +function openTorrentContextMenu (infoHash) { + var torrentSummary = getTorrentSummary(infoHash) var menu = new remote.Menu() menu.append(new remote.MenuItem({ label: 'Save Torrent File As...', diff --git a/renderer/lib/dispatcher.js b/renderer/lib/dispatcher.js new file mode 100644 index 00000000..8988f3a9 --- /dev/null +++ b/renderer/lib/dispatcher.js @@ -0,0 +1,36 @@ +module.exports = { + setDispatch, + dispatch, + dispatcher +} + +// _memoize most of our event handlers, which are functions in the form +// () => dispatch() +// ... this prevents virtual-dom from updating tons of listeners on every update() +var _dispatchers = {} +var _dispatch = () => {} + +function setDispatch (dispatch) { + _dispatch = dispatch +} + +// Get a _memoized event handler that calls dispatch() +// All args must be JSON-able +function dispatcher (...args) { + var json = JSON.stringify(args) + var handler = _dispatchers[json] + if (!handler) { + _dispatchers[json] = (e) => { + // Don't click on whatever is below the button + e.stopPropagation() + // Don't regisiter clicks on disabled buttons + if (e.target.classList.contains('disabled')) return + _dispatch.apply(null, args) + } + } + return handler +} + +function dispatch (...args) { + _dispatch.apply(null, args) +} diff --git a/renderer/views/header.js b/renderer/views/header.js index ab1fece1..dc4c09dd 100644 --- a/renderer/views/header.js +++ b/renderer/views/header.js @@ -4,7 +4,9 @@ var h = require('virtual-dom/h') var hyperx = require('hyperx') var hx = hyperx(h) -function Header (state, dispatch) { +var {dispatcher} = require('../lib/dispatcher') + +function Header (state) { return hx`
${getTitle()} @@ -12,13 +14,13 @@ function Header (state, dispatch) { dispatch('back')}> + onclick=${dispatcher('back')}> chevron_left dispatch('forward')}> + onclick=${dispatcher('forward')}> chevron_right
@@ -40,7 +42,7 @@ function Header (state, dispatch) { dispatch('showOpenTorrentFile')}> + onclick=${dispatcher('showOpenTorrentFile')}> add ` diff --git a/renderer/views/open-torrent-address-modal.js b/renderer/views/open-torrent-address-modal.js index 4006e6e3..46c008c2 100644 --- a/renderer/views/open-torrent-address-modal.js +++ b/renderer/views/open-torrent-address-modal.js @@ -4,7 +4,9 @@ var h = require('virtual-dom/h') var hyperx = require('hyperx') var hx = hyperx(h) -function OpenTorrentAddressModal (state, dispatch) { +var {dispatch} = require('../lib/dispatcher') + +function OpenTorrentAddressModal (state) { return hx`

Enter torrent address or magnet link

@@ -15,17 +17,17 @@ function OpenTorrentAddressModal (state, dispatch) {

` - - function handleKeyPress (e) { - if (e.which === 13) handleOK() /* hit Enter to submit */ - } - - function handleOK () { - dispatch('exitModal') - dispatch('addTorrent', document.querySelector('#add-torrent-url').value) - } - - function handleCancel () { - dispatch('exitModal') - } +} + +function handleKeyPress (e) { + if (e.which === 13) handleOK() /* hit Enter to submit */ +} + +function handleOK () { + dispatch('exitModal') + dispatch('addTorrent', document.querySelector('#add-torrent-url').value) +} + +function handleCancel () { + dispatch('exitModal') } diff --git a/renderer/views/player.js b/renderer/views/player.js index 30ee49bd..d183a291 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -5,23 +5,24 @@ var hyperx = require('hyperx') var hx = hyperx(h) var util = require('../util') +var {dispatch, dispatcher} = require('../lib/dispatcher') // Shows a streaming video player. Standard features + Chromecast + Airplay -function Player (state, dispatch) { +function Player (state) { // Show the video as large as will fit in the window, play immediately // If the video is on Chromecast or Airplay, show a title screen instead var showVideo = state.playing.location === 'local' return hx`
dispatch('mediaMouseMoved')}> - ${showVideo ? renderMedia(state, dispatch) : renderCastScreen(state, dispatch)} - ${renderPlayerControls(state, dispatch)} + onmousemove=${dispatcher('mediaMouseMoved')}> + ${showVideo ? renderMedia(state) : renderCastScreen(state)} + ${renderPlayerControls(state)}
` } -function renderMedia (state, dispatch) { +function renderMedia (state) { if (!state.server) return // Unfortunately, play/pause can't be done just by modifying HTML. @@ -54,11 +55,11 @@ function renderMedia (state, dispatch) { var mediaTag = hx`
dispatch('toggleFullScreen')} + ondblclick=${dispatcher('toggleFullScreen')} onloadedmetadata=${onLoadedMetadata} onended=${onEnded} - onplay=${() => dispatch('mediaPlaying')} - onpause=${() => dispatch('mediaPaused')} + onplay=${dispatcher('mediaPlaying')} + onpause=${dispatcher('mediaPaused')} autoplay>
` @@ -75,7 +76,7 @@ function renderMedia (state, dispatch) {
dispatch('mediaMouseMoved')}> + onmousemove=${dispatcher('mediaMouseMoved')}> ${mediaTag} ${renderAudioMetadata(state)}
@@ -126,7 +127,7 @@ function renderAudioMetadata (state) { return hx`
${elems}
` } -function renderCastScreen (state, dispatch) { +function renderCastScreen (state) { var isChromecast = state.playing.location.startsWith('chromecast') var isAirplay = state.playing.location.startsWith('airplay') var isStarting = state.playing.location.endsWith('-pending') @@ -166,7 +167,7 @@ function getPlayingTorrentSummary (state) { return state.saved.torrents.find((x) => x.infoHash === infoHash) } -function renderPlayerControls (state, dispatch) { +function renderPlayerControls (state) { var positionPercent = 100 * state.playing.currentTime / state.playing.duration var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } @@ -183,7 +184,7 @@ function renderPlayerControls (state, dispatch) { `, hx` dispatch('toggleFullScreen')}> + onclick=${dispatcher('toggleFullScreen')}> ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ` @@ -196,18 +197,18 @@ function renderPlayerControls (state, dispatch) { if (isOnChromecast) { chromecastClass = 'active' airplayClass = 'disabled' - chromecastHandler = () => dispatch('stopCasting') + chromecastHandler = dispatcher('stopCasting') airplayHandler = undefined } else if (isOnAirplay) { chromecastClass = 'disabled' airplayClass = 'active' chromecastHandler = undefined - airplayHandler = () => dispatch('stopCasting') + airplayHandler = dispatcher('stopCasting') } else { chromecastClass = '' airplayClass = '' - chromecastHandler = () => dispatch('openChromecast') - airplayHandler = () => dispatch('openAirplay') + chromecastHandler = dispatcher('openChromecast') + airplayHandler = dispatcher('openAirplay') } if (state.devices.chromecast || isOnChromecast) { elements.push(hx` @@ -233,7 +234,7 @@ function renderPlayerControls (state, dispatch) { if (process.platform !== 'darwin') { elements.push(hx` dispatch('back')}> + onclick=${dispatcher('back')}> chevron_left `) @@ -241,7 +242,7 @@ function renderPlayerControls (state, dispatch) { // Finally, the big button in the center plays or pauses the video elements.push(hx` - dispatch('playPause')}> + ${state.playing.isPaused ? 'play_arrow' : 'pause'} `) diff --git a/renderer/views/torrent-list.js b/renderer/views/torrent-list.js index 7d5e71f3..6a6b13d4 100644 --- a/renderer/views/torrent-list.js +++ b/renderer/views/torrent-list.js @@ -8,8 +8,9 @@ var prettyBytes = require('prettier-bytes') var util = require('../util') var TorrentPlayer = require('../lib/torrent-player') +var {dispatcher} = require('../lib/dispatcher') -function TorrentList (state, dispatch) { +function TorrentList (state) { var torrentRows = state.saved.torrents.map( (torrentSummary) => renderTorrent(torrentSummary)) return hx` @@ -53,8 +54,8 @@ function TorrentList (state, dispatch) { classes = classes.join(' ') return hx`
dispatch('openTorrentContextMenu', torrentSummary)} - onclick=${() => dispatch('toggleSelectTorrent', infoHash)}> + oncontextmenu=${dispatcher('openTorrentContextMenu', infoHash)} + onclick=${dispatcher('toggleSelectTorrent', infoHash)}> ${renderTorrentMetadata(torrent, torrentSummary)} ${renderTorrentButtons(torrentSummary)} ${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''} @@ -110,6 +111,8 @@ function TorrentList (state, dispatch) { // Download button toggles between torrenting (DL/seed) and paused // Play button starts streaming the torrent immediately, unpausing if needed function renderTorrentButtons (torrentSummary) { + var infoHash = torrentSummary.infoHash + var playIcon, playTooltip, playClass if (torrentSummary.playStatus === 'unplayable') { playIcon = 'play_arrow' @@ -141,34 +144,28 @@ function TorrentList (state, dispatch) { handleButton('play', e)}> + onclick=${dispatcher('play', infoHash)}> ${playIcon} handleButton('toggleTorrent', e)}> + onclick=${dispatcher('toggleTorrent', infoHash)}> ${downloadIcon} handleButton('deleteTorrent', e)}> + onclick=${dispatcher('deleteTorrent', infoHash)}> close
` - - function handleButton (action, e) { - // Prevent propagation so that we don't select/unselect the torrent - e.stopPropagation() - if (e.target.classList.contains('disabled')) return - dispatch(action, torrentSummary) - } } // Show files, per-file download status and play buttons, and so on function renderTorrentDetails (torrent, torrentSummary) { + var infoHash = torrentSummary.infoHash var filesElement if (!torrentSummary.files) { // We don't know what files this torrent contains @@ -183,7 +180,10 @@ function TorrentList (state, dispatch) { filesElement = hx`
Files - Open folder + + Open folder + ${fileRows}
@@ -196,11 +196,6 @@ function TorrentList (state, dispatch) { ${filesElement}
` - - function handleOpenFolder (e) { - e.stopPropagation() - dispatch('openFolder', torrentSummary) - } } // Show a single torrentSummary file in the details view for a single torrent @@ -210,15 +205,20 @@ function TorrentList (state, dispatch) { var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%' // Second, render the file as a table row + var infoHash = torrentSummary.infoHash var icon var rowClass = '' - if (state.playing.infoHash === torrentSummary.infoHash && state.playing.fileIndex === index) { + var handleClick + if (state.playing.infoHash === infoHash && state.playing.fileIndex === index) { icon = 'pause_arrow' /* playing? add option to pause */ + handleClick = undefined // TODO: pause audio } else if (TorrentPlayer.isPlayable(file)) { icon = 'play_arrow' /* playable? add option to play */ + handleClick = dispatcher('play', infoHash, index) } else { icon = 'description' /* file icon, opens in OS default app */ rowClass = isDone ? '' : 'disabled' + handleClick = dispatcher('openFile', infoHash, index) } return hx` @@ -230,17 +230,5 @@ function TorrentList (state, dispatch) { ${prettyBytes(file.length)} ` - - // Finally, let the user click on the row to play media or open files - function handleClick (e) { - e.stopPropagation() - if (icon === 'pause_arrow') { - throw new Error('Unimplemented') // TODO: pause audio - } else if (icon === 'play_arrow') { - dispatch('play', torrentSummary, index) - } else if (isDone) { - dispatch('openFile', torrentSummary, index) - } - } } }