Compare commits

..

50 Commits

Author SHA1 Message Date
DC
ab55852bb0 v0.1.1 2016-03-28 00:39:15 -07:00
Feross Aboukhadijeh
46bc1bacdd Merge pull request #250 from rom1504/add_binary
add binary, fix #247
2016-03-27 22:05:47 -07:00
Romain Beaumont
391a2004f4 add binary, fix #247
cmd.js is mostly taken from node_modules/electron-prebuilt/cli.js (what the node_modules/.bin/electron symlink points to)
2016-03-28 02:26:13 +02:00
DC
2341749074 Track progress for currently playing file. Fixes #244 2016-03-27 16:17:35 -07:00
DC
ac7431292e Show filename in window title. Fix #245
Also fix error popover z index
2016-03-27 16:17:35 -07:00
Feross Aboukhadijeh
127b1577ac check for updates 5 seconds after startup 2016-03-27 03:30:29 -07:00
Feross Aboukhadijeh
7562a3856d Merge pull request #240 from feross/changelog
Update CHANGELOG
2016-03-27 03:27:14 -07:00
Feross Aboukhadijeh
bc9ef95790 Update CHANGELOG 2016-03-27 03:13:38 -07:00
DC
3617c17300 Memoize event handlers
Stop virtualdom from swapping out every event handler on every update
2016-03-27 02:58:26 -07:00
Feross Aboukhadijeh
8e344bed20 Merge pull request #239 from feross/absolute-path-urls
Resolve posterURL and torrentPath at runtime
2016-03-27 02:12:41 -07:00
Feross Aboukhadijeh
eb59c11f85 Resolve posterURL and torrentPath at runtime
Fixes bug where posters and torrent files can’t be found in the built
app.
2016-03-27 02:10:58 -07:00
Feross Aboukhadijeh
339f472473 Merge pull request #237 from feross/fix-233
Fixes for PR #233
2016-03-27 01:16:56 -07:00
Feross Aboukhadijeh
75412388e5 Save .torrent dialog: Add "All Files" option 2016-03-27 01:10:45 -07:00
Feross Aboukhadijeh
aad3acfe91 Right click -> "Save torrent file" without using streams
For #233
2016-03-27 01:06:58 -07:00
Feross Aboukhadijeh
b9c012a587 Make right click -> "copy magnet uri" work for default torrents
For #233
2016-03-27 01:06:36 -07:00
Feross Aboukhadijeh
d5bea54a83 sintel.torrent: Use webtorrent.io torrent 2016-03-27 00:58:50 -07:00
Feross Aboukhadijeh
c7ee0aab01 Merge pull request #233 from Flet/torrent-context-menu
add context menu with share/save actions
2016-03-27 00:39:15 -07:00
Feross Aboukhadijeh
9b8a9e5aa3 Merge pull request #236 from feross/fix-mac-flash
Fix OS X flash of white
2016-03-27 00:31:08 -07:00
Feross Aboukhadijeh
40cec3a2f6 Delay lazy load of client
This works great on my slow Macbook 12”, so I assume it will work
without lag on most other people’s computers.
2016-03-27 00:28:33 -07:00
Feross Aboukhadijeh
589880f1e3 OS X: Prevent white flash on window open
We got the window to run less JS but now it’s shown by the main process
too soon! This fixes that with a setTimeout.

