diff --git a/main/index.css b/main/index.css index fca08549..130ccfa3 100644 --- a/main/index.css +++ b/main/index.css @@ -214,7 +214,7 @@ body.drag::before { width: calc(100% - 170px); } -.torrent .btn { +.torrent .btn, .torrent .delete { float: right; margin-top: 20px; margin-left: 15px; @@ -222,7 +222,12 @@ body.drag::before { display: none; } -.torrent:hover .btn { +.torrent .delete { + opacity: 0.5; + color: #bbb; +} + +.torrent:hover .btn, .torrent:hover .delete { display: block; } @@ -254,3 +259,50 @@ body.drag::before { .torrent .status :not(:last-child)::after { content: ' — '; } + + +/* + * VIDEO CONTROLS + */ +.player-controls .bottom-bar { + position: fixed; + width: 100%; + height: 38px; + bottom: 0; + opacity: 0; + background-color: rgba(0, 0, 0, 0.25); +} + +.player:hover .bottom-bar { + opacity: 1; +} + +.player-controls .loading-bar { + position: relative; + width: 100%; + height: 3px; + background-color: rgba(0, 0, 0, 0.5); +} + +.player-controls .loading-bar-part { + position: absolute; + top: 0; + height: 100%; + background-color: rgba(100, 0, 0, 0.8); +} + +.player-controls .playback-cursor { + position: absolute; + top: -2px; + width: 7px; + height: 7px; + border-radius: 2px; + border: 3px solid #bbbbbb; +} + +.player-controls .play-pause { + display: block; + width: 20px; + height: 20px; + margin: 5px auto; +} diff --git a/main/index.js b/main/index.js index 48cdcf20..049fbd79 100644 --- a/main/index.js +++ b/main/index.js @@ -29,7 +29,8 @@ global.WEBTORRENT_ANNOUNCE = createTorrent.announceList }) var state = global.state = { - server: null, + server: null, /* local WebTorrent-to-HTTP server */ + player: null, /* 'local', 'airplay', or 'chromecast'. persists across videos */ view: { url: '/', dock: { @@ -37,15 +38,18 @@ var state = global.state = { progress: 0 }, devices: { - airplay: null, - chromecast: null + airplay: null, /* airplay client. finds and manages AppleTVs */ + chromecast: null /* chromecast client. finds and manages Chromecasts */ }, - client: null, // TODO: remove this - // history: [], + client: null, /* the WebTorrent client */ + // history: [], /* track how we got to the current view. enables Back button */ // historyIndex: 0, isFocused: true, - mainWindowBounds: null, - title: 'WebTorrent' + mainWindowBounds: null, /* x y width height */ + title: 'WebTorrent' /* current window title */ + }, + video: { + isPaused: false } } @@ -129,6 +133,9 @@ function dispatch (action, ...args) { if (action === 'openPlayer') { openPlayer(args[0] /* torrent */) } + if (action === 'deleteTorrent') { + deleteTorrent(args[0] /* torrent */) + } if (action === 'openChromecast') { openChromecast(args[0] /* torrent */) } @@ -146,6 +153,10 @@ function dispatch (action, ...args) { state.view.url = '/' update() } + if (action === 'playPause') { + state.video.isPaused = !state.video.isPaused + update() + } } electron.ipcRenderer.on('addTorrent', function (e, torrentId) { @@ -243,6 +254,16 @@ function openPlayer (torrent) { }) } +function deleteTorrent (torrent) { + console.log('Deleting %o', torrent) + torrent.isDeleting = true + update() + state.view.client.remove(torrent.infoHash, function () { + console.log('Deleted torrent ' + torrent.infoHash) + update() + }) +} + function openChromecast (torrent) { startServer(torrent, function () { state.view.chromecast.play(state.server.networkURL, { title: 'WebTorrent — ' + torrent.name }) diff --git a/main/views/player.js b/main/views/player.js index 9fc1e678..7798e46e 100644 --- a/main/views/player.js +++ b/main/views/player.js @@ -3,15 +3,29 @@ module.exports = Player var h = require('virtual-dom/h') function Player (state, dispatch) { + // Unfortunately, play/pause can't be done just by modifying HTML. + // Instead, grab the DOM node and play/pause it if necessary + var videoElement = document.querySelector('video') + if (videoElement !== null && + videoElement.paused !== state.video.isPaused) { + if (state.video.isPaused) { + videoElement.pause() + } else { + videoElement.play() + } + } + return h('.player', [ h('video', { src: state.server.localURL, autoplay: true, - controls: true, onloadedmetadata: onLoadedMetadata - }) + }), + renderPlayerControls(state, dispatch) ]) + // As soon as the video loads far enough to know the dimensions, resize the + // window to match the video resolution function onLoadedMetadata (e) { var video = e.target var dimensions = { @@ -22,3 +36,37 @@ function Player (state, dispatch) { } } +// Renders all video controls: play/pause, scrub, loading bar +// TODO: cast buttons +function renderPlayerControls (state, dispatch) { + return h('.player-controls', [ + h('.bottom-bar', [ + h('.loading-bar', { + onclick: () => dispatch('playbackJump') + }, renderLoadingBar(state)), + h('.playback-cursor', { + style: {left: '125px'} + }), + h('i.icon.play-pause', { + onclick: () => dispatch('playPause') + }, state.video.isPaused ? 'play_arrow' : 'pause') + ]) + ]) +} + +// Renders the loading bar. Shows which parts of the torrent are loaded, which +// can be "spongey" / non-contiguous +function renderLoadingBar (state) { + // TODO: get real data from webtorrent + return [ + h('.loading-bar-part', { + style: {left: '10px', width: '50px'} + }), + h('.loading-bar-part', { + style: {left: '90px', width: '40px'} + }), + h('.loading-bar-part', { + style: {left: '135px', width: '5px'} + }) + ] +} diff --git a/main/views/torrent-list.js b/main/views/torrent-list.js index ee215281..88dfca3f 100644 --- a/main/views/torrent-list.js +++ b/main/views/torrent-list.js @@ -4,64 +4,66 @@ var h = require('virtual-dom/h') var prettyBytes = require('pretty-bytes') function TorrentList (state, dispatch) { - return h('.torrent-list', getList()) + var torrents = state.view.client + ? state.view.client.torrents + : [] - function getList () { - return state.view.client.torrents.map(function (torrent) { - var style = {} - if (torrent.posterURL) { - style['background-image'] = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%), url("' + torrent.posterURL + '")' - } - return h('.torrent', { - style: style - }, [ - h('.metadata', [ - h('.name.ellipsis', torrent.name || 'Loading torrent...'), - h('.status', [ - h('span.progress', Math.floor(100 * torrent.progress) + '%'), - (function () { - if (torrent.ready && torrent.files.length > 1) { - return h('span.files', torrent.files.length + ' files') - } - })(), - h('span', torrent.numPeers + ' ' + (torrent.numPeers === 1 ? 'peer' : 'peers')), - h('span', prettyBytes(torrent.downloadSpeed) + '/s'), - h('span', prettyBytes(torrent.uploadSpeed) + '/s') - ]) - ]), - h('i.btn.icon.play', { - className: !torrent.ready ? 'disabled' : '', - onclick: openPlayer - }, 'play_arrow'), - (function () { - if (state.view.chromecast) { - return h('i.btn.icon.chromecast', { - className: !torrent.ready ? 'disabled' : '', - onclick: openChromecast - }, 'cast') - } - })(), - (function () { - if (state.view.devices.airplay) { - return h('i.btn.icon.airplay', { - className: !torrent.ready ? 'disabled' : '', - onclick: openAirplay - }, 'airplay') - } - })() - ]) - - function openPlayer () { - dispatch('openPlayer', torrent) - } - - function openChromecast () { - dispatch('openChromecast', torrent) - } - - function openAirplay () { - dispatch('openAirplay', torrent) - } - }) - } + var list = torrents.map((torrent) => renderTorrent(state, dispatch, torrent)) + return h('.torrent-list', list) +} + +// Renders a torrent in the torrent list +// Includes name, download status, play button, background image +// May be expanded for additional info, including the list of files inside +function renderTorrent (state, dispatch, torrent) { + // Background image: show some nice visuals, like a frame from the movie, if possible + var style = {} + if (torrent.posterURL) { + style['background-image'] = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%), url("' + torrent.posterURL + '")' + } + + // Foreground: name of the torrent, basic info like size, play button, + // cast buttons if available, and delete + var elements = [ + renderTorrentMetadata(torrent), + h('i.icon.delete', { + onclick: () => dispatch('deleteTorrent', torrent) + }, 'close'), + h('i.btn.icon.play', { + className: !torrent.ready ? 'disabled' : '', + onclick: () => dispatch('openPlayer', torrent) + }, 'play_arrow') + ] + if (state.view.chromecast) { + elements.push(h('i.btn.icon.chromecast', { + className: !torrent.ready ? 'disabled' : '', + onclick: () => dispatch('openChromecast', torrent) + }, 'cast')) + } + if (state.view.devices.airplay) { + elements.push(h('i.btn.icon.airplay', { + className: !torrent.ready ? 'disabled' : '', + onclick: () => dispatch('openAirplay', torrent) + }, 'airplay')) + } + + return h('.torrent', {style: style}, elements) +} + +// Renders the torrent name and download progress +function renderTorrentMetadata (torrent) { + return h('.metadata', [ + h('.name.ellipsis', torrent.name || 'Loading torrent...'), + h('.status', [ + h('span.progress', Math.floor(100 * torrent.progress) + '%'), + (function () { + if (torrent.ready && torrent.files.length > 1) { + return h('span.files', torrent.files.length + ' files') + } + })(), + h('span', torrent.numPeers + ' ' + (torrent.numPeers === 1 ? 'peer' : 'peers')), + h('span', prettyBytes(torrent.downloadSpeed) + '/s'), + h('span', prettyBytes(torrent.uploadSpeed) + '/s') + ]) + ]) }