fixed merge conflicts

This commit is contained in:
Alberto Miranda
2016-08-19 01:09:43 -03:00
28 changed files with 493 additions and 203 deletions

View File

@@ -188,7 +188,7 @@ module.exports = class PlaybackController {
}, 10000) /* give it a few seconds */
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary)
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
} else {
@@ -250,7 +250,7 @@ module.exports = class PlaybackController {
}
// otherwise, play the video
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
state.window.title = torrentSummary.files[state.playing.fileIndex].name
this.update()
ipcRenderer.send('onPlayerOpen')

View File

@@ -1,5 +1,6 @@
const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
// Controls the Preferences screen
module.exports = class PrefsController {
@@ -15,11 +16,15 @@ module.exports = class PrefsController {
url: 'preferences',
setup: function (cb) {
// initialize preferences
dispatch('setTitle', 'Preferences')
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
ipcRenderer.send('setAllowNav', false)
cb()
},
destroy: () => this.save()
destroy: () => {
ipcRenderer.send('setAllowNav', true)
this.save()
}
})
}
@@ -41,7 +46,11 @@ module.exports = class PrefsController {
// All unsaved prefs take effect atomically, and are saved to config.json
save () {
var state = this.state
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
}
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state)
dispatch('checkDownloadPath')
}
}

View File

@@ -24,8 +24,8 @@ module.exports = class TorrentListController {
// Use path string instead of W3C File object
torrentId = torrentId.path
}
// Allow a instant.io link to be pasted
// TODO: remove this once support is added to webtorrent core
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
// Shows the Create Torrent page with options to seed a given file or folder
showCreateTorrent (files) {
// You can only create torrents from the home screen.
if (this.state.location.url() !== 'home') {
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
}
// Files will either be an array of file objects, which we can send directly
// to the create-torrent screen
if (files.length === 0 || typeof files[0] !== 'string') {
this.state.location.go({
url: 'create-torrent',
files: files
files: files,
setup: (cb) => {
this.state.window.title = 'Create New Torrent'
cb(null)
}
})
return
}
@@ -67,27 +76,29 @@ module.exports = class TorrentListController {
var state = this.state
var torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options)
state.location.backToFirst(function () {
state.location.clearForward('create-torrent')
})
state.location.cancel()
}
// Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentSummary) {
var s = torrentSummary
// Backward compatibility for config files save before we had torrentKey
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
startTorrentingSummary (torrentKey) {
var s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new Error('Missing key: ' + torrentKey)
// Use Downloads folder by default
if (!s.path) s.path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting',
s.torrentKey,
TorrentSummary.getTorrentID(s),
s.path,
s.fileModtimes,
s.selections)
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
if (err) {
s.error = 'path-missing'
return
}
ipcRenderer.send('wt-start-torrenting',
s.torrentKey,
TorrentSummary.getTorrentID(s),
s.path,
s.fileModtimes,
s.selections)
})
}
// TODO: use torrentKey, not infoHash
@@ -95,7 +106,7 @@ module.exports = class TorrentListController {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
this.startTorrentingSummary(torrentSummary)
this.startTorrentingSummary(torrentSummary.torrentKey)
sound.play('ENABLE')
} else {
torrentSummary.status = 'paused'
@@ -271,6 +282,7 @@ function saveTorrentFileAs (torrentSummary) {
]
}
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
if (!savePath) return // They clicked Cancel
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)

View File

@@ -25,6 +25,10 @@ function run (state) {
migrate_0_7_2(state.saved)
}
if (semver.lt(version, '0.11.0')) {
migrate_0_11_0(state.saved)
}
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
@@ -93,3 +97,10 @@ function migrate_0_7_2 (saved) {
}
}
}
function migrate_0_11_0 (saved) {
if (saved.prefs.isFileHandler === undefined) {
// The app used to make itself the default torrent file handler automatically
saved.prefs.isFileHandler = true
}
}