Perhaps when this issue is fixed
(https://github.com/atom/electron/issues/861) we can remove the timeout.
2016-03-27 00:27:28 -07:00
Feross Aboukhadijeh
6465c23127 Merge pull request #235 from feross/about-window
Windows/Linux: Add About Window (#220)
2016-03-26 23:53:37 -07:00
Feross Aboukhadijeh
203d058280 About window: increase size slightly 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
a116bf976a About window: only allow text selection 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
aa117054fb About window: font-size tweaks 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
5335bf39b5 Windows/Linux: Hide menu on About Window 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
b263a69716 Windows/Linux: Add About Window (#220) 2016-03-26 23:43:24 -07:00
DC
906da4d977 Speed up init() by >= 2x
Lazy load the WebTorrent, Chromecast, and Airplay modules
2016-03-26 23:31:32 -07:00
Dan Flettre
6c07c4763d add context menu with share/save actions 2016-03-26 23:10:27 -05:00
Feross Aboukhadijeh
1e6e101c4e Merge pull request #232 from feross/ui-responsiveness
UI responds instantly to torrent enable/disable (#208)
2016-03-26 20:40:12 -07:00
Feross Aboukhadijeh
4a627b6f03 UI responds instantly to torrent enable/disable (#208) 2016-03-26 20:36:57 -07:00
Feross Aboukhadijeh
a2b9a178b7 Merge pull request #231 from feross/shortcut-fix
Keyboard shortcuts: volume shortcuts should be local
2016-03-26 20:07:30 -07:00
Feross Aboukhadijeh
9ef1d0a605 Keyboard shortcuts: volume shortcuts should be local
`globalShortcut` will register the shortcut at the OS level, even when
the app is not focused.

Using `localShortcut` would work, but let's put it in the top menu
instead, where all the other shortcuts are.
2016-03-26 20:04:29 -07:00
Feross Aboukhadijeh
0cf89600c0 es6ify 2016-03-26 19:58:04 -07:00
DC
3928564314 Add (BETA) to window title
Also fix a bug: fix relative paths to the default torrents.
2016-03-26 18:11:40 -07:00
DC
0d5ff2d964 Use relative paths for default torrents
This keeps them working if a user opens the app from DMG, then installs it to a different path and opens it again
2016-03-26 16:15:24 -07:00
Feross Aboukhadijeh
b85f0b9489 Merge pull request #202 from grunjol/feature-volume-management-clean
Add volume management
2016-03-25 23:46:59 -07:00
Feross Aboukhadijeh
5b6e4ac394 Merge pull request #226 from feross/fix-ubuntu-crash
Linux: Ensure ".local/share/{applications,icons}" exists; plus perf fix
2016-03-25 22:58:16 -07:00
Feross Aboukhadijeh
59a1bc03f2 Perf: Remove all *Sync methods for Linux startup 2016-03-25 21:47:49 -07:00
Feross Aboukhadijeh
01e27b2691 Linux: Ensure ".local/share/{applications,icons}" exists 2016-03-25 18:56:22 -07:00
Feross Aboukhadijeh
656e811e84 Merge pull request #203 from grunjol/feature-unity-desktop-shortcuts
Add unity launcher icons
2016-03-25 18:50:02 -07:00
Feross Aboukhadijeh
db60b99982 window useContentSize 2016-03-25 17:50:29 -07:00
Feross Aboukhadijeh
180d756dc0 OS X packager: Fix missing DMG background image
Remove previous DMG file. This somehow fixes the issue.
2016-03-25 17:50:29 -07:00
Feross Aboukhadijeh
a029ea3b0a Revert "Merge pull request #205 from feross/compress"
This reverts commit bd04d76adf, reversing
changes made to 73d5a4e1ab.
2016-03-25 17:50:29 -07:00
grunjol
4673354703 fixes #116 Add Unity launcher icons 2016-03-25 16:31:12 -03:00
Nate Goldman
ded599328a add version badge, update release info 2016-03-25 11:45:53 -07:00
Nate Goldman
7dfc6fd98c fix package arg 2016-03-25 11:43:13 -07:00
Feross Aboukhadijeh
e0ed255fb4 improve v0.1.0. changelog 2016-03-25 04:14:18 -07:00
Feross Aboukhadijeh
d8f97c3b58 put package.json name back to webtorrent-app
webtorrent-www relies on this name. I originally changed it because I
thought the windows install builder was using it, but I pass all the
options into that explicitly now, and even pass an option to prevent it
from using package.json, so this should be okay.
2016-03-25 04:07:31 -07:00
Feross Aboukhadijeh
753cca7dfb remove duplicate path-exists dep 2016-03-25 03:57:16 -07:00
grunjol
fc6d8e7b7d add volume management 2016-03-23 09:01:07 -03:00
29 changed files with 715 additions and 314 deletions

View File

@@ -1,18 +1,37 @@
# WebTorrent.app Version History
## v0.1.1
- Performance improvements
- Improve app startup time by over 100%
- Reduce the number of DOM updates substantially
- Update UI immediately anytime state is changed, instead on 1 second interval
- Added right-click menu
- Save .torrent File
- Copy Instant.io Link to Clipboard
- Copy Magnet Link to Clipbaord
- Added keyboard shortcut for volume up (⌘/Ctrl + ↑) and volume down (⌘/Ctrl + ↓)
- Add desktop launcher shortcuts, like OS X has, for KDE and GNOME (Linux)
- Add "About" window (Windows, Linux)
- Better default window size that fits all the default torrents
- Fixed
- Crash when ".local/share/{applications,icons}" path did not exist (Linux)
- WebTorrent executable can be moved without breaking torrents in the client
## v0.1.0
- **Windows support!**
- See `WebTorrentSetup.exe` in the downloads below!
- Auto-updater included, just like the OS X version.
- Automatically installs desktop/start menu shortcuts
- Windows top menu is no longer automatically hidden.
- Includes auto-updater, just like the OS X version.
- Installs desktop and start menu shortcuts.
- **Audio file support!**
- Supports playback of .mp3, .aac, .ogg, .wav
- Audio file metadata gets shown in the UI
- Focus the WebTorrent window after opening magnet link in third-party app
- Subtler app sounds
- Fix for some magnet links failing to open
- Top menu is no longer automatically hidden (Windows)
- When magnet links are opened from third-party apps, the WebTorrent window now gets focus.
- Subtler app sounds.
- Fix for an issue that caused some magnet links to fail to open.
**NOTE:** OS X users must install v0.1.0 manually because the app bundle ID was changed in this release, and the auto-updater cannot handle this condition.
Thanks to @dcposch, @ngoldman, and @feross for contributing to this release.

View File

@@ -18,11 +18,15 @@
<img src="https://img.shields.io/travis/feross/webtorrent-app/master.svg"
alt="Travis Build">
</a>
<a href="https://github.com/feross/webtorrent-app/releases">
<img src="https://img.shields.io/github/release/feross/webtorrent-app.svg"
alt="Latest Release Version">
</a>
</p>
## Install
**WebTorrent.app** is still under very active development. An [alpha release](https://github.com/feross/webtorrent-app/releases) is currently available for OS X.
**WebTorrent.app** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-app/releases) page.
## Screenshot

10
bin/cmd.js Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
var electron = require('electron-prebuilt')
var proc = require('child_process')
var path = require('path')
var child = proc.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
child.on('close', function (code) {
process.exit(code)
})

View File

@@ -10,6 +10,7 @@ var electronPackager = require('electron-packager')
var fs = require('fs')
var path = require('path')
var pkg = require('../package.json')
var rimraf = require('rimraf')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
@@ -75,7 +76,7 @@ var all = {
prune: true,
// The Electron version with which the app is built (without the leading 'v')
version: pkg.devDependencies['electron-prebuilt']
version: pkg.dependencies['electron-prebuilt']
}
var darwin = {
@@ -213,10 +214,13 @@ function buildDarwin (cb) {
cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`)
console.log('Created OS X .zip file.')
var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg')
rimraf.sync(targetPath)
// Create a .dmg (OS X disk image) file, for easy user installation.
var dmgOpts = {
basepath: config.ROOT_PATH,
target: path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg'),
target: targetPath,
specification: {
title: config.APP_NAME,
icon: config.APP_ICON + '.icns',

View File

@@ -12,16 +12,15 @@ module.exports = {
APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
AUTO_UPDATE_CHECK_STARTUP_DELAY: 10 * 1000 /* 10 seconds */,
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
CONFIG_PATH: applicationConfigPath(APP_NAME),
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'),
INDEX: 'file://' + path.join(__dirname, 'renderer', 'index.html'),
IS_PRODUCTION: isProduction(),
ROOT_PATH: __dirname,
@@ -58,7 +57,10 @@ module.exports = {
SOUND_STARTUP: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
volume: 0.4
}
},
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
}
function isProduction () {

View File

@@ -2,49 +2,85 @@ module.exports = {
init
}
var path = require('path')
var log = require('./log')
function init () {
if (process.platform === 'win32') {
var path = require('path')
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
initWindows()
}
if (process.platform === 'linux') {
installDesktopFile()
installDesktopIcon()
initLinux()
}
}
function installDesktopFile () {
var config = require('../config')
var fs = require('fs')
var path = require('path')
var os = require('os')
var templatePath = path.join(config.STATIC_PATH, 'webtorrent.desktop')
var desktopFile = fs.readFileSync(templatePath, 'utf8')
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, path.dirname(process.execPath))
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, process.execPath)
var desktopFilePath = path.join(os.homedir(), '.local', 'share', 'applications', 'webtorrent.desktop')
fs.writeFileSync(desktopFilePath, desktopFile)
function initWindows () {
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
}
function installDesktopIcon () {
function initLinux () {
var config = require('../config')
var fs = require('fs')
var path = require('path')
var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path')
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
var iconFile = fs.readFileSync(iconStaticPath)
installDesktopFile()
installIconFile()
var iconFilePath = path.join(os.homedir(), '.local', 'share', 'icons', 'webtorrent.png')
fs.writeFileSync(iconFilePath, iconFile)
function installDesktopFile () {
var templatePath = path.join(config.STATIC_PATH, 'webtorrent.desktop')
fs.readFile(templatePath, 'utf8', writeDesktopFile)
}
function writeDesktopFile (err, desktopFile) {
if (err) return console.error(err.message)
var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH
var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
var tryExecPath = process.execPath
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath)
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath)
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent.desktop'
)
mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return console.error(err.message)
})
}
function installIconFile () {
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
fs.readFile(iconStaticPath, writeIconFile)
}
function writeIconFile (err, iconFile) {
if (err) return console.error(err.message)
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent.png'
)
mkdirp(path.dirname(iconFilePath))
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return console.error(err.message)
})
}
}
/**

View File

@@ -57,9 +57,7 @@ function init () {
app.on('ipcReady', function () {
log('Command line args:', argv)
argv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
processArgv(argv)
})
app.on('before-quit', function () {
@@ -67,11 +65,7 @@ function init () {
})
app.on('activate', function () {
if (windows.main) {
windows.main.show()
} else {
windows.createMainWindow()
}
windows.createMainWindow()
})
app.on('window-all-closed', function () {
@@ -90,7 +84,7 @@ function onOpen (e, torrentId) {
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusMainWindow()
windows.focusWindow(windows.main)
}, 100)
} else {
argv.push(torrentId)
@@ -102,11 +96,9 @@ function onAppOpen (newArgv) {
if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv)
windows.focusMainWindow()
windows.focusWindow(windows.main)
newArgv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
processArgv(newArgv)
} else {
argv.push(...newArgv)
}
@@ -116,6 +108,24 @@ function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
}
function processArgv (argv) {
argv.forEach(function (argvi) {
switch (argvi) {
case '-n':
windows.main.send('dispatch', 'showCreateTorrent')
break
case '-o':
windows.main.send('dispatch', 'showOpenTorrentFile')
break
case '-u':
windows.main.send('showOpenTorrentAddress')
break
default:
windows.main.send('dispatch', 'onOpen', argvi)
}
})
}
function setupCrashReporter () {
// require('crash-reporter').start({
// productName: 'WebTorrent',

View File

@@ -1,5 +1,5 @@
module.exports = {
init: init
init
}
var debug = require('debug')('webtorrent-app:ipcMain')
@@ -18,14 +18,16 @@ var powerSaveBlockID = 0
function init () {
ipcMain.on('ipcReady', function (e) {
console.timeEnd('init')
app.ipcReady = true
app.emit('ipcReady')
setTimeout(function () {
windows.main.show()
console.timeEnd('init')
}, 50)
})
ipcMain.on('showOpenTorrentFile', function (e) {
menu.showOpenTorrentFile()
})
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('showCreateTorrent', menu.showCreateTorrent)
ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize)
@@ -58,6 +60,9 @@ function init () {
ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', menu.onPlayerOpen)
ipcMain.on('onPlayerClose', menu.onPlayerClose)
}
function setBounds (bounds, maximize) {

View File

@@ -1,10 +1,13 @@
module.exports = {
init: init,
onToggleFullScreen: onToggleFullScreen,
onWindowHide: onWindowHide,
onWindowShow: onWindowShow,
showOpenTorrentFile: showOpenTorrentFile,
toggleFullScreen: toggleFullScreen
init,
onToggleFullScreen,
onWindowHide,
onWindowShow,
onPlayerOpen,
onPlayerClose,
showCreateTorrent,
showOpenTorrentFile,
toggleFullScreen
}
var debug = require('debug')('webtorrent-app:menu')
@@ -43,6 +46,18 @@ function toggleFloatOnTop (flag) {
}
}
function increaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
}
}
function decreaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', -0.1)
}
}
function toggleDevTools () {
debug('toggleDevTools')
if (windows.main) {
@@ -74,6 +89,16 @@ function onWindowHide () {
getMenuItem('Float on Top').enabled = false
}
function onPlayerOpen () {
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
}
function onPlayerClose () {
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
}
function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen)
@@ -195,6 +220,21 @@ function getAppMenuTemplate () {
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{
label: 'Developer',
submenu: [
@@ -260,12 +300,12 @@ function getAppMenuTemplate () {
]
if (process.platform === 'darwin') {
var name = app.getName()
// WebTorrent menu (OS X)
template.unshift({
label: name,
label: config.APP_NAME,
submenu: [
{
label: 'About ' + name,
label: 'About ' + config.APP_NAME,
role: 'about'
},
{
@@ -280,7 +320,7 @@ function getAppMenuTemplate () {
type: 'separator'
},
{
label: 'Hide ' + name,
label: 'Hide ' + config.APP_NAME,
accelerator: 'Command+H',
role: 'hide'
},
@@ -299,12 +339,12 @@ function getAppMenuTemplate () {
{
label: 'Quit',
accelerator: 'Command+Q',
click: function () { app.quit() }
click: () => app.quit()
}
]
})
// Window menu
// Window menu (OS X)
template[4].submenu.push(
{
type: 'separator'
@@ -314,6 +354,17 @@ function getAppMenuTemplate () {
role: 'front'
}
)
} else {
// Help menu (Windows, Linux)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow
}
)
}
return template

View File

@@ -1,5 +1,5 @@
module.exports = {
init: init
init
}
var electron = require('electron')

View File

@@ -1,7 +1,9 @@
var windows = module.exports = {
about: null,
main: null,
createAboutWindow: createAboutWindow,
createMainWindow: createMainWindow,
focusMainWindow: focusMainWindow
focusWindow: focusWindow
}
var electron = require('electron')
@@ -11,7 +13,45 @@ var app = electron.app
var config = require('../config')
var menu = require('./menu')
function createAboutWindow () {
if (windows.about) {
return focusWindow(windows.about)
}
var win = windows.about = new electron.BrowserWindow({
backgroundColor: '#ECECEC',
show: false,
center: true,
resizable: false,
icon: config.APP_ICON + '.png',
title: process.platform !== 'darwin'
? 'About ' + config.APP_WINDOW_TITLE
: '',
useContentSize: true, // Specify web page size without OS chrome
width: 300,
height: 170,
minimizable: false,
maximizable: false,
fullscreen: false,
skipTaskbar: true
})
win.loadURL(config.WINDOW_ABOUT)
// No window menu
win.setMenu(null)
win.webContents.on('did-finish-load', function () {
win.show()
})
win.once('closed', function () {
windows.about = null
})
}
function createMainWindow () {
if (windows.main) {
return focusWindow(windows.main)
}
var win = windows.main = new electron.BrowserWindow({
backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3)
@@ -19,21 +59,18 @@ function createMainWindow () {
minWidth: 375,
minHeight: 38 + (120 * 2), // header height + 2 torrents
show: false, // Hide window until DOM finishes loading
title: config.APP_NAME,
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome
width: 450,
height: 38 + (120 * 4) // header height + 4 torrents
})
win.loadURL(config.INDEX)
win.loadURL(config.WINDOW_MAIN)
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen()
})
win.webContents.on('did-finish-load', function () {
win.show()
})
win.on('blur', menu.onWindowHide)
win.on('focus', menu.onWindowShow)
@@ -53,9 +90,9 @@ function createMainWindow () {
})
}
function focusMainWindow () {
if (windows.main.isMinimized()) {
windows.main.restore()
function focusWindow (win) {
if (win.isMinimized()) {
win.restore()
}
windows.main.show() // shows and gives focus
win.show() // shows and gives focus
}

View File

@@ -1,7 +1,7 @@
{
"name": "WebTorrent",
"name": "webtorrent-app",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.1.0",
"version": "0.1.1",
"author": {
"name": "Feross Aboukhadijeh",
"email": "feross@feross.org",
@@ -19,6 +19,7 @@
"debug": "^2.2.0",
"drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0",
"electron-prebuilt": "0.37.2",
"hyperx": "^2.0.2",
"main-loop": "^3.2.0",
"mkdirp": "^0.5.1",
@@ -34,10 +35,8 @@
"devDependencies": {
"electron-osx-sign": "^0.3.0",
"electron-packager": "^5.0.0",
"electron-prebuilt": "0.37.2",
"electron-winstaller": "^2.0.5",
"gh-release": "^2.0.2",
"path-exists": "^2.1.0",
"gh-release": "^2.0.3",
"plist": "^1.2.0",
"rimraf": "^2.5.2",
"standard": "^6.0.5"
@@ -61,9 +60,12 @@
"clean": "node ./bin/clean.js",
"debug": "DEBUG=* electron .",
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
"size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"start": "electron .",
"test": "standard",
"update-authors": "./bin/update-authors.sh"
},
"bin": {
"webtorrent-app": "./bin/cmd.js"
}
}

35
renderer/about.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background-color: #ECECEC;
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
text-align: center;
overflow: hidden;
font-size: 16px;
-webkit-user-select: none;
}
img {
width: 65px;
height: 65px;
}
h1 {
font-size: 0.9em;
-webkit-user-select: text;
}
p {
font-size: 0.8em;
-webkit-user-select: text;
}
</style>
</head>
<body>
<img src="../static/WebTorrent.png">
<h1>WebTorrent</h1>
<p>Version <script>document.write(require('../package.json').version)</script></p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body>
</html>

View File

@@ -49,34 +49,6 @@ table {
background-color: rgb(40, 40, 40);
}
.loading {
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.loading .icon {
font-size: 42px;
display: block;
text-align: center;
animation: spin-ccw 2s infinite linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-ccw {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
@@ -769,6 +741,7 @@ body.drag .torrent-placeholder span {
margin: 0;
width: 100%;
overflow: hidden;
z-index: 1;
}
.app.hide-header .error-popover {

View File

@@ -12,24 +12,31 @@ var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var remote = require('remote')
var WebTorrent = require('webtorrent')
var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch')
var App = require('./views/app')
var Cast = require('./lib/cast')
var errors = require('./lib/errors')
var config = require('../config')
var TorrentPlayer = require('./lib/torrent-player')
var torrentPoster = require('./lib/torrent-poster')
var util = require('./util')
var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch)
// These two dependencies are the slowest-loading, so we lazy load them
// This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop
var WebTorrent = null
var Cast = null
// 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,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard
var dialog = remote.require('dialog')
// For easy debugging in Developer Tools
var state = global.state = require('./state')
@@ -53,20 +60,11 @@ loadState(init)
function init () {
state.location.go({ url: 'home' })
// Connect to the WebTorrent and BitTorrent networks
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
state.client = new WebTorrent()
state.client.on('warning', onWarning)
state.client.on('error', function (err) {
// TODO: WebTorrent should have semantic errors
if (err.message.startsWith('There is already a swarm')) {
onError(new Error('Couldn\'t add duplicate torrent'))
} else {
onError(err)
}
})
resumeTorrents() /* restart everything we were torrenting last time the app ran */
setInterval(updateTorrentProgress, 1000)
// Lazily load the WebTorrent, Chromecast, and Airplay modules
window.setTimeout(function () {
lazyLoadClient()
lazyLoadCast()
}, 750)
// 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
@@ -79,23 +77,10 @@ function init () {
})
document.body.appendChild(vdomLoop.target)
// Calling update() updates the UI given the current state
// Do this at least once a second to show latest state for each torrent
// (eg % downloaded) and to keep the cursor in sync when playing a video
setInterval(function () {
update()
updateClientProgress()
}, 1000)
// Save state on exit
window.addEventListener('beforeunload', saveState)
// listen for messages from the main process
setupIpc()
// OS integrations:
// ...Chromecast and Airplay
Cast.init(update)
// ...drag and drop a torrent or video file to play or seed
dragDrop('body', (files) => dispatch('onOpen', files))
@@ -129,16 +114,65 @@ function init () {
update()
})
// Listen for messages from the main process
setupIpc()
// Done! Ideally we want to get here <100ms after the user clicks the app
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */
playInterfaceSound('STARTUP')
console.timeEnd('init')
}
// Lazily loads the WebTorrent module and creates the WebTorrent client
function lazyLoadClient () {
if (!WebTorrent) initWebtorrent()
return state.client
}
// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
if (!Cast) {
Cast = require('./lib/cast')
Cast.init(update) // Search the local network for Chromecast and Airplays
}
return Cast
}
// Load the WebTorrent module, connect to both the WebTorrent and BitTorrent
// networks, resume torrents, start monitoring torrent progress
function initWebtorrent () {
WebTorrent = require('webtorrent')
// Connect to the WebTorrent and BitTorrent networks
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
state.client = new WebTorrent()
state.client.on('warning', onWarning)
state.client.on('error', function (err) {
// TODO: WebTorrent should have semantic errors
if (err.message.startsWith('There is already a swarm')) {
onError(new Error('Couldn\'t add duplicate torrent'))
} else {
onError(err)
}
})
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video
setInterval(function () {
if (!updateTorrentProgress()) {
update() // If we didn't just update(), do so now, for the video cursor
}
}, 1000)
}
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
// tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) {
return App(state, dispatch)
return App(state)
}
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
@@ -164,70 +198,73 @@ function updateElectron () {
// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
if (['mediaMouseMoved', 'playbackJump'].indexOf(action) === -1) {
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
// Log dispatch calls, for debugging
if (action !== 'mediaMouseMoved') {
console.log('dispatch: %s %o', action, args)
}
if (action === 'onOpen') {
onOpen(args[0] /* files */)
}
if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */)
}
if (action === 'showCreateTorrent') {
ipcRenderer.send('showCreateTorrent')
}
if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile')
}
if (action === 'seed') {
seed(args[0] /* files */)
}
if (action === 'play') {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
// TODO: handle audio. video only for now.
openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
}
if (action === 'openFile') {
openFile(args[0] /* torrentSummary */, args[1] /* index */)
openFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openFolder') {
openFolder(args[0] /* torrentSummary */)
openFolder(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* torrentSummary */)
toggleTorrent(args[0] /* infoHash */)
}
if (action === 'deleteTorrent') {
deleteTorrent(args[0] /* torrentSummary */)
deleteTorrent(args[0] /* infoHash */)
}
if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */)
}
if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */)
}
if (action === 'openChromecast') {
Cast.openChromecast()
lazyLoadCast().openChromecast()
}
if (action === 'openAirplay') {
Cast.openAirplay()
lazyLoadCast().openAirplay()
}
if (action === 'stopCasting') {
Cast.stopCasting()
lazyLoadCast().stopCasting()
}
if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */)
}
if (action === 'back') {
state.location.back()
update()
}
if (action === 'forward') {
state.location.forward()
update()
}
if (action === 'playPause') {
playPause()
}
if (action === 'play') {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
playPause(false)
}
if (action === 'pause') {
@@ -236,6 +273,9 @@ function dispatch (action, ...args) {
if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */)
}
if (action === 'changeVolume') {
changeVolume(args[0] /* increase */)
}
if (action === 'mediaPlaying') {
state.playing.isPaused = false
ipcRenderer.send('blockPowerSave')
@@ -245,15 +285,17 @@ function dispatch (action, ...args) {
ipcRenderer.send('unblockPowerSave')
}
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0])
update()
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
update()
}
if (action === 'exitModal') {
state.modal = null
}
// Update the virtual-dom, unless it's just a mouse move event
if (action !== 'mediaMouseMoved') {
update()
}
}
@@ -264,7 +306,7 @@ function playPause (isPaused) {
return // Nothing to do
}
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
if (Cast.isCasting()) {
if (lazyLoadCast().isCasting()) {
Cast.playPause()
}
state.playing.isPaused = !state.playing.isPaused
@@ -272,7 +314,7 @@ function playPause (isPaused) {
}
function jumpToTime (time) {
if (Cast.isCasting()) {
if (lazyLoadCast().isCasting()) {
Cast.seek(time)
} else {
state.playing.jumpToTime = time
@@ -280,6 +322,22 @@ function jumpToTime (time) {
}
}
function changeVolume (delta) {
// change volume with delta value
setVolume(state.playing.volume + delta)
}
function setVolume (volume) {
// check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume))
if (lazyLoadCast().isCasting()) {
Cast.setVolume(volume)
} else {
state.playing.setVolume = volume
update()
}
}
function setupIpc () {
ipcRenderer.send('ipcReady')
@@ -338,18 +396,6 @@ function saveState () {
})
}
function updateClientProgress () {
var progress = state.client.progress
var activeTorrentsExist = state.client.torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Hide progress bar when client has no torrents, or progress is 100%
if (!activeTorrentsExist || progress === 1) {
progress = -1
}
state.dock.progress = progress
}
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
@@ -393,7 +439,9 @@ function getTorrentSummary (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)
var pending = state.pendingTorrents[infoHash]
if (pending) return pending
return lazyLoadClient().torrents.find((x) => x.infoHash === infoHash)
}
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
@@ -419,8 +467,8 @@ function addTorrentToList (torrent) {
state.saved.torrents.push({
status: 'new',
name: torrent.name,
magnetURI: torrent.magnetURI,
infoHash: torrent.infoHash
infoHash: torrent.infoHash,
magnetURI: torrent.magnetURI
})
saveState()
playInterfaceSound('ADD')
@@ -430,16 +478,23 @@ function addTorrentToList (torrent) {
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
function startTorrentingSummary (torrentSummary) {
var s = torrentSummary
if (s.torrentPath) return startTorrentingID(s.torrentPath, s.path)
else if (s.magnetURI) return startTorrentingID(s.magnetURI, s.path)
else return startTorrentingID(s.infoHash, s.path)
if (s.torrentPath) {
var torrentPath = util.getAbsoluteStaticPath(s.torrentPath)
var ret = startTorrentingID(torrentPath, s.path)
if (s.infoHash) state.pendingTorrents[s.infoHash] = ret
return ret
} else if (s.magnetURI) {
return startTorrentingID(s.magnetURI, s.path)
} else {
return startTorrentingID(s.infoHash, s.path)
}
}
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrentingID (torrentID, path) {
console.log('Starting torrent ' + torrentID)
var torrent = state.client.add(torrentID, {
console.log('starting torrent ' + torrentID)
var torrent = lazyLoadClient().add(torrentID, {
path: path || state.saved.downloadPath // Use downloads folder
})
addTorrentEvents(torrent)
@@ -455,13 +510,17 @@ function stopTorrenting (infoHash) {
// Creates a torrent for a local file and starts seeding it
function seed (files) {
if (files.length === 0) return
var torrent = state.client.seed(files)
var torrent = lazyLoadClient().seed(files)
addTorrentToList(torrent)
addTorrentEvents(torrent)
}
function addTorrentEvents (torrent) {
torrent.on('infoHash', update)
torrent.on('infoHash', function () {
var infoHash = torrent.infoHash
if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash]
update()
})
torrent.on('ready', torrentReady)
torrent.on('done', torrentDone)
@@ -471,7 +530,6 @@ function addTorrentEvents (torrent) {
torrentSummary.status = 'downloading'
torrentSummary.ready = true
torrentSummary.name = torrentSummary.displayName || torrent.name
torrentSummary.infoHash = torrent.infoHash
torrentSummary.path = torrent.path
// Summarize torrent files
@@ -507,10 +565,25 @@ function addTorrentEvents (torrent) {
}
function updateTorrentProgress () {
var changed = false
// First, track overall progress
var progress = lazyLoadClient().progress
var activeTorrentsExist = lazyLoadClient().torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Hide progress bar when client has no torrents, or progress is 100%
if (!activeTorrentsExist || progress === 1) {
progress = -1
}
// Show progress bar under the WebTorrent taskbar icon, on OSX
if (state.dock.progress !== progress) changed = true
state.dock.progress = progress
// Track progress for every file in each torrentSummary
// TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield
var changed = false
state.client.torrents.forEach(function (torrent) {
lazyLoadClient().torrents.forEach(function (torrent) {
var torrentSummary = getTorrentSummary(torrent.infoHash)
if (!torrentSummary || !torrent.ready) return
torrent.files.forEach(function (file, index) {
@@ -530,6 +603,7 @@ function updateTorrentProgress () {
})
if (changed) update()
return changed
}
function generateTorrentPoster (torrent, torrentSummary) {
@@ -542,7 +616,7 @@ function generateTorrentPoster (torrent, torrentSummary) {
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return onWarning(err)
// show the poster
torrentSummary.posterURL = 'file:///' + posterFilePath
torrentSummary.posterURL = posterFilePath
update()
})
})
@@ -573,8 +647,8 @@ function saveTorrentFile (torrentSummary, torrent) {
// Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('Error saving torrent file %s: %o', torrentPath, err)
console.log('Saved torrent file %s', torrentPath)
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath)
torrentSummary.torrentPath = torrentPath
saveState()
})
@@ -615,7 +689,7 @@ function startServerFromReadyTorrent (torrent, index, cb) {
// if it's audio, parse out the metadata (artist, title, etc)
musicmetadata(file.createReadStream(), function (err, info) {
if (err) return
console.log('Got audio metadata for %s: %v', file.name, info)
console.log('got audio metadata for %s: %v', file.name, info)
state.playing.audioInfo = info
update()
})
@@ -665,8 +739,9 @@ function stopServer () {
}
// Opens the video player
function openPlayer (torrentSummary, index, cb) {
var torrent = state.client.get(torrentSummary.infoHash)
function openPlayer (infoHash, index, cb) {
var torrentSummary = getTorrentSummary(infoHash)
var torrent = lazyLoadClient().get(infoHash)
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
torrentSummary.playStatus = 'requested'
update()
@@ -692,22 +767,42 @@ function openPlayer (torrentSummary, index, cb) {
if (timedOut) return update()
// otherwise, play the video
state.window.title = torrentSummary.name
state.window.title = torrentSummary.files[state.playing.fileIndex].name
update()
ipcRenderer.send('onPlayerOpen')
cb()
})
}
function openFile (torrentSummary, index) {
var torrent = state.client.get(torrentSummary.infoHash)
function closePlayer (cb) {
state.window.title = config.APP_WINDOW_TITLE
update()
if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false)
}
restoreBounds()
stopServer()
update()
ipcRenderer.send('unblockPowerSave')
ipcRenderer.send('onPlayerClose')
cb()
}
function openFile (infoHash, index) {
var torrent = lazyLoadClient().get(infoHash)
if (!torrent) return
var filePath = path.join(torrent.path, torrent.files[index].path)
ipcRenderer.send('openItem', filePath)
}
function openFolder (torrentSummary) {
var torrent = state.client.get(torrentSummary.infoHash)
function openFolder (infoHash) {
var torrent = lazyLoadClient().get(infoHash)
if (!torrent) return
var folderPath = path.join(torrent.path, torrent.name)
@@ -721,23 +816,8 @@ function openFolder (torrentSummary) {
})
}
function closePlayer (cb) {
state.window.title = config.APP_NAME
update()
if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false)
}
restoreBounds()
stopServer()
update()
ipcRenderer.send('unblockPowerSave')
cb()
}
function toggleTorrent (torrentSummary) {
function toggleTorrent (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
startTorrentingSummary(torrentSummary)
@@ -749,8 +829,7 @@ function toggleTorrent (torrentSummary) {
}
}
function deleteTorrent (torrentSummary) {
var infoHash = torrentSummary.infoHash
function deleteTorrent (infoHash) {
var torrent = getTorrent(infoHash)
if (torrent) torrent.destroy()
@@ -767,6 +846,48 @@ function toggleSelectTorrent (infoHash) {
update()
}
function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
var menu = new remote.Menu()
menu.append(new remote.MenuItem({
label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary)
}))
menu.append(new remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard',
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new remote.MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => clipboard.writeText(torrentSummary.magnetURI)
}))
menu.popup(remote.getCurrentWindow())
}
function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(state.saved.downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
dialog.showSaveDialog(remote.getCurrentWindow(), opts, (savePath) => {
var torrentPath = util.getAbsoluteStaticPath(torrentSummary.torrentPath)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return onError(err)
})
})
})
}
// Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized

View File

@@ -14,6 +14,7 @@ module.exports = {
stopCasting,
playPause,
seek,
setVolume,
isCasting
}
@@ -56,15 +57,19 @@ function addAirplayEvents () {}
function pollCastStatus (state) {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.status(function (err, status) {
if (err) return console.log('Error getting %s status: %o', state.playing.location, err)
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
})
} else if (state.playing.location === 'airplay') {
state.devices.airplay.status(function (status) {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
// According to docs is in [-30 - 0] (db) range
// should be converted to [0 - 1] using (val / 30 + 1)
update()
})
}
@@ -154,6 +159,17 @@ function seek (time) {
}
}
function setVolume (volume) {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.volume(volume, castCallback)
} else if (state.playing.location === 'airplay') {
// TODO remove line below once we can fetch the information in status update
state.playing.volume = volume
volume = (volume - 1) * 30
state.devices.airplay.volume(volume, castCallback)
}
}
function castCallback () {
console.log(state.playing.location + ' callback: %o', arguments)
}

View File

@@ -0,0 +1,36 @@
module.exports = {
setDispatch,
dispatch,
dispatcher
}
// Memoize most of our event handlers, which are functions in the form
// () => dispatch(<args>)
// ... this prevents virtual-dom from updating every listener on every update()
var _dispatchers = {}
var _dispatch = () => {}
function setDispatch (dispatch) {
_dispatch = dispatch
}
// Get a _memoized event handler that calls dispatch()
// All args must be JSON-able
function dispatcher (...args) {
var json = JSON.stringify(args)
var handler = _dispatchers[json]
if (!handler) {
_dispatchers[json] = (e) => {
// Don't click on whatever is below the button
e.stopPropagation()
// Don't regisiter clicks on disabled buttons
if (e.target.classList.contains('disabled')) return
_dispatch.apply(null, args)
}
}
return handler
}
function dispatch (...args) {
_dispatch.apply(null, args)
}

View File

@@ -6,7 +6,6 @@
<link rel="stylesheet" href="index.css" charset="utf-8">
</head>
<body>
<div class="loading"><i class="icon">sync</i></div>
<script async src="index.js"></script>
</body>
</html>

View File

@@ -17,7 +17,7 @@ module.exports = {
bounds: null, /* {x, y, width, height } */
isFocused: true,
isFullScreen: false,
title: config.APP_NAME /* current window title */
title: config.APP_WINDOW_TITLE
},
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: { /* the media (audio or video) that we're currently playing */
@@ -30,6 +30,8 @@ module.exports = {
isPaused: true,
mouseStationarySince: 0 /* Unix time in ms */
},
audioInfo: null, /* set whenever an audio file is playing */
pendingTorrents: {}, /* infohash to WebTorrent handle */
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
@@ -63,9 +65,10 @@ module.exports = {
{
status: 'paused',
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
displayName: 'Big Buck Bunny',
posterURL: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.jpg'),
torrentPath: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.torrent'),
posterURL: 'bigBuckBunny.jpg',
torrentPath: 'bigBuckBunny.torrent',
files: [
{
'name': 'bbb_sunflower_1080p_30fps_normal.mp4',
@@ -78,9 +81,10 @@ module.exports = {
{
status: 'paused',
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
displayName: 'Sintel',
posterURL: path.join(config.ROOT_PATH, 'static', 'sintel.jpg'),
torrentPath: path.join(config.ROOT_PATH, 'static', 'sintel.torrent'),
posterURL: 'sintel.jpg',
torrentPath: 'sintel.torrent',
files: [
{
'name': 'sintel.mp4',
@@ -93,9 +97,10 @@ module.exports = {
{
status: 'paused',
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
displayName: 'Tears of Steel',
posterURL: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.jpg'),
torrentPath: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.torrent'),
posterURL: 'tearsOfSteel.jpg',
torrentPath: 'tearsOfSteel.torrent',
files: [
{
'name': 'tears_of_steel_1080p.webm',

9
renderer/util.js Normal file
View File

@@ -0,0 +1,9 @@
var path = require('path')
var config = require('../config')
exports.getAbsoluteStaticPath = function (filePath) {
return path.isAbsolute(filePath)
? filePath
: path.join(config.STATIC_PATH, filePath)
}

View File

@@ -4,7 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
function Header (state, dispatch) {
var {dispatcher} = require('../lib/dispatcher')
function Header (state) {
return hx`
<div class='header'>
${getTitle()}
@@ -12,13 +14,13 @@ function Header (state, dispatch) {
<i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'}
title='Back'
onclick=${() => dispatch('back')}>
onclick=${dispatcher('back')}>
chevron_left
</i>
<i.icon.forward
class=${state.location.hasForward() ? '' : 'disabled'}
title='Forward'
onclick=${() => dispatch('forward')}>
onclick=${dispatcher('forward')}>
chevron_right
</i>
</div>
@@ -40,7 +42,7 @@ function Header (state, dispatch) {
<i
class='icon add'
title='Add torrent'
onclick=${() => dispatch('showOpenTorrentFile')}>
onclick=${dispatcher('showOpenTorrentFile')}>
add
</i>
`

View File

@@ -4,7 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
function OpenTorrentAddressModal (state, dispatch) {
var {dispatch} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) {
return hx`
<div class='open-torrent-address-modal'>
<p><strong>Enter torrent address or magnet link</strong></p>
@@ -15,17 +17,17 @@ function OpenTorrentAddressModal (state, dispatch) {
</p>
</div>
`
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal')
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}
function handleCancel () {
dispatch('exitModal')
}
}
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal')
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}
function handleCancel () {
dispatch('exitModal')
}

View File

@@ -4,23 +4,25 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var util = require('../util')
var {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay
function Player (state, dispatch) {
function Player (state) {
// Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead
var showVideo = state.playing.location === 'local'
return hx`
<div
class='player'
onmousemove=${() => dispatch('mediaMouseMoved')}>
${showVideo ? renderMedia(state, dispatch) : renderCastScreen(state, dispatch)}
${renderPlayerControls(state, dispatch)}
onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)}
</div>
`
}
function renderMedia (state, dispatch) {
function renderMedia (state) {
if (!state.server) return
// Unfortunately, play/pause can't be done just by modifying HTML.
@@ -38,19 +40,26 @@ function renderMedia (state, dispatch) {
mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null
}
// set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume
state.playing.setVolume = null
}
state.playing.currentTime = mediaElement.currentTime
state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume
}
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
src='${state.server.localURL}'
ondblclick=${() => dispatch('toggleFullScreen')}
ondblclick=${dispatcher('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${() => dispatch('mediaPlaying')}
onpause=${() => dispatch('mediaPaused')}
onplay=${dispatcher('mediaPlaying')}
onpause=${dispatcher('mediaPaused')}
autoplay>
</div>
`
@@ -67,7 +76,7 @@ function renderMedia (state, dispatch) {
<div
class='letterbox'
style=${style}
onmousemove=${() => dispatch('mediaMouseMoved')}>
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderAudioMetadata(state)}
</div>
@@ -118,7 +127,7 @@ function renderAudioMetadata (state) {
return hx`<div class='audio-metadata'>${elems}</div>`
}
function renderCastScreen (state, dispatch) {
function renderCastScreen (state) {
var isChromecast = state.playing.location.startsWith('chromecast')
var isAirplay = state.playing.location.startsWith('airplay')
var isStarting = state.playing.location.endsWith('-pending')
@@ -146,7 +155,8 @@ function renderCastScreen (state, dispatch) {
function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary || !torrentSummary.posterURL) return ''
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
var cleanURL = posterURL.replace(/\\/g, '/')
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' +
`, url(${cleanURL})`
@@ -157,7 +167,7 @@ function getPlayingTorrentSummary (state) {
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
function renderPlayerControls (state, dispatch) {
function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
@@ -174,7 +184,7 @@ function renderPlayerControls (state, dispatch) {
`,
hx`
<i class='icon fullscreen'
onclick=${() => dispatch('toggleFullScreen')}>
onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
`
@@ -187,18 +197,18 @@ function renderPlayerControls (state, dispatch) {
if (isOnChromecast) {
chromecastClass = 'active'
airplayClass = 'disabled'
chromecastHandler = () => dispatch('stopCasting')
chromecastHandler = dispatcher('stopCasting')
airplayHandler = undefined
} else if (isOnAirplay) {
chromecastClass = 'disabled'
airplayClass = 'active'
chromecastHandler = undefined
airplayHandler = () => dispatch('stopCasting')
airplayHandler = dispatcher('stopCasting')
} else {
chromecastClass = ''
airplayClass = ''
chromecastHandler = () => dispatch('openChromecast')
airplayHandler = () => dispatch('openAirplay')
chromecastHandler = dispatcher('openChromecast')
airplayHandler = dispatcher('openAirplay')
}
if (state.devices.chromecast || isOnChromecast) {
elements.push(hx`
@@ -224,7 +234,7 @@ function renderPlayerControls (state, dispatch) {
if (process.platform !== 'darwin') {
elements.push(hx`
<i.icon.back
onclick=${() => dispatch('back')}>
onclick=${dispatcher('back')}>
chevron_left
</i>
`)
@@ -232,7 +242,7 @@ function renderPlayerControls (state, dispatch) {
// Finally, the big button in the center plays or pauses the video
elements.push(hx`
<i class='icon play-pause' onclick=${() => dispatch('playPause')}>
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`)
@@ -256,15 +266,16 @@ function renderLoadingBar (state) {
if (torrent === null) {
return []
}
var file = torrent.files[state.playing.fileIndex]
// Find all contiguous parts of the torrent which are loaded
var parts = []
var lastPartPresent = false
var numParts = torrent.pieces.length
for (var i = 0; i < numParts; i++) {
var numParts = file._endPiece - file._startPiece + 1
for (var i = file._startPiece; i <= file._endPiece; i++) {
var partPresent = torrent.bitfield.get(i)
if (partPresent && !lastPartPresent) {
parts.push({start: i, count: 1})
parts.push({start: i - file._startPiece, count: 1})
} else if (partPresent) {
parts[parts.length - 1].count++
}

View File

@@ -5,9 +5,12 @@ var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var TorrentPlayer = require('../lib/torrent-player')
var util = require('../util')
function TorrentList (state, dispatch) {
var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary))
return hx`
@@ -24,7 +27,9 @@ function TorrentList (state, dispatch) {
function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash
var torrent = state.client.torrents.find((x) => x.infoHash === infoHash)
var torrent = state.client
? state.client.torrents.find((x) => x.infoHash === infoHash)
: null
var isSelected = state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible
@@ -33,9 +38,10 @@ function TorrentList (state, dispatch) {
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
var cleanURL = posterURL.replace(/\\/g, '/')
style.backgroundImage = gradient + `, url('${cleanURL}')`
}
@@ -47,7 +53,9 @@ function TorrentList (state, dispatch) {
if (isSelected) classes.push('selected')
classes = classes.join(' ')
return hx`
<div style=${style} class=${classes} onclick=${() => dispatch('toggleSelectTorrent', infoHash)}>
<div style=${style} class=${classes}
oncontextmenu=${dispatcher('openTorrentContextMenu', infoHash)}
onclick=${dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrent, torrentSummary)}
${renderTorrentButtons(torrentSummary)}
${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''}
@@ -103,6 +111,8 @@ function TorrentList (state, dispatch) {
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'play_arrow'
@@ -134,34 +144,28 @@ function TorrentList (state, dispatch) {
<i.btn.icon.play
title=${playTooltip}
class=${playClass}
onclick=${(e) => handleButton('play', e)}>
onclick=${dispatcher('play', infoHash)}>
${playIcon}
</i>
<i.btn.icon.download
class=${torrentSummary.status}
title=${downloadTooltip}
onclick=${(e) => handleButton('toggleTorrent', e)}>
onclick=${dispatcher('toggleTorrent', infoHash)}>
${downloadIcon}
</i>
<i
class='icon delete'
title='Remove torrent'
onclick=${(e) => handleButton('deleteTorrent', e)}>
onclick=${dispatcher('deleteTorrent', infoHash)}>
close
</i>
</div>
`
function handleButton (action, e) {
// Prevent propagation so that we don't select/unselect the torrent
e.stopPropagation()
if (e.target.classList.contains('disabled')) return
dispatch(action, torrentSummary)
}
}
// Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrent, torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
@@ -176,7 +180,10 @@ function TorrentList (state, dispatch) {
filesElement = hx`
<div class='files'>
<strong>Files</strong>
<span class='open-folder' onclick=${handleOpenFolder}>Open folder</span>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table>
${fileRows}
</table>
@@ -189,11 +196,6 @@ function TorrentList (state, dispatch) {
${filesElement}
</div>
`
function handleOpenFolder (e) {
e.stopPropagation()
dispatch('openFolder', torrentSummary)
}
}
// Show a single torrentSummary file in the details view for a single torrent
@@ -203,15 +205,20 @@ function TorrentList (state, dispatch) {
var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%'
// Second, render the file as a table row
var infoHash = torrentSummary.infoHash
var icon
var rowClass = ''
if (state.playing.infoHash === torrentSummary.infoHash && state.playing.fileIndex === index) {
var handleClick
if (state.playing.infoHash === infoHash && state.playing.fileIndex === index) {
icon = 'pause_arrow' /* playing? add option to pause */
handleClick = undefined // TODO: pause audio
} else if (TorrentPlayer.isPlayable(file)) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index)
}
return hx`
<tr onclick=${handleClick} class='${rowClass}'>
@@ -223,17 +230,5 @@ function TorrentList (state, dispatch) {
<td class='col-size'>${prettyBytes(file.length)}</td>
</tr>
`
// Finally, let the user click on the row to play media or open files
function handleClick (e) {
e.stopPropagation()
if (icon === 'pause_arrow') {
throw new Error('Unimplemented') // TODO: pause audio
} else if (icon === 'play_arrow') {
dispatch('play', torrentSummary, index)
} else if (isDone) {
dispatch('openFile', torrentSummary, index)
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

View File

@@ -10,7 +10,24 @@ Icon=webtorrent
Terminal=false
Path=$APP_PATH
Exec=$EXEC_PATH %U
TryExec=$EXEC_PATH
TryExec=$TRY_EXEC_PATH
StartupNotify=false
Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;x-scheme-handler/magnet;
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
[Desktop Action CreateNewTorrent]
Name=Create New Torrent...
Exec=$EXEC_PATH -n
Path=$APP_PATH
[Desktop Action OpenTorrentFile]
Name=Open Torrent File...
Exec=$EXEC_PATH -o
Path=$APP_PATH
[Desktop Action OpenTorrentAddress]
Name=Open Torrent Address...
Exec=$EXEC_PATH -u
Path=$APP_PATH