Switch from virtualdom to React

This commit is contained in:
DC
2016-07-19 09:24:37 -07:00
parent fbcf718440
commit 2a1e987d42
68 changed files with 694 additions and 708 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules node_modules
dist build
dist

View File

@@ -45,7 +45,7 @@ var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES) 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() main()

View File

@@ -2,7 +2,7 @@
# This is a truly heinous hack, but it works pretty nicely. # This is a truly heinous hack, but it works pretty nicely.
# Find all modules we're requiring---even conditional requires. # Find all modules we're requiring---even conditional requires.
grep "require('" *.js bin/ main/ renderer/ -R | grep "require('" src/ bin/ -R |
grep '.js:' | grep '.js:' |
sed "s/.*require('\([^'\/]*\).*/\1/" | sed "s/.*require('\([^'\/]*\).*/\1/" |
grep -v '^\.' | grep -v '^\.' |

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
var config = require('../config') var config = require('../src/config')
var open = require('open') var open = require('open')
open(config.CONFIG_PATH) open(config.CONFIG_PATH)

View File

@@ -15,7 +15,7 @@ var rimraf = require('rimraf')
var series = require('run-series') var series = require('run-series')
var zip = require('cross-zip') var zip = require('cross-zip')
var config = require('../config') var config = require('../src/config')
var pkg = require('../package.json') var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION

View File

@@ -1 +1 @@
require('./main') require('./build/main')

View File

@@ -24,20 +24,19 @@
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-prebuilt": "1.2.1", "electron-prebuilt": "1.2.1",
"fs-extra": "^0.27.0", "fs-extra": "^0.27.0",
"hyperx": "^2.0.2",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"main-loop": "^3.2.0",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^5.7.3", "parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"react": "^15.2.1",
"react-dom": "^15.2.1",
"run-parallel": "^1.1.6", "run-parallel": "^1.1.6",
"semver": "^5.1.0", "semver": "^5.1.0",
"simple-concat": "^1.0.0", "simple-concat": "^1.0.0",
"simple-get": "^2.0.0", "simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1", "srt-to-vtt": "^1.1.1",
"virtual-dom": "^2.1.1",
"vlc-command": "^1.0.1", "vlc-command": "^1.0.1",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.2.0", "winreg": "^1.2.0",
@@ -54,6 +53,7 @@
"nobin-debian-installer": "^0.0.10", "nobin-debian-installer": "^0.0.10",
"open": "0.0.5", "open": "0.0.5",
"plist": "^1.2.0", "plist": "^1.2.0",
"react-tools": "^0.13.3",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"standard": "^7.0.0" "standard": "^7.0.0"

View File

@@ -1,5 +0,0 @@
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
module.exports = hx

View File

@@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="main.css" charset="utf-8">
</head>
<body>
<script async src="main.js"></script>
</body>
</html>

View File

@@ -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`
<div class='app ${cls.join(' ')}'>
${Header(state)}
${getErrorPopover(state)}
<div class='content'>${getView(state)}</div>
${getModal(state)}
</div>
`
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`<div class='error'>${error.message}</div>`
})
return hx`
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal (state) {
if (!state.modal) return
var contents = Modals[state.modal.id](state)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content'>
${contents}
</div>
</div>
`
}
function getView (state) {
var url = state.location.url()
return Views[url](state)
}

View File

