Merge remote-tracking branch 'feross/master'
This commit is contained in:
@@ -40,7 +40,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Show a file in the OS, eg in Finder on a Mac
|
// Show a file in the OS, eg in Finder on a Mac
|
||||||
openItem (infoHash, index) {
|
openItem (infoHash, index) {
|
||||||
var torrentSummary = torrentSummary.getByKey(this.state, infoHash)
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
var filePath = path.join(
|
var filePath = path.join(
|
||||||
torrentSummary.path,
|
torrentSummary.path,
|
||||||
torrentSummary.files[index].path)
|
torrentSummary.files[index].path)
|
||||||
|
|||||||
192
renderer/controllers/torrent-controller.js
Normal file
192
renderer/controllers/torrent-controller.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
|
const sound = require('../lib/sound')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class TorrentController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentInfoHash (torrentKey, infoHash) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
console.log('got infohash for %s torrent %s',
|
||||||
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
|
|
||||||
|
if (!torrentSummary) {
|
||||||
|
var torrents = this.state.saved.torrents
|
||||||
|
|
||||||
|
// Check if an existing (non-active) torrent has the same info hash
|
||||||
|
if (torrents.find((t) => t.infoHash === infoHash)) {
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
return dispatch('error', 'Cannot add duplicate torrent')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary = {
|
||||||
|
torrentKey: torrentKey,
|
||||||
|
status: 'new'
|
||||||
|
}
|
||||||
|
torrents.unshift(torrentSummary)
|
||||||
|
sound.play('ADD')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary.infoHash = infoHash
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentWarning (torrentKey, message) {
|
||||||
|
console.log('warning for torrent %s: %s', torrentKey, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentError (torrentKey, message) {
|
||||||
|
// TODO: WebTorrent needs semantic errors
|
||||||
|
if (message.startsWith('Cannot add duplicate torrent')) {
|
||||||
|
// Remove infohash from the message
|
||||||
|
message = 'Cannot add duplicate torrent'
|
||||||
|
}
|
||||||
|
dispatch('error', message)
|
||||||
|
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
if (torrentSummary) {
|
||||||
|
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||||
|
torrentSummary.status = 'paused'
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentMetadata (torrentKey, torrentInfo) {
|
||||||
|
// Summarize torrent
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'downloading'
|
||||||
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
|
torrentSummary.path = torrentInfo.path
|
||||||
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
|
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||||
|
// For now, check whether torrentSummary.files has already been set:
|
||||||
|
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||||
|
if (!hasDetailedFileInfo) {
|
||||||
|
torrentSummary.files = torrentInfo.files
|
||||||
|
}
|
||||||
|
if (!torrentSummary.selections) {
|
||||||
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
|
}
|
||||||
|
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
||||||
|
dispatch('update')
|
||||||
|
|
||||||
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
|
if (!torrentSummary.torrentFileName) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
||||||
|
|
||||||
|
// Auto-generate a poster image, if it hasn't been generated already
|
||||||
|
if (!torrentSummary.posterFileName) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentDone (torrentKey, torrentInfo) {
|
||||||
|
// Update the torrent summary
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'seeding'
|
||||||
|
|
||||||
|
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
||||||
|
// Don't notify if we merely finished verifying data files that were already on disk.
|
||||||
|
if (torrentInfo.bytesReceived > 0) {
|
||||||
|
if (!this.state.window.isFocused) {
|
||||||
|
this.state.dock.badge += 1
|
||||||
|
}
|
||||||
|
showDoneNotification(torrentSummary)
|
||||||
|
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentProgress (progressInfo) {
|
||||||
|
// Overall progress across all active torrents, 0 to 1
|
||||||
|
var progress = progressInfo.progress
|
||||||
|
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
||||||
|
|
||||||
|
// Hide progress bar when client has no torrents, or progress is 100%
|
||||||
|
// TODO: isn't this equivalent to: if (progress === 1) ?
|
||||||
|
if (!hasActiveTorrents || progress === 1) {
|
||||||
|
progress = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||||
|
this.state.dock.progress = progress
|
||||||
|
|
||||||
|
// Update progress for each individual torrent
|
||||||
|
progressInfo.torrents.forEach((p) => {
|
||||||
|
var torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||||
|
if (!torrentSummary) {
|
||||||
|
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
torrentSummary.progress = p
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Find an efficient way to re-enable this line, which allows subtitle
|
||||||
|
// files which are completed after a video starts to play to be added
|
||||||
|
// dynamically to the list of subtitles.
|
||||||
|
// checkForSubtitles()
|
||||||
|
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.posterFileName = posterFileName
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentAudioMetadata (infoHash, index, info) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(infoHash)
|
||||||
|
var fileSummary = torrentSummary.files[index]
|
||||||
|
fileSummary.audioInfo = info
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentServerRunning (serverInfo) {
|
||||||
|
this.state.server = serverInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
||||||
|
// Returns undefined if we don't know that infoHash
|
||||||
|
getTorrentSummary (torrentKey) {
|
||||||
|
return TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTorrentPath (torrentSummary) {
|
||||||
|
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
|
if (torrentSummary.files.length > 1) {
|
||||||
|
itemPath = path.dirname(itemPath)
|
||||||
|
}
|
||||||
|
return itemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDoneNotification (torrent) {
|
||||||
|
var notif = new window.Notification('Download Complete', {
|
||||||
|
body: torrent.name,
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
|
||||||
|
notif.onclick = function () {
|
||||||
|
ipcRenderer.send('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
sound.play('DONE')
|
||||||
|
}
|
||||||
@@ -112,6 +112,14 @@ module.exports = class TorrentListController {
|
|||||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmDeleteTorrent (infoHash, deleteData) {
|
||||||
|
this.state.modal = {
|
||||||
|
id: 'remove-torrent-modal',
|
||||||
|
infoHash,
|
||||||
|
deleteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
deleteTorrent (infoHash, deleteData) {
|
deleteTorrent (infoHash, deleteData) {
|
||||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
@@ -151,14 +159,12 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove From List',
|
label: 'Remove From List',
|
||||||
click: () => this.deleteTorrent(
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
|
||||||
torrentSummary.infoHash, false)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove Data File',
|
label: 'Remove Data File',
|
||||||
click: () => this.deleteTorrent(
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
|
||||||
torrentSummary.infoHash, true)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
@@ -239,27 +245,23 @@ function findFilesRecursive (paths, cb) {
|
|||||||
function deleteFile (path) {
|
function deleteFile (path) {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
fs.unlink(path, function (err) {
|
fs.unlink(path, function (err) {
|
||||||
if (err) dispatch('onError', err)
|
if (err) dispatch('error', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all files in a torren
|
// Delete all files in a torrent
|
||||||
function moveItemToTrash (torrentSummary) {
|
function moveItemToTrash (torrentSummary) {
|
||||||
// TODO: delete directories, not just files
|
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
torrentSummary.files.forEach(function (file) {
|
|
||||||
var filePath = path.join(torrentSummary.path, file.path)
|
|
||||||
console.log('DEBUG DELETING ' + filePath)
|
|
||||||
ipcRenderer.send('moveItemToTrash', filePath)
|
ipcRenderer.send('moveItemToTrash', filePath)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getTorrentPath(torrentSummary))
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
function saveTorrentFileAs (torrentSummary) {
|
||||||
var downloadPath = this.state.saved.prefs.downloadPath
|
var downloadPath = this.state.saved.prefs.downloadPath
|
||||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
var opts = {
|
var opts = {
|
||||||
title: 'Save Torrent File',
|
title: 'Save Torrent File',
|
||||||
defaultPath: path.join(downloadPath, newFileName),
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ module.exports = {
|
|||||||
isPlayable,
|
isPlayable,
|
||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
isPlayableTorrent,
|
isTorrent,
|
||||||
|
isPlayableTorrentSummary,
|
||||||
pickFileToPlay
|
pickFileToPlay
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
/**
|
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||||
* Determines whether a file in a torrent is audio/video we can play
|
// based on the file extension
|
||||||
*/
|
|
||||||
function isPlayable (file) {
|
function isPlayable (file) {
|
||||||
return isVideo(file) || isAudio(file)
|
return isVideo(file) || isAudio(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable video
|
||||||
function isVideo (file) {
|
function isVideo (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
|
||||||
return [
|
return [
|
||||||
'.avi',
|
'.avi',
|
||||||
'.m4v',
|
'.m4v',
|
||||||
@@ -27,21 +27,36 @@ function isVideo (file) {
|
|||||||
'.ogv',
|
'.ogv',
|
||||||
'.webm',
|
'.webm',
|
||||||
'.wmv'
|
'.wmv'
|
||||||
].includes(ext)
|
].includes(getFileExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable audio
|
||||||
function isAudio (file) {
|
function isAudio (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
|
||||||
return [
|
return [
|
||||||
'.aac',
|
'.aac',
|
||||||
'.ac3',
|
'.ac3',
|
||||||
'.mp3',
|
'.mp3',
|
||||||
'.ogg',
|
'.ogg',
|
||||||
'.wav'
|
'.wav'
|
||||||
].includes(ext)
|
].includes(getFileExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlayableTorrent (torrentSummary) {
|
// Checks if the argument is either:
|
||||||
|
// - a string that's a valid filename ending in .torrent
|
||||||
|
// - a file object where obj.name is ends in .torrent
|
||||||
|
// - a string that's a magnet link (magnet://...)
|
||||||
|
function isTorrent (file) {
|
||||||
|
var isTorrentFile = getFileExtension(file) === '.torrent'
|
||||||
|
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||||
|
return isTorrentFile || isMagnet
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension (file) {
|
||||||
|
var name = typeof file === 'string' ? file : file.name
|
||||||
|
return path.extname(name).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayableTorrentSummary (torrentSummary) {
|
||||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ module.exports = {
|
|||||||
getPosterPath,
|
getPosterPath,
|
||||||
getTorrentPath,
|
getTorrentPath,
|
||||||
getByKey,
|
getByKey,
|
||||||
getTorrentID
|
getTorrentID,
|
||||||
|
getFileOrFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
@@ -43,3 +44,13 @@ function getByKey (state, torrentKey) {
|
|||||||
return state.saved.torrents.find((x) =>
|
return state.saved.torrents.find((x) =>
|
||||||
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the path to either the file (in a single-file torrent) or the root
|
||||||
|
// folder (in multi-file torrent)
|
||||||
|
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
|
||||||
|
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||||
|
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||||
|
function getFileOrFolder (torrentSummary) {
|
||||||
|
var ts = torrentSummary
|
||||||
|
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||||
|
}
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent input.torrent-is-private {
|
.create-torrent input.torrent-is-private {
|
||||||
width: initial;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +403,7 @@ button.button-raised:active {
|
|||||||
* OTHER FORM ELEMENT DEFAULTS
|
* OTHER FORM ELEMENT DEFAULTS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
input {
|
input[type='text'] {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|||||||
230
renderer/main.js
230
renderer/main.js
@@ -6,7 +6,6 @@ crashReporter.init()
|
|||||||
const dragDrop = require('drag-drop')
|
const dragDrop = require('drag-drop')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const mainLoop = require('main-loop')
|
const mainLoop = require('main-loop')
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const createElement = require('virtual-dom/create-element')
|
const createElement = require('virtual-dom/create-element')
|
||||||
const diff = require('virtual-dom/diff')
|
const diff = require('virtual-dom/diff')
|
||||||
@@ -18,14 +17,14 @@ const telemetry = require('./lib/telemetry')
|
|||||||
const sound = require('./lib/sound')
|
const sound = require('./lib/sound')
|
||||||
const State = require('./lib/state')
|
const State = require('./lib/state')
|
||||||
const TorrentPlayer = require('./lib/torrent-player')
|
const TorrentPlayer = require('./lib/torrent-player')
|
||||||
const TorrentSummary = require('./lib/torrent-summary')
|
|
||||||
|
|
||||||
const MediaController = require('./controllers/media-controller')
|
const MediaController = require('./controllers/media-controller')
|
||||||
const UpdateController = require('./controllers/update-controller')
|
const UpdateController = require('./controllers/update-controller')
|
||||||
const PrefsController = require('./controllers/prefs-controller')
|
const PrefsController = require('./controllers/prefs-controller')
|
||||||
const TorrentListController = require('./controllers/torrent-list-controller')
|
|
||||||
const PlaybackController = require('./controllers/playback-controller')
|
const PlaybackController = require('./controllers/playback-controller')
|
||||||
const SubtitlesController = require('./controllers/subtitles-controller')
|
const SubtitlesController = require('./controllers/subtitles-controller')
|
||||||
|
const TorrentListController = require('./controllers/torrent-list-controller')
|
||||||
|
const TorrentController = require('./controllers/torrent-controller')
|
||||||
|
|
||||||
// Yo-yo pattern: state object lives here and percolates down thru all the views.
|
// Yo-yo pattern: state object lives here and percolates down thru all the views.
|
||||||
// Events come back up from the views via dispatch(...)
|
// Events come back up from the views via dispatch(...)
|
||||||
@@ -60,9 +59,10 @@ function onState (err, _state) {
|
|||||||
media: new MediaController(state),
|
media: new MediaController(state),
|
||||||
update: new UpdateController(state),
|
update: new UpdateController(state),
|
||||||
prefs: new PrefsController(state, config),
|
prefs: new PrefsController(state, config),
|
||||||
torrentList: new TorrentListController(state),
|
|
||||||
playback: new PlaybackController(state, config, update),
|
playback: new PlaybackController(state, config, update),
|
||||||
subtitles: new SubtitlesController(state)
|
subtitles: new SubtitlesController(state),
|
||||||
|
torrentList: new TorrentListController(state),
|
||||||
|
torrent: new TorrentController(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add first page to location history
|
// Add first page to location history
|
||||||
@@ -178,7 +178,8 @@ const dispatchHandlers = {
|
|||||||
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
||||||
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
||||||
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
|
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
|
||||||
'deleteTorrent': (infoHash) => controllers.torrentList.deleteTorrent(infoHash),
|
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
|
||||||
|
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||||
'startTorrentingSummary': (torrentSummary) =>
|
'startTorrentingSummary': (torrentSummary) =>
|
||||||
@@ -239,7 +240,9 @@ const dispatchHandlers = {
|
|||||||
'onOpen': onOpen,
|
'onOpen': onOpen,
|
||||||
'error': onError,
|
'error': onError,
|
||||||
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
||||||
'saveState': () => State.save(state)
|
'saveState': () => State.save(state),
|
||||||
|
'saveStateThrottled': () => State.saveThrottled(state),
|
||||||
|
'update': () => {} // No-op, just trigger an update
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events from the UI never modify state directly. Instead they call dispatch()
|
// Events from the UI never modify state directly. Instead they call dispatch()
|
||||||
@@ -269,18 +272,19 @@ function setupIpc () {
|
|||||||
|
|
||||||
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
||||||
|
|
||||||
ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
|
var tc = controllers.torrent
|
||||||
ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args))
|
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
||||||
ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
|
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
||||||
ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args))
|
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
||||||
ipcRenderer.on('wt-error', (e, ...args) => torrentError(...args))
|
ipcRenderer.on('wt-warning', (e, ...args) => tc.torrentWarning(...args))
|
||||||
|
ipcRenderer.on('wt-error', (e, ...args) => tc.torrentError(...args))
|
||||||
|
|
||||||
ipcRenderer.on('wt-progress', (e, ...args) => torrentProgress(...args))
|
ipcRenderer.on('wt-progress', (e, ...args) => tc.torrentProgress(...args))
|
||||||
ipcRenderer.on('wt-file-modtimes', (e, ...args) => torrentFileModtimes(...args))
|
ipcRenderer.on('wt-file-modtimes', (e, ...args) => tc.torrentFileModtimes(...args))
|
||||||
ipcRenderer.on('wt-file-saved', (e, ...args) => torrentFileSaved(...args))
|
ipcRenderer.on('wt-file-saved', (e, ...args) => tc.torrentFileSaved(...args))
|
||||||
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
ipcRenderer.on('wt-poster', (e, ...args) => tc.torrentPosterSaved(...args))
|
||||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
ipcRenderer.on('wt-audio-metadata', (e, ...args) => tc.torrentAudioMetadata(...args))
|
||||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
ipcRenderer.on('wt-server-running', (e, ...args) => tc.torrentServerRunning(...args))
|
||||||
|
|
||||||
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
|
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
|
||||||
|
|
||||||
@@ -325,177 +329,6 @@ function resumeTorrents () {
|
|||||||
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
|
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTorrent (file) {
|
|
||||||
var name = typeof file === 'string' ? file : file.name
|
|
||||||
var isTorrentFile = path.extname(name).toLowerCase() === '.torrent'
|
|
||||||
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
|
||||||
return isTorrentFile || isMagnet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
|
||||||
// Returns undefined if we don't know that infoHash
|
|
||||||
function getTorrentSummary (torrentKey) {
|
|
||||||
return TorrentSummary.getByKey(state, torrentKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentInfoHash (torrentKey, infoHash) {
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
console.log('got infohash for %s torrent %s',
|
|
||||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
|
||||||
|
|
||||||
if (!torrentSummary) {
|
|
||||||
// Check if an existing (non-active) torrent has the same info hash
|
|
||||||
if (state.saved.torrents.find((t) => t.infoHash === infoHash)) {
|
|
||||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
|
||||||
return onError(new Error('Cannot add duplicate torrent'))
|
|
||||||
}
|
|
||||||
|
|
||||||
torrentSummary = {
|
|
||||||
torrentKey: torrentKey,
|
|
||||||
status: 'new'
|
|
||||||
}
|
|
||||||
state.saved.torrents.unshift(torrentSummary)
|
|
||||||
sound.play('ADD')
|
|
||||||
}
|
|
||||||
|
|
||||||
torrentSummary.infoHash = infoHash
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentWarning (torrentKey, message) {
|
|
||||||
onWarning(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentError (torrentKey, message) {
|
|
||||||
// TODO: WebTorrent needs semantic errors
|
|
||||||
if (message.startsWith('Cannot add duplicate torrent')) {
|
|
||||||
// Remove infohash from the message
|
|
||||||
message = 'Cannot add duplicate torrent'
|
|
||||||
}
|
|
||||||
onError(message)
|
|
||||||
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
if (torrentSummary) {
|
|
||||||
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
|
||||||
torrentSummary.status = 'paused'
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentMetadata (torrentKey, torrentInfo) {
|
|
||||||
// Summarize torrent
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
torrentSummary.status = 'downloading'
|
|
||||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
|
||||||
torrentSummary.path = torrentInfo.path
|
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
|
||||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
|
||||||
// For now, check whether torrentSummary.files has already been set:
|
|
||||||
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
|
||||||
if (!hasDetailedFileInfo) {
|
|
||||||
torrentSummary.files = torrentInfo.files
|
|
||||||
}
|
|
||||||
if (!torrentSummary.selections) {
|
|
||||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
|
||||||
}
|
|
||||||
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
|
||||||
update()
|
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
|
||||||
if (!torrentSummary.torrentFileName) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
|
||||||
|
|
||||||
// Auto-generate a poster image, if it hasn't been generated already
|
|
||||||
if (!torrentSummary.posterFileName) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentDone (torrentKey, torrentInfo) {
|
|
||||||
// Update the torrent summary
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
torrentSummary.status = 'seeding'
|
|
||||||
|
|
||||||
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
|
||||||
// Don't notify if we merely finished verifying data files that were already on disk.
|
|
||||||
if (torrentInfo.bytesReceived > 0) {
|
|
||||||
if (!state.window.isFocused) {
|
|
||||||
state.dock.badge += 1
|
|
||||||
}
|
|
||||||
showDoneNotification(torrentSummary)
|
|
||||||
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
|
|
||||||
}
|
|
||||||
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentProgress (progressInfo) {
|
|
||||||
// Overall progress across all active torrents, 0 to 1
|
|
||||||
var progress = progressInfo.progress
|
|
||||||
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
|
||||||
|
|
||||||
// Hide progress bar when client has no torrents, or progress is 100%
|
|
||||||
// TODO: isn't this equivalent to: if (progress === 1) ?
|
|
||||||
if (!hasActiveTorrents || progress === 1) {
|
|
||||||
progress = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
|
||||||
state.dock.progress = progress
|
|
||||||
|
|
||||||
// Update progress for each individual torrent
|
|
||||||
progressInfo.torrents.forEach(function (p) {
|
|
||||||
var torrentSummary = getTorrentSummary(p.torrentKey)
|
|
||||||
if (!torrentSummary) {
|
|
||||||
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
torrentSummary.progress = p
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Find an efficient way to re-enable this line, which allows subtitle
|
|
||||||
// files which are completed after a video starts to play to be added
|
|
||||||
// dynamically to the list of subtitles.
|
|
||||||
// checkForSubtitles()
|
|
||||||
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentFileModtimes (torrentKey, fileModtimes) {
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
|
||||||
State.saveThrottled(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentFileSaved (torrentKey, torrentFileName) {
|
|
||||||
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
torrentSummary.torrentFileName = torrentFileName
|
|
||||||
State.saveThrottled(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentPosterSaved (torrentKey, posterFileName) {
|
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
|
||||||
torrentSummary.posterFileName = posterFileName
|
|
||||||
State.saveThrottled(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentAudioMetadata (infoHash, index, info) {
|
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
|
||||||
var fileSummary = torrentSummary.files[index]
|
|
||||||
fileSummary.audioInfo = info
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentServerRunning (serverInfo) {
|
|
||||||
state.server = serverInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTorrentPath (torrentSummary) {
|
|
||||||
var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path)
|
|
||||||
if (torrentSummary.files.length > 1) {
|
|
||||||
itemPath = path.dirname(itemPath)
|
|
||||||
}
|
|
||||||
return itemPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
function setDimensions (dimensions) {
|
function setDimensions (dimensions) {
|
||||||
// Don't modify the window size if it's already maximized
|
// Don't modify the window size if it's already maximized
|
||||||
@@ -535,19 +368,6 @@ function setDimensions (dimensions) {
|
|||||||
state.playing.aspectRatio = aspectRatio
|
state.playing.aspectRatio = aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDoneNotification (torrent) {
|
|
||||||
var notif = new window.Notification('Download Complete', {
|
|
||||||
body: torrent.name,
|
|
||||||
silent: true
|
|
||||||
})
|
|
||||||
|
|
||||||
notif.onclick = function () {
|
|
||||||
ipcRenderer.send('show')
|
|
||||||
}
|
|
||||||
|
|
||||||
sound.play('DONE')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||||
// via any method (drag-drop, drag to app icon, command line)
|
// via any method (drag-drop, drag to app icon, command line)
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
@@ -560,7 +380,7 @@ function onOpen (files) {
|
|||||||
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
||||||
|
|
||||||
if (state.location.url() === 'home' || subtitles.length === 0) {
|
if (state.location.url() === 'home' || subtitles.length === 0) {
|
||||||
if (files.every(isTorrent)) {
|
if (files.every(TorrentPlayer.isTorrent)) {
|
||||||
if (state.location.url() !== 'home') {
|
if (state.location.url() !== 'home') {
|
||||||
backToList()
|
backToList()
|
||||||
}
|
}
|
||||||
@@ -588,10 +408,6 @@ function onError (err) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWarning (err) {
|
|
||||||
console.log('warning: %s', err.message || err)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPaste (e) {
|
function onPaste (e) {
|
||||||
if (e.target.tagName.toLowerCase() === 'input') return
|
if (e.target.tagName.toLowerCase() === 'input') return
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ var Views = {
|
|||||||
|
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
|
'remove-torrent-modal': require('./remove-torrent-modal'),
|
||||||
'update-available-modal': require('./update-available-modal'),
|
'update-available-modal': require('./update-available-modal'),
|
||||||
'unsupported-media-modal': require('./unsupported-media-modal')
|
'unsupported-media-modal': require('./unsupported-media-modal')
|
||||||
}
|
}
|
||||||
|
|||||||
26
renderer/views/remove-torrent-modal.js
Normal file
26
renderer/views/remove-torrent-modal.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = RemoveTorrentModal
|
||||||
|
|
||||||
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
|
function RemoveTorrentModal (state) {
|
||||||
|
var message = state.modal.deleteData
|
||||||
|
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
|
||||||
|
: 'Are you sure you want to remove this torrent from the list?'
|
||||||
|
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div>
|
||||||
|
<p><strong>${message}</strong></p>
|
||||||
|
<p class='float-right'>
|
||||||
|
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||||
|
<button class='button button-raised' onclick=${handleRemove}>${buttonText}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
function handleRemove () {
|
||||||
|
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)
|
||||||
|
dispatch('exitModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,7 +148,7 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton
|
||||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
playButton = hx`
|
playButton = hx`
|
||||||
<i.button-round.icon.play
|
<i.button-round.icon.play
|
||||||
title=${playTooltip}
|
title=${playTooltip}
|
||||||
@@ -172,7 +172,7 @@ function TorrentList (state) {
|
|||||||
<i
|
<i
|
||||||
class='icon delete'
|
class='icon delete'
|
||||||
title='Remove torrent'
|
title='Remove torrent'
|
||||||
onclick=${dispatcher('deleteTorrent', infoHash)}>
|
onclick=${dispatcher('confirmDeleteTorrent', infoHash, false)}>
|
||||||
close
|
close
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user