Show Blender Foundation videos by default

* Finishes the saved torrents feature!
* Torrents load and save correctly. Poster is autogenerated only once.
* Torrents can be paused and restarted
* Download button indicates state:
 - White means paused
 - Pulsating green means downloading
 - Solid green means finished downloading, only seeding
This commit is contained in:
DC
2016-03-07 01:34:44 -08:00
parent 904d3a6c4b
commit 4b890ee9f6
10 changed files with 247 additions and 137 deletions

View File

@@ -64,7 +64,7 @@ function setAspectRatio (aspectRatio, extraSize) {
// Display string in dock badging area (OS X) // Display string in dock badging area (OS X)
function setBadge (text) { function setBadge (text) {
debug('setBadge %s', text) debug('setBadge %s', text)
electron.app.dock.setBadge(String(text)) if (electron.app.dock) electron.app.dock.setBadge(String(text))
} }
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. // Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.

View File

@@ -25,7 +25,7 @@ function createMainWindow (menu) {
title: config.APP_NAME, title: config.APP_NAME,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
width: 450, width: 450,
height: 300 height: 450
}) })
win.loadURL(config.INDEX) win.loadURL(config.INDEX)

View File

@@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.0", "application-config": "^0.2.0",
"application-config-path": "^0.1.0",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"create-torrent": "^3.22.1", "create-torrent": "^3.22.1",
"debug": "^2.2.0", "debug": "^2.2.0",
@@ -24,8 +25,7 @@
"pretty-bytes": "^3.0.0", "pretty-bytes": "^3.0.0",
"upload-element": "^1.0.1", "upload-element": "^1.0.1",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"webtorrent": "^0.82.1", "webtorrent": "^0.82.1"
"xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"electron-packager": "^5.0.0", "electron-packager": "^5.0.0",

View File