View File

@@ -200,6 +200,9 @@ function save (state, cb) {
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
if (key === 'error') {
continue // Don't save error states
}
torrent[key] = x[key]
}
return torrent

View File

@@ -7,6 +7,7 @@ const dragDrop = require('drag-drop')
const electron = require('electron')
const React = require('react')
const ReactDOM = require('react-dom')
const fs = require('fs')
const config = require('../config')
const App = require('./views/app')
@@ -77,21 +78,27 @@ function onState (err, _state) {
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Listen for messages from the main process
setupIpc()
// Calling update() updates the UI given the current 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)
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Listen for messages from the main process
setupIpc()
// Warn if the download dir is gone, eg b/c an external drive is unplugged
checkDownloadPath()
// OS integrations:
// ...drag and drop a torrent or video file to play or seed
dragDrop('body', onOpen)
// ...drag and drop files/text to start torrenting or seeding
dragDrop('body', {
onDrop: onOpen,
onDropText: onOpen
})
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
@@ -172,8 +179,7 @@ const dispatchHandlers = {
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentSummary) =>
controllers.torrentList.startTorrentingSummary(torrentSummary),
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
// Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
@@ -209,6 +215,7 @@ const dispatchHandlers = {
// Preferences screen
'preferences': () => controllers.prefs.show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
'checkDownloadPath': checkDownloadPath,
// Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update.updateAvailable(version),
@@ -220,6 +227,7 @@ const dispatchHandlers = {
'escapeBack': escapeBack,
'back': () => state.location.back(),
'forward': () => state.location.forward(),
'cancel': () => state.location.cancel(),
// Controlling the window
'setDimensions': setDimensions,
@@ -309,8 +317,14 @@ function escapeBack () {
// Starts all torrents that aren't paused on program startup
function resumeTorrents () {
state.saved.torrents
.filter((torrentSummary) => torrentSummary.status !== 'paused')
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
.map((torrentSummary) => {
// Torrent keys are ephemeral, reassigned each time the app runs.
// On startup, give all torrents a key, even the ones that are paused.
torrentSummary.torrentKey = state.nextTorrentKey++
return torrentSummary
})
.filter((s) => s.status !== 'paused')
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
}
// Set window dimensions to match video dimensions or fill the screen
@@ -357,25 +371,25 @@ function setDimensions (dimensions) {
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
if (state.modal) {
var url = state.location.url()
var allTorrents = files.every(TorrentPlayer.isTorrent)
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what
dispatch('backToList')
// All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file))
} else if (url === 'player' && allSubtitles) {
// Drop subtitles onto a playing video: add subtitles
controllers.subtitles.addSubtitles(files, true)
} else if (url === 'home') {
// Drop files onto home screen: show Create Torrent
state.modal = null
}
var subtitles = files.filter(controllers.subtitles.isSubtitle)
if (state.location.url() === 'home' || subtitles.length === 0) {
if (files.every(TorrentPlayer.isTorrent)) {
if (state.location.url() !== 'home') {
dispatch('backToList')
}
// All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file))
} else {
// Show the Create Torrent screen. Let's seed those files.
controllers.torrentList.showCreateTorrent(files)
}
} else if (state.location.url() === 'player') {
controllers.subtitles.addSubtitles(subtitles, true)
controllers.torrentList.showCreateTorrent(files)
} else {
// Drop files onto any other screen: show error
return onError('Please go back to the torrent list before creating a new torrent.')
}
update()
@@ -429,3 +443,14 @@ function onFullscreenChanged (e, isFullScreen) {
update()
}
function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) {
state.downloadPathStatus = 'missing'
return console.error(err)
}
if (stat.isDirectory()) state.downloadPathStatus = 'ok'
else state.downloadPathStatus = 'missing'
})
}

View File

@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
</p>
</p>
<p className='float-right'>
<button className='button-flat light' onClick={dispatcher('back')}>
<button className='button-flat light' onClick={dispatcher('cancel')}>
Cancel
</button>
</p>

View File

@@ -77,7 +77,7 @@ module.exports = class CreateTorrentPage extends React.Component {
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
<textarea className='torrent-attribute torrent-trackers' value={trackers}></textarea>
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers}></textarea>
</div>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
</div>
</div>
<div key='buttons' className='float-right'>
<button key='cancel' className='button-flat light' onClick={dispatcher('back')}>Cancel</button>
<button key='cancel' className='button-flat light' onClick={dispatcher('cancel')}>Cancel</button>
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
</div>
</div>

View File

@@ -23,7 +23,8 @@ function renderGeneralSection (state) {
description: '',
icon: 'settings'
}, [
renderDownloadDirSelector(state)
renderDownloadPathSelector(state),
renderFileHandlers(state)
])
}
@@ -50,7 +51,7 @@ function renderPlayInVlcSelector (state) {
})
}
function renderDownloadDirSelector (state) {
function renderDownloadPathSelector (state) {
return renderFileSelector({
key: 'download-path',
label: 'Download Path',
@@ -63,10 +64,33 @@ function renderDownloadDirSelector (state) {
},
state.unsaved.prefs.downloadPath,
function (filePath) {
setStateValue('downloadPath', filePath)
dispatch('updatePreferences', 'downloadPath', filePath)
})
}
function renderFileHandlers (state) {
var definition = {
key: 'file-handlers',
label: 'Handle Torrent Files'
}
var buttonText = state.unsaved.prefs.isFileHandler
? 'Remove default app for torrent files'
: 'Make WebTorrent the default app for torrent files'
var controls = [(
<button key='toggle-handlers'
className='btn'
onClick={toggleFileHandlers}>
{buttonText}
</button>
)]
return renderControlGroup(definition, controls)
function toggleFileHandlers () {
var isFileHandler = state.unsaved.prefs.isFileHandler
dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
}
}
// Renders a prefs section.
// - definition should be {icon, title, description}
// - controls should be an array of vdom elements
@@ -126,25 +150,24 @@ function renderCheckbox (definition, value, callback) {
// - 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 (
<div key={definition.key} 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 className='controls'>
<input type='text' className='file-picker-text'
id={definition.property}
disabled='disabled'
value={value} />
<button className='btn' onClick={handleClick}>
<i className='icon'>folder_open</i>
</button>
</div>
</div>
</div>
)
var controls = [(
<input
type='text'
className='file-picker-text'
key={definition.property}
id={definition.property}
disabled='disabled'
value={value} />
), (
<button
key={definition.property + '-btn'}
className='btn'
onClick={handleClick}>
<i className='icon'>folder_open</i>
</button>
)]
return renderControlGroup(definition, controls)
function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return
@@ -153,6 +176,18 @@ function renderFileSelector (definition, value, callback) {
}
}
function setStateValue (property, value) {
dispatch('updatePreferences', property, value)
function renderControlGroup (definition, controls) {
return (
<div key={definition.key} 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 className='controls'>
{controls}
</div>
</div>
</div>
)
}

View File

@@ -8,16 +8,32 @@ const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component {
render () {
var state = this.props.state
var torrentRows = state.saved.torrents.map(
var contents = []
if (state.downloadPathStatus === 'missing') {
contents.push(
<div key='torrent-missing-path'>
<p>Download path missing: {state.saved.prefs.downloadPath}</p>
<p>Check that all drives are connected?</p>
<p>Alternatively, choose a new download path
in <a href='#' onClick={dispatcher('preferences')}>Preferences</a>
</p>
</div>
)
}
var torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary)
)
contents.push(...torrentElems)
contents.push(
<div key='torrent-placeholder' className='torrent-placeholder'>
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div>
)
return (
<div key='torrent-list' className='torrent-list'>
{torrentRows}
<div key='torrent-placeholder' className='torrent-placeholder'>
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div>
{contents}
</div>
)
}
@@ -44,6 +60,7 @@ module.exports = class TorrentList extends React.Component {
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return (
<div
key={torrentSummary.torrentKey}
@@ -67,8 +84,14 @@ module.exports = class TorrentList extends React.Component {
// If it's downloading/seeding then show progress info
var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) {
elements.push((
if (torrentSummary.error) {
elements.push(
<div key='progress-info' className='ellipsis'>
{getErrorMessage(torrentSummary)}
</div>
)
} else if (torrentSummary.status !== 'paused' && prog) {
elements.push(
<div key='progress-info' className='ellipsis'>
{renderPercentProgress()}
{renderTotalProgress()}
@@ -77,7 +100,7 @@ module.exports = class TorrentList extends React.Component {
{renderUploadSpeed()}
{renderEta()}
</div>
))
)
}
return (<div key='metadata' className='metadata'>{elements}</div>)
@@ -174,8 +197,9 @@ module.exports = class TorrentList extends React.Component {
}
// Only show the play button for torrents that contain playable media
var playButton
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
var playButton, downloadButton
var noErrors = !torrentSummary.error
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
<i
key='play-button'
@@ -186,11 +210,8 @@ module.exports = class TorrentList extends React.Component {
</i>
)
}
return (
<div key='buttons' className='buttons'>
{positionElem}
{playButton}
if (noErrors) {
downloadButton = (
<i
key='download-button'
className={'button-round icon download ' + torrentSummary.status}
@@ -198,6 +219,14 @@ module.exports = class TorrentList extends React.Component {
onClick={dispatcher('toggleTorrent', infoHash)}>
{downloadIcon}
</i>
)
}
return (
<div key='buttons' className='buttons'>
{positionElem}
{playButton}
{downloadButton}
<i
key='delete-button'
className='icon delete'
@@ -212,12 +241,26 @@ module.exports = class TorrentList extends React.Component {
// Show files, per-file download status and play buttons, and so on
renderTorrentDetails (torrentSummary) {
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
var message = torrentSummary.status === 'paused'
? 'Failed to load torrent info. Click the download button to try again...'
: 'Downloading torrent info...'
filesElement = (<div key='files' className='files warning'>{message}</div>)
if (torrentSummary.error || !torrentSummary.files) {
var message = ''
if (torrentSummary.error === 'path-missing') {
// Special case error: this torrent's download dir or file is missing
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
} else if (torrentSummary.error) {
// General error for this torrent: just show the message
message = torrentSummary.error.message || torrentSummary.error
} else if (torrentSummary.status === 'paused') {
// No file info, no infohash, and we're not trying to download from the DHT
message = 'Failed to load torrent info. Click the download button to try again...'
} else {
// No file info, no infohash, trying to load from the DHT
message = 'Downloading torrent info...'
}
filesElement = (
<div key='files' className='files warning'>
{message}
</div>
)
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
@@ -255,7 +298,8 @@ module.exports = class TorrentList extends React.Component {
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) {
if (torrentSummary.progress && torrentSummary.progress.files &&
torrentSummary.progress.files[index]) {
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
@@ -327,3 +371,16 @@ module.exports = class TorrentList extends React.Component {
)
}
}
function getErrorMessage (torrentSummary) {
var err = torrentSummary.error
if (err === 'path-missing') {
return (
<span>
Path missing.<br />
Fix and restart the app, or delete the torrent.
</span>
)
}
return 'Error'
}

View File

@@ -11,18 +11,18 @@ module.exports = class UpdateAvailableModal extends React.Component {
<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 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>
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
</p>
</div>
)
function handleOK () {
function handleShow () {
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
dispatch('exitModal')
}
function handleCancel () {
function handleSkip () {
dispatch('skipVersion', state.modal.version)
dispatch('exitModal')
}