Compare commits

..

41 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
7d61968f64 0.1.0 2016-03-25 03:52:35 -07:00
Feross Aboukhadijeh
8637de27b9 Changelog: v0.1.0 2016-03-25 03:51:03 -07:00
Feross Aboukhadijeh
447413e4b9 delete commented out code 2016-03-25 03:40:18 -07:00
Feross Aboukhadijeh
82c8ad7562 windows build: don't use implicit package.json values 2016-03-25 03:37:18 -07:00
Feross Aboukhadijeh
f2bbd97eeb Merge pull request #218 from feross/windows-installer
Create Windows .exe installer
2016-03-25 03:35:43 -07:00
Feross Aboukhadijeh
c788b3358a Windows: fix magnet link handling 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
b0672cce9e npm install before packaging 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
0681169653 Windows: base Squirrel shortcut code on Nylas N1 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
ae6b86d233 Make install.gif not blink 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
8bba565609 Windows: create desktop/start menu shortcuts on install/update 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
78f08487c4 delay install splash screen so user sees it 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
bdf7110135 Move --squirrel-xxxx handling to new file 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
9d35ece954 Windows/linux: Don't autohide top menu bar (it's important) 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
00e4cc1864 Prevent --squirrel arguments from getting added as torrents 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
ad09012587 Windows installer: include icon url, setup icon, loading gif 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
8b5de572f1 package: conditionally require darwin packages 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
20c6b81047 simplify arguments to npm run package 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
aecead4a2d Windows: Create installer .exe file 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
7b02edca0f OS X packager: build to dist/ folder 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
109094d0e1 Merge pull request #217 from feross/greenkeeper-webtorrent-0.87.1
Update webtorrent to version 0.87.1 🚀
2016-03-24 20:09:02 -07:00
greenkeeperio-bot
e0856a5274 chore(package): update webtorrent to version 0.87.1
http://greenkeeper.io/
2016-03-24 19:58:13 -07:00
Feross Aboukhadijeh
c8886fb606 Merge pull request #213 from feross/magnet-focus
Show, unminimize, and focus window after opening magnet link (fix #210)
2016-03-24 02:59:39 -07:00
Feross Aboukhadijeh
fd5f4dd139 Show, unminimize, and focus window after opening magnet link (fix #210)
Requires a workaround for this Electron issue:
https://github.com/atom/electron/issues/4338
2016-03-24 02:56:34 -07:00
Feross Aboukhadijeh
0a51da13a4 docs: improve windows build notes 2016-03-24 02:54:49 -07:00
Feross Aboukhadijeh
5540ed9ce1 fix: exception when adding magnet links 2016-03-24 02:54:49 -07:00
Feross Aboukhadijeh
b6516dc40f Merge pull request #212 from feross/header-fix
fix invisible header bug
2016-03-23 23:20:49 -07:00
Nate Goldman
8b57e13735 fix #211 - invisible header bug 2016-03-23 23:16:33 -07:00
Feross Aboukhadijeh
cb3dd716dd Merge pull request #209 from feross/subtler-app-sounds
Sounds: subtler sounds
2016-03-23 21:18:23 -07:00
Feross Aboukhadijeh
4895fb930c Sounds: subtler sounds
This change sets different sounds to different volume levels, and
replaces the Play sound with one that sounds different than the Add
sound.
2016-03-23 20:44:40 -07:00
Feross Aboukhadijeh
1f2985bbc3 Merge pull request #207 from feross/ignore-appdmg
Package: remove optionalDependency "appdmg" from final bundle
2016-03-23 20:28:55 -07:00
Feross Aboukhadijeh
32ad0f0926 Package: remove optionalDependency "appdmg" from final bundle 2016-03-23 20:27:38 -07:00
Feross Aboukhadijeh
3e448da0ba Merge pull request #206 from feross/osx-bundle-id
OS X: pick a better bundle ID
2016-03-23 19:35:16 -07:00
Feross Aboukhadijeh
219e717021 OS X: pick a better bundle ID
The old bundle ID ended in .app, which OS X will interpret as an
executable app. This meant that our preferences folder was treated like
an app, lol.
2016-03-23 19:32:54 -07:00
Feross Aboukhadijeh
1885b6a89e Merge pull request #205 from feross/compress
losslessly compress images (w/ ImageOptim)
2016-03-23 19:28:29 -07:00
Feross Aboukhadijeh
d8a5b8a701 losslessly compress images (w/ ImageOptim) 2016-03-23 19:26:56 -07:00
DC
d41e08b209 Fix magnet link progress bug 2016-03-23 06:43:13 -07:00
Feross Aboukhadijeh
9518670c7b Merge pull request #201 from feross/readme
update readme with link to releases
2016-03-22 20:43:29 -07:00
Nate Goldman
eff0b6eb23 Update README.md 2016-03-22 15:30:56 -07:00
DC
f56af6402c Audio metadata 2016-03-22 03:52:27 -07:00
DC
ebcc814ca7 WebTorrent can now play audio 2016-03-22 02:26:28 -07:00
Feross Aboukhadijeh
f7029c811c fix zip bundle
Before this change, the zip command would include the full path on my
machine in the zip file, i.e. /Users/feross/…
2016-03-22 00:02:16 -07:00
22 changed files with 581 additions and 204 deletions

View File

@@ -1,5 +1,21 @@
# WebTorrent.app Version History
## 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.
- **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
Thanks to @dcposch, @ngoldman, and @feross for contributing to this release.
## v0.0.1
- Wait 10 seconds (instead of 60 seconds) after app launch before checking for updates.

View File

@@ -22,7 +22,7 @@
## Install
**WebTorrent.app** is still under very active development. Expect a release very soon!
**WebTorrent.app** is still under very active development. An [alpha release](https://github.com/feross/webtorrent-app/releases) is currently available for OS X.
## Screenshot
@@ -58,10 +58,20 @@ To build for one platform:
$ npm run package -- [platform]
```
Where `[platform]` is `--darwin`, `--linux`, or `--win32`.
Where `[platform]` is `darwin`, `linux`, or `win32`.
To package a Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/).
#### Windows build notes
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
to be installed.
On OS X, first install [XQuartz](http://www.xquartz.org/), then run:
```
brew install wine
```
(Requires the [Homebrew](http://brew.sh/) package manager.)
### Code Style

View File

@@ -15,11 +15,11 @@ var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
function build () {
var platform = process.argv[2]
if (platform === '--darwin') {
if (platform === 'darwin') {
buildDarwin(printDone)
} else if (platform === '--win32') {
} else if (platform === 'win32') {
buildWin32(printDone)
} else if (platform === '--linux') {
} else if (platform === 'linux') {
buildLinux(printDone)
} else {
buildDarwin(function (err, buildPath) {
@@ -59,7 +59,7 @@ var all = {
// Pattern which specifies which files to ignore when copying files to create the
// package(s).
ignore: /^\/dist|\/(appveyor.yml|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
ignore: /^\/dist|\/(appveyor.yml|.appveyor.yml|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
// The application name.
name: config.APP_NAME,
@@ -82,14 +82,14 @@ var darwin = {
platform: 'darwin',
// The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.app',
'app-bundle-id': 'io.webtorrent.webtorrent',
// The application category type, as shown in the Finder via "View" -> "Arrange by
// Application Category" when viewing the Applications directory (OS X only).
'app-category-type': 'public.app-category.utilities',
// The bundle identifier to use in the application helper's plist (OS X only).
'helper-bundle-id': 'io.webtorrent.app.helper',
'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
// Application icon.
icon: config.APP_ICON + '.icns'
@@ -137,9 +137,7 @@ var linux = {
build()
function buildDarwin (cb) {
var appDmg = require('appdmg')
var plist = require('plist')
var sign = require('electron-osx-sign')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err)
@@ -185,6 +183,9 @@ function buildDarwin (cb) {
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
if (process.platform === 'darwin') {
var appDmg = require('appdmg')
var sign = require('electron-osx-sign')
/*
* Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons:
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
@@ -208,13 +209,14 @@ function buildDarwin (cb) {
if (err) return cb(err)
// Create .zip file (used by the auto-updater)
var zipPath = path.join(buildPath[0], BUILD_NAME + '.zip')
cp.execSync(`zip -r -y ${zipPath} ${appPath}`)
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip')
cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`)
console.log('Created OS X .zip file.')
// Create a .dmg (OS X disk image) file, for easy user installation.
var dmgOpts = {
basepath: config.ROOT_PATH,
target: path.join(buildPath[0], BUILD_NAME + '.dmg'),
target: path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg'),
specification: {
title: config.APP_NAME,
icon: config.APP_ICON + '.icns',
@@ -239,6 +241,7 @@ function buildDarwin (cb) {
if (info.type === 'step-begin') console.log(info.title + '...')
})
dmg.on('finish', function (info) {
console.log('Created OS X disk image (.dmg) file.')
cb(null, buildPath)
})
})
@@ -247,7 +250,33 @@ function buildDarwin (cb) {
}
function buildWin32 (cb) {
electronPackager(Object.assign({}, all, win32), cb)
var installer = require('electron-winstaller')
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err)
console.log('Creating Windows installer...')
installer.createWindowsInstaller({
name: config.APP_NAME,
productName: config.APP_NAME,
title: config.APP_NAME,
exe: config.APP_NAME + '.exe',
appDirectory: buildPath[0],
outputDirectory: path.join(config.ROOT_PATH, 'dist'),
version: pkg.version,
description: config.APP_NAME,
authors: config.APP_TEAM,
iconUrl: config.APP_ICON + '.ico',
setupIcon: config.APP_ICON + '.ico',
// certificateFile: '', // TODO
usePackageJson: false,
loadingGif: path.join(config.STATIC_PATH, 'loading.gif')
}).then(function () {
console.log('Created Windows installer.')
cb(null, buildPath)
}).catch(cb)
})
}
function buildLinux (cb) {

View File

@@ -2,13 +2,15 @@ var applicationConfigPath = require('application-config-path')
var path = require('path')
var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project'
var APP_VERSION = require('./package.json').version
module.exports = {
APP_COPYRIGHT: 'Copyright © 2014-2016 The WebTorrent Project',
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION,
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
@@ -25,14 +27,38 @@ module.exports = {
ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'),
SOUND_ADD: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
SOUND_DELETE: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
SOUND_DISABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
SOUND_DONE: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
SOUND_ENABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
SOUND_ERROR: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
SOUND_PLAY: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
SOUND_STARTUP: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav')
SOUND_ADD: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
volume: 0.2
},
SOUND_DELETE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
volume: 0.1
},
SOUND_DISABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
volume: 0.2
},
SOUND_DONE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
volume: 0.2
},
SOUND_ENABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
volume: 0.2
},
SOUND_ERROR: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
volume: 0.2
},
SOUND_PLAY: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
volume: 0.2
},
SOUND_STARTUP: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
volume: 0.4
}
}
function isProduction () {

View File

@@ -1,6 +1,10 @@
module.exports = {
init
}
var log = require('./log')
module.exports = function () {
function init () {
if (process.platform === 'win32') {
var path = require('path')
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')

View File

@@ -4,94 +4,112 @@ var app = electron.app
var autoUpdater = require('./auto-updater')
var config = require('../config')
var handlers = require('./handlers')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
var registerProtocolHandler = require('./register-handlers')
var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32')
var windows = require('./windows')
// Prevent multiple instances of the app from running at the same time. New instances
// signal this instance and exit.
var shouldQuit = app.makeSingleInstance(function (newArgv) {
var shouldQuit = false
var argv = sliceArgv(process.argv)
if (process.platform === 'win32') {
shouldQuit = squirrelWin32.handleEvent(argv[0])
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
}
if (!shouldQuit) {
// Prevent multiple instances of app from running at same time. New instances signal
// this instance and quit.
shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) {
app.quit()
}
}
if (!shouldQuit) {
init()
}
function init () {
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
// Open handlers must be added as early as possible
app.on('open-file', onOpen)
app.on('open-url', onOpen)
ipc.init()
app.on('will-finish-launching', function () {
autoUpdater.init()
setupCrashReporter()
})
app.on('ready', function () {
menu.init()
windows.createMainWindow()
shortcuts.init()
if (process.platform !== 'win32') handlers.init()
})
app.on('ipcReady', function () {
log('Command line args:', argv)
argv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
})
app.on('before-quit', function () {
app.isQuitting = true
})
app.on('activate', function () {
if (windows.main) {
windows.main.show()
} else {
windows.createMainWindow()
}
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
}
function onOpen (e, torrentId) {
e.preventDefault()
if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId)
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusMainWindow()
}, 100)
} else {
argv.push(torrentId)
}
}
function onAppOpen (newArgv) {
newArgv = sliceArgv(newArgv)
if (app.ipcReady) {
log('Second app instance attempted to open but was prevented')
log('Second app instance opened, but was prevented:', newArgv)
windows.focusMainWindow()
newArgv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
if (windows.main.isMinimized()) {
windows.main.restore()
}
windows.main.focus()
} else {
argv.push(...newArgv)
}
})
if (shouldQuit) {
app.quit()
}
var argv = sliceArgv(process.argv)
app.on('open-file', onOpen)
app.on('open-url', onOpen)
app.on('will-finish-launching', function () {
autoUpdater.init()
setupCrashReporter()
})
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
app.on('ready', function () {
menu.init()
windows.createMainWindow()
shortcuts.init()
registerProtocolHandler()
})
app.on('ipcReady', function () {
log('IS_PRODUCTION:', config.IS_PRODUCTION)
if (argv.length) {
log('command line args:', process.argv)
}
argv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
})
app.on('before-quit', function () {
app.isQuitting = true
})
app.on('activate', function () {
if (windows.main) {
windows.main.show()
} else {
windows.createMainWindow(menu)
}
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
ipc.init()
function onOpen (e, torrentId) {
e.preventDefault()
if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId)
} else {
argv.push(torrentId)
}
}
function sliceArgv (argv) {

134
main/squirrel-win32.js Normal file
View File

@@ -0,0 +1,134 @@
module.exports = {
handleEvent
}
var cp = require('child_process')
var electron = require('electron')
var fs = require('fs')
var os = require('os')
var path = require('path')
var pathExists = require('path-exists')
var app = electron.app
var handlers = require('./handlers')
var exeName = path.basename(process.execPath)
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
// App was installed.
// Install protocol/file handlers, desktop/start menu shortcuts.
handlers.init()
createShortcuts(function () {
// Ensure user sees install splash screen so they realize that Setup.exe actually
// installed an application and isn't the application itself.
setTimeout(function () {
app.quit()
}, 5000)
})
return true
}
if (cmd === '--squirrel-updated') {
// App was updated. (Called on new version of app)
updateShortcuts(function () {
app.quit()
})
return true
}
if (cmd === '--squirrel-uninstall') {
// App was just uninstalled. Undo anything we did in the --squirrel-install and
// --squirrel-updated handlers
removeShortcuts(function () {
app.quit()
})
return true
}
if (cmd === '--squirrel-obsolete') {
// App will be updated. (Called on outgoing version of app)
app.quit()
return true
}
if (cmd === '--squirrel-firstrun') {
// This is called on the app's first run. Do not quit, allow startup to continue.
return false
}
return false
}
// Spawn a command and invoke the callback when it completes with an error and the output
// from standard out.
function spawn (command, args, cb) {
var stdout = ''
var child
try {
child = cp.spawn(command, args)
} catch (err) {
// Spawn can throw an error
process.nextTick(function () {
cb(error, stdout)
})
return
}
child.stdout.on('data', function (data) {
stdout += data
})
var error = null
child.on('error', function (processError) {
error = processError
})
child.on('close', function (code, signal) {
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
if (error) error.stdout = stdout
cb(error, stdout)
})
}
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
// command completes.
function spawnUpdate (args, cb) {
spawn(updateDotExe, args, cb)
}
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
function createShortcuts (cb) {
spawnUpdate(['--createShortcut', exeName], cb)
}
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
function updateShortcuts (cb) {
var homeDir = os.homedir()
if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted
// if it was
pathExists(desktopShortcutPath).then(function (desktopShortcutExists) {
createShortcuts(function () {
if (desktopShortcutExists) {
cb()
} else {
// Remove the unwanted desktop shortcut that was recreated
fs.unlink(desktopShortcutPath, cb)
}
})
})
} else {
createShortcuts(cb)
}
}
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', exeName], cb)
}

