diff --git a/.gitignore b/.gitignore index 76add878..ab57381f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +build +dist diff --git a/bin/check-deps.js b/bin/check-deps.js index d17e4d09..f5e9e42c 100755 --- a/bin/check-deps.js +++ b/bin/check-deps.js @@ -45,7 +45,7 @@ var BUILT_IN_ELECTRON_MODULES = [ 'electron' ] var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES) -var EXECUTABLE_DEPS = ['gh-release', 'standard'] +var EXECUTABLE_DEPS = ['gh-release', 'standard', 'react-tools'] main() diff --git a/bin/list-deps.sh b/bin/list-deps.sh index ac492cd4..b1ff6c32 100755 --- a/bin/list-deps.sh +++ b/bin/list-deps.sh @@ -2,7 +2,7 @@ # This is a truly heinous hack, but it works pretty nicely. # Find all modules we're requiring---even conditional requires. -grep "require('" *.js bin/ main/ renderer/ -R | +grep "require('" src/ bin/ -R | grep '.js:' | sed "s/.*require('\([^'\/]*\).*/\1/" | grep -v '^\.' | diff --git a/bin/open-config.js b/bin/open-config.js index 3c9a5990..e2756e15 100755 --- a/bin/open-config.js +++ b/bin/open-config.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -var config = require('../config') +var config = require('../src/config') var open = require('open') open(config.CONFIG_PATH) diff --git a/bin/package.js b/bin/package.js index 4708524b..cf28d56e 100755 --- a/bin/package.js +++ b/bin/package.js @@ -15,7 +15,7 @@ var rimraf = require('rimraf') var series = require('run-series') var zip = require('cross-zip') -var config = require('../config') +var config = require('../src/config') var pkg = require('../package.json') var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION diff --git a/index.js b/index.js index 140c9430..67582133 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -require('./main') +require('./build/main') diff --git a/package.json b/package.json index b3f266db..3df00984 100644 --- a/package.json +++ b/package.json @@ -24,20 +24,19 @@ "drag-drop": "^2.11.0", "electron-prebuilt": "1.2.1", "fs-extra": "^0.27.0", - "hyperx": "^2.0.2", "iso-639-1": "^1.2.1", "languagedetect": "^1.1.1", - "main-loop": "^3.2.0", "musicmetadata": "^2.0.2", "network-address": "^1.1.0", "parse-torrent": "^5.7.3", "prettier-bytes": "^1.0.1", + "react": "^15.2.1", + "react-dom": "^15.2.1", "run-parallel": "^1.1.6", "semver": "^5.1.0", "simple-concat": "^1.0.0", "simple-get": "^2.0.0", "srt-to-vtt": "^1.1.1", - "virtual-dom": "^2.1.1", "vlc-command": "^1.0.1", "webtorrent": "0.x", "winreg": "^1.2.0", @@ -54,6 +53,7 @@ "nobin-debian-installer": "^0.0.10", "open": "0.0.5", "plist": "^1.2.0", + "react-tools": "^0.13.3", "rimraf": "^2.5.2", "run-series": "^1.1.4", "standard": "^7.0.0" diff --git a/renderer/lib/hx.js b/renderer/lib/hx.js deleted file mode 100644 index d1434273..00000000 --- a/renderer/lib/hx.js +++ /dev/null @@ -1,5 +0,0 @@ -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - -module.exports = hx diff --git a/renderer/main.html b/renderer/main.html deleted file mode 100644 index e2f14ed4..00000000 --- a/renderer/main.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/renderer/views/app.js b/renderer/views/app.js deleted file mode 100644 index caa6fdbe..00000000 --- a/renderer/views/app.js +++ /dev/null @@ -1,86 +0,0 @@ -module.exports = App - -var hx = require('../lib/hx') -var Header = require('./header') - -var Views = { - 'home': require('./torrent-list'), - 'player': require('./player'), - 'create-torrent': require('./create-torrent'), - 'preferences': require('./preferences') -} - -var Modals = { - 'open-torrent-address-modal': require('./open-torrent-address-modal'), - 'remove-torrent-modal': require('./remove-torrent-modal'), - 'update-available-modal': require('./update-available-modal'), - 'unsupported-media-modal': require('./unsupported-media-modal') -} - -function App (state) { - console.time('render app') - // Hide player controls while playing video, if the mouse stays still for a while - // Never hide the controls when: - // * The mouse is over the controls or we're scrubbing (see CSS) - // * The video is paused - // * The video is playing remotely on Chromecast or Airplay - var hideControls = state.location.url() === 'player' && - state.playing.mouseStationarySince !== 0 && - new Date().getTime() - state.playing.mouseStationarySince > 2000 && - !state.playing.isPaused && - state.playing.location === 'local' && - state.playing.playbackRate === 1 - - var cls = [ - 'view-' + state.location.url(), /* e.g. view-home, view-player */ - 'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */ - ] - if (state.window.isFullScreen) cls.push('is-fullscreen') - if (state.window.isFocused) cls.push('is-focused') - if (hideControls) cls.push('hide-video-controls') - - var vdom = hx` -
- ${Header(state)} - ${getErrorPopover(state)} -
${getView(state)}
- ${getModal(state)} -
- ` - console.timeEnd('render app') - return vdom -} - -function getErrorPopover (state) { - var now = new Date().getTime() - var recentErrors = state.errors.filter((x) => now - x.time < 5000) - var hasErrors = recentErrors.length > 0 - - var errorElems = recentErrors.map(function (error) { - return hx`
${error.message}
` - }) - return hx` -
-
Error
- ${errorElems} -
- ` -} - -function getModal (state) { - if (!state.modal) return - var contents = Modals[state.modal.id](state) - return hx` - - ` -} - -function getView (state) { - var url = state.location.url() - return Views[url](state) -} diff --git a/renderer/views/header.js b/renderer/views/header.js deleted file mode 100644 index 21b9f0c7..00000000 --- a/renderer/views/header.js +++ /dev/null @@ -1,48 +0,0 @@ -module.exports = Header - -var {dispatcher} = require('../lib/dispatcher') -var hx = require('../lib/hx') - -function Header (state) { - return hx` -
- ${getTitle()} - - -
- ` - - function getTitle () { - if (process.platform === 'darwin') { - return hx`
${state.window.title}
` - } - } - - function getAddButton () { - if (state.location.url() === 'home') { - return hx` - - add - - ` - } - } -} diff --git a/config.js b/src/config.js similarity index 86% rename from config.js rename to src/config.js index d01b5390..eb839f8c 100644 --- a/config.js +++ b/src/config.js @@ -4,7 +4,7 @@ var path = require('path') var APP_NAME = 'WebTorrent' var APP_TEAM = 'WebTorrent, LLC' -var APP_VERSION = require('./package.json').version +var APP_VERSION = require('../package.json').version var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') @@ -15,8 +15,8 @@ module.exports = { TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, - APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), - APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), + APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'), + APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'), APP_NAME: APP_NAME, APP_TEAM: APP_TEAM, APP_VERSION: APP_VERSION, @@ -67,12 +67,12 @@ module.exports = { POSTER_PATH: path.join(getConfigPath(), 'Posters'), ROOT_PATH: __dirname, - STATIC_PATH: path.join(__dirname, 'static'), + STATIC_PATH: path.join(__dirname, '..', 'static'), TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), - WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'), - WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'), - WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'), + WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'), + WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'), + WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'), WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents WINDOW_MIN_WIDTH: 425 diff --git a/crash-reporter.js b/src/crash-reporter.js similarity index 100% rename from crash-reporter.js rename to src/crash-reporter.js diff --git a/main/announcement.js b/src/main/announcement.js similarity index 100% rename from main/announcement.js rename to src/main/announcement.js diff --git a/main/dialog.js b/src/main/dialog.js similarity index 100% rename from main/dialog.js rename to src/main/dialog.js diff --git a/main/dock.js b/src/main/dock.js similarity index 100% rename from main/dock.js rename to src/main/dock.js diff --git a/main/handlers.js b/src/main/handlers.js similarity index 100% rename from main/handlers.js rename to src/main/handlers.js diff --git a/main/index.js b/src/main/index.js similarity index 100% rename from main/index.js rename to src/main/index.js diff --git a/main/ipc.js b/src/main/ipc.js similarity index 100% rename from main/ipc.js rename to src/main/ipc.js diff --git a/main/log.js b/src/main/log.js similarity index 100% rename from main/log.js rename to src/main/log.js diff --git a/main/menu.js b/src/main/menu.js similarity index 100% rename from main/menu.js rename to src/main/menu.js diff --git a/main/power-save-blocker.js b/src/main/power-save-blocker.js similarity index 100% rename from main/power-save-blocker.js rename to src/main/power-save-blocker.js diff --git a/main/shell.js b/src/main/shell.js similarity index 100% rename from main/shell.js rename to src/main/shell.js diff --git a/main/shortcuts.js b/src/main/shortcuts.js similarity index 100% rename from main/shortcuts.js rename to src/main/shortcuts.js diff --git a/main/squirrel-win32.js b/src/main/squirrel-win32.js similarity index 100% rename from main/squirrel-win32.js rename to src/main/squirrel-win32.js diff --git a/main/thumbar.js b/src/main/thumbar.js similarity index 100% rename from main/thumbar.js rename to src/main/thumbar.js diff --git a/main/tray.js b/src/main/tray.js similarity index 100% rename from main/tray.js rename to src/main/tray.js diff --git a/main/updater.js b/src/main/updater.js similarity index 100% rename from main/updater.js rename to src/main/updater.js diff --git a/main/vlc.js b/src/main/vlc.js similarity index 100% rename from main/vlc.js rename to src/main/vlc.js diff --git a/main/windows/about.js b/src/main/windows/about.js similarity index 100% rename from main/windows/about.js rename to src/main/windows/about.js diff --git a/main/windows/index.js b/src/main/windows/index.js similarity index 100% rename from main/windows/index.js rename to src/main/windows/index.js diff --git a/main/windows/main.js b/src/main/windows/main.js similarity index 100% rename from main/windows/main.js rename to src/main/windows/main.js diff --git a/main/windows/webtorrent.js b/src/main/windows/webtorrent.js similarity index 100% rename from main/windows/webtorrent.js rename to src/main/windows/webtorrent.js diff --git a/renderer/controllers/media-controller.js b/src/renderer/controllers/media-controller.js similarity index 100% rename from renderer/controllers/media-controller.js rename to src/renderer/controllers/media-controller.js diff --git a/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js similarity index 56% rename from renderer/controllers/playback-controller.js rename to src/renderer/controllers/playback-controller.js index 204557f9..8a84cefa 100644 --- a/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -30,9 +30,9 @@ module.exports = class PlaybackController { url: 'player', onbeforeload: (cb) => { this.play() - openPlayer(this.state, infoHash, index, cb) + this.openPlayer(infoHash, index, cb) }, - onbeforeunload: (cb) => closePlayer(this.state, this.config, cb) + onbeforeunload: (cb) => this.closePlayer(cb) }, (err) => { if (err) dispatch('error', err) }) @@ -162,134 +162,136 @@ module.exports = class PlaybackController { } return false } -} -// Opens the video player to a specific torrent -function openPlayer (state, infoHash, index, cb) { - var torrentSummary = TorrentSummary.getByKey(state, infoHash) + // Opens the video player to a specific torrent + openPlayer (infoHash, index, cb) { + var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) - // automatically choose which file in the torrent to play, if necessary - if (index === undefined) index = torrentSummary.defaultPlayFileIndex - if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) - if (index === undefined) return cb(new errors.UnplayableError()) + // automatically choose which file in the torrent to play, if necessary + if (index === undefined) index = torrentSummary.defaultPlayFileIndex + if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) + if (index === undefined) return cb(new errors.UnplayableError()) - // update UI to show pending playback - if (torrentSummary.progress !== 1) sound.play('PLAY') - // TODO: remove torrentSummary.playStatus - torrentSummary.playStatus = 'requested' - this.update() - - var timeout = setTimeout(() => { - telemetry.logPlayAttempt('timeout') + // update UI to show pending playback + if (torrentSummary.progress !== 1) sound.play('PLAY') // TODO: remove torrentSummary.playStatus - torrentSummary.playStatus = 'timeout' /* no seeders available? */ - sound.play('ERROR') - cb(new Error('Playback timed out. Try again.')) + torrentSummary.playStatus = 'requested' this.update() - }, 10000) /* give it a few seconds */ - if (torrentSummary.status === 'paused') { - dispatch('startTorrentingSummary', torrentSummary) - ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, - () => openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb)) - } else { - openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb) - } -} + var timeout = setTimeout(() => { + telemetry.logPlayAttempt('timeout') + // TODO: remove torrentSummary.playStatus + torrentSummary.playStatus = 'timeout' /* no seeders available? */ + sound.play('ERROR') + cb(new Error('Playback timed out. Try again.')) + this.update() + }, 10000) /* give it a few seconds */ -function openPlayerFromActiveTorrent (state, torrentSummary, index, timeout, cb) { - var fileSummary = torrentSummary.files[index] - - // update state - state.playing.infoHash = torrentSummary.infoHash - state.playing.fileIndex = index - state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video' - : TorrentPlayer.isAudio(fileSummary) ? 'audio' - : 'other' - - // pick up where we left off - if (fileSummary.currentTime) { - var fraction = fileSummary.currentTime / fileSummary.duration - var secondsLeft = fileSummary.duration - fileSummary.currentTime - if (fraction < 0.9 && secondsLeft > 10) { - state.playing.jumpToTime = fileSummary.currentTime + if (torrentSummary.status === 'paused') { + dispatch('startTorrentingSummary', torrentSummary) + ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, + () => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)) + } else { + this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb) } } - // if it's audio, parse out the metadata (artist, title, etc) - if (state.playing.type === 'audio' && !fileSummary.audioInfo) { - ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) - } + openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) { + var fileSummary = torrentSummary.files[index] - // if it's video, check for subtitles files that are done downloading - dispatch('checkForSubtitles') + // update state + var state = this.state + state.playing.infoHash = torrentSummary.infoHash + state.playing.fileIndex = index + state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video' + : TorrentPlayer.isAudio(fileSummary) ? 'audio' + : 'other' - // enable previously selected subtitle track - if (fileSummary.selectedSubtitle) { - dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) - } - - ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) - ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => { - clearTimeout(timeout) - - // if we timed out (user clicked play a long time ago), don't autoplay - var timedOut = torrentSummary.playStatus === 'timeout' - delete torrentSummary.playStatus - if (timedOut) { - ipcRenderer.send('wt-stop-server') - return this.update() + // pick up where we left off + if (fileSummary.currentTime) { + var fraction = fileSummary.currentTime / fileSummary.duration + var secondsLeft = fileSummary.duration - fileSummary.currentTime + if (fraction < 0.9 && secondsLeft > 10) { + state.playing.jumpToTime = fileSummary.currentTime + } } - // otherwise, play the video - state.window.title = torrentSummary.files[state.playing.fileIndex].name + // if it's audio, parse out the metadata (artist, title, etc) + if (state.playing.type === 'audio' && !fileSummary.audioInfo) { + ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) + } + + // if it's video, check for subtitles files that are done downloading + dispatch('checkForSubtitles') + + // enable previously selected subtitle track + if (fileSummary.selectedSubtitle) { + dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) + } + + ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) + ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => { + clearTimeout(timeout) + + // if we timed out (user clicked play a long time ago), don't autoplay + var timedOut = torrentSummary.playStatus === 'timeout' + delete torrentSummary.playStatus + if (timedOut) { + ipcRenderer.send('wt-stop-server') + return this.update() + } + + // otherwise, play the video + state.window.title = torrentSummary.files[state.playing.fileIndex].name + this.update() + + ipcRenderer.send('onPlayerOpen') + + cb() + }) + } + + closePlayer (cb) { + console.log('closePlayer') + + // Quit any external players, like Chromecast/Airplay/etc or VLC + var state = this.state + if (isCasting(state)) { + Cast.stop() + } + if (state.playing.location === 'vlc') { + ipcRenderer.send('vlcQuit') + } + + // Save volume (this session only, not in state.saved) + state.previousVolume = state.playing.volume + + // Telemetry: track what happens after the user clicks play + var result = state.playing.result // 'success' or 'error' + if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed + else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc + else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame + else console.error('Unknown state.playing.result', state.playing.result) + + // Reset the window contents back to the home screen + state.window.title = this.config.APP_WINDOW_TITLE + state.playing = State.getDefaultPlayState() + state.server = null + + // Reset the window size and location back to where it was + if (state.window.isFullScreen) { + dispatch('toggleFullScreen', false) + } + restoreBounds(state) + + // Tell the WebTorrent process to kill the torrent-to-HTTP server + ipcRenderer.send('wt-stop-server') + + ipcRenderer.send('onPlayerClose') + this.update() - - ipcRenderer.send('onPlayerOpen') - cb() - }) -} - -function closePlayer (state, config, cb) { - console.log('closePlayer') - - // Quit any external players, like Chromecast/Airplay/etc or VLC - if (isCasting(state)) { - Cast.stop() } - if (state.playing.location === 'vlc') { - ipcRenderer.send('vlcQuit') - } - - // Save volume (this session only, not in state.saved) - state.previousVolume = state.playing.volume - - // Telemetry: track what happens after the user clicks play - var result = state.playing.result // 'success' or 'error' - if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed - else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc - else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame - else console.error('Unknown state.playing.result', state.playing.result) - - // Reset the window contents back to the home screen - state.window.title = config.APP_WINDOW_TITLE - state.playing = State.getDefaultPlayState() - state.server = null - - // Reset the window size and location back to where it was - if (state.window.isFullScreen) { - dispatch('toggleFullScreen', false) - } - restoreBounds(state) - - // Tell the WebTorrent process to kill the torrent-to-HTTP server - ipcRenderer.send('wt-stop-server') - - ipcRenderer.send('onPlayerClose') - - this.update() - cb() } // Checks whether we are connected and already casting diff --git a/renderer/controllers/prefs-controller.js b/src/renderer/controllers/prefs-controller.js similarity index 96% rename from renderer/controllers/prefs-controller.js rename to src/renderer/controllers/prefs-controller.js index 9979a2b0..66bc0093 100644 --- a/renderer/controllers/prefs-controller.js +++ b/src/renderer/controllers/prefs-controller.js @@ -28,7 +28,7 @@ module.exports = class PrefsController { } // Updates a single property in the UNSAVED prefs - // For example: updatePreferences("foo.bar", "baz") + // For example: updatePreferences('foo.bar', 'baz') // Call savePreferences to save to config.json update (property, value) { var path = property.split('.') diff --git a/renderer/controllers/subtitles-controller.js b/src/renderer/controllers/subtitles-controller.js similarity index 95% rename from renderer/controllers/subtitles-controller.js rename to src/renderer/controllers/subtitles-controller.js index e1cb1425..cb63d7ce 100644 --- a/renderer/controllers/subtitles-controller.js +++ b/src/renderer/controllers/subtitles-controller.js @@ -116,17 +116,17 @@ function loadSubtitle (file, cb) { }) } -// Checks whether a language name like "English" or "German" matches the system +// Checks whether a language name like 'English' or 'German' matches the system // language, aka the current locale function isSystemLanguage (language) { var iso639 = require('iso-639-1') - var osLangISO = window.navigator.language.split('-')[0] // eg "en" - var langIso = iso639.getCode(language) // eg "de" if language is "German" + var osLangISO = window.navigator.language.split('-')[0] // eg 'en' + var langIso = iso639.getCode(language) // eg 'de' if language is 'German' return langIso === osLangISO } // Make sure we don't have two subtitle tracks with the same label -// Labels each track by language, eg "German", "English", "English 2", ... +// Labels each track by language, eg 'German', 'English', 'English 2', ... function relabelSubtitles (subtitles) { var counts = {} subtitles.tracks.forEach(function (track) { diff --git a/renderer/controllers/torrent-controller.js b/src/renderer/controllers/torrent-controller.js similarity index 99% rename from renderer/controllers/torrent-controller.js rename to src/renderer/controllers/torrent-controller.js index 0225add7..c956fab1 100644 --- a/renderer/controllers/torrent-controller.js +++ b/src/renderer/controllers/torrent-controller.js @@ -184,7 +184,7 @@ function showDoneNotification (torrent) { silent: true }) - notif.onclick = function () { + notif.onClick = function () { ipcRenderer.send('show') } diff --git a/renderer/controllers/torrent-list-controller.js b/src/renderer/controllers/torrent-list-controller.js similarity index 100% rename from renderer/controllers/torrent-list-controller.js rename to src/renderer/controllers/torrent-list-controller.js diff --git a/renderer/controllers/update-controller.js b/src/renderer/controllers/update-controller.js similarity index 100% rename from renderer/controllers/update-controller.js rename to src/renderer/controllers/update-controller.js diff --git a/renderer/lib/capture-video-frame.js b/src/renderer/lib/capture-video-frame.js similarity index 100% rename from renderer/lib/capture-video-frame.js rename to src/renderer/lib/capture-video-frame.js diff --git a/renderer/lib/cast.js b/src/renderer/lib/cast.js similarity index 100% rename from renderer/lib/cast.js rename to src/renderer/lib/cast.js diff --git a/renderer/lib/dispatcher.js b/src/renderer/lib/dispatcher.js similarity index 91% rename from renderer/lib/dispatcher.js rename to src/renderer/lib/dispatcher.js index eba84df7..a17f1a5c 100644 --- a/renderer/lib/dispatcher.js +++ b/src/renderer/lib/dispatcher.js @@ -17,7 +17,7 @@ function dispatch (...args) { // Most DOM event handlers are trivial functions like `() => dispatch()`. // For these, `dispatcher()` is preferred because it memoizes the handler -// function. This prevents virtual-dom from updating the listener functions on +// function. This prevents React from updating the listener functions on // each update(). function dispatcher (...args) { var str = JSON.stringify(args) diff --git a/renderer/lib/errors.js b/src/renderer/lib/errors.js similarity index 100% rename from renderer/lib/errors.js rename to src/renderer/lib/errors.js diff --git a/renderer/lib/location-history.js b/src/renderer/lib/location-history.js similarity index 98% rename from renderer/lib/location-history.js rename to src/renderer/lib/location-history.js index 2c5d88f3..483370dd 100644 --- a/renderer/lib/location-history.js +++ b/src/renderer/lib/location-history.js @@ -1,7 +1,6 @@ module.exports = LocationHistory function LocationHistory () { - if (!new.target) return new LocationHistory() this._history = [] this._forward = [] this._pending = false diff --git a/renderer/lib/migrations.js b/src/renderer/lib/migrations.js similarity index 98% rename from renderer/lib/migrations.js rename to src/renderer/lib/migrations.js index 424f7626..7c1aa8a6 100644 --- a/renderer/lib/migrations.js +++ b/src/renderer/lib/migrations.js @@ -10,7 +10,7 @@ var config = require('../../config') // Change `state.saved` (which will be saved back to config.json on exit) as // needed, for example to deal with config.json format changes across versions function run (state) { - // Replace "{ version: 1 }" with app version (semver) + // Replace '{ version: 1 }' with app version (semver) if (!semver.valid(state.saved.version)) { state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations } diff --git a/renderer/lib/sound.js b/src/renderer/lib/sound.js similarity index 100% rename from renderer/lib/sound.js rename to src/renderer/lib/sound.js diff --git a/renderer/lib/state.js b/src/renderer/lib/state.js similarity index 100% rename from renderer/lib/state.js rename to src/renderer/lib/state.js diff --git a/renderer/lib/telemetry.js b/src/renderer/lib/telemetry.js similarity index 99% rename from renderer/lib/telemetry.js rename to src/renderer/lib/telemetry.js index 7d9203aa..23893492 100644 --- a/renderer/lib/telemetry.js +++ b/src/renderer/lib/telemetry.js @@ -140,7 +140,7 @@ function logUncaughtError (procName, err) { } // The user pressed play. It either worked, timed out, or showed the -// "Play in VLC" codec error +// 'Play in VLC' codec error function logPlayAttempt (result) { if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) { return console.error('Unknown play attempt result', result) diff --git a/renderer/lib/torrent-player.js b/src/renderer/lib/torrent-player.js similarity index 100% rename from renderer/lib/torrent-player.js rename to src/renderer/lib/torrent-player.js diff --git a/renderer/lib/torrent-poster.js b/src/renderer/lib/torrent-poster.js similarity index 100% rename from renderer/lib/torrent-poster.js rename to src/renderer/lib/torrent-poster.js diff --git a/renderer/lib/torrent-summary.js b/src/renderer/lib/torrent-summary.js similarity index 100% rename from renderer/lib/torrent-summary.js rename to src/renderer/lib/torrent-summary.js diff --git a/renderer/main.js b/src/renderer/main.js similarity index 91% rename from renderer/main.js rename to src/renderer/main.js index 453c982d..2ef43d9c 100644 --- a/renderer/main.js +++ b/src/renderer/main.js @@ -5,11 +5,8 @@ crashReporter.init() const dragDrop = require('drag-drop') const electron = require('electron') -const mainLoop = require('main-loop') - -const createElement = require('virtual-dom/create-element') -const diff = require('virtual-dom/diff') -const patch = require('virtual-dom/patch') +const React = require('react') +const ReactDOM = require('react-dom') const config = require('../config') const App = require('./views/app') @@ -43,7 +40,10 @@ var ipcRenderer = electron.ipcRenderer // 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. -var state, vdomLoop +var state + +// Root React component +var app State.load(onState) @@ -74,17 +74,6 @@ function onState (err, _state) { // Lazy-load other stuff, like the AppleTV module, later to keep startup fast window.setTimeout(delayedInit, config.DELAYED_INIT) - // 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) - // Listen for messages from the main process setupIpc() @@ -92,7 +81,8 @@ function onState (err, _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) - requestAnimationFrame(redrawIfNecessary) + window.requestAnimationFrame(renderIfNecessary) + app = ReactDOM.render(, document.querySelector('body')) // OS integrations: // ...drag and drop a torrent or video file to play or seed @@ -101,7 +91,7 @@ function onState (err, _state) { // ...same thing if you paste a torrent document.addEventListener('paste', onPaste) - // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX + // ...focus and blur. Needed to show correct dock icon text ('badge') in OSX window.addEventListener('focus', onFocus) window.addEventListener('blur', onBlur) @@ -133,33 +123,23 @@ function lazyLoadCast () { return Cast } -// 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. // Runs at 60fps, but only executes when necessary -var needsRedraw = 0 +var needsRender = 0 -function redrawIfNecessary () { - if (needsRedraw > 1) console.log('combining %d update() calls into one update', needsRedraw) - if (needsRedraw) { +function renderIfNecessary () { + if (needsRender > 1) console.log('combining %d update() calls into one update', needsRender) + if (needsRender) { controllers.playback.showOrHidePlayerControls() - vdomLoop.update(state) + app.setState(state) updateElectron() - needsRedraw = 0 + needsRender = 0 } - requestAnimationFrame(redrawIfNecessary) + window.requestAnimationFrame(renderIfNecessary) } function update () { - needsRedraw++ + needsRender++ } // Some state changes can't be reflected in the DOM, instead we have to @@ -269,7 +249,7 @@ function dispatch (action, ...args) { if (handler) handler(...args) else console.error('Missing dispatch handler: ' + action) - // Update the virtual-dom, unless it's just a mouse move event + // Update the virtual DOM, unless it's just a mouse move event if (action !== 'mediaMouseMoved' || controllers.playback.showOrHidePlayerControls()) { update() @@ -315,6 +295,7 @@ function backToList () { var contentTag = document.querySelector('.content') if (contentTag) contentTag.scrollTop = 0 + // TODO dcposch: is this still required with React? // 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 diff --git a/src/renderer/views/app.js b/src/renderer/views/app.js new file mode 100644 index 00000000..e451c3a6 --- /dev/null +++ b/src/renderer/views/app.js @@ -0,0 +1,95 @@ +const React = require('react') + +const Header = require('./header') + +const Views = { + 'home': require('./torrent-list'), + 'player': require('./player'), + 'create-torrent': require('./create-torrent'), + 'preferences': require('./preferences') +} + +const Modals = { + 'open-torrent-address-modal': require('./open-torrent-address-modal'), + 'remove-torrent-modal': require('./remove-torrent-modal'), + 'update-available-modal': require('./update-available-modal'), + 'unsupported-media-modal': require('./unsupported-media-modal') +} + +module.exports = class App extends React.Component { + + constructor (props) { + super(props) + this.state = props.state + } + + render () { + console.time('render app') + var state = this.state + + // Hide player controls while playing video, if the mouse stays still for a while + // Never hide the controls when: + // * The mouse is over the controls or we're scrubbing (see CSS) + // * The video is paused + // * The video is playing remotely on Chromecast or Airplay + var hideControls = state.location.url() === 'player' && + state.playing.mouseStationarySince !== 0 && + new Date().getTime() - state.playing.mouseStationarySince > 2000 && + !state.playing.isPaused && + state.playing.location === 'local' && + state.playing.playbackRate === 1 + + var cls = [ + 'view-' + state.location.url(), /* e.g. view-home, view-player */ + 'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */ + ] + if (state.window.isFullScreen) cls.push('is-fullscreen') + if (state.window.isFocused) cls.push('is-focused') + if (hideControls) cls.push('hide-video-controls') + + var vdom = ( +
+ {Header(state)} + {getErrorPopover(state)} +
{getView(state)}
+ {getModal(state)} +
+ ) + console.timeEnd('render app') + return vdom + } +} + +function getErrorPopover (state) { + var now = new Date().getTime() + var recentErrors = state.errors.filter((x) => now - x.time < 5000) + var hasErrors = recentErrors.length > 0 + + var errorElems = recentErrors.map(function (error) { + return (
{error.message}
) + }) + return ( +
+
Error
+ {errorElems} +
+ ) +} + +function getModal (state) { + if (!state.modal) return + var contents = Modals[state.modal.id](state) + return ( +
+
+
+ {contents} +
+
+ ) +} + +function getView (state) { + var url = state.location.url() + return Views[url](state) +} diff --git a/renderer/views/create-torrent.js b/src/renderer/views/create-torrent.js similarity index 64% rename from renderer/views/create-torrent.js rename to src/renderer/views/create-torrent.js index 42585431..5eab4215 100644 --- a/renderer/views/create-torrent.js +++ b/src/renderer/views/create-torrent.js @@ -1,11 +1,11 @@ module.exports = CreateTorrentPage -var createTorrent = require('create-torrent') -var path = require('path') -var prettyBytes = require('prettier-bytes') +const React = require('react') +const createTorrent = require('create-torrent') +const path = require('path') +const prettyBytes = require('prettier-bytes') -var {dispatch, dispatcher} = require('../lib/dispatcher') -var hx = require('../lib/hx') +const {dispatch, dispatcher} = require('../lib/dispatcher') function CreateTorrentPage (state) { var info = state.location.current() @@ -36,63 +36,63 @@ function CreateTorrentPage (state) { // as the default name. Show all files relative to the base folder. var defaultName, basePath if (files.length === 1) { - // Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b" + // Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b' defaultName = files[0].name basePath = pathPrefix } else { - // Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a" + // Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a' defaultName = path.basename(pathPrefix) basePath = path.dirname(pathPrefix) } var maxFileElems = 100 var fileElems = files.slice(0, maxFileElems).map(function (file) { var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) - return hx`
${relativePath}
` + return (
{relativePath}
) }) if (files.length > maxFileElems) { - fileElems.push(hx`
+ ${maxFileElems - files.length} more
`) + fileElems.push(
+ {maxFileElems - files.length} more
) } var trackers = createTorrent.announceList.join('\n') var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed' - return hx` -
-

