Merge branch 'master' into playback-priority
# Conflicts: # src/renderer/controllers/playback-controller.js # src/renderer/controllers/torrent-list-controller.js # src/renderer/main.js
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const React = require('react')
|
||||
|
||||
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||
|
||||
const Header = require('../components/Header')
|
||||
|
||||
const Views = {
|
||||
'home': require('./TorrentListPage'),
|
||||
'player': require('./PlayerPage'),
|
||||
'create-torrent': require('./CreateTorrentPage'),
|
||||
'preferences': require('./PreferencesPage')
|
||||
}
|
||||
|
||||
const Modals = {
|
||||
'open-torrent-address-modal': require('../components/open-torrent-address-modal'),
|
||||
'remove-torrent-modal': require('../components/remove-torrent-modal'),
|
||||
'update-available-modal': require('../components/update-available-modal'),
|
||||
'unsupported-media-modal': require('../components/unsupported-media-modal')
|
||||
}
|
||||
|
||||
darkBaseTheme.fontFamily = process.platform === 'win32'
|
||||
? '"Segoe UI", sans-serif'
|
||||
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||
darkBaseTheme.palette.primary1Color = colors.cyan500
|
||||
darkBaseTheme.palette.primary2Color = colors.cyan500
|
||||
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||
darkBaseTheme.palette.accent3Color = colors.redA100
|
||||
|
||||
class App extends React.Component {
|
||||
render () {
|
||||
var state = this.props.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 = (
|
||||
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
|
||||
<div className={'app ' + cls.join(' ')}>
|
||||
<Header state={state} />
|
||||
{this.getErrorPopover()}
|
||||
<div key='content' className='content'>{this.getView()}</div>
|
||||
{this.getModal()}
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
|
||||
return vdom
|
||||
}
|
||||
|
||||
getErrorPopover () {
|
||||
var state = this.props.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, i) {
|
||||
return (<div key={i} className='error'>{error.message}</div>)
|
||||
})
|
||||
return (
|
||||
<div key='errors'
|
||||
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
|
||||
<div key='title' className='title'>Error</div>
|
||||
{errorElems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getModal () {
|
||||
var state = this.props.state
|
||||
if (!state.modal) return
|
||||
var ModalContents = Modals[state.modal.id]
|
||||
return (
|
||||
<div key='modal' className='modal'>
|
||||
<div key='modal-background' className='modal-background' />
|
||||
<div key='modal-content' className='modal-content'>
|
||||
<ModalContents state={state} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getView () {
|
||||
var state = this.props.state
|
||||
var View = Views[state.location.url()]
|
||||
return (<View state={state} />)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = App
|
||||
@@ -1,171 +0,0 @@
|
||||
const createTorrent = require('create-torrent')
|
||||
const path = require('path')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const React = require('react')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const TextField = require('material-ui/TextField').default
|
||||
|
||||
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
|
||||
const Heading = require('../components/Heading')
|
||||
const ShowMore = require('../components/ShowMore')
|
||||
|
||||
class CreateTorrentPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
var state = this.props.state
|
||||
var info = state.location.current()
|
||||
|
||||
// Preprocess: exclude .DS_Store and other dotfiles
|
||||
var files = info.files
|
||||
.filter((f) => !f.name.startsWith('.'))
|
||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
||||
|
||||
// First, extract the base folder that the files are all in
|
||||
var pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: show the number of files and total size
|
||||
var numFiles = files.length
|
||||
var totalBytes = files
|
||||
.map((f) => f.size)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||
|
||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||
// 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'
|
||||
defaultName = files[0].name
|
||||
basePath = pathPrefix
|
||||
} else {
|
||||
// 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, i) {
|
||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
||||
return (<div key={i}>{relativePath}</div>)
|
||||
})
|
||||
if (files.length > maxFileElems) {
|
||||
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
|
||||
}
|
||||
var trackers = createTorrent.announceList.join('\n')
|
||||
|
||||
this.state = {
|
||||
basePath,
|
||||
defaultName,
|
||||
fileElems,
|
||||
torrentInfo,
|
||||
trackers
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit () {
|
||||
var announceList = document.querySelector('.torrent-trackers').value
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
var isPrivate = document.querySelector('.torrent-is-private').checked
|
||||
var comment = document.querySelector('.torrent-comment').value.trim()
|
||||
var options = {
|
||||
// We can't let the user choose their own name if we want WebTorrent
|
||||
// to use the files in place rather than creating a new folder.
|
||||
// If we ever want to add support for that:
|
||||
// name: document.querySelector('.torrent-name').value
|
||||
name: this.state.defaultName,
|
||||
path: this.state.basePath,
|
||||
files: this.state.files,
|
||||
announce: announceList,
|
||||
private: isPrivate,
|
||||
comment: comment
|
||||
}
|
||||
dispatch('createTorrent', options)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<Heading level={1}>
|
||||
Create torrent "{this.state.defaultName}"
|
||||
</Heading>
|
||||
<div className='torrent-info'>
|
||||
{this.state.torrentInfo}
|
||||
</div>
|
||||
<div className='torrent-attribute'>
|
||||
<label>Path:</label>
|
||||
<div className='torrent-attribute'>{this.state.pathPrefix}</div>
|
||||
</div>
|
||||
<ShowMore
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
hideLabel='Hide advanced settings...'
|
||||
showLabel='Show advanced settings...'
|
||||
>
|
||||
<div key='advanced' className='create-torrent-advanced'>
|
||||
<div key='private' className='torrent-attribute'>
|
||||
<label>Private:</label>
|
||||
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
|
||||
</div>
|
||||
<Heading level={2}>Trackers:</Heading>
|
||||
<TextField
|
||||
className='torrent-trackers'
|
||||
hintText='Tracker'
|
||||
multiLine
|
||||
rows={2}
|
||||
rowsMax={10}
|
||||
defaultValue={this.state.trackers}
|
||||
/>
|
||||
<div key='comment' className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<textarea className='torrent-attribute torrent-comment' />
|
||||
</div>
|
||||
<div key='files' className='torrent-attribute'>
|
||||
<label>Files:</label>
|
||||
<div>{this.state.fileElems}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShowMore>
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
label='Cancel'
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={dispatcher('cancel')}
|
||||
/>
|
||||
<RaisedButton
|
||||
label='Create Torrent'
|
||||
primary
|
||||
onClick={this.handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
function findCommonPrefix (a, b) {
|
||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||
}
|
||||
if (i === a.length) return a
|
||||
if (i === b.length) return b
|
||||
return a.substring(0, i)
|
||||
}
|
||||
|
||||
module.exports = CreateTorrentPage
|
||||
130
src/renderer/pages/app.js
Normal file
130
src/renderer/pages/app.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const createGetter = require('fn-getter')
|
||||
const React = require('react')
|
||||
|
||||
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||
|
||||
const Header = require('../components/header')
|
||||
|
||||
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||
const TorrentListPage = require('./torrent-list-page')
|
||||
|
||||
const Views = {
|
||||
'home': createGetter(() => TorrentListPage),
|
||||
'player': createGetter(() => require('./player-page')),
|
||||
'create-torrent': createGetter(() => require('./create-torrent-page')),
|
||||
'preferences': createGetter(() => require('./preferences-page'))
|
||||
}
|
||||
|
||||
const Modals = {
|
||||
'open-torrent-address-modal': createGetter(
|
||||
() => require('../components/open-torrent-address-modal')
|
||||
),
|
||||
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
|
||||
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
|
||||
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
|
||||
}
|
||||
|
||||
const fontFamily = process.platform === 'win32'
|
||||
? '"Segoe UI", sans-serif'
|
||||
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||
|
||||
darkBaseTheme.fontFamily = fontFamily
|
||||
darkBaseTheme.userAgent = false
|
||||
darkBaseTheme.palette.primary1Color = colors.grey50
|
||||
darkBaseTheme.palette.primary2Color = colors.grey50
|
||||
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||
darkBaseTheme.palette.accent3Color = colors.redA100
|
||||
|
||||
let darkMuiTheme
|
||||
let lightMuiTheme
|
||||
|
||||
class App extends React.Component {
|
||||
render () {
|
||||
const state = this.props.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
|
||||
const hideControls = state.shouldHidePlayerControls()
|
||||
|
||||
const 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')
|
||||
|
||||
if (!darkMuiTheme) {
|
||||
darkMuiTheme = getMuiTheme(darkBaseTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={darkMuiTheme}>
|
||||
<div className={'app ' + cls.join(' ')}>
|
||||
<Header state={state} />
|
||||
{this.getErrorPopover()}
|
||||
<div key='content' className='content'>{this.getView()}</div>
|
||||
{this.getModal()}
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
getErrorPopover () {
|
||||
const state = this.props.state
|
||||
const now = new Date().getTime()
|
||||
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||
const hasErrors = recentErrors.length > 0
|
||||
|
||||
const errorElems = recentErrors.map(function (error, i) {
|
||||
return (<div key={i} className='error'>{error.message}</div>)
|
||||
})
|
||||
return (
|
||||
<div key='errors'
|
||||
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
|
||||
<div key='title' className='title'>Error</div>
|
||||
{errorElems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getModal () {
|
||||
const state = this.props.state
|
||||
if (!state.modal) return
|
||||
|
||||
if (!lightMuiTheme) {
|
||||
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
|
||||
lightBaseTheme.fontFamily = fontFamily
|
||||
lightBaseTheme.userAgent = false
|
||||
lightMuiTheme = getMuiTheme(lightBaseTheme)
|
||||
}
|
||||
|
||||
const ModalContents = Modals[state.modal.id]()
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={lightMuiTheme}>
|
||||
<div key='modal' className='modal'>
|
||||
<div key='modal-background' className='modal-background' />
|
||||
<div key='modal-content' className='modal-content'>
|
||||
<ModalContents state={state} />
|
||||
</div>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
getView () {
|
||||
const state = this.props.state
|
||||
const View = Views[state.location.url()]()
|
||||
return (<View state={state} />)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = App
|
||||
216
src/renderer/pages/create-torrent-page.js
Normal file
216
src/renderer/pages/create-torrent-page.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const createTorrent = require('create-torrent')
|
||||
const path = require('path')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const React = require('react')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const TextField = require('material-ui/TextField').default
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
|
||||
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
|
||||
const Heading = require('../components/heading')
|
||||
const ShowMore = require('../components/show-more')
|
||||
|
||||
// Shows a basic UI to create a torrent, from an already-selected file or folder.
|
||||
// Includes a "Show Advanced..." button and more advanced UI.
|
||||
class CreateTorrentPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const state = this.props.state
|
||||
const info = state.location.current()
|
||||
|
||||
// First, extract the base folder that the files are all in
|
||||
let pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Then, exclude .DS_Store and other dotfiles
|
||||
const files = info.files
|
||||
.filter((f) => !containsDots(f.path, pathPrefix))
|
||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
||||
|
||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||
// as the default name. Show all files relative to the base folder.
|
||||
let defaultName, basePath
|
||||
if (files.length === 1) {
|
||||
// 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'
|
||||
defaultName = path.basename(pathPrefix)
|
||||
basePath = path.dirname(pathPrefix)
|
||||
}
|
||||
|
||||
// Default trackers
|
||||
const trackers = createTorrent.announceList.join('\n')
|
||||
|
||||
this.state = {
|
||||
comment: '',
|
||||
isPrivate: false,
|
||||
pathPrefix,
|
||||
basePath,
|
||||
defaultName,
|
||||
files,
|
||||
trackers
|
||||
}
|
||||
|
||||
// Create React event handlers only once
|
||||
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
|
||||
this.setComment = (_, comment) => this.setState({comment})
|
||||
this.setTrackers = (_, trackers) => this.setState({trackers})
|
||||
this.handleSubmit = handleSubmit.bind(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
const files = this.state.files
|
||||
|
||||
// Sanity check: show the number of files and total size
|
||||
const numFiles = files.length
|
||||
const totalBytes = files
|
||||
.map((f) => f.size)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
|
||||
<div className='torrent-info'>{torrentInfo}</div>
|
||||
<div className='torrent-attribute'>
|
||||
<label>Path:</label>
|
||||
<div>{this.state.pathPrefix}</div>
|
||||
</div>
|
||||
<ShowMore
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
hideLabel='Hide advanced settings...'
|
||||
showLabel='Show advanced settings...'>
|
||||
{this.renderAdvanced()}
|
||||
</ShowMore>
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control cancel'
|
||||
label='Cancel'
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={dispatcher('cancel')} />
|
||||
<RaisedButton
|
||||
className='control create-torrent-button'
|
||||
label='Create Torrent'
|
||||
primary
|
||||
onClick={this.handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Renders everything after clicking Show Advanced...:
|
||||
// * Is Private? (private torrents, not announced to DHT)
|
||||
// * Announce list (trackers)
|
||||
// * Comment
|
||||
renderAdvanced () {
|
||||
// Create file list
|
||||
const maxFileElems = 100
|
||||
const files = this.state.files
|
||||
const fileElems = files.slice(0, maxFileElems).map((file, i) => {
|
||||
const relativePath = path.relative(this.state.pathPrefix, file.path)
|
||||
return (<div key={i}>{relativePath}</div>)
|
||||
})
|
||||
if (files.length > maxFileElems) {
|
||||
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
|
||||
}
|
||||
|
||||
// Align the text fields
|
||||
const textFieldStyle = { width: '' }
|
||||
const textareaStyle = { margin: 0 }
|
||||
|
||||
return (
|
||||
<div key='advanced' className='create-torrent-advanced'>
|
||||
<div key='private' className='torrent-attribute'>
|
||||
<label>Private:</label>
|
||||
<Checkbox
|
||||
className='torrent-is-private control'
|
||||
style={{display: ''}}
|
||||
checked={this.state.isPrivate}
|
||||
onCheck={this.setIsPrivate} />
|
||||
</div>
|
||||
<div key='trackers' className='torrent-attribute'>
|
||||
<label>Trackers:</label>
|
||||
<TextField
|
||||
className='torrent-trackers control'
|
||||
style={textFieldStyle}
|
||||
textareaStyle={textareaStyle}
|
||||
multiLine
|
||||
rows={2}
|
||||
rowsMax={10}
|
||||
value={this.state.trackers}
|
||||
onChange={this.setTrackers} />
|
||||
</div>
|
||||
<div key='comment' className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<TextField
|
||||
className='torrent-comment control'
|
||||
style={textFieldStyle}
|
||||
textareaStyle={textareaStyle}
|
||||
hintText='Optionally describe your torrent...'
|
||||
multiLine
|
||||
rows={2}
|
||||
rowsMax={10}
|
||||
value={this.state.comment}
|
||||
onChange={this.setComment} />
|
||||
</div>
|
||||
<div key='files' className='torrent-attribute'>
|
||||
<label>Files:</label>
|
||||
<div>{fileElems}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit () {
|
||||
const announceList = this.state.trackers
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
const options = {
|
||||
// We can't let the user choose their own name if we want WebTorrent
|
||||
// to use the files in place rather than creating a new folder.
|
||||
name: this.state.defaultName,
|
||||
path: this.state.basePath,
|
||||
files: this.state.files,
|
||||
announce: announceList,
|
||||
private: this.state.isPrivate,
|
||||
comment: this.state.comment.trim()
|
||||
}
|
||||
dispatch('createTorrent', options)
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
function findCommonPrefix (a, b) {
|
||||
let i
|
||||
for (i = 0; i < a.length && i < b.length; i++) {
|
||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||
}
|
||||
if (i === a.length) return a
|
||||
if (i === b.length) return b
|
||||
return a.substring(0, i)
|
||||
}
|
||||
|
||||
function containsDots (path, pathPrefix) {
|
||||
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
|
||||
return ('/' + suffix).includes('/.')
|
||||
}
|
||||
|
||||
module.exports = CreateTorrentPage
|
||||
@@ -2,19 +2,20 @@ const React = require('react')
|
||||
const Bitfield = require('bitfield')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const zeroFill = require('zero-fill')
|
||||
const path = require('path')
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const Playlist = require('../lib/playlist')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||
module.exports = class Player extends React.Component {
|
||||
render () {
|
||||
// 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 state = this.props.state
|
||||
var showVideo = state.playing.location === 'local'
|
||||
var showControls = state.playing.location !== 'external'
|
||||
const state = this.props.state
|
||||
const showVideo = state.playing.location === 'local'
|
||||
const showControls = state.playing.location !== 'external'
|
||||
return (
|
||||
<div
|
||||
className='player'
|
||||
@@ -25,6 +26,15 @@ module.exports = class Player extends React.Component {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onComponentWillUnmount () {
|
||||
// Unload the media element so that Chromium stops trying to fetch data
|
||||
const tag = document.querySelector('audio,video')
|
||||
if (!tag) return
|
||||
tag.pause()
|
||||
tag.src = ''
|
||||
tag.load()
|
||||
}
|
||||
}
|
||||
|
||||
// Handles volume change by wheel
|
||||
@@ -38,7 +48,7 @@ function renderMedia (state) {
|
||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||
// Instead, grab the DOM node and play/pause it if necessary
|
||||
// Get the <video> or <audio> tag
|
||||
var mediaElement = document.querySelector(state.playing.type)
|
||||
const mediaElement = document.querySelector(state.playing.type)
|
||||
if (mediaElement !== null) {
|
||||
if (state.playing.isPaused && !mediaElement.paused) {
|
||||
mediaElement.pause()
|
||||
@@ -66,20 +76,20 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Switch to the newly added subtitle track, if available
|
||||
var tracks = mediaElement.textTracks || []
|
||||
for (var j = 0; j < tracks.length; j++) {
|
||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
const tracks = mediaElement.textTracks || []
|
||||
for (let j = 0; j < tracks.length; j++) {
|
||||
const isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||
}
|
||||
|
||||
// Save video position
|
||||
var file = state.getPlayingFileSummary()
|
||||
const file = state.getPlayingFileSummary()
|
||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||
file.duration = state.playing.duration = mediaElement.duration
|
||||
|
||||
// Save selected subtitle
|
||||
if (state.playing.subtitles.selectedIndex !== -1) {
|
||||
var index = state.playing.subtitles.selectedIndex
|
||||
const index = state.playing.subtitles.selectedIndex
|
||||
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
||||
} else if (file.selectedSubtitle != null) {
|
||||
delete file.selectedSubtitle
|
||||
@@ -89,11 +99,11 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Add subtitles to the <video> tag
|
||||
var trackTags = []
|
||||
const trackTags = []
|
||||
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||
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
|
||||
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||
const track = state.playing.subtitles.tracks[i]
|
||||
const isSelected = state.playing.subtitles.selectedIndex === i
|
||||
trackTags.push(
|
||||
<track
|
||||
key={i}
|
||||
@@ -106,10 +116,10 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Create the <audio> or <video> tag
|
||||
var MediaTagName = state.playing.type
|
||||
var mediaTag = (
|
||||
const MediaTagName = state.playing.type
|
||||
const mediaTag = (
|
||||
<MediaTagName
|
||||
src={state.server.localURL}
|
||||
src={Playlist.getCurrentLocalURL(state)}
|
||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||
onLoadedMetadata={onLoadedMetadata}
|
||||
onEnded={onEnded}
|
||||
@@ -136,21 +146,25 @@ function renderMedia (state) {
|
||||
// As soon as we know the video dimensions, resize the window
|
||||
function onLoadedMetadata (e) {
|
||||
if (state.playing.type !== 'video') return
|
||||
var video = e.target
|
||||
var dimensions = {
|
||||
const video = e.target
|
||||
const dimensions = {
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
}
|
||||
dispatch('setDimensions', dimensions)
|
||||
}
|
||||
|
||||
// When the video completes, pause the video instead of looping
|
||||
function onEnded (e) {
|
||||
state.playing.isPaused = true
|
||||
if (Playlist.hasNext(state)) {
|
||||
dispatch('nextTrack')
|
||||
} else {
|
||||
// When the last video completes, pause the video instead of looping
|
||||
state.playing.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
function onCanPlay (e) {
|
||||
var elem = e.target
|
||||
const elem = e.target
|
||||
if (state.playing.type === 'video' &&
|
||||
elem.webkitVideoDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Video codec unsupported')
|
||||
@@ -164,15 +178,15 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
function renderOverlay (state) {
|
||||
var elems = []
|
||||
var audioMetadataElem = renderAudioMetadata(state)
|
||||
var spinnerElem = renderLoadingSpinner(state)
|
||||
const elems = []
|
||||
const audioMetadataElem = renderAudioMetadata(state)
|
||||
const spinnerElem = renderLoadingSpinner(state)
|
||||
if (audioMetadataElem) elems.push(audioMetadataElem)
|
||||
if (spinnerElem) elems.push(spinnerElem)
|
||||
|
||||
// Video fills the window, centered with black bars if necessary
|
||||
// Audio gets a static poster image and a summary of the file metadata.
|
||||
var style
|
||||
let style
|
||||
if (state.playing.type === 'audio') {
|
||||
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
||||
} else if (elems.length !== 0) {
|
||||
@@ -190,27 +204,27 @@ function renderOverlay (state) {
|
||||
}
|
||||
|
||||
function renderAudioMetadata (state) {
|
||||
var fileSummary = state.getPlayingFileSummary()
|
||||
const fileSummary = state.getPlayingFileSummary()
|
||||
if (!fileSummary.audioInfo) return
|
||||
var info = fileSummary.audioInfo
|
||||
const info = fileSummary.audioInfo
|
||||
|
||||
// Get audio track info
|
||||
var title = info.title
|
||||
let title = info.title
|
||||
if (!title) {
|
||||
title = fileSummary.name
|
||||
}
|
||||
var artist = info.artist && info.artist[0]
|
||||
var album = info.album
|
||||
let artist = info.artist && info.artist[0]
|
||||
let album = info.album
|
||||
if (album && info.year && !album.includes(info.year)) {
|
||||
album += ' (' + info.year + ')'
|
||||
}
|
||||
var track
|
||||
let track
|
||||
if (info.track && info.track.no && info.track.of) {
|
||||
track = info.track.no + ' of ' + info.track.of
|
||||
}
|
||||
|
||||
// Show a small info box in the middle of the screen with title/album/etc
|
||||
var elems = []
|
||||
const elems = []
|
||||
if (artist) {
|
||||
elems.push((
|
||||
<div key='artist' className='audio-artist'>
|
||||
@@ -234,7 +248,7 @@ function renderAudioMetadata (state) {
|
||||
}
|
||||
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = (<label />)
|
||||
const emptyLabel = (<label />)
|
||||
elems.unshift((
|
||||
<div key='title' className='audio-title'>
|
||||
{elems.length ? emptyLabel : undefined}{title}
|
||||
@@ -246,14 +260,14 @@ function renderAudioMetadata (state) {
|
||||
|
||||
function renderLoadingSpinner (state) {
|
||||
if (state.playing.isPaused) return
|
||||
var isProbablyStalled = state.playing.isStalled ||
|
||||
const isProbablyStalled = state.playing.isStalled ||
|
||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||
if (!isProbablyStalled) return
|
||||
|
||||
var prog = state.getPlayingTorrentSummary().progress || {}
|
||||
var fileProgress = 0
|
||||
const prog = state.getPlayingTorrentSummary().progress || {}
|
||||
let fileProgress = 0
|
||||
if (prog.files) {
|
||||
var file = prog.files[state.playing.fileIndex]
|
||||
const file = prog.files[state.playing.fileIndex]
|
||||
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||
}
|
||||
|
||||
@@ -261,16 +275,16 @@ function renderLoadingSpinner (state) {
|
||||
<div key='loading' className='media-stalled'>
|
||||
<div key='loading-spinner' className='loading-spinner'> </div>
|
||||
<div key='loading-progress' className='loading-status ellipsis'>
|
||||
<span className='progress'>{fileProgress}%</span> downloaded,
|
||||
<span>↓ {prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||
<span>↑ {prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||
<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) {
|
||||
var castIcon, castType, isCast
|
||||
let castIcon, castType, isCast
|
||||
if (state.playing.location.startsWith('chromecast')) {
|
||||
castIcon = 'cast_connected'
|
||||
castType = 'Chromecast'
|
||||
@@ -284,11 +298,8 @@ function renderCastScreen (state) {
|
||||
castType = 'DLNA'
|
||||
isCast = true
|
||||
} else if (state.playing.location === 'external') {
|
||||
// TODO: get the player name in a more reliable way
|
||||
var playerPath = state.saved.prefs.externalPlayerPath
|
||||
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
||||
castIcon = 'tv'
|
||||
castType = playerName
|
||||
castType = state.getExternalPlayerName()
|
||||
isCast = false
|
||||
} else if (state.playing.location === 'error') {
|
||||
castIcon = 'error_outline'
|
||||
@@ -296,15 +307,15 @@ function renderCastScreen (state) {
|
||||
isCast = false
|
||||
}
|
||||
|
||||
var isStarting = state.playing.location.endsWith('-pending')
|
||||
var castName = state.playing.castName
|
||||
var castStatus
|
||||
const isStarting = state.playing.location.endsWith('-pending')
|
||||
const castName = state.playing.castName
|
||||
let castStatus
|
||||
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||
else castStatus = ''
|
||||
|
||||
// Show a nice title image, if possible
|
||||
var style = {
|
||||
const style = {
|
||||
backgroundImage: cssBackgroundImagePoster(state)
|
||||
}
|
||||
|
||||
@@ -322,12 +333,12 @@ function renderCastScreen (state) {
|
||||
function renderCastOptions (state) {
|
||||
if (!state.devices.castMenu) return
|
||||
|
||||
var {location, devices} = state.devices.castMenu
|
||||
var player = state.devices[location]
|
||||
const {location, devices} = state.devices.castMenu
|
||||
const player = state.devices[location]
|
||||
|
||||
var items = devices.map(function (device, ix) {
|
||||
var isSelected = player.device === device
|
||||
var name = device.name
|
||||
const items = devices.map(function (device, ix) {
|
||||
const isSelected = player.device === device
|
||||
const name = device.name
|
||||
return (
|
||||
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
||||
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
@@ -344,11 +355,11 @@ function renderCastOptions (state) {
|
||||
}
|
||||
|
||||
function renderSubtitleOptions (state) {
|
||||
var subtitles = state.playing.subtitles
|
||||
const subtitles = state.playing.subtitles
|
||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||
|
||||
var items = subtitles.tracks.map(function (track, ix) {
|
||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
const items = subtitles.tracks.map(function (track, ix) {
|
||||
const isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
return (
|
||||
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
|
||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
@@ -357,8 +368,8 @@ function renderSubtitleOptions (state) {
|
||||
)
|
||||
})
|
||||
|
||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
const noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
return (
|
||||
<ul key='subtitle-options' className='options-list'>
|
||||
{items}
|
||||
@@ -371,32 +382,39 @@ function renderSubtitleOptions (state) {
|
||||
}
|
||||
|
||||
function renderPlayerControls (state) {
|
||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
const captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
? 'disabled'
|
||||
: state.playing.subtitles.selectedIndex >= 0
|
||||
? 'active'
|
||||
: ''
|
||||
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||
|
||||
var elements = [
|
||||
const elements = [
|
||||
<div key='playback-bar' className='playback-bar'>
|
||||
{renderLoadingBar(state)}
|
||||
<div
|
||||
key='cursor'
|
||||
className='playback-cursor'
|
||||
style={playbackCursorStyle}
|
||||
/>
|
||||
style={playbackCursorStyle} />
|
||||
<div
|
||||
key='scrub-bar'
|
||||
className='scrub-bar'
|
||||
draggable='true'
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleScrub}
|
||||
onDrag={handleScrub}
|
||||
/>
|
||||
onDrag={handleScrub} />
|
||||
</div>,
|
||||
|
||||
<i
|
||||
key='skip-previous'
|
||||
className={'icon skip-previous float-left ' + prevClass}
|
||||
onClick={dispatcher('previousTrack')}>
|
||||
skip_previous
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='play'
|
||||
className='icon play-pause float-left'
|
||||
@@ -404,6 +422,13 @@ function renderPlayerControls (state) {
|
||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='skip-next'
|
||||
className={'icon skip-next float-left ' + nextClass}
|
||||
onClick={dispatcher('nextTrack')}>
|
||||
skip_next
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='fullscreen'
|
||||
className='icon fullscreen float-right'
|
||||
@@ -413,7 +438,7 @@ function renderPlayerControls (state) {
|
||||
]
|
||||
|
||||
if (state.playing.type === 'video') {
|
||||
// show closed captions icon
|
||||
// Show closed captions icon
|
||||
elements.push((
|
||||
<i
|
||||
key='subtitles'
|
||||
@@ -425,24 +450,24 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
|
||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
var isCastingAnywhere = castTypes.some(
|
||||
const castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
const isCastingAnywhere = castTypes.some(
|
||||
(castType) => state.playing.location.startsWith(castType))
|
||||
|
||||
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||
var buttonIcons = {
|
||||
const buttonIcons = {
|
||||
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||
'airplay': {true: 'airplay', false: 'airplay'},
|
||||
'dlna': {true: 'tv', false: 'tv'}
|
||||
}
|
||||
castTypes.forEach(function (castType) {
|
||||
// Do we show this button (eg. the Chromecast button) at all?
|
||||
var isCasting = state.playing.location.startsWith(castType)
|
||||
var player = state.devices[castType]
|
||||
const isCasting = state.playing.location.startsWith(castType)
|
||||
const player = state.devices[castType]
|
||||
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||
|
||||
// Show the button. Three options for eg the Chromecast button:
|
||||
var buttonClass, buttonHandler
|
||||
let buttonClass, buttonHandler
|
||||
if (isCasting) {
|
||||
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||
buttonClass = 'active'
|
||||
@@ -456,7 +481,7 @@ function renderPlayerControls (state) {
|
||||
buttonClass = ''
|
||||
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||
}
|
||||
var buttonIcon = buttonIcons[castType][isCasting]
|
||||
const buttonIcon = buttonIcons[castType][isCasting]
|
||||
|
||||
elements.push((
|
||||
<i
|
||||
@@ -469,20 +494,18 @@ function renderPlayerControls (state) {
|
||||
})
|
||||
|
||||
// Render volume slider
|
||||
var volume = state.playing.volume
|
||||
var volumeIcon = 'volume_' + (
|
||||
const volume = state.playing.volume
|
||||
const volumeIcon = 'volume_' + (
|
||||
volume === 0 ? 'off'
|
||||
: volume < 0.3 ? 'mute'
|
||||
: volume < 0.6 ? 'down'
|
||||
: 'up')
|
||||
var volumeStyle = {
|
||||
const volumeStyle = {
|
||||
background: '-webkit-gradient(linear, left top, right top, ' +
|
||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||
}
|
||||
|
||||
// TODO: dcposch change the range input to use value / onChanged instead of
|
||||
// "readonly" / onMouse[Down,Move,Up]
|
||||
elements.push((
|
||||
<div key='volume' className='volume float-left'>
|
||||
<i
|
||||
@@ -495,21 +518,20 @@ function renderPlayerControls (state) {
|
||||
type='range' min='0' max='1' step='0.05'
|
||||
value={volume}
|
||||
onChange={handleVolumeScrub}
|
||||
style={volumeStyle}
|
||||
/>
|
||||
style={volumeStyle} />
|
||||
</div>
|
||||
))
|
||||
|
||||
// Show video playback progress
|
||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||
var durationStr = formatTime(state.playing.duration)
|
||||
const currentTimeStr = formatTime(state.playing.currentTime)
|
||||
const durationStr = formatTime(state.playing.duration)
|
||||
elements.push((
|
||||
<span key='time' className='time float-left'>
|
||||
{currentTimeStr} / {durationStr}
|
||||
</span>
|
||||
))
|
||||
|
||||
// render playback rate
|
||||
// Render playback rate
|
||||
if (state.playing.playbackRate !== 1) {
|
||||
elements.push((
|
||||
<span key='rate' className='rate float-left'>
|
||||
@@ -519,7 +541,9 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='controls' className='controls'>
|
||||
<div key='controls' className='controls'
|
||||
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
|
||||
{elements}
|
||||
{renderCastOptions(state)}
|
||||
{renderSubtitleOptions(state)}
|
||||
@@ -529,7 +553,7 @@ function renderPlayerControls (state) {
|
||||
function handleDragStart (e) {
|
||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||
if (e.dataTransfer) {
|
||||
var dt = e.dataTransfer
|
||||
const dt = e.dataTransfer
|
||||
dt.effectAllowed = 'none'
|
||||
}
|
||||
}
|
||||
@@ -538,9 +562,9 @@ function renderPlayerControls (state) {
|
||||
function handleScrub (e) {
|
||||
if (!e.clientX) return
|
||||
dispatch('mediaMouseMoved')
|
||||
var windowWidth = document.querySelector('body').clientWidth
|
||||
var fraction = e.clientX / windowWidth
|
||||
var position = fraction * state.playing.duration /* seconds */
|
||||
const windowWidth = document.querySelector('body').clientWidth
|
||||
const fraction = e.clientX / windowWidth
|
||||
const position = fraction * state.playing.duration /* seconds */
|
||||
dispatch('skipTo', position)
|
||||
}
|
||||
|
||||
@@ -571,18 +595,23 @@ function renderPlayerControls (state) {
|
||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||
// can be 'spongey' / non-contiguous
|
||||
function renderLoadingBar (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary.progress) {
|
||||
return []
|
||||
return null
|
||||
}
|
||||
|
||||
// Find all contiguous parts of the torrent which are loaded
|
||||
var prog = torrentSummary.progress
|
||||
var fileProg = prog.files[state.playing.fileIndex]
|
||||
var parts = []
|
||||
var lastPiecePresent = false
|
||||
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||
const prog = torrentSummary.progress
|
||||
const fileProg = prog.files[state.playing.fileIndex]
|
||||
|
||||
if (!fileProg) return null
|
||||
|
||||
const parts = []
|
||||
let lastPiecePresent = false
|
||||
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||
if (partPresent && !lastPiecePresent) {
|
||||
parts.push({start: i - fileProg.startPiece, count: 1})
|
||||
} else if (partPresent) {
|
||||
@@ -592,21 +621,22 @@ function renderLoadingBar (state) {
|
||||
}
|
||||
|
||||
// Output some bars to show which parts of the file are loaded
|
||||
var loadingBarElems = parts.map(function (part, i) {
|
||||
var style = {
|
||||
const loadingBarElems = parts.map(function (part, i) {
|
||||
const style = {
|
||||
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
return (<div key={i} className='loading-bar-part' style={style} />)
|
||||
})
|
||||
|
||||
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
||||
}
|
||||
|
||||
// Returns the CSS background-image string for a poster image + dark vignette
|
||||
function cssBackgroundImagePoster (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
if (!posterPath) return ''
|
||||
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
|
||||
}
|
||||
@@ -621,12 +651,12 @@ function formatTime (time) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
var hours = Math.floor(time / 3600)
|
||||
var minutes = Math.floor(time % 3600 / 60)
|
||||
let hours = Math.floor(time / 3600)
|
||||
let minutes = Math.floor(time % 3600 / 60)
|
||||
if (hours > 0) {
|
||||
minutes = zeroFill(2, minutes)
|
||||
}
|
||||
var seconds = zeroFill(2, Math.floor(time % 60))
|
||||
let seconds = zeroFill(2, Math.floor(time % 60))
|
||||
|
||||
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const path = require('path')
|
||||
const React = require('react')
|
||||
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
const Heading = require('../components/Heading')
|
||||
const PathSelector = require('../components/PathSelector')
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const Heading = require('../components/heading')
|
||||
const PathSelector = require('../components/path-selector')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
class PreferencesPage extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
|
||||
|
||||
this.handleExternalPlayerPathChange =
|
||||
this.handleExternalPlayerPathChange.bind(this)
|
||||
|
||||
this.handleStartupChange =
|
||||
this.handleStartupChange.bind(this)
|
||||
}
|
||||
|
||||
downloadPathSelector () {
|
||||
@@ -33,8 +37,7 @@ class PreferencesPage extends React.Component {
|
||||
}}
|
||||
onChange={this.handleDownloadPathChange}
|
||||
title='Download location'
|
||||
value={this.props.state.unsaved.prefs.downloadPath}
|
||||
/>
|
||||
value={this.props.state.unsaved.prefs.downloadPath} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
@@ -50,8 +53,7 @@ class PreferencesPage extends React.Component {
|
||||
className='control'
|
||||
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
|
||||
label={'Play torrent media files using WebTorrent'}
|
||||
onCheck={this.handleOpenExternalPlayerChange}
|
||||
/>
|
||||
onCheck={this.handleOpenExternalPlayerChange} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +69,7 @@ class PreferencesPage extends React.Component {
|
||||
className='control'
|
||||
checked={this.props.state.unsaved.prefs.highestPlaybackPriority}
|
||||
label={'Highest Playback Priority'}
|
||||
onCheck={this.handleHighestPlaybackPriorityChange}
|
||||
onCheck={this.handleHighestPlaybackPriorityChange}
|
||||
/>
|
||||
<p>Pauses all active torrents to allow playback to use all of the available bandwidth.</p>
|
||||
</Preference>
|
||||
@@ -79,14 +81,12 @@ class PreferencesPage extends React.Component {
|
||||
}
|
||||
|
||||
externalPlayerPathSelector () {
|
||||
const playerName = path.basename(
|
||||
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
|
||||
)
|
||||
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
|
||||
const playerName = this.props.state.getExternalPlayerName()
|
||||
|
||||
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
||||
? `Torrent media files will always play in ${playerName}.`
|
||||
: `Torrent media files will play in ${playerName} if WebTorrent cannot ` +
|
||||
'play them.'
|
||||
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
|
||||
|
||||
return (
|
||||
<Preference>
|
||||
@@ -99,48 +99,70 @@ class PreferencesPage extends React.Component {
|
||||
displayValue={playerName}
|
||||
onChange={this.handleExternalPlayerPathChange}
|
||||
title='External player'
|
||||
value={this.props.state.unsaved.prefs.externalPlayerPath}
|
||||
/>
|
||||
value={playerPath ? path.dirname(playerPath) : null} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleExternalPlayerPathChange (filePath) {
|
||||
if (path.extname(filePath) === '.app') {
|
||||
// Mac: Use executable in packaged .app bundle
|
||||
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
|
||||
}
|
||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||
}
|
||||
|
||||
setDefaultAppButton () {
|
||||
const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
|
||||
if (isFileHandler) {
|
||||
return (
|
||||
<Preference>
|
||||
<p>WebTorrent is your default torrent app. Hooray!</p>
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Preference>
|
||||
<p>WebTorrent is not currently the default torrent app.</p>
|
||||
<RaisedButton
|
||||
className='control'
|
||||
onClick={this.handleSetDefaultApp}
|
||||
label='Make WebTorrent the default'
|
||||
/>
|
||||
label='Make WebTorrent the default' />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleStartupChange (e, isChecked) {
|
||||
dispatch('updatePreferences', 'startup', isChecked)
|
||||
}
|
||||
|
||||
setStartupSection () {
|
||||
if (config.IS_PORTABLE) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesSection title='Startup'>
|
||||
<Preference>
|
||||
<Checkbox
|
||||
className='control'
|
||||
checked={this.props.state.unsaved.prefs.startup}
|
||||
label={'Open WebTorrent on startup.'}
|
||||
onCheck={this.handleStartupChange}
|
||||
/>
|
||||
</Preference>
|
||||
</PreferencesSection>
|
||||
)
|
||||
}
|
||||
|
||||
handleSetDefaultApp () {
|
||||
window.alert('TODO')
|
||||
// var isFileHandler = state.unsaved.prefs.isFileHandler
|
||||
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
|
||||
dispatch('updatePreferences', 'isFileHandler', true)
|
||||
}
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
color: colors.grey400,
|
||||
marginLeft: 25,
|
||||
marginRight: 25
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: colors.grey400,
|
||||
marginLeft: 25,
|
||||
marginRight: 25
|
||||
}}
|
||||
>
|
||||
<div style={style}>
|
||||
<PreferencesSection title='Downloads'>
|
||||
{this.downloadPathSelector()}
|
||||
</PreferencesSection>
|
||||
@@ -152,6 +174,7 @@ class PreferencesPage extends React.Component {
|
||||
<PreferencesSection title='Default torrent app'>
|
||||
{this.setDefaultAppButton()}
|
||||
</PreferencesSection>
|
||||
{this.setStartupSection()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -165,13 +188,12 @@ class PreferencesSection extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
marginBottom: 25,
|
||||
marginTop: 25
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<div style={style}>
|
||||
<Heading level={2}>{this.props.title}</Heading>
|
||||
{this.props.children}
|
||||
</div>
|
||||
@@ -181,15 +203,8 @@ class PreferencesSection extends React.Component {
|
||||
|
||||
class Preference extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
const style = { marginBottom: 10 }
|
||||
return (<div style={style}>{this.props.children}</div>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
const React = require('react')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
const LinearProgress = require('material-ui/LinearProgress').default
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class TorrentList extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
const state = this.props.state
|
||||
|
||||
var contents = []
|
||||
const contents = []
|
||||
if (state.downloadPathStatus === 'missing') {
|
||||
contents.push(
|
||||
<div key='torrent-missing-path'>
|
||||
@@ -21,7 +24,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
var torrentElems = state.saved.torrents.map(
|
||||
const torrentElems = state.saved.torrents.map(
|
||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||
)
|
||||
contents.push(...torrentElems)
|
||||
@@ -39,30 +42,27 @@ module.exports = class TorrentList extends React.Component {
|
||||
}
|
||||
|
||||
renderTorrent (torrentSummary) {
|
||||
var state = this.props.state
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
const state = this.props.state
|
||||
const infoHash = torrentSummary.infoHash
|
||||
const isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
|
||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||
var style = {}
|
||||
const style = {}
|
||||
if (torrentSummary.posterFileName) {
|
||||
var gradient = isSelected
|
||||
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
style.backgroundImage = gradient + `, url('${posterPath}')`
|
||||
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
||||
}
|
||||
|
||||
// Foreground: name of the torrent, basic info like size, play button,
|
||||
// cast buttons if available, and delete
|
||||
var classes = ['torrent']
|
||||
// playStatus turns the play button into a loading spinner or error icon
|
||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||
const classes = ['torrent']
|
||||
if (isSelected) classes.push('selected')
|
||||
if (!infoHash) classes.push('disabled')
|
||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||
return (
|
||||
<div
|
||||
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||
key={torrentSummary.torrentKey}
|
||||
style={style}
|
||||
className={classes.join(' ')}
|
||||
@@ -71,48 +71,96 @@ module.exports = class TorrentList extends React.Component {
|
||||
{this.renderTorrentMetadata(torrentSummary)}
|
||||
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
||||
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
||||
<hr />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show name, download status, % complete
|
||||
renderTorrentMetadata (torrentSummary) {
|
||||
var name = torrentSummary.name || 'Loading torrent...'
|
||||
var elements = [(
|
||||
const name = torrentSummary.name || 'Loading torrent...'
|
||||
const elements = [(
|
||||
<div key='name' className='name ellipsis'>{name}</div>
|
||||
)]
|
||||
|
||||
// If it's downloading/seeding then show progress info
|
||||
var prog = torrentSummary.progress
|
||||
const prog = torrentSummary.progress
|
||||
let progElems
|
||||
if (torrentSummary.error) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{getErrorMessage(torrentSummary)}
|
||||
</div>
|
||||
)
|
||||
progElems = [getErrorMessage(torrentSummary)]
|
||||
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{renderPercentProgress()}
|
||||
{renderTotalProgress()}
|
||||
{renderPeers()}
|
||||
{renderDownloadSpeed()}
|
||||
{renderUploadSpeed()}
|
||||
{renderEta()}
|
||||
progElems = [
|
||||
renderDownloadCheckbox(),
|
||||
renderTorrentStatus(),
|
||||
renderProgressBar(),
|
||||
renderPercentProgress(),
|
||||
renderTotalProgress(),
|
||||
renderPeers(),
|
||||
renderSpeeds(),
|
||||
renderEta()
|
||||
]
|
||||
} else {
|
||||
progElems = [
|
||||
renderDownloadCheckbox(),
|
||||
renderTorrentStatus()
|
||||
]
|
||||
}
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{progElems}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||
|
||||
function renderDownloadCheckbox () {
|
||||
const infoHash = torrentSummary.infoHash
|
||||
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
|
||||
return (
|
||||
<Checkbox
|
||||
key='download-button'
|
||||
className={'control download ' + torrentSummary.status}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 32
|
||||
}}
|
||||
iconStyle={{
|
||||
width: 20,
|
||||
height: 20
|
||||
}}
|
||||
checked={isActive}
|
||||
onClick={stopPropagation}
|
||||
onCheck={dispatcher('toggleTorrent', infoHash)} />
|
||||
)
|
||||
}
|
||||
|
||||
function renderProgressBar () {
|
||||
const progress = Math.floor(100 * prog.progress)
|
||||
const styles = {
|
||||
wrapper: {
|
||||
display: 'inline-block',
|
||||
marginRight: 8
|
||||
},
|
||||
progress: {
|
||||
height: 8,
|
||||
width: 30
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={styles.wrapper}>
|
||||
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||
|
||||
function renderPercentProgress () {
|
||||
var progress = Math.floor(100 * prog.progress)
|
||||
const progress = Math.floor(100 * prog.progress)
|
||||
return (<span key='percent-progress'>{progress}%</span>)
|
||||
}
|
||||
|
||||
function renderTotalProgress () {
|
||||
var downloaded = prettyBytes(prog.downloaded)
|
||||
var total = prettyBytes(prog.length || 0)
|
||||
const downloaded = prettyBytes(prog.downloaded)
|
||||
const total = prettyBytes(prog.length || 0)
|
||||
if (downloaded === total) {
|
||||
return (<span key='total-progress'>{downloaded}</span>)
|
||||
} else {
|
||||
@@ -122,112 +170,78 @@ module.exports = class TorrentList extends React.Component {
|
||||
|
||||
function renderPeers () {
|
||||
if (prog.numPeers === 0) return
|
||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||
const count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
||||
}
|
||||
|
||||
function renderDownloadSpeed () {
|
||||
if (prog.downloadSpeed === 0) return
|
||||
return (<span key='download'>↓ {prettyBytes(prog.downloadSpeed)}/s</span>)
|
||||
}
|
||||
|
||||
function renderUploadSpeed () {
|
||||
if (prog.uploadSpeed === 0) return
|
||||
return (<span key='upload'>↑ {prettyBytes(prog.uploadSpeed)}/s</span>)
|
||||
function renderSpeeds () {
|
||||
let str = ''
|
||||
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
|
||||
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
|
||||
if (str === '') return
|
||||
return (<span key='download'>{str}</span>)
|
||||
}
|
||||
|
||||
function renderEta () {
|
||||
var downloaded = prog.downloaded
|
||||
var total = prog.length || 0
|
||||
var missing = total - downloaded
|
||||
var downloadSpeed = prog.downloadSpeed
|
||||
const downloaded = prog.downloaded
|
||||
const total = prog.length || 0
|
||||
const missing = total - downloaded
|
||||
const downloadSpeed = prog.downloadSpeed
|
||||
if (downloadSpeed === 0 || missing === 0) return
|
||||
|
||||
var rawEta = missing / downloadSpeed
|
||||
var hours = Math.floor(rawEta / 3600) % 24
|
||||
var minutes = Math.floor(rawEta / 60) % 60
|
||||
var seconds = Math.floor(rawEta % 60)
|
||||
const rawEta = missing / downloadSpeed
|
||||
const hours = Math.floor(rawEta / 3600) % 24
|
||||
const minutes = Math.floor(rawEta / 60) % 60
|
||||
const seconds = Math.floor(rawEta % 60)
|
||||
|
||||
// Only display hours and minutes if they are greater than 0 but always
|
||||
// display minutes if hours is being displayed
|
||||
var hoursStr = hours ? hours + 'h' : ''
|
||||
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||
var secondsStr = seconds + 's'
|
||||
const hoursStr = hours ? hours + 'h' : ''
|
||||
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||
const secondsStr = seconds + 's'
|
||||
|
||||
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
||||
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
|
||||
}
|
||||
|
||||
function renderTorrentStatus () {
|
||||
let status
|
||||
if (torrentSummary.status === 'paused') {
|
||||
if (!torrentSummary.progress) status = ''
|
||||
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
||||
else status = 'Paused'
|
||||
} else if (torrentSummary.status === 'downloading') {
|
||||
status = 'Downloading'
|
||||
} else if (torrentSummary.status === 'seeding') {
|
||||
status = 'Seeding'
|
||||
} else { // torrentSummary.status is 'new' or something unexpected
|
||||
status = ''
|
||||
}
|
||||
return (<span>{status}</span>)
|
||||
}
|
||||
}
|
||||
|
||||
// Download button toggles between torrenting (DL/seed) and paused
|
||||
// Play button starts streaming the torrent immediately, unpausing if needed
|
||||
renderTorrentButtons (torrentSummary) {
|
||||
var infoHash = torrentSummary.infoHash
|
||||
|
||||
var playIcon, playTooltip, playClass
|
||||
if (torrentSummary.playStatus === 'timeout') {
|
||||
playIcon = 'warning'
|
||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||
} else {
|
||||
playIcon = 'play_arrow'
|
||||
playTooltip = 'Start streaming'
|
||||
}
|
||||
|
||||
var downloadIcon, downloadTooltip
|
||||
if (torrentSummary.status === 'seeding') {
|
||||
downloadIcon = 'file_upload'
|
||||
downloadTooltip = 'Seeding. Click to stop.'
|
||||
} else if (torrentSummary.status === 'downloading') {
|
||||
downloadIcon = 'file_download'
|
||||
downloadTooltip = 'Torrenting. Click to stop.'
|
||||
} else {
|
||||
downloadIcon = 'file_download'
|
||||
downloadTooltip = 'Click to start torrenting.'
|
||||
}
|
||||
const infoHash = torrentSummary.infoHash
|
||||
|
||||
// Only show the play/dowload buttons for torrents that contain playable media
|
||||
var playButton, downloadButton, positionElem
|
||||
if (!torrentSummary.error) {
|
||||
downloadButton = (
|
||||
let playButton
|
||||
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
playButton = (
|
||||
<i
|
||||
key='download-button'
|
||||
className={'button-round icon download ' + torrentSummary.status}
|
||||
title={downloadTooltip}
|
||||
onClick={dispatcher('toggleTorrent', infoHash)}
|
||||
>
|
||||
{downloadIcon}
|
||||
key='play-button'
|
||||
title='Start streaming'
|
||||
className={'icon play'}
|
||||
onClick={dispatcher('playFile', infoHash)}>
|
||||
play_circle_outline
|
||||
</i>
|
||||
)
|
||||
|
||||
// Do we have a saved position? Show it using a radial progress bar on top
|
||||
// of the play button, unless already showing a spinner there:
|
||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||
var defaultFile = torrentSummary.files &&
|
||||
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
||||
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
||||
var fraction = defaultFile.currentTime / defaultFile.duration
|
||||
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||
playClass = 'resume-position'
|
||||
}
|
||||
|
||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
playButton = (
|
||||
<i
|
||||
key='play-button'
|
||||
title={playTooltip}
|
||||
className={'button-round icon play ' + playClass}
|
||||
onClick={dispatcher('playFile', infoHash)}
|
||||
>
|
||||
{playIcon}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='buttons' className='buttons'>
|
||||
{positionElem}
|
||||
<div className='torrent-controls'>
|
||||
{playButton}
|
||||
{downloadButton}
|
||||
<i
|
||||
key='delete-button'
|
||||
className='icon delete'
|
||||
@@ -241,9 +255,9 @@ module.exports = class TorrentList extends React.Component {
|
||||
|
||||
// Show files, per-file download status and play buttons, and so on
|
||||
renderTorrentDetails (torrentSummary) {
|
||||
var filesElement
|
||||
let filesElement
|
||||
if (torrentSummary.error || !torrentSummary.files) {
|
||||
var message = ''
|
||||
let message = ''
|
||||
if (torrentSummary.error === 'path-missing') {
|
||||
// Special case error: this torrent's download dir or file is missing
|
||||
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
@@ -264,14 +278,9 @@ module.exports = class TorrentList extends React.Component {
|
||||
)
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files
|
||||
const fileRows = torrentSummary.files
|
||||
.filter((file) => !file.path.includes('/.____padding_file/'))
|
||||
.map((file, index) => ({ file, index }))
|
||||
.sort(function (a, b) {
|
||||
if (a.file.name < b.file.name) return -1
|
||||
if (b.file.name < a.file.name) return 1
|
||||
return 0
|
||||
})
|
||||
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||
|
||||
filesElement = (
|
||||
@@ -296,28 +305,28 @@ module.exports = class TorrentList extends React.Component {
|
||||
renderFileRow (torrentSummary, file, index) {
|
||||
// First, find out how much of the file we've downloaded
|
||||
// Are we even torrenting it?
|
||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||
var isDone = false // Are we finished torrenting it?
|
||||
var progress = ''
|
||||
const isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||
let isDone = false // Are we finished torrenting it?
|
||||
let progress = ''
|
||||
if (torrentSummary.progress && torrentSummary.progress.files &&
|
||||
torrentSummary.progress.files[index]) {
|
||||
var fileProg = torrentSummary.progress.files[index]
|
||||
const fileProg = torrentSummary.progress.files[index]
|
||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
// Second, for media files where we saved our position, show how far we got
|
||||
var positionElem
|
||||
let positionElem
|
||||
if (file.currentTime) {
|
||||
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
||||
}
|
||||
|
||||
// Finally, render the file as a table row
|
||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var icon
|
||||
var handleClick
|
||||
const isPlayable = TorrentPlayer.isPlayable(file)
|
||||
const infoHash = torrentSummary.infoHash
|
||||
let icon
|
||||
let handleClick
|
||||
if (isPlayable) {
|
||||
icon = 'play_arrow' /* playable? add option to play */
|
||||
handleClick = dispatcher('playFile', infoHash, index)
|
||||
@@ -328,7 +337,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
: (e) => e.stopPropagation() // noop if file is not ready
|
||||
}
|
||||
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
|
||||
var rowClass = ''
|
||||
let rowClass = ''
|
||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||
return (
|
||||
@@ -348,16 +357,16 @@ module.exports = class TorrentList extends React.Component {
|
||||
</td>
|
||||
<td className='col-select'
|
||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
||||
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
renderRadialProgressBar (fraction, cssClass) {
|
||||
var rotation = 360 * fraction
|
||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
const rotation = 360 * fraction
|
||||
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
|
||||
return (
|
||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||
@@ -376,8 +385,12 @@ module.exports = class TorrentList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
function stopPropagation (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
function getErrorMessage (torrentSummary) {
|
||||
var err = torrentSummary.error
|
||||
const err = torrentSummary.error
|
||||
if (err === 'path-missing') {
|
||||
return (
|
||||
<span>
|
||||
Reference in New Issue
Block a user