View File

@@ -1,6 +1,7 @@
var windows = module.exports = {
main: null,
createMainWindow: createMainWindow
createMainWindow: createMainWindow,
focusMainWindow: focusMainWindow
}
var electron = require('electron')
@@ -12,7 +13,6 @@ var menu = require('./menu')
function createMainWindow () {
var win = windows.main = new electron.BrowserWindow({
autoHideMenuBar: true, // Hide top menu bar unless Alt key is pressed (Windows, Linux)
backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3)
icon: config.APP_ICON + '.png',
@@ -52,3 +52,10 @@ function createMainWindow () {
windows.main = null
})
}
function focusMainWindow () {
if (windows.main.isMinimized()) {
windows.main.restore()
}
windows.main.show() // shows and gives focus
}

View File

@@ -1,7 +1,7 @@
{
"name": "webtorrent-app",
"name": "WebTorrent",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.0.1",
"version": "0.1.0",
"author": {
"name": "Feross Aboukhadijeh",
"email": "feross@feross.org",
@@ -22,24 +22,29 @@
"hyperx": "^2.0.2",
"main-loop": "^3.2.0",
"mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"path-exists": "^2.1.0",
"prettier-bytes": "^1.0.1",
"upload-element": "^1.0.1",
"virtual-dom": "^2.1.1",
"webtorrent": "^0.86.0",
"webtorrent": "^0.87.1",
"winreg": "^1.0.1"
},
"devDependencies": {
"appdmg": "^0.3.6",
"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",
"plist": "^1.2.0",
"rimraf": "^2.5.2",
"standard": "^6.0.5"
},
"optionalDependencies": {
"appdmg": "^0.3.6"
},
"homepage": "https://webtorrent.io",
"keywords": [
"electron",
@@ -55,7 +60,7 @@
"scripts": {
"clean": "node ./bin/clean.js",
"debug": "DEBUG=* electron .",
"package": "npm prune && npm dedupe && node ./bin/package.js",
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
"size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"start": "electron .",
"test": "standard",

View File

@@ -181,10 +181,10 @@ i:not(.disabled):hover {
left: 0;
top: 0;
right: 0;
z-index: 1000;
transition: opacity 0.15s ease-out;
font-size: 14px;
line-height: 1.5em;
z-index: 1;
}
.app:not(.is-focused) .header {
@@ -456,8 +456,7 @@ input {
background-color: #F44336;
}
.torrent.timeout .play,
.torrent.unplayable .play {
.torrent.timeout .play {
padding-top: 8px;
}
@@ -732,13 +731,40 @@ body.drag .torrent-placeholder span {
font-weight: bold;
}
/*
* AUDIO DETAILS
*/
.audio-metadata {
width: 500px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
align-self: center;
margin: 0 auto;
font-weight: bold;
font-size: 24px;
line-height: 2;
}
.audio-metadata .audio-title {
font-size: 32px;
}
.audio-metadata label {
display:inline-block;
width: 100px;
text-align: right;
font-weight: normal;
margin-right: 25px;
}
/*
* ERRORS
*/
.error-popover {
position: fixed;
z-index: 1001;
top: 36px;
margin: 0;
width: 100%;

View File

@@ -8,6 +8,7 @@ var EventEmitter = require('events')
var fs = require('fs')
var mainLoop = require('main-loop')
var mkdirp = require('mkdirp')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var remote = require('remote')
@@ -130,7 +131,7 @@ function init () {
// 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(config.SOUND_STARTUP)
playInterfaceSound('STARTUP')
console.timeEnd('init')
}
@@ -163,7 +164,7 @@ function updateElectron () {
// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
if (['videoMouseMoved', 'playbackJump'].indexOf(action) === -1) {
if (['mediaMouseMoved', 'playbackJump'].indexOf(action) === -1) {
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
}
if (action === 'onOpen') {
@@ -235,20 +236,20 @@ function dispatch (action, ...args) {
if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */)
}
if (action === 'videoPlaying') {
state.video.isPaused = false
if (action === 'mediaPlaying') {
state.playing.isPaused = false
ipcRenderer.send('blockPowerSave')
}
if (action === 'videoPaused') {
state.video.isPaused = true
if (action === 'mediaPaused') {
state.playing.isPaused = true
ipcRenderer.send('unblockPowerSave')
}
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0])
update()
}
if (action === 'videoMouseMoved') {
state.video.mouseStationarySince = new Date().getTime()
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
update()
}
if (action === 'exitModal') {
@@ -259,14 +260,14 @@ function dispatch (action, ...args) {
// Plays or pauses the video. If isPaused is undefined, acts as a toggle
function playPause (isPaused) {
if (isPaused === state.video.isPaused) {
if (isPaused === state.playing.isPaused) {
return // Nothing to do
}
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
if (Cast.isCasting()) {
Cast.playPause()
}
state.video.isPaused = !state.video.isPaused
state.playing.isPaused = !state.playing.isPaused
update()
}
@@ -274,7 +275,7 @@ function jumpToTime (time) {
if (Cast.isCasting()) {
Cast.seek(time)
} else {
state.video.jumpToTime = time
state.playing.jumpToTime = time
update()
}
}
@@ -422,7 +423,7 @@ function addTorrentToList (torrent) {
infoHash: torrent.infoHash
})
saveState()
playInterfaceSound(config.SOUND_ADD)
playInterfaceSound('ADD')
}
}
@@ -511,7 +512,7 @@ function updateTorrentProgress () {
var changed = false
state.client.torrents.forEach(function (torrent) {
var torrentSummary = getTorrentSummary(torrent.infoHash)
if (!torrentSummary) return
if (!torrentSummary || !torrent.ready) return
torrent.files.forEach(function (file, index) {
var numPieces = file._endPiece - file._startPiece + 1
var numPiecesPresent = 0
@@ -601,21 +602,25 @@ function startServer (torrentSummary, index, cb) {
function startServerFromReadyTorrent (torrent, index, cb) {
// automatically choose which file in the torrent to play, if necessary
if (!index) {
// filter out file formats that the <video> tag definitely can't play
var files = torrent.files.filter(TorrentPlayer.isPlayable)
if (files.length === 0) return cb(new errors.UnplayableError())
// use largest file
var largestFile = files.reduce(function (a, b) {
return a.length > b.length ? a : b
})
index = torrent.files.indexOf(largestFile)
}
if (index === undefined) index = pickFileToPlay(torrent.files)
if (index === undefined) return cb(new errors.UnplayableError())
var file = torrent.files[index]
// update state
state.playing.infoHash = torrent.infoHash
state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(file) ? 'video' : 'audio'
state.playing.audioInfo = null
// 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)
state.playing.audioInfo = info
update()
})
// either way, start a streaming torrent-to-http server
var server = torrent.createServer()
server.listen(0, function () {
var port = server.address().port
@@ -629,6 +634,28 @@ function startServerFromReadyTorrent (torrent, index, cb) {
})
}
// Picks the default file to play from a list of torrent or torrentSummary files
// Returns an index or undefined, if no files are playable
function pickFileToPlay (files) {
// first, try to find the biggest video file
var videoFiles = files.filter(TorrentPlayer.isVideo)
if (videoFiles.length > 0) {
var largestVideoFile = videoFiles.reduce(function (a, b) {
return a.length > b.length ? a : b
})
return files.indexOf(largestVideoFile)
}
// if there are no videos, play the first audio file
var audioFiles = files.filter(TorrentPlayer.isAudio)
if (audioFiles.length > 0) {
return files.indexOf(audioFiles[0])
}
// no video or audio means nothing is playable
return undefined
}
function stopServer () {
if (!state.server) return
state.server.server.destroy()
@@ -640,13 +667,13 @@ function stopServer () {
// Opens the video player
function openPlayer (torrentSummary, index, cb) {
var torrent = state.client.get(torrentSummary.infoHash)
if (!torrent || !torrent.done) playInterfaceSound(config.SOUND_PLAY)
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
torrentSummary.playStatus = 'requested'
update()
var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */
playInterfaceSound(config.SOUND_ERROR)
playInterfaceSound('ERROR')
update()
}, 10000) /* give it a few seconds */
@@ -654,7 +681,7 @@ function openPlayer (torrentSummary, index, cb) {
clearTimeout(timeout)
if (err) {
torrentSummary.playStatus = 'unplayable'
playInterfaceSound(config.SOUND_ERROR)
playInterfaceSound('ERROR')
update()
return onError(err)
}
@@ -714,11 +741,11 @@ function toggleTorrent (torrentSummary) {
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
startTorrentingSummary(torrentSummary)
playInterfaceSound(config.SOUND_ENABLE)
playInterfaceSound('ENABLE')
} else {
torrentSummary.status = 'paused'
stopTorrenting(torrentSummary.infoHash)
playInterfaceSound(config.SOUND_DISABLE)
playInterfaceSound('DISABLE')
}
}
@@ -731,7 +758,7 @@ function deleteTorrent (torrentSummary) {
if (index > -1) state.saved.torrents.splice(index, 1)
saveState()
state.location.clearForward() // prevent user from going forward to a deleted torrent
playInterfaceSound(config.SOUND_DELETE)
playInterfaceSound('DELETE')
}
function toggleSelectTorrent (infoHash) {
@@ -785,7 +812,7 @@ function restoreBounds () {
function onError (err) {
console.error(err.stack || err)
playInterfaceSound(config.SOUND_ERROR)
playInterfaceSound('ERROR')
state.errors.push({
time: new Date().getTime(),
message: err.message || err
@@ -809,12 +836,15 @@ function showDoneNotification (torrent) {
window.focus()
}
playInterfaceSound(config.SOUND_DONE)
playInterfaceSound('DONE')
}
function playInterfaceSound (url) {
function playInterfaceSound (name) {
var sound = config[`SOUND_${name}`]
if (!sound) throw new Error('Invalid sound name')
var audio = new window.Audio()
audio.volume = 0.3
audio.src = url
audio.volume = sound.volume
audio.src = sound.url
audio.play()
}

View File

@@ -57,14 +57,14 @@ 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)
state.video.isPaused = status.playerState === 'PAUSED'
state.video.currentTime = status.currentTime
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
update()
})
} else if (state.playing.location === 'airplay') {
state.devices.airplay.status(function (status) {
state.video.isPaused = status.rate === 0
state.video.currentTime = status.position
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
update()
})
}
@@ -122,7 +122,7 @@ function stopCasting () {
function stoppedCasting () {
state.playing.location = 'local'
state.video.jumpToTime = state.video.currentTime
state.playing.jumpToTime = state.playing.currentTime
update()
}
@@ -137,11 +137,11 @@ function playPause () {
var device
if (state.playing.location === 'chromecast') {
device = state.devices.chromecast
if (!state.video.isPaused) device.pause(castCallback)
if (!state.playing.isPaused) device.pause(castCallback)
else device.play(null, null, castCallback)
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay
if (!state.video.isPaused) device.rate(0, castCallback)
if (!state.playing.isPaused) device.rate(0, castCallback)
else device.rate(1, castCallback)
}
}

View File

@@ -1,5 +1,7 @@
module.exports = {
isPlayable: isPlayable
isPlayable,
isVideo,
isAudio
}
var path = require('path')
@@ -8,6 +10,15 @@ var path = require('path')
* Determines whether a file in a torrent is audio/video we can play
*/
function isPlayable (file) {
var extname = path.extname(file.name)
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(extname) !== -1
return isVideo(file) || isAudio(file)
}
function isVideo (file) {
var ext = path.extname(file.name)
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
}
function isAudio (file) {
var ext = path.extname(file.name)
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
}

View File

@@ -20,21 +20,20 @@ module.exports = {
title: config.APP_NAME /* current window title */
},
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: { /* the torrent and file we're currently streaming */
playing: { /* the media (audio or video) that we're currently playing */
infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */
location: 'local' /* 'local', 'chromecast', 'airplay' */
},
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
},
video: { /* state of the video player screen */
location: 'local', /* 'local', 'chromecast', 'airplay' */
type: null, /* 'audio' or 'video' */
currentTime: 0, /* seconds */
duration: 1, /* seconds */
isPaused: true,
mouseStationarySince: 0 /* Unix time in ms */
},
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
},
dock: {
badge: 0,
progress: 0

View File

@@ -18,9 +18,9 @@ function App (state, dispatch) {
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.current().url === 'player' &&
state.video.mouseStationarySince !== 0 &&
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
!state.video.isPaused &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
// Hide the header on Windows/Linux when in the player

View File

@@ -4,6 +4,7 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
// Shows a streaming video player. Standard features + Chromecast + Airplay
function Player (state, dispatch) {
// Show the video as large as will fit in the window, play immediately
@@ -12,50 +13,69 @@ function Player (state, dispatch) {
return hx`
<div
class='player'
onmousemove=${() => dispatch('videoMouseMoved')}>
${showVideo ? renderVideo(state, dispatch) : renderCastScreen(state, dispatch)}
onmousemove=${() => dispatch('mediaMouseMoved')}>
${showVideo ? renderMedia(state, dispatch) : renderCastScreen(state, dispatch)}
${renderPlayerControls(state, dispatch)}
</div>
`
}
function renderVideo (state, dispatch) {
function renderMedia (state, dispatch) {
if (!state.server) return
// Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary
var videoElement = document.querySelector('video')
if (videoElement !== null) {
if (state.video.isPaused && !videoElement.paused) {
videoElement.pause()
} else if (!state.video.isPaused && videoElement.paused) {
videoElement.play()
var mediaType = state.playing.type /* 'audio' or 'video' */
var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause()
} else if (!state.playing.isPaused && mediaElement.paused) {
mediaElement.play()
}
// When the user clicks or drags on the progress bar, jump to that position
if (state.video.jumpToTime) {
videoElement.currentTime = state.video.jumpToTime
state.video.jumpToTime = null
if (state.playing.jumpToTime) {
mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null
}
state.video.currentTime = videoElement.currentTime
state.video.duration = videoElement.duration
state.playing.currentTime = mediaElement.currentTime
state.playing.duration = mediaElement.duration
}
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
src='${state.server.localURL}'
ondblclick=${() => dispatch('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${() => dispatch('mediaPlaying')}
onpause=${() => dispatch('mediaPaused')}
autoplay>
</div>
`
mediaTag.tagName = mediaType
// Show the media.
// Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata.
var isAudio = mediaType === 'audio'
var style = {
backgroundImage: isAudio ? cssBackgroundImagePoster(state) : ''
}
return hx`
<div
class='letterbox'
onmousemove=${() => dispatch('videoMouseMoved')}>
<video
src='${state.server.localURL}'
ondblclick=${() => dispatch('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${() => dispatch('videoPlaying')}
onpause=${() => dispatch('videoPaused')}
autoplay>
</video>
style=${style}
onmousemove=${() => dispatch('mediaMouseMoved')}>
${mediaTag}
${renderAudioMetadata(state)}
</div>
`
// As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (mediaType !== 'video') return
var video = e.target
var dimensions = {
width: video.videoWidth,
@@ -66,10 +86,38 @@ function renderVideo (state, dispatch) {
// When the video completes, pause the video instead of looping
function onEnded (e) {
state.video.isPaused = true
state.playing.isPaused = true
}
}
function renderAudioMetadata (state) {
if (!state.playing.audioInfo) return
var info = state.playing.audioInfo
// Get audio track info
var title = info.title
if (!title) {
var torrentSummary = getPlayingTorrentSummary(state)
title = torrentSummary.files[state.playing.fileIndex].name
}
var artist = info.artist && info.artist[0]
var album = info.album
if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')'
}
var track
if (info.track && info.track.no && info.track.of) {
track = info.track.no + ' of ' + info.track.of
}
// Show a small info box in the middle of the screen
var elems = [hx`<div class='audio-title'><label></label>${title}</div>`]
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
return hx`<div class='audio-metadata'>${elems}</div>`
}
function renderCastScreen (state, dispatch) {
var isChromecast = state.playing.location.startsWith('chromecast')
var isAirplay = state.playing.location.startsWith('airplay')
@@ -77,12 +125,8 @@ function renderCastScreen (state, dispatch) {
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
// Show a nice title image, if possible
var style = {}
var infoHash = state.playing.infoHash
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === infoHash)
if (torrentSummary && torrentSummary.posterURL) {
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
style.backgroundImage = `radial-gradient(circle at center, rgba(0,0,0,0.4) 0%,rgba(0,0,0,1) 100%), url(${cleanURL})`
var style = {
backgroundImage: cssBackgroundImagePoster(state)
}
// Show whether we're connected to Chromecast / Airplay
@@ -98,8 +142,23 @@ function renderCastScreen (state, dispatch) {
`
}
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary || !torrentSummary.posterURL) return ''
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' +
`, url(${cleanURL})`
}
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
function renderPlayerControls (state, dispatch) {
var positionPercent = 100 * state.video.currentTime / state.video.duration
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var elements = [
@@ -174,7 +233,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')}>
${state.video.isPaused ? 'play_arrow' : 'pause'}
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`)
@@ -182,10 +241,10 @@ function renderPlayerControls (state, dispatch) {
// Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) {
dispatch('videoMouseMoved')
dispatch('mediaMouseMoved')
var windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth
var position = fraction * state.video.duration /* seconds */
var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position)
}
}

View File

@@ -103,9 +103,10 @@ 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 playIcon, playTooltip
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'warning'
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
@@ -131,13 +132,14 @@ function TorrentList (state, dispatch) {
return hx`
<div class='buttons'>
<i.btn.icon.play
title='${playTooltip}'
title=${playTooltip}
class=${playClass}
onclick=${(e) => handleButton('play', e)}>
${playIcon}
</i>
<i.btn.icon.download
class='${torrentSummary.status}'
title='${downloadTooltip}'
class=${torrentSummary.status}
title=${downloadTooltip}
onclick=${(e) => handleButton('toggleTorrent', e)}>
${downloadIcon}
</i>
@@ -153,6 +155,7 @@ function TorrentList (state, dispatch) {
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)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 303 KiB

BIN
static/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.