@@ -21,6 +21,7 @@
|
|||||||
"electron-localshortcut": "^0.6.0",
|
"electron-localshortcut": "^0.6.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"upload-element": "^1.0.1",
|
"upload-element": "^1.0.1",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ var electron = require('electron')
|
|||||||
var EventEmitter = require('events')
|
var EventEmitter = require('events')
|
||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
|
var mkdirp = require('mkdirp')
|
||||||
var networkAddress = require('network-address')
|
var networkAddress = require('network-address')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var WebTorrent = require('webtorrent')
|
var WebTorrent = require('webtorrent')
|
||||||
@@ -16,10 +17,10 @@ var diff = require('virtual-dom/diff')
|
|||||||
var patch = require('virtual-dom/patch')
|
var patch = require('virtual-dom/patch')
|
||||||
|
|
||||||
var App = require('./views/app')
|
var App = require('./views/app')
|
||||||
var config = require('../config')
|
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
|
||||||
var Cast = require('./lib/cast')
|
var Cast = require('./lib/cast')
|
||||||
|
var config = require('../config')
|
||||||
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
|
var torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
// Electron apps have two processes: a main process (node) runs first and starts
|
// Electron apps have two processes: a main process (node) runs first and starts
|
||||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||||
@@ -32,12 +33,8 @@ var state = global.state = require('./state')
|
|||||||
|
|
||||||
// Force use of webtorrent trackers on all torrents
|
// Force use of webtorrent trackers on all torrents
|
||||||
global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
|
global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
|
||||||
.map(function (arr) {
|
.map((arr) => arr[0])
|
||||||
return arr[0]
|
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
|
||||||
})
|
|
||||||
.filter(function (url) {
|
|
||||||
return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
var vdomLoop
|
var vdomLoop
|
||||||
|
|
||||||
@@ -51,14 +48,19 @@ loadState(init)
|
|||||||
* the dock icon and drag+drop.
|
* the dock icon and drag+drop.
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
// 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', function (err) {
|
state.client.on('error', function (err) {
|
||||||
// TODO: WebTorrent should have semantic errors
|
// TODO: WebTorrent should have semantic errors
|
||||||
if (err.message.startsWith('There is already a swarm')) onError('Couldn\'t add duplicate torrent')
|
if (err.message.startsWith('There is already a swarm')) {
|
||||||
else onError(err)
|
onError(new Error('Couldn\'t add duplicate torrent'))
|
||||||
|
} else {
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
resumeTorrents() /* restart everything we were torrenting last time the app ran */
|
resumeTorrents() /* restart everything we were torrenting last time the app ran */
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ function updateElectron () {
|
|||||||
|
|
||||||
// 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) {
|
||||||
if (['videoMouseMoved', 'playbackJump'].indexOf(action) < 0) {
|
if (['videoMouseMoved', 'playbackJump'].indexOf(action) === -1) {
|
||||||
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
|
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
|
||||||
}
|
}
|
||||||
if (action === 'onOpen') {
|
if (action === 'onOpen') {
|
||||||
@@ -174,8 +176,14 @@ function dispatch (action, ...args) {
|
|||||||
seed(args[0] /* files */)
|
seed(args[0] /* files */)
|
||||||
}
|
}
|
||||||
if (action === 'play') {
|
if (action === 'play') {
|
||||||
|
state.location.go({
|
||||||
|
url: 'player',
|
||||||
|
onbeforeload: function (cb) {
|
||||||
// TODO: handle audio. video only for now.
|
// TODO: handle audio. video only for now.
|
||||||
openPlayer(args[0] /* torrentSummary */, args[1] /* index */)
|
openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb)
|
||||||
|
},
|
||||||
|
onbeforeunload: closePlayer
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (action === 'openFile') {
|
if (action === 'openFile') {
|
||||||
openFile(args[0] /* torrentSummary */, args[1] /* index */)
|
openFile(args[0] /* torrentSummary */, args[1] /* index */)
|
||||||
@@ -205,14 +213,12 @@ function dispatch (action, ...args) {
|
|||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
}
|
}
|
||||||
if (action === 'back') {
|
if (action === 'back') {
|
||||||
// TODO
|
state.location.back()
|
||||||
// window.history.back()
|
update()
|
||||||
ipcRenderer.send('unblockPowerSave')
|
|
||||||
closePlayer()
|
|
||||||
}
|
}
|
||||||
if (action === 'forward') {
|
if (action === 'forward') {
|
||||||
// TODO
|
state.location.forward()
|
||||||
// window.history.forward()
|
update()
|
||||||
}
|
}
|
||||||
if (action === 'playPause') {
|
if (action === 'playPause') {
|
||||||
playPause()
|
playPause()
|
||||||
@@ -284,7 +290,7 @@ function setupIpc () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load state.saved from the JSON state file
|
// Load state.saved from the JSON state file
|
||||||
function loadState (callback) {
|
function loadState (cb) {
|
||||||
cfg.read(function (err, data) {
|
cfg.read(function (err, data) {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
console.log('loaded state from ' + cfg.filePath)
|
console.log('loaded state from ' + cfg.filePath)
|
||||||
@@ -294,9 +300,8 @@ function loadState (callback) {
|
|||||||
state.saved.torrents.forEach(function (torrentSummary) {
|
state.saved.torrents.forEach(function (torrentSummary) {
|
||||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||||
})
|
})
|
||||||
saveState()
|
|
||||||
|
|
||||||
if (callback) callback()
|
if (cb) cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +480,8 @@ function generateTorrentPoster (torrent, torrentSummary) {
|
|||||||
torrentPoster(torrent, function (err, buf) {
|
torrentPoster(torrent, function (err, buf) {
|
||||||
if (err) return onWarning(err)
|
if (err) return onWarning(err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
fs.mkdir(config.CONFIG_POSTER_PATH, function (_) {
|
mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
||||||
|
if (err) return onWarning(err)
|
||||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + '.jpg')
|
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + '.jpg')
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return onWarning(err)
|
if (err) return onWarning(err)
|
||||||
@@ -567,7 +573,7 @@ function stopServer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Opens the video player
|
// Opens the video player
|
||||||
function openPlayer (torrentSummary, index) {
|
function openPlayer (torrentSummary, index, cb) {
|
||||||
var torrent = state.client.get(torrentSummary.infoHash)
|
var torrent = state.client.get(torrentSummary.infoHash)
|
||||||
if (!torrent || !torrent.done) playInterfaceSound(config.SOUND_PLAY)
|
if (!torrent || !torrent.done) playInterfaceSound(config.SOUND_PLAY)
|
||||||
torrentSummary.playStatus = 'requested'
|
torrentSummary.playStatus = 'requested'
|
||||||
@@ -589,9 +595,9 @@ function openPlayer (torrentSummary, index) {
|
|||||||
if (timedOut) return
|
if (timedOut) return
|
||||||
|
|
||||||
// otherwise, play the video
|
// otherwise, play the video
|
||||||
state.url = 'player'
|
|
||||||
state.window.title = torrentSummary.name
|
state.window.title = torrentSummary.name
|
||||||
update()
|
update()
|
||||||
|
cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,18 +624,20 @@ function openFolder (torrentSummary) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePlayer () {
|
function closePlayer (cb) {
|
||||||
state.url = 'home'
|
|
||||||
state.window.title = config.APP_NAME
|
state.window.title = config.APP_NAME
|
||||||
update()
|
update()
|
||||||
|
|
||||||
if (state.window.isFullScreen) {
|
if (state.window.isFullScreen) {
|
||||||
dispatch('toggleFullScreen', false)
|
dispatch('toggleFullScreen', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreBounds()
|
restoreBounds()
|
||||||
stopServer()
|
stopServer()
|
||||||
update()
|
update()
|
||||||
|
|
||||||
|
ipcRenderer.send('unblockPowerSave')
|
||||||
|
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTorrent (torrentSummary) {
|
function toggleTorrent (torrentSummary) {
|
||||||
@@ -652,6 +660,7 @@ function deleteTorrent (torrentSummary) {
|
|||||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||||
saveState()
|
saveState()
|
||||||
|
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
||||||
playInterfaceSound(config.SOUND_DELETE)
|
playInterfaceSound(config.SOUND_DELETE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +706,7 @@ function restoreBounds () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onError (err) {
|
function onError (err) {
|
||||||
if (err.stack) console.error(err.stack)
|
console.error(err.stack || err)
|
||||||
playInterfaceSound(config.SOUND_ERROR)
|
playInterfaceSound(config.SOUND_ERROR)
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
|
|||||||
61
renderer/lib/location-history.js
Normal file
61
renderer/lib/location-history.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
module.exports = LocationHistory
|
||||||
|
|
||||||
|
function LocationHistory () {
|
||||||
|
if (!new.target) return new LocationHistory()
|
||||||
|
this._history = []
|
||||||
|
this._forward = []
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.go = function (page) {
|
||||||
|
console.log('go', page)
|
||||||
|
this.clearForward()
|
||||||
|
this._go(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._go = function (page) {
|
||||||
|
if (page.onbeforeload) {
|
||||||
|
page.onbeforeload((err) => {
|
||||||
|
if (err) return
|
||||||
|
this._history.push(page)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this._history.push(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.back = function () {
|
||||||
|
if (this._history.length <= 1) return
|
||||||
|
|
||||||
|
var page = this._history.pop()
|
||||||
|
|
||||||
|
if (page.onbeforeunload) {
|
||||||
|
page.onbeforeunload(() => {
|
||||||
|
this._forward.push(page)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this._forward.push(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.forward = function () {
|
||||||
|
if (this._forward.length === 0) return
|
||||||
|
|
||||||
|
var page = this._forward.pop()
|
||||||
|
this._go(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.clearForward = function () {
|
||||||
|
this._forward = []
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.current = function () {
|
||||||
|
return this._history[this._history.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.hasBack = function () {
|
||||||
|
return this._history.length > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.hasForward = function () {
|
||||||
|
return this._forward.length > 0
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ var os = require('os')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var LocationHistory = require('./lib/location-history')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/*
|
/*
|
||||||
@@ -11,9 +12,9 @@ module.exports = {
|
|||||||
client: null, /* the WebTorrent client */
|
client: null, /* the WebTorrent client */
|
||||||
server: null, /* local WebTorrent-to-HTTP server */
|
server: null, /* local WebTorrent-to-HTTP server */
|
||||||
prev: {}, /* used for state diffing in updateElectron() */
|
prev: {}, /* used for state diffing in updateElectron() */
|
||||||
url: 'home',
|
location: new LocationHistory(),
|
||||||
window: {
|
window: {
|
||||||
bounds: null, /* x y width height */
|
bounds: null, /* {x, y, width, height } */
|
||||||
isFocused: true,
|
isFocused: true,
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
title: config.APP_NAME /* current window title */
|
title: config.APP_NAME /* current window title */
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function App (state, dispatch) {
|
|||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
var hideControls = state.url === 'player' &&
|
var hideControls = state.location.current().url === 'player' &&
|
||||||
state.video.mouseStationarySince !== 0 &&
|
state.video.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
|
||||||
!state.video.isPaused &&
|
!state.video.isPaused &&
|
||||||
@@ -25,10 +25,10 @@ function App (state, dispatch) {
|
|||||||
|
|
||||||
// Hide the header on Windows/Linux when in the player
|
// Hide the header on Windows/Linux when in the player
|
||||||
// On OSX, the header appears as part of the title bar
|
// On OSX, the header appears as part of the title bar
|
||||||
var hideHeader = process.platform !== 'darwin' && state.url === 'player'
|
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
|
||||||
|
|
||||||
var cls = [
|
var cls = [
|
||||||
'view-' + state.url, /* e.g. view-home, view-player */
|
'view-' + state.location.current().url, /* e.g. view-home, view-player */
|
||||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||||
]
|
]
|
||||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||||
@@ -75,9 +75,9 @@ function App (state, dispatch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getView () {
|
function getView () {
|
||||||
if (state.url === 'home') {
|
if (state.location.current().url === 'home') {
|
||||||
return TorrentList(state, dispatch)
|
return TorrentList(state, dispatch)
|
||||||
} else if (state.url === 'player') {
|
} else if (state.location.current().url === 'player') {
|
||||||
return Player(state, dispatch)
|
return Player(state, dispatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ function Header (state, dispatch) {
|
|||||||
<div class='header'>
|
<div class='header'>
|
||||||
${getTitle()}
|
${getTitle()}
|
||||||
<div class='nav left'>
|
<div class='nav left'>
|
||||||
<i
|
<i.icon.back
|
||||||
class='icon back'
|
class=${state.location.hasBack() ? '' : 'disabled'}
|
||||||
title='back'
|
title='back'
|
||||||
onclick=${() => dispatch('back')}>
|
onclick=${() => dispatch('back')}>
|
||||||
chevron_left
|
chevron_left
|
||||||
</i>
|
</i>
|
||||||
<i
|
<i.icon.forward
|
||||||
class='icon forward'
|
class=${state.location.hasForward() ? '' : 'disabled'}
|
||||||
title='forward'
|
title='forward'
|
||||||
onclick=${() => dispatch('forward')}>
|
onclick=${() => dispatch('forward')}>
|
||||||
chevron_right
|
chevron_right
|
||||||
@@ -35,7 +35,7 @@ function Header (state, dispatch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.url !== 'player') {
|
if (state.location.current().url !== 'player') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
Reference in New Issue
Block a user