diff --git a/package.json b/package.json index 57424eca..97dddbbe 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "bitfield": "^1.0.2", "chromecasts": "^1.8.0", "concat-stream": "^1.5.1", - "create-torrent": "^3.22.1", + "create-torrent": "^3.24.5", "deep-equal": "^1.0.1", "dlnacasts": "^0.0.3", "drag-drop": "^2.11.0", diff --git a/renderer/index.css b/renderer/index.css index aeec9f28..c1b151b8 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -117,6 +117,30 @@ table { float: right; } +.expand-collapse { + cursor: pointer; +} + +.expand-collapse.expanded::before { + content: '▲' +} + +.expand-collapse.collapsed::before { + content: '▼' +} + +.expand-collapse::before { + margin-right: 5px; +} + +.expand-collapse.collapsed { + display: block; +} + +.collapsed { + display: none; +} + /* * HEADER */ @@ -260,23 +284,54 @@ table { width: 100%; } -.create-torrent-modal .torrent-attribute { +.create-torrent-page { + padding: 10px 25px; + overflow: hidden; +} + +.create-torrent-page .torrent-attribute { white-space: nowrap; } -.create-torrent-modal .torrent-attribute>* { +.create-torrent-page .torrent-attribute>* { display: inline-block; } -.create-torrent-modal .torrent-attribute label { +.create-torrent-page .torrent-attribute label { width: 60px; margin-right: 10px; vertical-align: top; } -.create-torrent-modal .torrent-attribute div { - font-family: Consolas, monospace; +.create-torrent-page .torrent-attribute>div { + width: calc(100% - 90px); +} + +.create-torrent-page .torrent-attribute div { white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.create-torrent-page .torrent-attribute textarea { + width: calc(100% - 80px); + height: 80px; + color: #eee; + background-color: transparent; + line-height: 1.5; + font-size: 14px; + font-family: inherit; + border-radius: 2px; + padding: 4px 6px; +} + +.create-torrent-page textarea.torrent-trackers { + height: 200px; +} + +.create-torrent-page input.torrent-is-private { + width: initial; + margin: 0; } /* @@ -322,6 +377,10 @@ button.button-flat { padding: 7px 18px; } +button.button-flat.light { + color: #eee; +} + button.button-flat:hover, button.button-flat:focus { /* Material design: focused */ background-color: rgba(153, 153, 153, 0.2); diff --git a/renderer/index.js b/renderer/index.js index ef19400d..fb641990 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -523,12 +523,7 @@ function onOpen (files) { // everything else = seed these files var rest = files.filter(not(isTorrent)).filter(not(isSubtitle)) - if (rest.length > 0) { - state.modal = { - id: 'create-torrent-modal', - files: rest - } - } + if (rest.length > 0) showCreateTorrent(rest) } function isTorrent (file) { @@ -640,34 +635,49 @@ function startTorrentingSummary (torrentSummary) { // Shows the Create Torrent page with options to seed a given file or folder function showCreateTorrent (files) { if (Array.isArray(files)) { - state.modal = { - id: 'create-torrent-modal', + if (state.location.pending() || state.location.current().url !== 'home') return + state.location.go({ + url: 'create-torrent', files: files - } + }) return } var fileOrFolder = files + findFilesRecursive(fileOrFolder, showCreateTorrent) +} + +// Recursively finds {name, length, path} for all files in a folder +// Calls `cb` on success, calls `onError` on failure +function findFilesRecursive (fileOrFolder, cb) { fs.stat(fileOrFolder, function (err, stat) { if (err) return onError(err) - if (stat.isDirectory()) { - fs.readdir(fileOrFolder, function (err, fileNames) { - if (err) return onError(err) - // TODO: support nested folders - var fileObjs = fileNames.map(function (fileName) { - return { - name: fileName, - path: path.join(fileOrFolder, fileName) - } - }) - showCreateTorrent(fileObjs) - }) - } else { - showCreateTorrent([{ - name: path.basename(fileOrFolder), - path: fileOrFolder + + // Files: return name, path, and size + if (!stat.isDirectory()) { + var filePath = fileOrFolder + return cb([{ + name: path.basename(filePath), + path: filePath, + length: stat.size }]) } + + // Folders: recurse, make a list of all the files + var folderPath = fileOrFolder + fs.readdir(folderPath, function (err, fileNames) { + if (err) return onError(err) + var numComplete = 0 + var ret = [] + fileNames.forEach(function (fileName) { + findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) { + ret = ret.concat(fileObjs) + if (++numComplete === fileNames.length) { + cb(ret) + } + }) + }) + }) }) } diff --git a/renderer/views/app.js b/renderer/views/app.js index a300fa7b..18f7c068 100644 --- a/renderer/views/app.js +++ b/renderer/views/app.js @@ -5,15 +5,17 @@ var hyperx = require('hyperx') var hx = hyperx(h) var Header = require('./header') -var Player = require('./player') -var TorrentList = require('./torrent-list') +var Views = { + 'home': require('./torrent-list'), + 'player': require('./player'), + 'create-torrent': require('./create-torrent-page') +} var Modals = { 'open-torrent-address-modal': require('./open-torrent-address-modal'), - 'update-available-modal': require('./update-available-modal'), - 'create-torrent-modal': require('./create-torrent-modal') + 'update-available-modal': require('./update-available-modal') } -function App (state, dispatch) { +function App (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) @@ -40,47 +42,43 @@ function App (state, dispatch) { return hx`
- ${Header(state, dispatch)} - ${getErrorPopover()} -
${getView()}
- ${getModal()} + ${Header(state)} + ${getErrorPopover(state)} +
${getView(state)}
+ ${getModal(state)}
` - - function getErrorPopover () { - var now = new Date().getTime() - var recentErrors = state.errors.filter((x) => now - x.time < 5000) - - var errorElems = recentErrors.map(function (error) { - return hx`
${error.message}
` - }) - return hx` -
-
Error
- ${errorElems} -
- ` - } - - function getModal () { - if (state.modal) { - var contents = Modals[state.modal.id](state, dispatch) - return hx` - - ` - } - } - - function getView () { - if (state.location.current().url === 'home') { - return TorrentList(state, dispatch) - } else if (state.location.current().url === 'player') { - return Player(state, dispatch) - } - } +} + +function getErrorPopover (state) { + var now = new Date().getTime() + var recentErrors = state.errors.filter((x) => now - x.time < 5000) + + var errorElems = recentErrors.map(function (error) { + return hx`
${error.message}
` + }) + return hx` +
+
Error
+ ${errorElems} +
+ ` +} + +function getModal (state) { + if (!state.modal) return + var contents = Modals[state.modal.id](state) + return hx` + + ` +} + +function getView (state) { + var url = state.location.current().url + return Views[url](state) } diff --git a/renderer/views/create-torrent-modal.js b/renderer/views/create-torrent-modal.js deleted file mode 100644 index d085f811..00000000 --- a/renderer/views/create-torrent-modal.js +++ /dev/null @@ -1,85 +0,0 @@ -module.exports = UpdateAvailableModal - -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - -var path = require('path') - -var {dispatch} = require('../lib/dispatcher') - -function UpdateAvailableModal (state) { - var info = state.modal - - // First, extract the base folder that the files are all in - var files = info.files - var pathPrefix = info.folderPath - if (!pathPrefix) { - if (files.length > 0) { - pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix) - if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) { - pathPrefix = path.dirname(pathPrefix) - } - } else { - pathPrefix = files[0] - } - } - - // 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 = path.basename(pathPrefix) - var basePath = path.dirname(pathPrefix) - var fileElems = files.map(function (file) { - var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) - return hx`
${relativePath}
` - }) - - return hx` -
-

Create New Torrent

-

- -

${defaultName}
-

-

- -

${pathPrefix}
-

-

- -

${fileElems}
-

-

- - -

-
- ` - - function handleOK () { - var options = { - // TODO: 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: document.querySelector('.torrent-name').value - name: defaultName, - path: basePath, - files: files - } - dispatch('createTorrent', options) - dispatch('exitModal') - } - - function handleCancel () { - dispatch('exitModal') - } -} - -// 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) -} diff --git a/renderer/views/create-torrent-page.js b/renderer/views/create-torrent-page.js new file mode 100644 index 00000000..a6ef0fab --- /dev/null +++ b/renderer/views/create-torrent-page.js @@ -0,0 +1,133 @@ +module.exports = CreateTorrentPage + +var h = require('virtual-dom/h') +var hyperx = require('hyperx') +var hx = hyperx(h) + +var createTorrent = require('create-torrent') +var path = require('path') +var prettyBytes = require('prettier-bytes') + +var {dispatch} = require('../lib/dispatcher') + +function CreateTorrentPage (state) { + var info = state.location.current() + + // First, extract the base folder that the files are all in + var files = info.files + var pathPrefix = info.folderPath + if (!pathPrefix) { + if (files.length > 0) { + pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix) + if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) { + pathPrefix = path.dirname(pathPrefix) + } + } else { + pathPrefix = files[0] + } + } + + // Sanity check: show the number of files and total size + var numFiles = files.length + var totalBytes = files + .map((f) => f.length) + .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 = path.basename(pathPrefix) + var basePath = path.dirname(pathPrefix) + var maxFileElems = 100 + var fileElems = files.slice(0, maxFileElems).map(function (file) { + var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) + return hx`
${relativePath}
` + }) + if (files.length > maxFileElems) { + fileElems.push(hx`
+ ${maxFileElems - files.length} more
`) + } + var trackers = createTorrent.announceList.join('\n') + var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed' + + return hx` +
+

Create torrent ${defaultName}

+

+ ${torrentInfo} +

+

+ +

${pathPrefix}
+

+
+ ${info.showAdvanced ? 'Basic' : 'Advanced'} +
+
+

+ + +

+

+ + +

+

+ + +

+

+ +

${fileElems}
+

+
+

+ + +

+
+ ` + + function handleOK () { + 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: defaultName, + path: basePath, + files: files, + announce: announceList, + private: isPrivate, + comment: comment + } + dispatch('createTorrent', options) + dispatch('backToList') + } + + function handleCancel () { + dispatch('backToList') + } + + function handleToggleShowAdvanced () { + // TODO: what's the clean way to handle this? + // Should every button on every screen have its own dispatch()? + info.showAdvanced = !info.showAdvanced + dispatch('update') + } +} + +// 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) +} diff --git a/renderer/webtorrent.js b/renderer/webtorrent.js index 48a769bb..26338ac2 100644 --- a/renderer/webtorrent.js +++ b/renderer/webtorrent.js @@ -95,8 +95,9 @@ function stopTorrenting (infoHash) { // Create a new torrent, start seeding function createTorrent (torrentKey, options) { - console.log('creating torrent %s', torrentKey, options) - var torrent = client.seed(options.files, options) + console.log('creating torrent', torrentKey, options) + var paths = options.files.map((f) => f.path) + var torrent = client.seed(paths, options) torrent.key = torrentKey addTorrentEvents(torrent) ipc.send('wt-new-torrent')