@@ -1,48 +0,0 @@
module.exports = Header
var {dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function Header (state) {
return hx`
<div class='header'>
${getTitle()}
<div class='nav left float-left'>
<i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'}
title='Back'
onclick=${dispatcher('back')}>
chevron_left
</i>
<i.icon.forward
class=${state.location.hasForward() ? '' : 'disabled'}
title='Forward'
onclick=${dispatcher('forward')}>
chevron_right
</i>
</div>
<div class='nav right float-right'>
${getAddButton()}
</div>
</div>
`
function getTitle () {
if (process.platform === 'darwin') {
return hx`<div class='title ellipsis'>${state.window.title}</div>`
}
}
function getAddButton () {
if (state.location.url() === 'home') {
return hx`
<i
class='icon add'
title='Add torrent'
onclick=${dispatcher('openFiles')}>
add
</i>
`
}
}
}

View File

@@ -4,7 +4,7 @@ var path = require('path')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'WebTorrent, LLC' 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') var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
@@ -15,8 +15,8 @@ module.exports = {
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME: APP_NAME, APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM, APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
@@ -67,12 +67,12 @@ module.exports = {
POSTER_PATH: path.join(getConfigPath(), 'Posters'), POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'), STATIC_PATH: path.join(__dirname, '..', 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'), WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'), WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'), WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
WINDOW_MIN_WIDTH: 425 WINDOW_MIN_WIDTH: 425

View File

@@ -30,9 +30,9 @@ module.exports = class PlaybackController {
url: 'player', url: 'player',
onbeforeload: (cb) => { onbeforeload: (cb) => {
this.play() 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) => { }, (err) => {
if (err) dispatch('error', err) if (err) dispatch('error', err)
}) })
@@ -162,134 +162,136 @@ module.exports = class PlaybackController {
} }
return false return false
} }
}
// Opens the video player to a specific torrent // Opens the video player to a specific torrent
function openPlayer (state, infoHash, index, cb) { openPlayer (infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(state, infoHash) var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
// automatically choose which file in the torrent to play, if necessary // automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError()) if (index === undefined) return cb(new errors.UnplayableError())
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested'
this.update()
var timeout = setTimeout(() => {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus // TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'requested'
sound.play('ERROR')
cb(new Error('Playback timed out. Try again.'))
this.update() this.update()
}, 10000) /* give it a few seconds */
if (torrentSummary.status === 'paused') { var timeout = setTimeout(() => {
dispatch('startTorrentingSummary', torrentSummary) telemetry.logPlayAttempt('timeout')
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, // TODO: remove torrentSummary.playStatus
() => openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb)) torrentSummary.playStatus = 'timeout' /* no seeders available? */
} else { sound.play('ERROR')
openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb) cb(new Error('Playback timed out. Try again.'))
} this.update()
} }, 10000) /* give it a few seconds */
function openPlayerFromActiveTorrent (state, torrentSummary, index, timeout, cb) { if (torrentSummary.status === 'paused') {
var fileSummary = torrentSummary.files[index] dispatch('startTorrentingSummary', torrentSummary)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
// update state () => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
state.playing.infoHash = torrentSummary.infoHash } else {
state.playing.fileIndex = index this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
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 it's audio, parse out the metadata (artist, title, etc) openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
if (state.playing.type === 'audio' && !fileSummary.audioInfo) { var fileSummary = torrentSummary.files[index]
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
// if it's video, check for subtitles files that are done downloading // update state
dispatch('checkForSubtitles') 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 // pick up where we left off
if (fileSummary.selectedSubtitle) { if (fileSummary.currentTime) {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) var fraction = fileSummary.currentTime / fileSummary.duration
} var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) state.playing.jumpToTime = fileSummary.currentTime
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 // if it's audio, parse out the metadata (artist, title, etc)
state.window.title = torrentSummary.files[state.playing.fileIndex].name 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() this.update()
ipcRenderer.send('onPlayerOpen')
cb() 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 // Checks whether we are connected and already casting

View File

@@ -28,7 +28,7 @@ module.exports = class PrefsController {
} }
// Updates a single property in the UNSAVED prefs // 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 // Call savePreferences to save to config.json
update (property, value) { update (property, value) {
var path = property.split('.') var path = property.split('.')

View File

@@ -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 // language, aka the current locale
function isSystemLanguage (language) { function isSystemLanguage (language) {
var iso639 = require('iso-639-1') var iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg "en" var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
var langIso = iso639.getCode(language) // eg "de" if language is "German" var langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO return langIso === osLangISO
} }
// Make sure we don't have two subtitle tracks with the same label // 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) { function relabelSubtitles (subtitles) {
var counts = {} var counts = {}
subtitles.tracks.forEach(function (track) { subtitles.tracks.forEach(function (track) {

View File

@@ -184,7 +184,7 @@ function showDoneNotification (torrent) {
silent: true silent: true
}) })
notif.onclick = function () { notif.onClick = function () {
ipcRenderer.send('show') ipcRenderer.send('show')
} }

View File

@@ -17,7 +17,7 @@ function dispatch (...args) {
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`. // Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler // For these, `dispatcher(<args>)` 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(). // each update().
function dispatcher (...args) { function dispatcher (...args) {
var str = JSON.stringify(args) var str = JSON.stringify(args)

View File

@@ -1,7 +1,6 @@
module.exports = LocationHistory module.exports = LocationHistory
function LocationHistory () { function LocationHistory () {
if (!new.target) return new LocationHistory()
this._history = [] this._history = []
this._forward = [] this._forward = []
this._pending = false this._pending = false

View File

@@ -10,7 +10,7 @@ var config = require('../../config')
// Change `state.saved` (which will be saved back to config.json on exit) as // 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 // needed, for example to deal with config.json format changes across versions
function run (state) { function run (state) {
// Replace "{ version: 1 }" with app version (semver) // Replace '{ version: 1 }' with app version (semver)
if (!semver.valid(state.saved.version)) { if (!semver.valid(state.saved.version)) {
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
} }

View File

@@ -140,7 +140,7 @@ function logUncaughtError (procName, err) {
} }
// The user pressed play. It either worked, timed out, or showed the // 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) { function logPlayAttempt (result) {
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) { if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
return console.error('Unknown play attempt result', result) return console.error('Unknown play attempt result', result)

View File

@@ -5,11 +5,8 @@ crashReporter.init()
const dragDrop = require('drag-drop') const dragDrop = require('drag-drop')
const electron = require('electron') const electron = require('electron')
const mainLoop = require('main-loop') const React = require('react')
const ReactDOM = require('react-dom')
const createElement = require('virtual-dom/create-element')
const diff = require('virtual-dom/diff')
const patch = require('virtual-dom/patch')
const config = require('../config') const config = require('../config')
const App = require('./views/app') 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 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. // 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) State.load(onState)
@@ -74,17 +74,6 @@ function onState (err, _state) {
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT) 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 // Listen for messages from the main process
setupIpc() setupIpc()
@@ -92,7 +81,8 @@ function onState (err, _state) {
// Do this at least once a second to give every file in every torrentSummary // 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 // a progress bar and to keep the cursor in sync when playing a video
setInterval(update, 1000) setInterval(update, 1000)
requestAnimationFrame(redrawIfNecessary) window.requestAnimationFrame(renderIfNecessary)
app = ReactDOM.render(<App state={state} />, document.querySelector('body'))
// OS integrations: // OS integrations:
// ...drag and drop a torrent or video file to play or seed // ...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 // ...same thing if you paste a torrent
document.addEventListener('paste', onPaste) 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('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
@@ -133,33 +123,23 @@ function lazyLoadCast () {
return Cast 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. // Calls render() to go from state -> UI, then applies to vdom to the real DOM.
// Runs at 60fps, but only executes when necessary // Runs at 60fps, but only executes when necessary
var needsRedraw = 0 var needsRender = 0
function redrawIfNecessary () { function renderIfNecessary () {
if (needsRedraw > 1) console.log('combining %d update() calls into one update', needsRedraw) if (needsRender > 1) console.log('combining %d update() calls into one update', needsRender)
if (needsRedraw) { if (needsRender) {
controllers.playback.showOrHidePlayerControls() controllers.playback.showOrHidePlayerControls()
vdomLoop.update(state) app.setState(state)
updateElectron() updateElectron()
needsRedraw = 0 needsRender = 0
} }
requestAnimationFrame(redrawIfNecessary) window.requestAnimationFrame(renderIfNecessary)
} }
function update () { function update () {
needsRedraw++ needsRender++
} }
// Some state changes can't be reflected in the DOM, instead we have to // 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) if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action) 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' || if (action !== 'mediaMouseMoved' ||
controllers.playback.showOrHidePlayerControls()) { controllers.playback.showOrHidePlayerControls()) {
update() update()
@@ -315,6 +295,7 @@ function backToList () {
var contentTag = document.querySelector('.content') var contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 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, // Work around virtual-dom issue: it doesn't expose its redraw function,
// and only redraws on requestAnimationFrame(). That means when the user // and only redraws on requestAnimationFrame(). That means when the user
// closes the window (hide window / minimize to tray) and we want to pause // closes the window (hide window / minimize to tray) and we want to pause

95
src/renderer/views/app.js Normal file
View File

@@ -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 = (
<div className={'app ' + cls.join(' ')}>
{Header(state)}
{getErrorPopover(state)}
<div className='content'>{getView(state)}</div>
{getModal(state)}
</div>
)
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 (<div className='error'>{error.message}</div>)
})
return (
<div className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
<div className='title'>Error</div>
{errorElems}
</div>
)
}
function getModal (state) {
if (!state.modal) return
var contents = Modals[state.modal.id](state)
return (
<div className='modal'>
<div className='modal-background'></div>
<div className='modal-content'>
{contents}
</div>
</div>
)
}
function getView (state) {
var url = state.location.url()
return Views[url](state)
}

View File

@@ -1,11 +1,11 @@
module.exports = CreateTorrentPage module.exports = CreateTorrentPage
var createTorrent = require('create-torrent') const React = require('react')
var path = require('path') const createTorrent = require('create-torrent')
var prettyBytes = require('prettier-bytes') const path = require('path')
const prettyBytes = require('prettier-bytes')
var {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function CreateTorrentPage (state) { function CreateTorrentPage (state) {
var info = state.location.current() var info = state.location.current()
@@ -36,63 +36,63 @@ function CreateTorrentPage (state) {
// as the default name. Show all files relative to the base folder. // as the default name. Show all files relative to the base folder.
var defaultName, basePath var defaultName, basePath
if (files.length === 1) { 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 defaultName = files[0].name
basePath = pathPrefix basePath = pathPrefix
} else { } 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) defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix) basePath = path.dirname(pathPrefix)
} }
var maxFileElems = 100 var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) { var fileElems = files.slice(0, maxFileElems).map(function (file) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
return hx`<div>${relativePath}</div>` return (<div>{relativePath}</div>)
}) })
if (files.length > maxFileElems) { if (files.length > maxFileElems) {
fileElems.push(hx`<div>+ ${maxFileElems - files.length} more</div>`) fileElems.push(<div>+ {maxFileElems - files.length} more</div>)
} }
var trackers = createTorrent.announceList.join('\n') var trackers = createTorrent.announceList.join('\n')
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed' var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
return hx` return (
<div class='create-torrent'> <div className='create-torrent'>
<h2>Create torrent ${defaultName}</h2> <h2>Create torrent {defaultName}</h2>
<p class="torrent-info"> <p className='torrent-info'>
${torrentInfo} {torrentInfo}
</p> </p>
<p class='torrent-attribute'> <p className='torrent-attribute'>
<label>Path:</label> <label>Path:</label>
<div class='torrent-attribute'>${pathPrefix}</div> <div className='torrent-attribute'>{pathPrefix}</div>
</p> </p>
<div class='expand-collapse ${collapsedClass}' <div className={'expand-collapse ' + collapsedClass}
onclick=${dispatcher('toggleCreateTorrentAdvanced')}> onClick={dispatcher('toggleCreateTorrentAdvanced')}>
${info.showAdvanced ? 'Basic' : 'Advanced'} {info.showAdvanced ? 'Basic' : 'Advanced'}
</div> </div>
<div class="create-torrent-advanced ${collapsedClass}"> <div className={'create-torrent-advanced ' + collapsedClass}>
<p class='torrent-attribute'> <p className='torrent-attribute'>
<label>Comment:</label> <label>Comment:</label>
<textarea class='torrent-attribute torrent-comment'></textarea> <textarea className='torrent-attribute torrent-comment'></textarea>
</p> </p>
<p class='torrent-attribute'> <p className='torrent-attribute'>
<label>Trackers:</label> <label>Trackers:</label>
<textarea class='torrent-attribute torrent-trackers'>${trackers}</textarea> <textarea className='torrent-attribute torrent-trackers'>{trackers}</textarea>
</p> </p>
<p class='torrent-attribute'> <p className='torrent-attribute'>
<label>Private:</label> <label>Private:</label>
<input type='checkbox' class='torrent-is-private' value='torrent-is-private'> <input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
</p> </p>
<p class='torrent-attribute'> <p className='torrent-attribute'>
<label>Files:</label> <label>Files:</label>
<div>${fileElems}</div> <div>{fileElems}</div>
</p> </p>
</div> </div>
<p class="float-right"> <p className='float-right'>
<button class='button-flat light' onclick=${dispatcher('back')}>Cancel</button> <button className='button-flat light' onClick={dispatcher('back')}>Cancel</button>
<button class='button-raised' onclick=${handleOK}>Create Torrent</button> <button className='button-raised' onClick={handleOK}>Create Torrent</button>
</p> </p>
</div> </div>
` )
function handleOK () { function handleOK () {
var announceList = document.querySelector('.torrent-trackers').value var announceList = document.querySelector('.torrent-trackers').value
@@ -118,10 +118,10 @@ function CreateTorrentPage (state) {
} }
function CreateTorrentErrorPage () { function CreateTorrentErrorPage () {
return hx` return (
<div class='create-torrent'> <div className='create-torrent'>
<h2>Create torrent</h2> <h2>Create torrent</h2>
<p class="torrent-info"> <p className='torrent-info'>
<p> <p>
Sorry, you must select at least one file that is not a hidden file. Sorry, you must select at least one file that is not a hidden file.
</p> </p>
@@ -129,13 +129,13 @@ function CreateTorrentErrorPage () {
Hidden files, starting with a . character, are not included. Hidden files, starting with a . character, are not included.
</p> </p>
</p> </p>
<p class="float-right"> <p className='float-right'>
<button class='button-flat light' onclick=${dispatcher('back')}> <button className='button-flat light' onClick={dispatcher('back')}>
Cancel Cancel
</button> </button>
</p> </p>
</div> </div>
` )
} }
// Finds the longest common prefix // Finds the longest common prefix

View File

@@ -0,0 +1,47 @@
module.exports = Header
const React = require('react')
const {dispatcher} = require('../lib/dispatcher')
function Header (state) {
return (
<div className='header'>
{getTitle()}
<div className='nav left float-left'>
<i
className={'icon back ' + (state.location.hasBack() ? '' : 'disabled')}
title='Back'
onClick={dispatcher('back')}>
chevron_left
</i>
<i
className={'icon forward ' + (state.location.hasForward() ? '' : 'disabled')}
title='Forward'
onClick={dispatcher('forward')}>
chevron_right
</i>
</div>
<div className='nav right float-right'>
{getAddButton()}
</div>
</div>
)
function getTitle () {
if (process.platform !== 'darwin') return null
return (<div className='title ellipsis'>{state.window.title}</div>)
}
function getAddButton () {
if (state.location.url() !== 'home') return null
return (
<i
className='icon add'
title='Add torrent'
onClick={dispatcher('openFiles')}>
add
</i>
)
}
}

View File

@@ -1,22 +1,23 @@
module.exports = OpenTorrentAddressModal module.exports = OpenTorrentAddressModal
var {dispatch, dispatcher} = require('../lib/dispatcher') const React = require('react')
var hx = require('../lib/hx')
const {dispatch, dispatcher} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) { function OpenTorrentAddressModal (state) {
return hx` return (
<div class='open-torrent-address-modal'> <div className='open-torrent-address-modal'>
<p><label>Enter torrent address or magnet link</label></p> <p><label>Enter torrent address or magnet link</label></p>
<p> <p>
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} /> <input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
</p> </p>
<p class='float-right'> <p className='float-right'>
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button> <button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button class='button button-raised' onclick=${handleOK}>OK</button> <button className='button button-raised' onClick={handleOK}>OK</button>
</p> </p>
<script>document.querySelector('#add-torrent-url').focus()</script> <script>document.querySelector('#add-torrent-url').focus()</script>
</div> </div>
` )
} }
function handleKeyPress (e) { function handleKeyPress (e) {

View File

@@ -1,27 +1,27 @@
module.exports = Player module.exports = Player
var Bitfield = require('bitfield') const React = require('react')
var prettyBytes = require('prettier-bytes') const Bitfield = require('bitfield')
var zeroFill = require('zero-fill') const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill')
var hx = require('../lib/hx') const TorrentSummary = require('../lib/torrent-summary')
var TorrentSummary = require('../lib/torrent-summary') const {dispatch, dispatcher} = require('../lib/dispatcher')
var {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
function Player (state) { function Player (state) {
// Show the video as large as will fit in the window, play immediately // 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 // If the video is on Chromecast or Airplay, show a title screen instead
var showVideo = state.playing.location === 'local' var showVideo = state.playing.location === 'local'
return hx` return (
<div <div
class='player' className='player'
onwheel=${handleVolumeWheel} onWheel={handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}> onMouseMove={dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)} {showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)} {renderPlayerControls(state)}
</div> </div>
` )
} }
// Handles volume change by wheel // Handles volume change by wheel
@@ -91,42 +91,42 @@ function renderMedia (state) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx` trackTags.push(
<track <track
${isSelected ? 'default' : ''} default={isSelected ? 'default' : ''}
label=${track.label} label={track.label}
type='subtitles' type='subtitles'
src=${track.buffer}> src={track.buffer} />
`) )
} }
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
var mediaTag = hx` var MediaTagName = state.playing.type
<div var mediaTag = (
src='${state.server.localURL}' <MediaTagName
ondblclick=${dispatcher('toggleFullScreen')} src={state.server.localURL}
onloadedmetadata=${onLoadedMetadata} onDoubleClick={dispatcher('toggleFullScreen')}
onended=${onEnded} onLoadedMetadata={onLoadedMetadata}
onstalling=${dispatcher('mediaStalled')} onEnded={onEnded}
onerror=${dispatcher('mediaError')} onStalling={dispatcher('mediaStalled')}
ontimeupdate=${dispatcher('mediaTimeUpdate')} onError={dispatcher('mediaError')}
onencrypted=${dispatcher('mediaEncrypted')} onTimeUpdate={dispatcher('mediaTimeUpdate')}
oncanplay=${onCanPlay}> onEncrypted={dispatcher('mediaEncrypted')}
${trackTags} onCanPlay={onCanPlay}>
</div> {trackTags}
` </MediaTagName>
mediaTag.tagName = state.playing.type // conditional tag name )
// Show the media. // Show the media.
return hx` return (
<div <div
class='letterbox' className='letterbox'
onmousemove=${dispatcher('mediaMouseMoved')}> onMouseMove={dispatcher('mediaMouseMoved')}>
${mediaTag} {mediaTag}
${renderOverlay(state)} {renderOverlay(state)}
</div> </div>
` )
// As soon as we know the video dimensions, resize the window // As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
@@ -177,11 +177,11 @@ function renderOverlay (state) {
return return
} }
return hx` return (
<div class='media-overlay-background' style=${style}> <div className='media-overlay-background' style={style}>
<div class='media-overlay'>${elems}</div> <div className='media-overlay'>{elems}</div>
</div> </div>
` )
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
@@ -207,36 +207,36 @@ function renderAudioMetadata (state) {
// Show a small info box in the middle of the screen with title/album/etc // Show a small info box in the middle of the screen with title/album/etc
var elems = [] var elems = []
if (artist) { if (artist) {
elems.push(hx` elems.push((
<div class='audio-artist'> <div className='audio-artist'>
<label>Artist</label>${artist} <label>Artist</label>{artist}
</div> </div>
`) ))
} }
if (album) { if (album) {
elems.push(hx` elems.push((
<div class='audio-album'> <div className='audio-album'>
<label>Album</label>${album} <label>Album</label>{album}
</div> </div>
`) ))
} }
if (track) { if (track) {
elems.push(hx` elems.push((
<div class='audio-track'> <div className='audio-track'>
<label>Track</label>${track} <label>Track</label>{track}
</div> </div>
`) ))
} }
// Align the title with the other info, if available. Otherwise, center title // Align the title with the other info, if available. Otherwise, center title
var emptyLabel = hx`<label></label>` var emptyLabel = (<label></label>)
elems.unshift(hx` elems.unshift((
<div class='audio-title'> <div className='audio-title'>
${elems.length ? emptyLabel : undefined}${title} {elems.length ? emptyLabel : undefined}{title}
</div> </div>
`) ))
return hx`<div class='audio-metadata'>${elems}</div>` return (<div className='audio-metadata'>{elems}</div>)
} }
function renderLoadingSpinner (state) { function renderLoadingSpinner (state) {
@@ -252,16 +252,16 @@ function renderLoadingSpinner (state) {
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces) fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
} }
return hx` return (
<div class='media-stalled'> <div className='media-stalled'>
<div class='loading-spinner'>&nbsp;</div> <div className='loading-spinner'>&nbsp;</div>
<div class='loading-status ellipsis'> <div className='loading-status ellipsis'>
<span class='progress'>${fileProgress}%</span> downloaded, <span className='progress'>{fileProgress}%</span> downloaded,
<span> ${prettyBytes(prog.downloadSpeed || 0)}/s</span> <span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> ${prettyBytes(prog.uploadSpeed || 0)}/s</span> <span> {prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div> </div>
</div> </div>
` )
} }
function renderCastScreen (state) { function renderCastScreen (state) {
@@ -300,15 +300,15 @@ function renderCastScreen (state) {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
return hx` return (
<div class='letterbox' style=${style}> <div className='letterbox' style={style}>
<div class='cast-screen'> <div className='cast-screen'>
<i class='icon'>${castIcon}</i> <i className='icon'>{castIcon}</i>
<div class='cast-type'>${castType}</div> <div className='cast-type'>{castType}</div>
<div class='cast-status'>${castStatus}</div> <div className='cast-status'>{castStatus}</div>
</div> </div>
</div> </div>
` )
} }
function renderCastOptions (state) { function renderCastOptions (state) {
@@ -320,19 +320,19 @@ function renderCastOptions (state) {
var items = devices.map(function (device, ix) { var items = devices.map(function (device, ix) {
var isSelected = player.device === device var isSelected = player.device === device
var name = device.name var name = device.name
return hx` return (
<li onclick=${dispatcher('selectCastDevice', ix)}> <li onClick={dispatcher('selectCastDevice', ix)}>
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${name} {name}
</li> </li>
` )
}) })
return hx` return (
<ul.options-list> <ul className='options-list'>
${items} {items}
</ul> </ul>
` )
} }
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
@@ -341,25 +341,25 @@ function renderSubtitlesOptions (state) {
var items = subtitles.tracks.map(function (track, ix) { var items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix var isSelected = state.playing.subtitles.selectedIndex === ix
return hx` return (
<li onclick=${dispatcher('selectSubtitle', ix)}> <li onClick={dispatcher('selectSubtitle', ix)}>
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i> <i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
${track.label} {track.label}
</li> </li>
` )
}) })
var noneSelected = state.playing.subtitles.selectedIndex === -1 var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked') var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx` return (
<ul.options-list> <ul className='options-list'>
${items} {items}
<li onclick=${dispatcher('selectSubtitle', -1)}> <li onClick={dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i> <i className='icon'>{noneClass}</i>
None None
</li> </li>
</ul> </ul>
` )
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
@@ -372,45 +372,41 @@ function renderPlayerControls (state) {
: '' : ''
var elements = [ var elements = [
hx` <div className='playback-bar'>
<div class='playback-bar'> {renderLoadingBar(state)}
${renderLoadingBar(state)} <div
<div className='playback-cursor'
class='playback-cursor' style={playbackCursorStyle}>
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub},
ondrag=${handleScrub}>
</div>
</div> </div>
`, <div
hx` className='scrub-bar'
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}> draggable='true'
${state.playing.isPaused ? 'play_arrow' : 'pause'} onDragStart={handleDragStart}
</i> onClick={handleScrub}
`, onDrag={handleScrub}>
hx` </div>
<i </div>,
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}> <i className='icon play-pause float-left' onClick={dispatcher('playPause')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} {state.playing.isPaused ? 'play_arrow' : 'pause'}
</i> </i>,
`
<i
className='icon fullscreen float-right'
onClick={dispatcher('toggleFullScreen')}>
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
] ]
if (state.playing.type === 'video') { if (state.playing.type === 'video') {
// show closed captions icon // show closed captions icon
elements.push(hx` elements.push((
<i.icon.closed-caption.float-right <i
class=${captionsClass} className={'icon closed-caption float-right ' + captionsClass}
onclick=${handleSubtitles}> onClick={handleSubtitles}>
closed_caption closed_caption
</i> </i>
`) ))
} }
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
@@ -447,13 +443,13 @@ function renderPlayerControls (state) {
} }
var buttonIcon = buttonIcons[castType][isCasting] var buttonIcon = buttonIcons[castType][isCasting]
elements.push(hx` elements.push((
<i.icon.device.float-right <i
class=${buttonClass} className={'icon device float-right ' + buttonClass}
onclick=${buttonHandler}> onClick={buttonHandler}>
${buttonIcon} {buttonIcon}
</i> </i>
`) ))
}) })
// Render volume slider // Render volume slider
@@ -469,50 +465,50 @@ function renderPlayerControls (state) {
'color-stop(' + (volume * 100) + '%, #727272))' 'color-stop(' + (volume * 100) + '%, #727272))'
} }
elements.push(hx` elements.push((
<div class='volume float-left'> <div className='volume float-left'>
<i <i
class='icon volume-icon float-left' className='icon volume-icon float-left'
onmousedown=${handleVolumeMute}> onMouseDown={handleVolumeMute}>
${volumeIcon} {volumeIcon}
</i> </i>
<input <input
class='volume-slider float-right' className='volume-slider float-right'
type='range' min='0' max='1' step='0.05' type='range' min='0' max='1' step='0.05'
value=${volumeChanging !== false ? volumeChanging : volume} value={volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub} onMouseDown={handleVolumeScrub}
onmouseup=${handleVolumeScrub} onMouseUp={handleVolumeScrub}
onmousemove=${handleVolumeScrub} onMouseMove={handleVolumeScrub}
style=${volumeStyle} style={volumeStyle}
/> />
</div> </div>
`) ))
// Show video playback progress // Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime) var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration) var durationStr = formatTime(state.playing.duration)
elements.push(hx` elements.push((
<span class='time float-left'> <span className='time float-left'>
${currentTimeStr} / ${durationStr} {currentTimeStr} / {durationStr}
</span> </span>
`) ))
// render playback rate // render playback rate
if (state.playing.playbackRate !== 1) { if (state.playing.playbackRate !== 1) {
elements.push(hx` elements.push((
<span class='rate float-left'> <span className='rate float-left'>
${state.playing.playbackRate}x {state.playing.playbackRate}x
</span> </span>
`) ))
} }
return hx` return (
<div class='controls'> <div className='controls'>
${elements} {elements}
${renderCastOptions(state)} {renderCastOptions(state)}
${renderSubtitlesOptions(state)} {renderSubtitlesOptions(state)}
</div> </div>
` )
function handleDragStart (e) { function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac // Prevent the cursor from changing, eg to a green + icon on Mac
@@ -546,7 +542,7 @@ function renderPlayerControls (state) {
switch (e.type) { switch (e.type) {
case 'mouseup': case 'mouseup':
volumeChanging = false volumeChanging = false
dispatch('setVolume', e.offsetX / 50) dispatch('setVolume', e.nativeEvent.offsetX / 50)
break break
case 'mousedown': case 'mousedown':
volumeChanging = this.value volumeChanging = this.value
@@ -574,7 +570,7 @@ function renderPlayerControls (state) {
var volumeChanging = false var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous // can be 'spongey' / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrentSummary = state.getPlayingTorrentSummary() var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
@@ -597,18 +593,15 @@ function renderLoadingBar (state) {
} }
// Output some bars to show which parts of the file are loaded // Output some bars to show which parts of the file are loaded
return hx` var loadingBarElems = parts.map(function (part) {
<div class='loading-bar'> var style = {
${parts.map(function (part) { left: (100 * part.start / fileProg.numPieces) + '%',
var style = { width: (100 * part.count / fileProg.numPieces) + '%'
left: (100 * part.start / fileProg.numPieces) + '%', }
width: (100 * part.count / fileProg.numPieces) + '%'
}
return hx`<div class='loading-bar-part' style=${style}></div>` return (<div className='loading-bar-part' style={style}></div>)
})} })
</div> return (<div className='loading-bar'>{loadingBarElems}</div>)
`
} }
// Returns the CSS background-image string for a poster image + dark vignette // Returns the CSS background-image string for a poster image + dark vignette

View File

@@ -1,17 +1,17 @@
module.exports = Preferences module.exports = Preferences
var hx = require('../lib/hx') const React = require('react')
var {dispatch} = require('../lib/dispatcher') const remote = require('electron').remote
const dialog = remote.dialog
var remote = require('electron').remote const {dispatch} = require('../lib/dispatcher')
var dialog = remote.dialog
function Preferences (state) { function Preferences (state) {
return hx` return (
<div class='preferences'> <div className='preferences'>
${renderGeneralSection(state)} {renderGeneralSection(state)}
</div> </div>
` )
} }
function renderGeneralSection (state) { function renderGeneralSection (state) {
@@ -44,24 +44,24 @@ function renderDownloadDirSelector (state) {
// - definition should be {icon, title, description} // - definition should be {icon, title, description}
// - controls should be an array of vdom elements // - controls should be an array of vdom elements
function renderSection (definition, controls) { function renderSection (definition, controls) {
var helpElem = !definition.description ? null : hx` var helpElem = !definition.description ? null : (
<div class='help text'> <div className='help text'>
<i.icon>help_outline</i>${definition.description} <i className='icon'>help_outline</i>{definition.description}
</div> </div>
` )
return hx` return (
<section class='section preferences-panel'> <section className='section preferences-panel'>
<div class='section-container'> <div className='section-container'>
<div class='section-heading'> <div className='section-heading'>
<i.icon>${definition.icon}</i>${definition.title} <i className='icon'>{definition.icon}</i>{definition.title}
</div> </div>
${helpElem} {helpElem}
<div class='section-body'> <div className='section-body'>
${controls} {controls}
</div> </div>
</div> </div>
</section> </section>
` )
} }
// Creates a file chooser // Creates a file chooser
@@ -70,25 +70,25 @@ function renderSection (definition, controls) {
// - value should be the current pref, a file or folder path // - value should be the current pref, a file or folder path
// - callback takes a new file or folder path // - callback takes a new file or folder path
function renderFileSelector (definition, value, callback) { function renderFileSelector (definition, value, callback) {
return hx` return (
<div class='control-group'> <div className='control-group'>
<div class='controls'> <div className='controls'>
<label class='control-label'> <label className='control-label'>
<div class='preference-title'>${definition.label}</div> <div className='preference-title'>{definition.label}</div>
<div class='preference-description'>${definition.description}</div> <div className='preference-description'>{definition.description}</div>
</label> </label>
<div class='controls'> <div className='controls'>
<input type='text' class='file-picker-text' <input type='text' className='file-picker-text'
id=${definition.property} id={definition.property}
disabled='disabled' disabled='disabled'
value=${value} /> value={value} />
<button class='btn' onclick=${handleClick}> <button className='btn' onClick={handleClick}>
<i.icon>folder_open</i> <i className='icon'>folder_open</i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
` )
function handleClick () { function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) { dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return if (!Array.isArray(filenames)) return

View File

@@ -1,7 +1,8 @@
module.exports = RemoveTorrentModal module.exports = RemoveTorrentModal
var {dispatch, dispatcher} = require('../lib/dispatcher') const React = require('react')
var hx = require('../lib/hx')
const {dispatch, dispatcher} = require('../lib/dispatcher')
function RemoveTorrentModal (state) { function RemoveTorrentModal (state) {
var message = state.modal.deleteData var message = state.modal.deleteData
@@ -9,15 +10,15 @@ function RemoveTorrentModal (state) {
: 'Are you sure you want to remove this torrent from the list?' : 'Are you sure you want to remove this torrent from the list?'
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove' var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
return hx` return (
<div> <div>
<p><strong>${message}</strong></p> <p><strong>{message}</strong></p>
<p class='float-right'> <p className='float-right'>
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button> <button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button class='button button-raised' onclick=${handleRemove}>${buttonText}</button> <button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
</p> </p>
</div> </div>
` )
function handleRemove () { function handleRemove () {
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData) dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)

View File

@@ -1,24 +1,25 @@
module.exports = TorrentList module.exports = TorrentList
var prettyBytes = require('prettier-bytes') const React = require('react')
const prettyBytes = require('prettier-bytes')
var hx = require('../lib/hx') const TorrentSummary = require('../lib/torrent-summary')
var TorrentSummary = require('../lib/torrent-summary') const TorrentPlayer = require('../lib/torrent-player')
var TorrentPlayer = require('../lib/torrent-player') const {dispatcher} = require('../lib/dispatcher')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) { function TorrentList (state) {
var torrentRows = state.saved.torrents.map( var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary) (torrentSummary) => renderTorrent(torrentSummary)
) )
return hx` return (
<div class='torrent-list'> <div className='torrent-list'>
${torrentRows} {torrentRows}
<div class='torrent-placeholder'> <div className='torrent-placeholder'>
<span class='ellipsis'>Drop a torrent file here or paste a magnet link</span> <span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div> </div>
</div>` </div>
)
function renderTorrent (torrentSummary) { function renderTorrent (torrentSummary) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
@@ -41,71 +42,70 @@ function TorrentList (state) {
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus) if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled') if (!infoHash) classes.push('disabled')
classes = classes.join(' ') return (
return hx` <div style={style} className={classes.join(' ')}
<div style=${style} class=${classes} onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)} onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}> {renderTorrentMetadata(torrentSummary)}
${renderTorrentMetadata(torrentSummary)} {infoHash ? renderTorrentButtons(torrentSummary) : ''}
${infoHash ? renderTorrentButtons(torrentSummary) : ''} {isSelected ? renderTorrentDetails(torrentSummary) : ''}
${isSelected ? renderTorrentDetails(torrentSummary) : ''}
</div> </div>
` )
} }
// Show name, download status, % complete // Show name, download status, % complete
function renderTorrentMetadata (torrentSummary) { function renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...' var name = torrentSummary.name || 'Loading torrent...'
var elements = [hx` var elements = [(
<div class='name ellipsis'>${name}</div> <div className='name ellipsis'>{name}</div>
`] )]
// If it's downloading/seeding then show progress info // If it's downloading/seeding then show progress info
var prog = torrentSummary.progress var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) { if (torrentSummary.status !== 'paused' && prog) {
elements.push(hx` elements.push((
<div class='ellipsis'> <div className='ellipsis'>
${renderPercentProgress()} {renderPercentProgress()}
${renderTotalProgress()} {renderTotalProgress()}
${renderPeers()} {renderPeers()}
${renderDownloadSpeed()} {renderDownloadSpeed()}
${renderUploadSpeed()} {renderUploadSpeed()}
${renderEta()} {renderEta()}
</div> </div>
`) ))
} }
return hx`<div class='metadata'>${elements}</div>` return (<div className='metadata'>{elements}</div>)
function renderPercentProgress () { function renderPercentProgress () {
var progress = Math.floor(100 * prog.progress) var progress = Math.floor(100 * prog.progress)
return hx`<span>${progress}%</span>` return (<span>{progress}%</span>)
} }
function renderTotalProgress () { function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded) var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0) var total = prettyBytes(prog.length || 0)
if (downloaded === total) { if (downloaded === total) {
return hx`<span>${downloaded}</span>` return (<span>{downloaded}</span>)
} else { } else {
return hx`<span>${downloaded} / ${total}</span>` return (<span>{downloaded} / {total}</span>)
} }
} }
function renderPeers () { function renderPeers () {
if (prog.numPeers === 0) return if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers' var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>` return (<span>{prog.numPeers} {count}</span>)
} }
function renderDownloadSpeed () { function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return if (prog.downloadSpeed === 0) return
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>` return (<span> {prettyBytes(prog.downloadSpeed)}/s</span>)
} }
function renderUploadSpeed () { function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>` return (<span> {prettyBytes(prog.uploadSpeed)}/s</span>)
} }
function renderEta () { function renderEta () {
@@ -126,7 +126,7 @@ function TorrentList (state) {
var minutesStr = (hours || minutes) ? minutes + 'm' : '' var minutesStr = (hours || minutes) ? minutes + 'm' : ''
var secondsStr = seconds + 's' var secondsStr = seconds + 's'
return hx`<span>ETA: ${hoursStr} ${minutesStr} ${secondsStr}</span>` return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
} }
} }
@@ -171,34 +171,34 @@ function TorrentList (state) {
// Only show the play button for torrents that contain playable media // Only show the play button for torrents that contain playable media
var playButton var playButton
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) { if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = hx` playButton = (
<i.button-round.icon.play <i
title=${playTooltip} title={playTooltip}
class=${playClass} className={'button-round icon play ' + playClass}
onclick=${dispatcher('playFile', infoHash)}> onClick={dispatcher('playFile', infoHash)}>
${playIcon} {playIcon}
</i> </i>
` )
} }
return hx` return (
<div class='buttons'> <div className='buttons'>
${positionElem} {positionElem}
${playButton} {playButton}
<i.button-round.icon.download <i
class=${torrentSummary.status} className={'button-round icon download ' + torrentSummary.status}
title=${downloadTooltip} title={downloadTooltip}
onclick=${dispatcher('toggleTorrent', infoHash)}> onClick={dispatcher('toggleTorrent', infoHash)}>
${downloadIcon} {downloadIcon}
</i> </i>
<i <i
class='icon delete' className='icon delete'
title='Remove torrent' title='Remove torrent'
onclick=${dispatcher('confirmDeleteTorrent', infoHash, false)}> onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}>
close close
</i> </i>
</div> </div>
` )
} }
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
@@ -209,7 +209,7 @@ function TorrentList (state) {
var message = torrentSummary.status === 'paused' var message = torrentSummary.status === 'paused'
? 'Failed to load torrent info. Click the download button to try again...' ? 'Failed to load torrent info. Click the download button to try again...'
: 'Downloading torrent info...' : 'Downloading torrent info...'
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = (<div className='files warning'>{message}</div>)
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files var fileRows = torrentSummary.files
@@ -221,20 +221,20 @@ function TorrentList (state) {
}) })
.map((object) => renderFileRow(torrentSummary, object.file, object.index)) .map((object) => renderFileRow(torrentSummary, object.file, object.index))
filesElement = hx` filesElement = (
<div class='files'> <div className='files'>
<table> <table>
${fileRows} {fileRows}
</table> </table>
</div> </div>
` )
} }
return hx` return (
<div class='torrent-details'> <div className='torrent-details'>
${filesElement} {filesElement}
</div> </div>
` )
} }
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
@@ -272,27 +272,27 @@ function TorrentList (state) {
var rowClass = '' var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx` return (
<tr onclick=${handleClick}> <tr onClick={handleClick}>
<td class='col-icon ${rowClass}'> <td className={'col-icon ' + rowClass}>
${positionElem} {positionElem}
<i class='icon'>${icon}</i> <i className='icon'>{icon}</i>
</td> </td>
<td class='col-name ${rowClass}'> <td className={'col-name ' + rowClass}>
${file.name} {file.name}
</td> </td>
<td class='col-progress ${rowClass}'> <td className={'col-progress ' + rowClass}>
${isSelected ? progress : ''} {isSelected ? progress : ''}
</td> </td>
<td class='col-size ${rowClass}'> <td className={'col-size ' + rowClass}>
${prettyBytes(file.length)} {prettyBytes(file.length)}
</td> </td>
<td class='col-select' <td className='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}> onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i> <i className='icon'>{isSelected ? 'close' : 'add'}</i>
</td> </td>
</tr> </tr>
` )
} }
} }
@@ -301,18 +301,18 @@ function renderRadialProgressBar (fraction, cssClass) {
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'} var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'} var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx` return (
<div class="radial-progress ${cssClass}"> <div className={'radial-progress ' + cssClass}>
<div class="circle"> <div className='circle'>
<div class="mask full" style=${transformFill}> <div className='mask full' style={transformFill}>
<div class="fill" style=${transformFill}></div> <div className='fill' style={transformFill}></div>
</div> </div>
<div class="mask half"> <div className='mask half'>
<div class="fill" style=${transformFill}></div> <div className='fill' style={transformFill}></div>
<div class="fill fix" style=${transformFix}></div> <div className='fill fix' style={transformFix}></div>
</div> </div>
</div> </div>
<div class="inset"></div> <div className='inset'></div>
</div> </div>
` )
} }

View File

@@ -1,9 +1,9 @@
module.exports = UnsupportedMediaModal module.exports = UnsupportedMediaModal
var electron = require('electron') const React = require('react')
const electron = require('electron')
var {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UnsupportedMediaModal (state) { function UnsupportedMediaModal (state) {
var err = state.modal.error var err = state.modal.error
@@ -11,22 +11,22 @@ function UnsupportedMediaModal (state) {
? err.getMessage() ? err.getMessage()
: err : err
var actionButton = state.modal.vlcInstalled var actionButton = state.modal.vlcInstalled
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>` ? (<button className='button-raised' onClick={onPlay}>Play in VLC</button>)
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>` : (<button className='button-raised' onClick={onInstall}>Install VLC</button>)
var vlcMessage = state.modal.vlcNotFound var vlcMessage = state.modal.vlcNotFound
? 'Couldn\'t run VLC. Please make sure it\'s installed.' ? 'Couldn\'t run VLC. Please make sure it\'s installed.'
: '' : ''
return hx` return (
<div> <div>
<p><strong>Sorry, we can't play that file.</strong></p> <p><strong>Sorry, we can't play that file.</strong></p>
<p>${message}</p> <p>{message}</p>
<p class='float-right'> <p className='float-right'>
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button> <button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
${actionButton} {actionButton}
</p> </p>
<p class='error-text'>${vlcMessage}</p> <p className='error-text'>{vlcMessage}</p>
</div> </div>
` )
function onInstall () { function onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/') electron.shell.openExternal('http://www.videolan.org/vlc/')

View File

@@ -1,21 +1,21 @@
module.exports = UpdateAvailableModal module.exports = UpdateAvailableModal
var electron = require('electron') const React = require('react')
const electron = require('electron')
var {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UpdateAvailableModal (state) { function UpdateAvailableModal (state) {
return hx` return (
<div class='update-available-modal'> <div className='update-available-modal'>
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p> <p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p> <p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
<p class='float-right'> <p className='float-right'>
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button> <button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button> <button className='button button-raised' onClick={handleOK}>Show Download Page</button>
</p> </p>
</div> </div>
` )
function handleOK () { function handleOK () {
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases') electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')

View File

@@ -82,7 +82,7 @@ table {
font-weight: 400; font-weight: 400;
src: local('Material Icons'), src: local('Material Icons'),
local('MaterialIcons-Regular'), local('MaterialIcons-Regular'),
url(../static/MaterialIcons-Regular.woff2) format('woff2'); url(MaterialIcons-Regular.woff2) format('woff2');
} }
.icon { .icon {

13
static/main.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title>
<link rel="stylesheet" href="main.css">
<script>
require('../build/renderer/main.js')
</script>
</head>
<body></body>
</html>

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title>
<style> <style>
body { body {
background-color: #282828; background-color: #282828;
@@ -14,9 +15,11 @@
height: 140px; height: 140px;
} }
</style> </style>
<script>
require('../build/renderer/webtorrent.js')
</script>
</head> </head>
<body> <body>
<script async src="webtorrent.js"></script> <img alt="WebTorrent" src="WebTorrent.png">
<img src="../static/WebTorrent.png">
</body> </body>
</html> </html>