Convert Create Torrent modal to page, clean up App

This commit is contained in:
DC
2016-04-21 02:57:43 -07:00
committed by DC
parent 31ef283e7b
commit 1479369db1
7 changed files with 280 additions and 164 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -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)
}
})
})
})
})
}

View File

@@ -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`
<div class='app ${cls.join(' ')}'>
${Header(state, dispatch)}
${getErrorPopover()}
<div class='content'>${getView()}</div>
${getModal()}
${Header(state)}
${getErrorPopover(state)}
<div class='content'>${getView(state)}</div>
${getModal(state)}
</div>
`
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`<div class='error'>${error.message}</div>`
})
return hx`
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal () {
if (state.modal) {
var contents = Modals[state.modal.id](state, dispatch)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content'>
${contents}
</div>
</div>
`
}
}
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`<div class='error'>${error.message}</div>`
})
return hx`
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal (state) {
if (!state.modal) return
var contents = Modals[state.modal.id](state)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content'>
${contents}
</div>
</div>
`
}
function getView (state) {
var url = state.location.current().url
return Views[url](state)
}

View File

@@ -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`<div>${relativePath}</div>`
})
return hx`
<div class='create-torrent-modal'>
<p><strong>Create New Torrent</strong></p>
<p class='torrent-attribute'>
<label>Name:</label>
<div class='torrent-attribute'>${defaultName}</div>
</p>
<p class='torrent-attribute'>
<label>Path:</label>
<div class='torrent-attribute'>${pathPrefix}</div>
</p>
<p class='torrent-attribute'>
<label>Files:</label>
<div>${fileElems}</div>
</p>
<p>
<button class='primary' onclick=${handleOK}>Create Torrent</button>
<button class='cancel' onclick=${handleCancel}>Cancel</button>
</p>
</div>
`
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)
}

View File

@@ -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`<div>${relativePath}</div>`
})
if (files.length > maxFileElems) {
fileElems.push(hx`<div>+ ${maxFileElems - files.length} more</div>`)
}
var trackers = createTorrent.announceList.join('\n')
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
return hx`
<div class='create-torrent-page'>
<h2>Create torrent ${defaultName}</h2>
<p class="torrent-info">
${torrentInfo}
</p>
<p class='torrent-attribute'>
<label>Path:</label>
<div class='torrent-attribute'>${pathPrefix}</div>
</p>
<div class='expand-collapse ${collapsedClass}' onclick=${handleToggleShowAdvanced}>
${info.showAdvanced ? 'Basic' : 'Advanced'}
</div>
<div class="create-torrent-advanced ${collapsedClass}">
<p class='torrent-attribute'>
<label>Comment:</label>
<textarea class='torrent-attribute torrent-comment'></textarea>
</p>
<p class='torrent-attribute'>
<label>Trackers:</label>
<textarea class='torrent-attribute torrent-trackers'>${trackers}</textarea>
</p>
<p class='torrent-attribute'>
<label>Private:</label>
<input type='checkbox' class='torrent-is-private' value='torrent-is-private'>
</p>
<p class='torrent-attribute'>
<label>Files:</label>
<div>${fileElems}</div>
</p>
</div>
<p class="float-right">
<button class='button-flat light' onclick=${handleCancel}>Cancel</button>
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
</p>
</div>
`
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)
}

View File

@@ -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')