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`
-
- `
-
- 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 (
+
+ )
+}
+
+function getModal (state) {
+ if (!state.modal) return
+ var contents = Modals[state.modal.id](state)
+ return (
+
+ )
+}
+
+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}
-
+
Path:
-
${pathPrefix}
+
{pathPrefix}
-
- ${info.showAdvanced ? 'Basic' : 'Advanced'}
+
+ {info.showAdvanced ? 'Basic' : 'Advanced'}
-
-
+
+
Comment:
-
+
-
+
Trackers:
-
+
-
+
Private:
-
+
-
+
Files:
-
${fileElems}
+
{fileElems}
-
- Cancel
- Create Torrent
+
+ Cancel
+ Create Torrent
- `
+ )
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.
-
-
+
+
Cancel
- `
+ )
}
// 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 or tag
- var mediaTag = hx`
-
- ${trackTags}
-
- `
- mediaTag.tagName = state.playing.type // conditional tag name
+ var MediaTagName = state.playing.type
+ var mediaTag = (
+
+ {trackTags}
+
+ )
// Show the media.
- return hx`
+ return (
- ${mediaTag}
- ${renderOverlay(state)}
+ className='letterbox'
+ onMouseMove={dispatcher('mediaMouseMoved')}>
+ {mediaTag}
+ {renderOverlay(state)}
- `
+ )
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
@@ -177,11 +177,11 @@ function renderOverlay (state) {
return
}
- return hx`
-