Switch from virtualdom to React
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
dist
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 '^\.' |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
module.exports = hx
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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('.')
|
||||
@@ -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) {
|
||||
@@ -184,7 +184,7 @@ function showDoneNotification (torrent) {
|
||||
silent: true
|
||||
})
|
||||
|
||||
notif.onclick = function () {
|
||||
notif.onClick = function () {
|
||||
ipcRenderer.send('show')
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ function dispatch (...args) {
|
||||
|
||||
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
||||
// 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().
|
||||
function dispatcher (...args) {
|
||||
var str = JSON.stringify(args)
|
||||
@@ -1,7 +1,6 @@
|
||||
module.exports = LocationHistory
|
||||
|
||||
function LocationHistory () {
|
||||
if (!new.target) return new LocationHistory()
|
||||
this._history = []
|
||||
this._forward = []
|
||||
this._pending = false
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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(<App state={state} />, 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
|
||||
95
src/renderer/views/app.js
Normal file
95
src/renderer/views/app.js
Normal 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)
|
||||
}
|
||||
@@ -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`<div>${relativePath}</div>`
|
||||
return (<div>{relativePath}</div>)
|
||||
})
|
||||
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 collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||
|
||||
return hx`
|
||||
<div class='create-torrent'>
|
||||
<h2>Create torrent ${defaultName}</h2>
|
||||
<p class="torrent-info">
|
||||
${torrentInfo}
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<h2>Create torrent {defaultName}</h2>
|
||||
<p className='torrent-info'>
|
||||
{torrentInfo}
|
||||
</p>
|
||||
<p class='torrent-attribute'>
|
||||
<p className='torrent-attribute'>
|
||||
<label>Path:</label>
|
||||
<div class='torrent-attribute'>${pathPrefix}</div>
|
||||
<div className='torrent-attribute'>{pathPrefix}</div>
|
||||
</p>
|
||||
<div class='expand-collapse ${collapsedClass}'
|
||||
onclick=${dispatcher('toggleCreateTorrentAdvanced')}>
|
||||
${info.showAdvanced ? 'Basic' : 'Advanced'}
|
||||
<div className={'expand-collapse ' + collapsedClass}
|
||||
onClick={dispatcher('toggleCreateTorrentAdvanced')}>
|
||||
{info.showAdvanced ? 'Basic' : 'Advanced'}
|
||||
</div>
|
||||
<div class="create-torrent-advanced ${collapsedClass}">
|
||||
<p class='torrent-attribute'>
|
||||
<div className={'create-torrent-advanced ' + collapsedClass}>
|
||||
<p className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<textarea class='torrent-attribute torrent-comment'></textarea>
|
||||
<textarea className='torrent-attribute torrent-comment'></textarea>
|
||||
</p>
|
||||
<p class='torrent-attribute'>
|
||||
<p className='torrent-attribute'>
|
||||
<label>Trackers:</label>
|
||||
<textarea class='torrent-attribute torrent-trackers'>${trackers}</textarea>
|
||||
<textarea className='torrent-attribute torrent-trackers'>{trackers}</textarea>
|
||||
</p>
|
||||
<p class='torrent-attribute'>
|
||||
<p className='torrent-attribute'>
|
||||
<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 class='torrent-attribute'>
|
||||
<p className='torrent-attribute'>
|
||||
<label>Files:</label>
|
||||
<div>${fileElems}</div>
|
||||
<div>{fileElems}</div>
|
||||
</p>
|
||||
</div>
|
||||
<p class="float-right">
|
||||
<button class='button-flat light' onclick=${dispatcher('back')}>Cancel</button>
|
||||
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
|
||||
<p className='float-right'>
|
||||
<button className='button-flat light' onClick={dispatcher('back')}>Cancel</button>
|
||||
<button className='button-raised' onClick={handleOK}>Create Torrent</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
function handleOK () {
|
||||
var announceList = document.querySelector('.torrent-trackers').value
|
||||
@@ -118,10 +118,10 @@ function CreateTorrentPage (state) {
|
||||
}
|
||||
|
||||
function CreateTorrentErrorPage () {
|
||||
return hx`
|
||||
<div class='create-torrent'>
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<h2>Create torrent</h2>
|
||||
<p class="torrent-info">
|
||||
<p className='torrent-info'>
|
||||
<p>
|
||||
Sorry, you must select at least one file that is not a hidden file.
|
||||
</p>
|
||||
@@ -129,13 +129,13 @@ function CreateTorrentErrorPage () {
|
||||
Hidden files, starting with a . character, are not included.
|
||||
</p>
|
||||
</p>
|
||||
<p class="float-right">
|
||||
<button class='button-flat light' onclick=${dispatcher('back')}>
|
||||
<p className='float-right'>
|
||||
<button className='button-flat light' onClick={dispatcher('back')}>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
47
src/renderer/views/header.js
Normal file
47
src/renderer/views/header.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
<div class='open-torrent-address-modal'>
|
||||
return (
|
||||
<div className='open-torrent-address-modal'>
|
||||
<p><label>Enter torrent address or magnet link</label></p>
|
||||
<p>
|
||||
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
|
||||
</p>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
||||
<button className='button button-raised' onClick={handleOK}>OK</button>
|
||||
</p>
|
||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function handleKeyPress (e) {
|
||||
@@ -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 (
|
||||
<div
|
||||
class='player'
|
||||
onwheel=${handleVolumeWheel}
|
||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||
${renderPlayerControls(state)}
|
||||
</div>
|
||||
`
|
||||
className='player'
|
||||
onWheel={handleVolumeWheel}
|
||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||
{renderPlayerControls(state)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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(
|
||||
<track
|
||||
${isSelected ? 'default' : ''}
|
||||
label=${track.label}
|
||||
default={isSelected ? 'default' : ''}
|
||||
label={track.label}
|
||||
type='subtitles'
|
||||
src=${track.buffer}>
|
||||
`)
|
||||
src={track.buffer} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the <audio> or <video> tag
|
||||
var mediaTag = hx`
|
||||
<div
|
||||
src='${state.server.localURL}'
|
||||
ondblclick=${dispatcher('toggleFullScreen')}
|
||||
onloadedmetadata=${onLoadedMetadata}
|
||||
onended=${onEnded}
|
||||
onstalling=${dispatcher('mediaStalled')}
|
||||
onerror=${dispatcher('mediaError')}
|
||||
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
||||
onencrypted=${dispatcher('mediaEncrypted')}
|
||||
oncanplay=${onCanPlay}>
|
||||
${trackTags}
|
||||
</div>
|
||||
`
|
||||
mediaTag.tagName = state.playing.type // conditional tag name
|
||||
var MediaTagName = state.playing.type
|
||||
var mediaTag = (
|
||||
<MediaTagName
|
||||
src={state.server.localURL}
|
||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||
onLoadedMetadata={onLoadedMetadata}
|
||||
onEnded={onEnded}
|
||||
onStalling={dispatcher('mediaStalled')}
|
||||
onError={dispatcher('mediaError')}
|
||||
onTimeUpdate={dispatcher('mediaTimeUpdate')}
|
||||
onEncrypted={dispatcher('mediaEncrypted')}
|
||||
onCanPlay={onCanPlay}>
|
||||
{trackTags}
|
||||
</MediaTagName>
|
||||
)
|
||||
|
||||
// Show the media.
|
||||
return hx`
|
||||
return (
|
||||
<div
|
||||
class='letterbox'
|
||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||
${mediaTag}
|
||||
${renderOverlay(state)}
|
||||
className='letterbox'
|
||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||
{mediaTag}
|
||||
{renderOverlay(state)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
// As soon as we know the video dimensions, resize the window
|
||||
function onLoadedMetadata (e) {
|
||||
@@ -177,11 +177,11 @@ function renderOverlay (state) {
|
||||
return
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='media-overlay-background' style=${style}>
|
||||
<div class='media-overlay'>${elems}</div>
|
||||
return (
|
||||
<div className='media-overlay-background' style={style}>
|
||||
<div className='media-overlay'>{elems}</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
var elems = []
|
||||
if (artist) {
|
||||
elems.push(hx`
|
||||
<div class='audio-artist'>
|
||||
<label>Artist</label>${artist}
|
||||
elems.push((
|
||||
<div className='audio-artist'>
|
||||
<label>Artist</label>{artist}
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
}
|
||||
if (album) {
|
||||
elems.push(hx`
|
||||
<div class='audio-album'>
|
||||
<label>Album</label>${album}
|
||||
elems.push((
|
||||
<div className='audio-album'>
|
||||
<label>Album</label>{album}
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
}
|
||||
if (track) {
|
||||
elems.push(hx`
|
||||
<div class='audio-track'>
|
||||
<label>Track</label>${track}
|
||||
elems.push((
|
||||
<div className='audio-track'>
|
||||
<label>Track</label>{track}
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
}
|
||||
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = hx`<label></label>`
|
||||
elems.unshift(hx`
|
||||
<div class='audio-title'>
|
||||
${elems.length ? emptyLabel : undefined}${title}
|
||||
var emptyLabel = (<label></label>)
|
||||
elems.unshift((
|
||||
<div className='audio-title'>
|
||||
{elems.length ? emptyLabel : undefined}{title}
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
|
||||
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||
return (<div className='audio-metadata'>{elems}</div>)
|
||||
}
|
||||
|
||||
function renderLoadingSpinner (state) {
|
||||
@@ -252,16 +252,16 @@ function renderLoadingSpinner (state) {
|
||||
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='media-stalled'>
|
||||
<div class='loading-spinner'> </div>
|
||||
<div class='loading-status ellipsis'>
|
||||
<span class='progress'>${fileProgress}%</span> downloaded,
|
||||
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||
return (
|
||||
<div className='media-stalled'>
|
||||
<div className='loading-spinner'> </div>
|
||||
<div className='loading-status ellipsis'>
|
||||
<span className='progress'>{fileProgress}%</span> downloaded,
|
||||
<span>↓ {prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||
<span>↑ {prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function renderCastScreen (state) {
|
||||
@@ -300,15 +300,15 @@ function renderCastScreen (state) {
|
||||
backgroundImage: cssBackgroundImagePoster(state)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='letterbox' style=${style}>
|
||||
<div class='cast-screen'>
|
||||
<i class='icon'>${castIcon}</i>
|
||||
<div class='cast-type'>${castType}</div>
|
||||
<div class='cast-status'>${castStatus}</div>
|
||||
return (
|
||||
<div className='letterbox' style={style}>
|
||||
<div className='cast-screen'>
|
||||
<i className='icon'>{castIcon}</i>
|
||||
<div className='cast-type'>{castType}</div>
|
||||
<div className='cast-status'>{castStatus}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function renderCastOptions (state) {
|
||||
@@ -320,19 +320,19 @@ function renderCastOptions (state) {
|
||||
var items = devices.map(function (device, ix) {
|
||||
var isSelected = player.device === device
|
||||
var name = device.name
|
||||
return hx`
|
||||
<li onclick=${dispatcher('selectCastDevice', ix)}>
|
||||
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
${name}
|
||||
return (
|
||||
<li onClick={dispatcher('selectCastDevice', ix)}>
|
||||
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
{name}
|
||||
</li>
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
return hx`
|
||||
<ul.options-list>
|
||||
${items}
|
||||
return (
|
||||
<ul className='options-list'>
|
||||
{items}
|
||||
</ul>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function renderSubtitlesOptions (state) {
|
||||
@@ -341,25 +341,25 @@ function renderSubtitlesOptions (state) {
|
||||
|
||||
var items = subtitles.tracks.map(function (track, ix) {
|
||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
return hx`
|
||||
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
${track.label}
|
||||
return (
|
||||
<li onClick={dispatcher('selectSubtitle', ix)}>
|
||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
{track.label}
|
||||
</li>
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
return hx`
|
||||
<ul.options-list>
|
||||
${items}
|
||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||
<i.icon>${noneClass}</i>
|
||||
return (
|
||||
<ul className='options-list'>
|
||||
{items}
|
||||
<li onClick={dispatcher('selectSubtitle', -1)}>
|
||||
<i className='icon'>{noneClass}</i>
|
||||
None
|
||||
</li>
|
||||
</ul>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function renderPlayerControls (state) {
|
||||
@@ -372,45 +372,41 @@ function renderPlayerControls (state) {
|
||||
: ''
|
||||
|
||||
var elements = [
|
||||
hx`
|
||||
<div class='playback-bar'>
|
||||
${renderLoadingBar(state)}
|
||||
<div
|
||||
class='playback-cursor'
|
||||
style=${playbackCursorStyle}>
|
||||
</div>
|
||||
<div
|
||||
class='scrub-bar'
|
||||
draggable='true'
|
||||
ondragstart=${handleDragStart}
|
||||
onclick=${handleScrub},
|
||||
ondrag=${handleScrub}>
|
||||
</div>
|
||||
<div className='playback-bar'>
|
||||
{renderLoadingBar(state)}
|
||||
<div
|
||||
className='playback-cursor'
|
||||
style={playbackCursorStyle}>
|
||||
</div>
|
||||
`,
|
||||
hx`
|
||||
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
|
||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>
|
||||
`,
|
||||
hx`
|
||||
<i
|
||||
class='icon fullscreen float-right'
|
||||
onclick=${dispatcher('toggleFullScreen')}>
|
||||
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
</i>
|
||||
`
|
||||
<div
|
||||
className='scrub-bar'
|
||||
draggable='true'
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleScrub}
|
||||
onDrag={handleScrub}>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
<i className='icon play-pause float-left' onClick={dispatcher('playPause')}>
|
||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>,
|
||||
|
||||
<i
|
||||
className='icon fullscreen float-right'
|
||||
onClick={dispatcher('toggleFullScreen')}>
|
||||
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
</i>
|
||||
]
|
||||
|
||||
if (state.playing.type === 'video') {
|
||||
// show closed captions icon
|
||||
elements.push(hx`
|
||||
<i.icon.closed-caption.float-right
|
||||
class=${captionsClass}
|
||||
onclick=${handleSubtitles}>
|
||||
elements.push((
|
||||
<i
|
||||
className={'icon closed-caption float-right ' + captionsClass}
|
||||
onClick={handleSubtitles}>
|
||||
closed_caption
|
||||
</i>
|
||||
`)
|
||||
))
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
elements.push(hx`
|
||||
<i.icon.device.float-right
|
||||
class=${buttonClass}
|
||||
onclick=${buttonHandler}>
|
||||
${buttonIcon}
|
||||
elements.push((
|
||||
<i
|
||||
className={'icon device float-right ' + buttonClass}
|
||||
onClick={buttonHandler}>
|
||||
{buttonIcon}
|
||||
</i>
|
||||
`)
|
||||
))
|
||||
})
|
||||
|
||||
// Render volume slider
|
||||
@@ -469,50 +465,50 @@ function renderPlayerControls (state) {
|
||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||
}
|
||||
|
||||
elements.push(hx`
|
||||
<div class='volume float-left'>
|
||||
elements.push((
|
||||
<div className='volume float-left'>
|
||||
<i
|
||||
class='icon volume-icon float-left'
|
||||
onmousedown=${handleVolumeMute}>
|
||||
${volumeIcon}
|
||||
className='icon volume-icon float-left'
|
||||
onMouseDown={handleVolumeMute}>
|
||||
{volumeIcon}
|
||||
</i>
|
||||
<input
|
||||
class='volume-slider float-right'
|
||||
className='volume-slider float-right'
|
||||
type='range' min='0' max='1' step='0.05'
|
||||
value=${volumeChanging !== false ? volumeChanging : volume}
|
||||
onmousedown=${handleVolumeScrub}
|
||||
onmouseup=${handleVolumeScrub}
|
||||
onmousemove=${handleVolumeScrub}
|
||||
style=${volumeStyle}
|
||||
value={volumeChanging !== false ? volumeChanging : volume}
|
||||
onMouseDown={handleVolumeScrub}
|
||||
onMouseUp={handleVolumeScrub}
|
||||
onMouseMove={handleVolumeScrub}
|
||||
style={volumeStyle}
|
||||
/>
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
|
||||
// Show video playback progress
|
||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||
var durationStr = formatTime(state.playing.duration)
|
||||
elements.push(hx`
|
||||
<span class='time float-left'>
|
||||
${currentTimeStr} / ${durationStr}
|
||||
elements.push((
|
||||
<span className='time float-left'>
|
||||
{currentTimeStr} / {durationStr}
|
||||
</span>
|
||||
`)
|
||||
))
|
||||
|
||||
// render playback rate
|
||||
if (state.playing.playbackRate !== 1) {
|
||||
elements.push(hx`
|
||||
<span class='rate float-left'>
|
||||
${state.playing.playbackRate}x
|
||||
elements.push((
|
||||
<span className='rate float-left'>
|
||||
{state.playing.playbackRate}x
|
||||
</span>
|
||||
`)
|
||||
))
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='controls'>
|
||||
${elements}
|
||||
${renderCastOptions(state)}
|
||||
${renderSubtitlesOptions(state)}
|
||||
return (
|
||||
<div className='controls'>
|
||||
{elements}
|
||||
{renderCastOptions(state)}
|
||||
{renderSubtitlesOptions(state)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
function handleDragStart (e) {
|
||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||
@@ -546,7 +542,7 @@ function renderPlayerControls (state) {
|
||||
switch (e.type) {
|
||||
case 'mouseup':
|
||||
volumeChanging = false
|
||||
dispatch('setVolume', e.offsetX / 50)
|
||||
dispatch('setVolume', e.nativeEvent.offsetX / 50)
|
||||
break
|
||||
case 'mousedown':
|
||||
volumeChanging = this.value
|
||||
@@ -574,7 +570,7 @@ function renderPlayerControls (state) {
|
||||
var volumeChanging = false
|
||||
|
||||
// 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) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary.progress) {
|
||||
@@ -597,18 +593,15 @@ function renderLoadingBar (state) {
|
||||
}
|
||||
|
||||
// Output some bars to show which parts of the file are loaded
|
||||
return hx`
|
||||
<div class='loading-bar'>
|
||||
${parts.map(function (part) {
|
||||
var style = {
|
||||
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||
}
|
||||
var loadingBarElems = parts.map(function (part) {
|
||||
var style = {
|
||||
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
return hx`<div class='loading-bar-part' style=${style}></div>`
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
return (<div className='loading-bar-part' style={style}></div>)
|
||||
})
|
||||
return (<div className='loading-bar'>{loadingBarElems}</div>)
|
||||
}
|
||||
|
||||
// Returns the CSS background-image string for a poster image + dark vignette
|
||||
@@ -1,17 +1,17 @@
|
||||
module.exports = Preferences
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
const React = require('react')
|
||||
const remote = require('electron').remote
|
||||
const dialog = remote.dialog
|
||||
|
||||
var remote = require('electron').remote
|
||||
var dialog = remote.dialog
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
function Preferences (state) {
|
||||
return hx`
|
||||
<div class='preferences'>
|
||||
${renderGeneralSection(state)}
|
||||
return (
|
||||
<div className='preferences'>
|
||||
{renderGeneralSection(state)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
function renderGeneralSection (state) {
|
||||
@@ -44,24 +44,24 @@ function renderDownloadDirSelector (state) {
|
||||
// - definition should be {icon, title, description}
|
||||
// - controls should be an array of vdom elements
|
||||
function renderSection (definition, controls) {
|
||||
var helpElem = !definition.description ? null : hx`
|
||||
<div class='help text'>
|
||||
<i.icon>help_outline</i>${definition.description}
|
||||
var helpElem = !definition.description ? null : (
|
||||
<div className='help text'>
|
||||
<i className='icon'>help_outline</i>{definition.description}
|
||||
</div>
|
||||
`
|
||||
return hx`
|
||||
<section class='section preferences-panel'>
|
||||
<div class='section-container'>
|
||||
<div class='section-heading'>
|
||||
<i.icon>${definition.icon}</i>${definition.title}
|
||||
)
|
||||
return (
|
||||
<section className='section preferences-panel'>
|
||||
<div className='section-container'>
|
||||
<div className='section-heading'>
|
||||
<i className='icon'>{definition.icon}</i>{definition.title}
|
||||
</div>
|
||||
${helpElem}
|
||||
<div class='section-body'>
|
||||
${controls}
|
||||
{helpElem}
|
||||
<div className='section-body'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
// Creates a file chooser
|
||||
@@ -70,25 +70,25 @@ function renderSection (definition, controls) {
|
||||
// - value should be the current pref, a file or folder path
|
||||
// - callback takes a new file or folder path
|
||||
function renderFileSelector (definition, value, callback) {
|
||||
return hx`
|
||||
<div class='control-group'>
|
||||
<div class='controls'>
|
||||
<label class='control-label'>
|
||||
<div class='preference-title'>${definition.label}</div>
|
||||
<div class='preference-description'>${definition.description}</div>
|
||||
return (
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
<div className='preference-description'>{definition.description}</div>
|
||||
</label>
|
||||
<div class='controls'>
|
||||
<input type='text' class='file-picker-text'
|
||||
id=${definition.property}
|
||||
<div className='controls'>
|
||||
<input type='text' className='file-picker-text'
|
||||
id={definition.property}
|
||||
disabled='disabled'
|
||||
value=${value} />
|
||||
<button class='btn' onclick=${handleClick}>
|
||||
<i.icon>folder_open</i>
|
||||
value={value} />
|
||||
<button className='btn' onClick={handleClick}>
|
||||
<i className='icon'>folder_open</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
function handleClick () {
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||
if (!Array.isArray(filenames)) return
|
||||
@@ -1,7 +1,8 @@
|
||||
module.exports = RemoveTorrentModal
|
||||
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
const React = require('react')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
function RemoveTorrentModal (state) {
|
||||
var message = state.modal.deleteData
|
||||
@@ -9,15 +10,15 @@ function RemoveTorrentModal (state) {
|
||||
: 'Are you sure you want to remove this torrent from the list?'
|
||||
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
|
||||
|
||||
return hx`
|
||||
return (
|
||||
<div>
|
||||
<p><strong>${message}</strong></p>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||
<button class='button button-raised' onclick=${handleRemove}>${buttonText}</button>
|
||||
<p><strong>{message}</strong></p>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
||||
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
function handleRemove () {
|
||||
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)
|
||||
@@ -1,24 +1,25 @@
|
||||
module.exports = TorrentList
|
||||
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
const React = require('react')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var TorrentSummary = require('../lib/torrent-summary')
|
||||
var TorrentPlayer = require('../lib/torrent-player')
|
||||
var {dispatcher} = require('../lib/dispatcher')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
function TorrentList (state) {
|
||||
var torrentRows = state.saved.torrents.map(
|
||||
(torrentSummary) => renderTorrent(torrentSummary)
|
||||
)
|
||||
|
||||
return hx`
|
||||
<div class='torrent-list'>
|
||||
${torrentRows}
|
||||
<div class='torrent-placeholder'>
|
||||
<span class='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||
return (
|
||||
<div className='torrent-list'>
|
||||
{torrentRows}
|
||||
<div className='torrent-placeholder'>
|
||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||
</div>
|
||||
</div>`
|
||||
</div>
|
||||
)
|
||||
|
||||
function renderTorrent (torrentSummary) {
|
||||
var infoHash = torrentSummary.infoHash
|
||||
@@ -41,71 +42,70 @@ function TorrentList (state) {
|
||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||
if (isSelected) classes.push('selected')
|
||||
if (!infoHash) classes.push('disabled')
|
||||
classes = classes.join(' ')
|
||||
return hx`
|
||||
<div style=${style} class=${classes}
|
||||
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
||||
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
|
||||
${renderTorrentMetadata(torrentSummary)}
|
||||
${infoHash ? renderTorrentButtons(torrentSummary) : ''}
|
||||
${isSelected ? renderTorrentDetails(torrentSummary) : ''}
|
||||
return (
|
||||
<div style={style} className={classes.join(' ')}
|
||||
onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
||||
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
|
||||
{renderTorrentMetadata(torrentSummary)}
|
||||
{infoHash ? renderTorrentButtons(torrentSummary) : ''}
|
||||
{isSelected ? renderTorrentDetails(torrentSummary) : ''}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
// Show name, download status, % complete
|
||||
function renderTorrentMetadata (torrentSummary) {
|
||||
var name = torrentSummary.name || 'Loading torrent...'
|
||||
var elements = [hx`
|
||||
<div class='name ellipsis'>${name}</div>
|
||||
`]
|
||||
var elements = [(
|
||||
<div className='name ellipsis'>{name}</div>
|
||||
)]
|
||||
|
||||
// If it's downloading/seeding then show progress info
|
||||
var prog = torrentSummary.progress
|
||||
if (torrentSummary.status !== 'paused' && prog) {
|
||||
elements.push(hx`
|
||||
<div class='ellipsis'>
|
||||
${renderPercentProgress()}
|
||||
${renderTotalProgress()}
|
||||
${renderPeers()}
|
||||
${renderDownloadSpeed()}
|
||||
${renderUploadSpeed()}
|
||||
${renderEta()}
|
||||
elements.push((
|
||||
<div className='ellipsis'>
|
||||
{renderPercentProgress()}
|
||||
{renderTotalProgress()}
|
||||
{renderPeers()}
|
||||
{renderDownloadSpeed()}
|
||||
{renderUploadSpeed()}
|
||||
{renderEta()}
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
}
|
||||
|
||||
return hx`<div class='metadata'>${elements}</div>`
|
||||
return (<div className='metadata'>{elements}</div>)
|
||||
|
||||
function renderPercentProgress () {
|
||||
var progress = Math.floor(100 * prog.progress)
|
||||
return hx`<span>${progress}%</span>`
|
||||
return (<span>{progress}%</span>)
|
||||
}
|
||||
|
||||
function renderTotalProgress () {
|
||||
var downloaded = prettyBytes(prog.downloaded)
|
||||
var total = prettyBytes(prog.length || 0)
|
||||
if (downloaded === total) {
|
||||
return hx`<span>${downloaded}</span>`
|
||||
return (<span>{downloaded}</span>)
|
||||
} else {
|
||||
return hx`<span>${downloaded} / ${total}</span>`
|
||||
return (<span>{downloaded} / {total}</span>)
|
||||
}
|
||||
}
|
||||
|
||||
function renderPeers () {
|
||||
if (prog.numPeers === 0) return
|
||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||
return hx`<span>${prog.numPeers} ${count}</span>`
|
||||
return (<span>{prog.numPeers} {count}</span>)
|
||||
}
|
||||
|
||||
function renderDownloadSpeed () {
|
||||
if (prog.downloadSpeed === 0) return
|
||||
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
|
||||
return (<span>↓ {prettyBytes(prog.downloadSpeed)}/s</span>)
|
||||
}
|
||||
|
||||
function renderUploadSpeed () {
|
||||
if (prog.uploadSpeed === 0) return
|
||||
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
|
||||
return (<span>↑ {prettyBytes(prog.uploadSpeed)}/s</span>)
|
||||
}
|
||||
|
||||
function renderEta () {
|
||||
@@ -126,7 +126,7 @@ function TorrentList (state) {
|
||||
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||
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
|
||||
var playButton
|
||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
playButton = hx`
|
||||
<i.button-round.icon.play
|
||||
title=${playTooltip}
|
||||
class=${playClass}
|
||||
onclick=${dispatcher('playFile', infoHash)}>
|
||||
${playIcon}
|
||||
playButton = (
|
||||
<i
|
||||
title={playTooltip}
|
||||
className={'button-round icon play ' + playClass}
|
||||
onClick={dispatcher('playFile', infoHash)}>
|
||||
{playIcon}
|
||||
</i>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='buttons'>
|
||||
${positionElem}
|
||||
${playButton}
|
||||
<i.button-round.icon.download
|
||||
class=${torrentSummary.status}
|
||||
title=${downloadTooltip}
|
||||
onclick=${dispatcher('toggleTorrent', infoHash)}>
|
||||
${downloadIcon}
|
||||
return (
|
||||
<div className='buttons'>
|
||||
{positionElem}
|
||||
{playButton}
|
||||
<i
|
||||
className={'button-round icon download ' + torrentSummary.status}
|
||||
title={downloadTooltip}
|
||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||
{downloadIcon}
|
||||
</i>
|
||||
<i
|
||||
class='icon delete'
|
||||
className='icon delete'
|
||||
title='Remove torrent'
|
||||
onclick=${dispatcher('confirmDeleteTorrent', infoHash, false)}>
|
||||
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}>
|
||||
close
|
||||
</i>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
// Show files, per-file download status and play buttons, and so on
|
||||
@@ -209,7 +209,7 @@ function TorrentList (state) {
|
||||
var message = torrentSummary.status === 'paused'
|
||||
? 'Failed to load torrent info. Click the download button to try again...'
|
||||
: 'Downloading torrent info...'
|
||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||
filesElement = (<div className='files warning'>{message}</div>)
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files
|
||||
@@ -221,20 +221,20 @@ function TorrentList (state) {
|
||||
})
|
||||
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
|
||||
|
||||
filesElement = hx`
|
||||
<div class='files'>
|
||||
filesElement = (
|
||||
<div className='files'>
|
||||
<table>
|
||||
${fileRows}
|
||||
{fileRows}
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='torrent-details'>
|
||||
${filesElement}
|
||||
return (
|
||||
<div className='torrent-details'>
|
||||
{filesElement}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
// Show a single torrentSummary file in the details view for a single torrent
|
||||
@@ -272,27 +272,27 @@ function TorrentList (state) {
|
||||
var rowClass = ''
|
||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||
return hx`
|
||||
<tr onclick=${handleClick}>
|
||||
<td class='col-icon ${rowClass}'>
|
||||
${positionElem}
|
||||
<i class='icon'>${icon}</i>
|
||||
return (
|
||||
<tr onClick={handleClick}>
|
||||
<td className={'col-icon ' + rowClass}>
|
||||
{positionElem}
|
||||
<i className='icon'>{icon}</i>
|
||||
</td>
|
||||
<td class='col-name ${rowClass}'>
|
||||
${file.name}
|
||||
<td className={'col-name ' + rowClass}>
|
||||
{file.name}
|
||||
</td>
|
||||
<td class='col-progress ${rowClass}'>
|
||||
${isSelected ? progress : ''}
|
||||
<td className={'col-progress ' + rowClass}>
|
||||
{isSelected ? progress : ''}
|
||||
</td>
|
||||
<td class='col-size ${rowClass}'>
|
||||
${prettyBytes(file.length)}
|
||||
<td className={'col-size ' + rowClass}>
|
||||
{prettyBytes(file.length)}
|
||||
</td>
|
||||
<td class='col-select'
|
||||
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
|
||||
<td className='col-select'
|
||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,18 +301,18 @@ function renderRadialProgressBar (fraction, cssClass) {
|
||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
|
||||
return hx`
|
||||
<div class="radial-progress ${cssClass}">
|
||||
<div class="circle">
|
||||
<div class="mask full" style=${transformFill}>
|
||||
<div class="fill" style=${transformFill}></div>
|
||||
return (
|
||||
<div className={'radial-progress ' + cssClass}>
|
||||
<div className='circle'>
|
||||
<div className='mask full' style={transformFill}>
|
||||
<div className='fill' style={transformFill}></div>
|
||||
</div>
|
||||
<div class="mask half">
|
||||
<div class="fill" style=${transformFill}></div>
|
||||
<div class="fill fix" style=${transformFix}></div>
|
||||
<div className='mask half'>
|
||||
<div className='fill' style={transformFill}></div>
|
||||
<div className='fill fix' style={transformFix}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inset"></div>
|
||||
<div className='inset'></div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = UnsupportedMediaModal
|
||||
|
||||
var electron = require('electron')
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
function UnsupportedMediaModal (state) {
|
||||
var err = state.modal.error
|
||||
@@ -11,22 +11,22 @@ function UnsupportedMediaModal (state) {
|
||||
? err.getMessage()
|
||||
: err
|
||||
var actionButton = state.modal.vlcInstalled
|
||||
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
|
||||
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
|
||||
? (<button className='button-raised' onClick={onPlay}>Play in VLC</button>)
|
||||
: (<button className='button-raised' onClick={onInstall}>Install VLC</button>)
|
||||
var vlcMessage = state.modal.vlcNotFound
|
||||
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
|
||||
: ''
|
||||
return hx`
|
||||
return (
|
||||
<div>
|
||||
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||
<p>${message}</p>
|
||||
<p class='float-right'>
|
||||
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
|
||||
${actionButton}
|
||||
<p>{message}</p>
|
||||
<p className='float-right'>
|
||||
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
||||
{actionButton}
|
||||
</p>
|
||||
<p class='error-text'>${vlcMessage}</p>
|
||||
<p className='error-text'>{vlcMessage}</p>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
function onInstall () {
|
||||
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||
@@ -1,21 +1,21 @@
|
||||
module.exports = UpdateAvailableModal
|
||||
|
||||
var electron = require('electron')
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
function UpdateAvailableModal (state) {
|
||||
return hx`
|
||||
<div class='update-available-modal'>
|
||||
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
||||
return (
|
||||
<div className='update-available-modal'>
|
||||
<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 class='float-right'>
|
||||
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
|
||||
<button className='button button-raised' onClick={handleOK}>Show Download Page</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
function handleOK () {
|
||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
||||
@@ -82,7 +82,7 @@ table {
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../static/MaterialIcons-Regular.woff2) format('woff2');
|
||||
url(MaterialIcons-Regular.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.icon {
|
||||
13
static/main.html
Normal file
13
static/main.html
Normal 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>
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebTorrent Desktop</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #282828;
|
||||
@@ -14,9 +15,11 @@
|
||||
height: 140px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
require('../build/renderer/webtorrent.js')
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script async src="webtorrent.js"></script>
|
||||
<img src="../static/WebTorrent.png">
|
||||
<img alt="WebTorrent" src="WebTorrent.png">
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user