Create torrent ${defaultName}

-

- ${torrentInfo} + return ( +

+

Create torrent {defaultName}

+

+ {torrentInfo}

-

+

-

${pathPrefix}
+
{pathPrefix}

-
- ${info.showAdvanced ? 'Basic' : 'Advanced'} +
+ {info.showAdvanced ? 'Basic' : 'Advanced'}
-
-

+

+

- +

-

+

- +

-

+

- +

-

+

-

${fileElems}
+
{fileElems}

-

- - +

+ +

- ` + ) function handleOK () { var announceList = document.querySelector('.torrent-trackers').value @@ -118,10 +118,10 @@ function CreateTorrentPage (state) { } function CreateTorrentErrorPage () { - return hx` -
+ return ( +

Create torrent

-

+

Sorry, you must select at least one file that is not a hidden file.

@@ -129,13 +129,13 @@ function CreateTorrentErrorPage () { Hidden files, starting with a . character, are not included.

-

-

- ` + ) } // Finds the longest common prefix diff --git a/src/renderer/views/header.js b/src/renderer/views/header.js new file mode 100644 index 00000000..75a49878 --- /dev/null +++ b/src/renderer/views/header.js @@ -0,0 +1,47 @@ +module.exports = Header + +const React = require('react') + +const {dispatcher} = require('../lib/dispatcher') + +function Header (state) { + return ( +
+ {getTitle()} +
+ + chevron_left + + + chevron_right + +
+
+ {getAddButton()} +
+
+ ) + + function getTitle () { + if (process.platform !== 'darwin') return null + return (
{state.window.title}
) + } + + function getAddButton () { + if (state.location.url() !== 'home') return null + return ( + + add + + ) + } +} diff --git a/renderer/views/open-torrent-address-modal.js b/src/renderer/views/open-torrent-address-modal.js similarity index 52% rename from renderer/views/open-torrent-address-modal.js rename to src/renderer/views/open-torrent-address-modal.js index a2d5c7de..a004d6d1 100644 --- a/renderer/views/open-torrent-address-modal.js +++ b/src/renderer/views/open-torrent-address-modal.js @@ -1,22 +1,23 @@ module.exports = OpenTorrentAddressModal -var {dispatch, dispatcher} = require('../lib/dispatcher') -var hx = require('../lib/hx') +const React = require('react') + +const {dispatch, dispatcher} = require('../lib/dispatcher') function OpenTorrentAddressModal (state) { - return hx` -
+ return ( +

- +

-

- - +

+ +

- ` + ) } function handleKeyPress (e) { diff --git a/renderer/views/player.js b/src/renderer/views/player.js similarity index 72% rename from renderer/views/player.js rename to src/renderer/views/player.js index 3c80d035..295b9866 100644 --- a/renderer/views/player.js +++ b/src/renderer/views/player.js @@ -1,27 +1,27 @@ module.exports = Player -var Bitfield = require('bitfield') -var prettyBytes = require('prettier-bytes') -var zeroFill = require('zero-fill') +const React = require('react') +const Bitfield = require('bitfield') +const prettyBytes = require('prettier-bytes') +const zeroFill = require('zero-fill') -var hx = require('../lib/hx') -var TorrentSummary = require('../lib/torrent-summary') -var {dispatch, dispatcher} = require('../lib/dispatcher') +const TorrentSummary = require('../lib/torrent-summary') +const {dispatch, dispatcher} = require('../lib/dispatcher') // Shows a streaming video player. Standard features + Chromecast + Airplay 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` + return (
- ${showVideo ? renderMedia(state) : renderCastScreen(state)} - ${renderPlayerControls(state)} -
- ` + className='player' + onWheel={handleVolumeWheel} + onMouseMove={dispatcher('mediaMouseMoved')}> + {showVideo ? renderMedia(state) : renderCastScreen(state)} + {renderPlayerControls(state)} +
+ ) } // Handles volume change by wheel @@ -91,42 +91,42 @@ function renderMedia (state) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { var track = state.playing.subtitles.tracks[i] var isSelected = state.playing.subtitles.selectedIndex === i - trackTags.push(hx` + trackTags.push( - `) + src={track.buffer} /> + ) } } // Create the