@@ -288,6 +288,7 @@ body.drag::before {
position: absolute; position: absolute;
top: 20px; top: 20px;
left: 20px; left: 20px;
width: calc(100% - 100px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px; text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
} }
@@ -317,6 +318,22 @@ body.drag::before {
padding-top: 6px; padding-top: 6px;
} }
.torrent .buttons .download.downloading {
animation-name: greenpulse;
animation-duration: 0.8s;
animation-direction: alternate;
animation-iteration-count: infinite;
}
@keyframes greenpulse {
0% { color: #ffffff }
100% { color: #44dd44 }
}
.torrent .buttons .download.seeding {
color: #44dd44;
}
.torrent .buttons .play { .torrent .buttons .play {
padding-top: 10px; padding-top: 10px;
background-color: #F44336; background-color: #F44336;

View File

@@ -1,18 +1,16 @@
/* global URL, Blob */
console.time('init') console.time('init')
var airplay = require('airplay-js') var airplay = require('airplay-js')
var cfg = require('application-config')('WebTorrent') var cfg = require('application-config')('WebTorrent')
var cfgDirectory = require('application-config-path')('WebTorrent')
var chromecasts = require('chromecasts')() var chromecasts = require('chromecasts')()
var config = require('../config') var config = require('../config')
var createTorrent = require('create-torrent') var createTorrent = require('create-torrent')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var electron = require('electron') var electron = require('electron')
var EventEmitter = require('events') var EventEmitter = require('events')
var extend = require('xtend') var fs = require('fs')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var networkAddress = require('network-address') var networkAddress = require('network-address')
var os = require('os')
var path = require('path') var path = require('path')
var torrentPoster = require('./lib/torrent-poster') var torrentPoster = require('./lib/torrent-poster')
var WebTorrent = require('webtorrent') var WebTorrent = require('webtorrent')
@@ -23,8 +21,6 @@ var patch = require('virtual-dom/patch')
var App = require('./views/app') var App = require('./views/app')
var HOME = os.homedir()
// For easy debugging in Developer Tools // For easy debugging in Developer Tools
var state = global.state = require('./state') var state = global.state = require('./state')
@@ -38,10 +34,6 @@ global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
}) })
var vdomLoop var vdomLoop
var defaultSaved = {
torrents: [],
downloadPath: path.join(HOME, 'Downloads')
}
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
@@ -53,14 +45,12 @@ loadState(init)
* the dock icon and drag+drop. * the dock icon and drag+drop.
*/ */
function init () { function init () {
document.querySelector('.loading').remove()
// Connect to the WebTorrent and BitTorrent networks // Connect to the WebTorrent and BitTorrent networks
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq // WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
state.client = new WebTorrent() state.client = new WebTorrent()
state.client.on('warning', onWarning) state.client.on('warning', onWarning)
state.client.on('error', onError) state.client.on('error', onError)
state.client.on('torrent', saveTorrentData) resumeTorrents() /* restart everything we were torrenting last time the app ran */
// The UI is built with virtual-dom, a minimalist library extracted from React // The UI is built with virtual-dom, a minimalist library extracted from React
// The concepts--one way data flow, a pure function that renders state to a // The concepts--one way data flow, a pure function that renders state to a
@@ -78,8 +68,8 @@ function init () {
// (eg % downloaded) and to keep the cursor in sync when playing a video // (eg % downloaded) and to keep the cursor in sync when playing a video
setInterval(update, 1000) setInterval(update, 1000)
// Resume all saved torrents now that state is loaded and vdom is ready // All state lives in state.js. `state.saved` is read from and written to a
resumeAllTorrents() // file. All other state is ephemeral. Here we'll load state.saved:
window.addEventListener('beforeunload', saveState) window.addEventListener('beforeunload', saveState)
// listen for messages from the main process // listen for messages from the main process
@@ -121,6 +111,8 @@ function init () {
state.isFocused = false state.isFocused = false
}) })
// Done! Ideally we want to get here <100ms after the user clicks the app
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */
console.timeEnd('init') console.timeEnd('init')
} }
@@ -138,24 +130,29 @@ function 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()
function dispatch (action, ...args) { function dispatch (action, ...args) {
console.log('dispatch: %s %o', action, args) if (['videoMouseMoved', 'playbackJump'].indexOf(action) < 0) {
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
}
if (action === 'addTorrent') { if (action === 'addTorrent') {
addTorrent(args[0] /* torrentId */) addTorrent(args[0] /* torrent */)
} }
if (action === 'seed') { if (action === 'seed') {
seed(args[0] /* files */) seed(args[0] /* files */)
} }
if (action === 'openPlayer') { if (action === 'openPlayer') {
openPlayer(args[0] /* torrent */) openPlayer(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */)
} }
if (action === 'deleteTorrent') { if (action === 'deleteTorrent') {
deleteTorrent(args[0] /* torrent */) deleteTorrent(args[0] /* infoHash */)
} }
if (action === 'openChromecast') { if (action === 'openChromecast') {
openChromecast(args[0] /* torrent */) openChromecast(args[0] /* infoHash */)
} }
if (action === 'openAirplay') { if (action === 'openAirplay') {
openAirplay(args[0] /* torrent */) openAirplay(args[0] /* infoHash */)
} }
if (action === 'setDimensions') { if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
@@ -226,14 +223,17 @@ function loadState (callback) {
electron.ipcRenderer.send('log', 'loaded state from ' + cfg.filePath) electron.ipcRenderer.send('log', 'loaded state from ' + cfg.filePath)
// populate defaults if they're not there // populate defaults if they're not there
state.saved = extend(defaultSaved, data) state.saved = Object.assign({}, state.defaultSavedState, data)
if (callback) callback() if (callback) callback()
}) })
} }
function resumeAllTorrents () { // Starts all torrents that aren't paused on program startup
state.saved.torrents.forEach((x) => startTorrenting(x.infoHash)) function resumeTorrents () {
state.saved.torrents
.filter((x) => x.status !== 'paused')
.forEach((x) => startTorrenting(x.infoHash))
} }
// Write state.saved to the JSON state file // Write state.saved to the JSON state file
@@ -279,57 +279,60 @@ function isNotTorrentFile (file) {
return !isTorrentFile(file) return !isTorrentFile(file)
} }
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
// Returns undefined if we don't know that infoHash
function getTorrentSummary (infoHash) {
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
// Get an active torrent from state.client.torrents
// Returns undefined if we are not currently torrenting that infoHash
function getTorrent (infoHash) {
return state.client.torrents.find((x) => x.infoHash === infoHash)
}
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
function addTorrent (torrentId) { function addTorrent (torrentId) {
if (!torrentId) torrentId = 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4' // Charlie Chaplin: 'magnet:?xt=urn:btih:cddf0459a718523480f7499da5ed1a504cffecb8&dn=charlie%5Fchaplin%5Ffilm%5Ffestival'
if (!torrentId) torrentId = 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle'
var torrent = startTorrenting(torrentId) var torrent = startTorrenting(torrentId)
// check if torrent is duplicate // If torrentId is a torrent file, wait for WebTorrent to finish reading it
var exists = state.saved.torrents.find((x) => x.infoHash === torrent.infoHash) if (!torrent.infoHash) torrent.on('infoHash', addTorrentToList)
if (exists) return window.alert('That torrent is already downloading.') else addTorrentToList()
// save only if infoHash is available function addTorrentToList () {
if (torrent.infoHash) { if (getTorrentSummary(torrent.infoHash)) {
return // Skip, torrent is already in state.saved
}
state.saved.torrents.push({ state.saved.torrents.push({
status: 'new',
name: torrent.name,
magnetURI: torrent.magnetURI,
infoHash: torrent.infoHash infoHash: torrent.infoHash
}) })
} else { saveState()
torrent.on('infoHash', () => saveTorrentData(torrent))
} }
saveState()
} }
// add torrent metadata to state once it's available // Starts downloading and/or seeding a given torrent, torrentSummary or magnet URI
function saveTorrentData (torrent) { function startTorrenting (infoHash) {
var ix = state.saved.torrents.findIndex((x) => x.infoHash === torrent.infoHash) var torrent = state.client.add(infoHash, {
var data = { // Use downloads folder
name: torrent.name,
magnetURI: torrent.magnetURI,
infoHash: torrent.infoHash,
path: torrent.path,
xt: torrent.xt,
dn: torrent.dn,
announce: torrent.announce
}
if (ix === -1) state.saved.torrents.push(data)
else state.saved.torrents[ix] = data
saveState()
}
// Starts downloading and/or seeding a given torrent file or magnet URI
function startTorrenting (torrentId) {
var torrent = state.client.add(torrentId, {
// use downloads folder
path: state.saved.downloadPath path: state.saved.downloadPath
}) })
addTorrentEvents(torrent) addTorrentEvents(torrent)
return torrent return torrent
} }
// Stops downloading and/or seeding. See startTorrenting
function stopTorrenting (infoHash) {
var torrent = getTorrent(infoHash)
if (torrent) torrent.destroy()
}
// Creates a torrent for a local file and starts seeding it // Creates a torrent for a local file and starts seeding it
function seed (files) { function seed (files) {
if (files.length === 0) return if (files.length === 0) return
@@ -339,33 +342,60 @@ function seed (files) {
function addTorrentEvents (torrent) { function addTorrentEvents (torrent) {
torrent.on('infoHash', update) torrent.on('infoHash', update)
torrent.on('ready', torrentReady) torrent.on('ready', torrentReady)
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
update()
function torrentReady () { function torrentReady () {
torrentPoster(torrent, function (err, buf) { var torrentSummary = getTorrentSummary(torrent.infoHash)
if (err) return onWarning(err) torrentSummary.status = 'downloading'
torrent.posterURL = URL.createObjectURL(new Blob([ buf ], { type: 'image/png' })) torrentSummary.ready = true
update() torrentSummary.name = torrent.name
}) torrentSummary.infoHash = torrent.infoHash
if (!torrentSummary.posterURL) {
generateTorrentPoster(torrent, torrentSummary)
}
update() update()
} }
function torrentDone () { function torrentDone () {
var torrentSummary = getTorrentSummary(torrent.infoHash)
torrentSummary.status = 'seeding'
if (!state.isFocused) { if (!state.isFocused) {
state.dock.badge += 1 state.dock.badge += 1
electron.ipcRenderer.send('setBadge', state.dock.badge) electron.ipcRenderer.send('setBadge', state.dock.badge)
} }
update() update()
} }
} }
function startServer (torrent, cb) { function generateTorrentPoster (torrent, torrentSummary) {
torrentPoster(torrent, function (err, buf) {
if (err) return onWarning(err)
// save it for next time
var posterFilePath = path.join(cfgDirectory, torrent.infoHash + '.jpg')
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return onWarning(err)
// show the poster
torrentSummary.posterURL = 'file:///' + posterFilePath
update()
})
})
}
function startServer (infoHash, cb) {
if (state.server) return cb() if (state.server) return cb()
var torrent = getTorrent(infoHash)
if (!torrent) torrent = startTorrenting(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, cb)
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, cb))
}
function startServerFromReadyTorrent (torrent, cb) {
// use largest file // use largest file
state.torrentPlaying = torrent.files.reduce(function (a, b) { state.torrentPlaying = torrent.files.reduce(function (a, b) {
return a.length > b.length ? a : b return a.length > b.length ? a : b
@@ -385,15 +415,15 @@ function startServer (torrent, cb) {
}) })
} }
function closeServer () { function stopServer () {
state.server.server.destroy() state.server.server.destroy()
state.server = null state.server = null
} }
function openPlayer (torrent) { function openPlayer (infoHash) {
startServer(torrent, function () { startServer(infoHash, function () {
state.url = '/player' state.url = '/player'
state.title = torrent.name /* TODO: set state.title to the clean name of the torrent */
update() update()
}) })
} }
@@ -405,20 +435,36 @@ function closePlayer () {
electron.ipcRenderer.send('toggleFullScreen') electron.ipcRenderer.send('toggleFullScreen')
} }
restoreBounds() restoreBounds()
closeServer() stopServer()
update() update()
} }
function deleteTorrent (torrent) { function toggleTorrent (infoHash) {
var ix = state.saved.torrents.findIndex((x) => x.infoHash === torrent.infoHash) var torrentSummary = getTorrentSummary(infoHash)
if (ix > -1) state.saved.torrents.splice(ix, 1) if (!torrentSummary) return
torrent.destroy(saveState) if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
startTorrenting(torrentSummary.infoHash)
} else {
torrentSummary.status = 'paused'
stopTorrenting(torrentSummary.infoHash)
}
} }
function openChromecast (torrent) { function deleteTorrent (infoHash) {
startServer(torrent, function () { var torrent = getTorrent(infoHash)
torrent.destroy()
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) state.saved.torrents.splice(index, 1)
saveState()
}
function openChromecast (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
startServer(infoHash, function () {
state.devices.chromecast.play(state.server.networkURL, { state.devices.chromecast.play(state.server.networkURL, {
title: config.APP_NAME + ' — ' + torrent.name title: config.APP_NAME + ' — ' + torrentSummary.name
}) })
state.devices.chromecast.on('error', function (err) { state.devices.chromecast.on('error', function (err) {
err.message = 'Chromecast: ' + err.message err.message = 'Chromecast: ' + err.message
@@ -428,8 +474,8 @@ function openChromecast (torrent) {
}) })
} }
function openAirplay (torrent) { function openAirplay (infoHash) {
startServer(torrent, function () { startServer(infoHash, function () {
state.devices.airplay.play(state.server.networkURL, 0, function () { state.devices.airplay.play(state.server.networkURL, 0, function () {
// TODO: handle airplay errors // TODO: handle airplay errors
}) })

View File

@@ -1,3 +1,6 @@
var os = require('os')
var path = require('path')
var config = require('../config') var config = require('../config')
module.exports = { module.exports = {
@@ -44,5 +47,31 @@ module.exports = {
*/ */
saved: { saved: {
torrents: [] torrents: []
},
/* If the saved state file doesn't exist yet, here's what we use instead */
defaultSavedState: {
version: 1, /* make sure we can upgrade gracefully later */
torrents: [
{
status: 'paused',
infoHash: 'f84b51f0d2c3455ab5dabb6643b4340234cd036e',
displayName: 'Big Buck Bunny',
posterURL: '../resources/bigBuckBunny.jpg'
},
{
status: 'paused',
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
displayName: 'Sintel',
posterURL: '../resources/sintel.jpg'
},
{
status: 'paused',
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
displayName: 'Tears of Steel',
posterURL: '../resources/tearsOfSteel.jpg'
}
],
downloadPath: path.join(os.homedir(), 'Downloads')
} }
} }

View File

@@ -7,7 +7,8 @@ var hx = hyperx(h)
var prettyBytes = require('pretty-bytes') var prettyBytes = require('pretty-bytes')
function TorrentList (state, dispatch) { function TorrentList (state, dispatch) {
var list = state.client.torrents.map((torrent) => renderTorrent(torrent, dispatch)) var list = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary, state, dispatch))
if (list.length === 0) list = emptyList() if (list.length === 0) list = emptyList()
return hx`<div class='torrent-list'>${list}</div>` return hx`<div class='torrent-list'>${list}</div>`
} }
@@ -24,76 +25,93 @@ function emptyList () {
// Renders a torrent in the torrent list // Renders a torrent in the torrent list
// Includes name, download status, play button, background image // Includes name, download status, play button, background image
// May be expanded for additional info, including the list of files inside // May be expanded for additional info, including the list of files inside
function renderTorrent (torrent, dispatch) { function renderTorrent (torrentSummary, state, dispatch) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var torrent = state.client.torrents.find((x) => x.infoHash === torrentSummary.infoHash)
// Background image: show some nice visuals, like a frame from the movie, if possible // Background image: show some nice visuals, like a frame from the movie, if possible
var style = {} var style = {}
if (torrent.posterURL) { if (torrentSummary.posterURL) {
style['background-image'] = `linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%), url("${torrent.posterURL}")` style['background-image'] = 'linear-gradient(to bottom, ' +
'rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%), ' +
`url("${torrentSummary.posterURL}")`
} }
// Foreground: name of the torrent, basic info like size, play button, // Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete // cast buttons if available, and delete
return hx` return hx`
<div class='torrent' style=${style}> <div class='torrent' style=${style}>
${renderTorrentMetadata(torrent)} ${renderTorrentMetadata(torrent, torrentSummary)}
${renderTorrentButtons(torrent)} ${renderTorrentButtons(torrentSummary, dispatch)}
</div> </div>
` `
}
function renderTorrentMetadata () { // Show name, download status, % complete
function renderTorrentMetadata (torrent, torrentSummary) {
var name = torrentSummary.displayName || torrentSummary.name || 'Loading torrent...'
var elements = [hx`
<div class='name ellipsis'>${name}</div>
`]
// If a torrent is paused and we only get the torrentSummary
// If it's downloading/seeding then we have more information
if (torrent) {
var progress = Math.floor(100 * torrent.progress) var progress = Math.floor(100 * torrent.progress)
var downloaded = prettyBytes(torrent.downloaded) var downloaded = prettyBytes(torrent.downloaded)
var total = prettyBytes(torrent.length || 0) var total = prettyBytes(torrent.length || 0)
if (downloaded !== total) downloaded += ` / ${total}` if (downloaded !== total) downloaded += ` / ${total}`
return hx` elements.push(hx`
<div class='metadata'> <div class='status ellipsis'>
<div class='name ellipsis'>${torrent.name || 'Loading torrent...'}</div> ${getFilesLength()}
<div class='status ellipsis'> <span>${getPeers()}</span>
${getFilesLength()} <span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
<span>${getPeers()}</span> <span>${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
</div>
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div>
</div> </div>
` `)
elements.push(hx`
function getPeers () { <div class='status2 ellipsis'>
var count = torrent.numPeers === 1 ? 'peer' : 'peers' <span class='progress'>${progress}%</span>
return `${torrent.numPeers} ${count}` <span>${downloaded}</span>
} </div>
`)
function getFilesLength () {
if (torrent.ready && torrent.files.length > 1) {
return hx`<span class='files'>${torrent.files.length} files</span>`
}
}
} }
function renderTorrentButtons () { return hx`<div class='metadata'>${elements}</div>`
return hx`
<div class="buttons"> function getPeers () {
<i.btn.icon.download var count = torrent.numPeers === 1 ? 'peer' : 'peers'
class='${torrent.state}' return `${torrent.numPeers} ${count}`
onclick=${() => dispatch('toggleTorrent', torrent)}> }
file_download
</i> function getFilesLength () {
<i.btn.icon.play if (torrent.ready && torrent.files.length > 1) {
class='${!torrent.ready ? 'disabled' : ''}' return hx`<span class='files'>${torrent.files.length} files</span>`
onclick=${() => dispatch('openPlayer', torrent)}> }
play_arrow
</i>
<i
class='icon delete'
onclick=${() => dispatch('deleteTorrent', torrent)}>
close
</i>
</div>
`
} }
} }
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary, dispatch) {
var infoHash = torrentSummary.infoHash
return hx`
<div class="buttons">
<i.btn.icon.download
class='${torrentSummary.status}'
onclick=${() => dispatch('toggleTorrent', infoHash)}>
file_download
</i>
<i.btn.icon.play
onclick=${() => dispatch('openPlayer', infoHash)}>
play_arrow
</i>
<i
class='icon delete'
onclick=${() => dispatch('deleteTorrent', infoHash)}>
close
</i>
</div>
`
}

BIN
resources/bigBuckBunny.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
resources/sintel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
resources/tearsOfSteel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB