merged
This commit is contained in:
49
AUTHORS.md
49
AUTHORS.md
@@ -2,29 +2,30 @@
|
|||||||
|
|
||||||
#### Ordered by first contribution.
|
#### Ordered by first contribution.
|
||||||
|
|
||||||
- Feross Aboukhadijeh <feross@feross.org>
|
- Feross Aboukhadijeh (feross@feross.org)
|
||||||
- DC <dcposch@dcpos.ch>
|
- DC (dcposch@dcpos.ch)
|
||||||
- Nate Goldman <nate@ngoldman.me>
|
- Nate Goldman (nate@ngoldman.me)
|
||||||
- Chris Morris <chris@chrismorris.org>
|
- Chris Morris (chris@chrismorris.org)
|
||||||
- Giuseppe Crinò <giuscri@gmail.com>
|
- Giuseppe Crinò (giuscri@gmail.com)
|
||||||
- Romain Beaumont <romain.rom1@gmail.com>
|
- Romain Beaumont (romain.rom1@gmail.com)
|
||||||
- Dan Flettre <fletd01@yahoo.com>
|
- Dan Flettre (fletd01@yahoo.com)
|
||||||
- Liam Gray <liam.r.gray@gmail.com>
|
- Liam Gray (liam.r.gray@gmail.com)
|
||||||
- grunjol <grunjol@users.noreply.github.com>
|
- grunjol (grunjol@users.noreply.github.com)
|
||||||
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
||||||
- Evan Miller <miller.evan815@gmail.com>
|
- Evan Miller (miller.evan815@gmail.com)
|
||||||
- Alex <alxmorais8@msn.com>
|
- Alex (alxmorais8@msn.com)
|
||||||
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
|
- Diego Rodríguez Baquero (diegorbaquero@gmail.com)
|
||||||
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
- Karlo Luis Martinez Martos (karlo.luis.m@gmail.com)
|
||||||
- gabriel <furstenheim@gmail.com>
|
- gabriel (furstenheim@gmail.com)
|
||||||
- Rolando Guedes <rolando.guedes@3gnt.net>
|
- Rolando Guedes (rolando.guedes@3gnt.net)
|
||||||
- Benjamin Tan <demoneaux@gmail.com>
|
- Benjamin Tan (demoneaux@gmail.com)
|
||||||
- Mathias Rasmussen <mathiasvr@gmail.com>
|
- Mathias Rasmussen (mathiasvr@gmail.com)
|
||||||
- Sergey Bargamon <sergey@bargamon.ru>
|
- Sergey Bargamon (sergey@bargamon.ru)
|
||||||
- Thomas Watson Steen <w@tson.dk>
|
- Thomas Watson Steen (w@tson.dk)
|
||||||
- anonymlol <anonymlol7@gmail.com>
|
- anonymlol (anonymlol7@gmail.com)
|
||||||
- Gediminas Petrikas <gedas18@gmail.com>
|
- Gediminas Petrikas (gedas18@gmail.com)
|
||||||
- Adam Gotlib <gotlib.adam+dev@gmail.com>
|
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||||
- Rémi Jouannet <remijouannet@gmail.com>
|
- Rémi Jouannet (remijouannet@gmail.com)
|
||||||
|
- Andrea Tupini (tupini07@gmail.com)
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,17 +1,51 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.10.0 - 2016-08-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Drag-and-drop magnet links (selected text) is now supported (#284)
|
||||||
|
- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
|
||||||
|
- Linux: Show badge count for completed torrent downloads
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
|
||||||
|
- Switch UI to React to improve UI rendering speed (#729)
|
||||||
|
- The primary bottleneck was actually `hyperx`, not `virtual-dom`.
|
||||||
|
- Update Electron to 1.3.2 (#738) (#739) (#740) (#747) (#756)
|
||||||
|
- Mac 10.9: Fix the fullscreen button showing
|
||||||
|
- Mac 10.9: Fix window having border
|
||||||
|
- Mac 10.9: Fix occasional crash
|
||||||
|
- Mac: Update Squirrel.Mac to 0.2.1 (fixes situations in which updates would not get applied)
|
||||||
|
- Mac: Fix window not showing in Window menu
|
||||||
|
- Mac: Fix context menu always choosing first item by default
|
||||||
|
- Linux: Fix startup crashes (some Linux distros)
|
||||||
|
- Linux: Fix menubar not hiding after entering fullscreen (some Linux distros)
|
||||||
|
- Improved location history (back/forward buttons) to fix rare exceptions (#687) (#748)
|
||||||
|
- Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- When streaming to VLC, set VLC window title to torrent file name (#746)
|
||||||
|
- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
|
||||||
|
- Fix rare case where config file could not be completely written (#733)
|
||||||
|
|
||||||
## v0.9.0 - 2016-07-20
|
## v0.9.0 - 2016-07-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Save selected subtitles
|
- Save selected subtitles
|
||||||
- Ask for confirmation before deleting torrents
|
- Ask for confirmation before deleting torrents
|
||||||
- Support Debian Jessie
|
- Support Debian Jessie
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Only send telemetry in production
|
- Only send telemetry in production
|
||||||
- Clean up the code. Split main.js, refactor lots of things
|
- Clean up the code. Split main.js, refactor lots of things
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix state.playing.jumpToTime behavior
|
- Fix state.playing.jumpToTime behavior
|
||||||
- Remove torrent file and poster image when deleting a torrent
|
- Remove torrent file and poster image when deleting a torrent
|
||||||
|
|
||||||
|
|||||||
@@ -73,3 +73,23 @@ By making a contribution to this project, I certify that:
|
|||||||
record of the contribution (including all personal information I submit with it,
|
record of the contribution (including all personal information I submit with it,
|
||||||
including my sign-off) is maintained indefinitely and may be redistributed consistent
|
including my sign-off) is maintained indefinitely and may be redistributed consistent
|
||||||
with this project or the open source license(s) involved.
|
with this project or the open source license(s) involved.
|
||||||
|
|
||||||
|
## Smoke Tests
|
||||||
|
|
||||||
|
Before a release, check that the following basic use cases work correctly:
|
||||||
|
|
||||||
|
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
|
||||||
|
- Ensure that seeking to undownloaded region works and plays immediately.
|
||||||
|
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
|
||||||
|
|
||||||
|
2. Check that the auto-updater works
|
||||||
|
- Open the console and check for the line "No update available" to indicate
|
||||||
|
|
||||||
|
3. Add a new .torrent file via drag-and-drop.
|
||||||
|
- Ensure that it gets added to the list and starts downloading
|
||||||
|
|
||||||
|
4. Remove a torrent from the client
|
||||||
|
- Ensure that the file is removed from `~/Downloads`
|
||||||
|
|
||||||
|
5. Create and seed a new a torrent via drag-and-drop.
|
||||||
|
- Ensure that the torrent gets created and seeding begins.
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
|||||||
|
|
||||||
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
||||||
|
|
||||||
var EXECUTABLE_DEPS = ['gh-release', 'standard', 'react-tools']
|
var EXECUTABLE_DEPS = [
|
||||||
|
'gh-release',
|
||||||
|
'standard',
|
||||||
|
'babel-cli',
|
||||||
|
'babel-plugin-syntax-jsx',
|
||||||
|
'babel-plugin-transform-react-jsx'
|
||||||
|
]
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var config = require('../src/config')
|
|||||||
var pkg = require('../package.json')
|
var pkg = require('../package.json')
|
||||||
|
|
||||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||||
|
var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
|
||||||
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||||
|
|
||||||
var argv = minimist(process.argv.slice(2), {
|
var argv = minimist(process.argv.slice(2), {
|
||||||
@@ -36,6 +37,12 @@ var argv = minimist(process.argv.slice(2), {
|
|||||||
|
|
||||||
function build () {
|
function build () {
|
||||||
rimraf.sync(DIST_PATH)
|
rimraf.sync(DIST_PATH)
|
||||||
|
rimraf.sync(BUILD_PATH)
|
||||||
|
|
||||||
|
console.log('Babel: Building JSX...')
|
||||||
|
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
||||||
|
console.log('Babel: Built JSX.')
|
||||||
|
|
||||||
var platform = argv._[0]
|
var platform = argv._[0]
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
buildDarwin(printDone)
|
buildDarwin(printDone)
|
||||||
@@ -82,7 +89,7 @@ var all = {
|
|||||||
|
|
||||||
// Pattern which specifies which files to ignore when copying files to create the
|
// Pattern which specifies which files to ignore when copying files to create the
|
||||||
// package(s).
|
// package(s).
|
||||||
ignore: /^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|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|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
|
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|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.
|
// The application name.
|
||||||
name: config.APP_NAME,
|
name: config.APP_NAME,
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
# Update AUTHORS.md based on git history.
|
# Update AUTHORS.md based on git history.
|
||||||
|
|
||||||
git log --reverse --format='%aN <%aE>' | perl -we '
|
git log --reverse --format='%aN (%aE)' | perl -we '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
%seen = (), @authors = ();
|
%seen = (), @authors = ();
|
||||||
}
|
}
|
||||||
while (<>) {
|
while (<>) {
|
||||||
next if $seen{$_};
|
next if $seen{$_};
|
||||||
next if /<support\@greenkeeper.io>/;
|
next if /(support\@greenkeeper.io)/;
|
||||||
next if /<ungoldman\@gmail.com>/;
|
next if /(ungoldman\@gmail.com)/;
|
||||||
next if /<dc\@DCs-MacBook.local>/;
|
next if /(dc\@DCs-MacBook.local)/;
|
||||||
next if /<rolandoguedes\@gmail.com>/;
|
next if /(rolandoguedes\@gmail.com)/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.12.1",
|
||||||
"electron-prebuilt": "1.3.2",
|
"electron-prebuilt": "1.3.3",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
"zero-fill": "^2.2.3"
|
"zero-fill": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.11.4",
|
||||||
|
"babel-plugin-syntax-jsx": "^6.13.0",
|
||||||
|
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||||
"cross-zip": "^2.0.1",
|
"cross-zip": "^2.0.1",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^7.0.0",
|
"electron-packager": "^7.0.0",
|
||||||
@@ -55,7 +58,6 @@
|
|||||||
"nobin-debian-installer": "^0.0.10",
|
"nobin-debian-installer": "^0.0.10",
|
||||||
"open": "0.0.5",
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
"react-tools": "^0.13.3",
|
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
"standard": "^7.0.0"
|
"standard": "^7.0.0"
|
||||||
@@ -85,10 +87,12 @@
|
|||||||
"url": "git://github.com/feross/webtorrent-desktop.git"
|
"url": "git://github.com/feross/webtorrent-desktop.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "babel --quiet src --out-dir build",
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
"open-config": "node ./bin/open-config.js",
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "rimraf build/ && jsx --es6module src/ build/ && node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"start": "jsx --es6module src/ build/ && electron .",
|
"prepublish": "npm run build",
|
||||||
|
"start": "npm run build && electron .",
|
||||||
"test": "standard && node ./bin/check-deps.js",
|
"test": "standard && node ./bin/check-deps.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"update-authors": "./bin/update-authors.sh"
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/.babelrc
Normal file
6
src/.babelrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"syntax-jsx",
|
||||||
|
"transform-react-jsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ function init () {
|
|||||||
|
|
||||||
function onResponse (err, res, data) {
|
function onResponse (err, res, data) {
|
||||||
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
|
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
|
||||||
if (res.statusCode !== 200) return log('No announcement exists')
|
if (res.statusCode !== 200) return log('No announcement available')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(data.toString())
|
data = JSON.parse(data.toString())
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ var config = require('../config')
|
|||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
var dialog = require('./dialog')
|
var dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
var dock = require('./dock')
|
||||||
var handlers = require('./handlers')
|
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
@@ -23,7 +22,11 @@ var windows = require('./windows')
|
|||||||
var shouldQuit = false
|
var shouldQuit = false
|
||||||
var argv = sliceArgv(process.argv)
|
var argv = sliceArgv(process.argv)
|
||||||
|
|
||||||
if (!argv.includes('--dev')) process.env.NODE_ENV = 'production'
|
if (config.IS_PRODUCTION) {
|
||||||
|
// When Electron is running in production mode (packaged app), then run React
|
||||||
|
// in production mode too.
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
||||||
@@ -107,7 +110,6 @@ function init () {
|
|||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
announcement.init()
|
announcement.init()
|
||||||
dock.init()
|
dock.init()
|
||||||
handlers.install()
|
|
||||||
tray.init()
|
tray.init()
|
||||||
updater.init()
|
updater.init()
|
||||||
userTasks.init()
|
userTasks.init()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ var app = electron.app
|
|||||||
|
|
||||||
var dialog = require('./dialog')
|
var dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
var dock = require('./dock')
|
||||||
|
var handlers = require('./handlers')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var powerSaveBlocker = require('./power-save-blocker')
|
var powerSaveBlocker = require('./power-save-blocker')
|
||||||
@@ -60,14 +61,14 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.setPlayerOpen(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.setPlayerOpen(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
@@ -91,6 +92,14 @@ function init () {
|
|||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File handlers
|
||||||
|
*/
|
||||||
|
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||||
|
if (flag) handlers.install()
|
||||||
|
else handlers.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +112,7 @@ function init () {
|
|||||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||||
ipc.on('show', () => main.show())
|
ipc.on('show', () => main.show())
|
||||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||||
|
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VLC
|
* VLC
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
onPlayerClose,
|
setPlayerOpen,
|
||||||
onPlayerOpen,
|
setWindowFocus,
|
||||||
|
setAllowNav,
|
||||||
onToggleAlwaysOnTop,
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen
|
||||||
onWindowBlur,
|
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -24,26 +23,28 @@ function init () {
|
|||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function setPlayerOpen (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = false
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = flag
|
||||||
getMenuItem('Step Forward').enabled = false
|
getMenuItem('Step Forward').enabled = flag
|
||||||
getMenuItem('Step Backward').enabled = false
|
getMenuItem('Step Backward').enabled = flag
|
||||||
getMenuItem('Increase Speed').enabled = false
|
getMenuItem('Increase Speed').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = false
|
getMenuItem('Decrease Speed').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
getMenuItem('Add Subtitles File...').enabled = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function setWindowFocus (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = true
|
getMenuItem('Full Screen').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Float on Top').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
}
|
||||||
getMenuItem('Step Forward').enabled = true
|
|
||||||
getMenuItem('Step Backward').enabled = true
|
// Disallow opening more screens on top of the current one.
|
||||||
getMenuItem('Increase Speed').enabled = true
|
function setAllowNav (flag) {
|
||||||
getMenuItem('Decrease Speed').enabled = true
|
getMenuItem('Preferences').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = true
|
getMenuItem('Create New Torrent...').enabled = flag
|
||||||
|
var item = getMenuItem('Create New Torrent from File...')
|
||||||
|
if (item) item.enabled = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAlwaysOnTop (flag) {
|
function onToggleAlwaysOnTop (flag) {
|
||||||
@@ -54,16 +55,6 @@ function onToggleFullScreen (flag) {
|
|||||||
getMenuItem('Full Screen').checked = flag
|
getMenuItem('Full Screen').checked = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
|
||||||
getMenuItem('Full Screen').enabled = false
|
|
||||||
getMenuItem('Float on Top').enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
getMenuItem('Full Screen').enabled = true
|
|
||||||
getMenuItem('Float on Top').enabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (var i = 0; i < menu.items.length; i++) {
|
for (var i = 0; i < menu.items.length; i++) {
|
||||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
@@ -130,14 +121,6 @@ function getMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Preferences',
|
|
||||||
accelerator: 'CmdOrCtrl+,',
|
|
||||||
click: () => windows.main.dispatch('preferences')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -350,6 +333,17 @@ function getMenuTemplate () {
|
|||||||
click: () => dialog.openSeedFile()
|
click: () => dialog.openSeedFile()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Edit menu (Windows, Linux)
|
||||||
|
template[1].submenu.push(
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => windows.main.dispatch('preferences')
|
||||||
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[4].submenu.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
hasTray,
|
hasTray,
|
||||||
init,
|
init,
|
||||||
onWindowBlur,
|
setWindowFocus
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -31,12 +30,7 @@ function hasTray () {
|
|||||||
return !!tray
|
return !!tray
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function setWindowFocus (flag) {
|
||||||
if (!tray) return
|
|
||||||
updateTrayMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
updateTrayMenu()
|
updateTrayMenu()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function initDarwinWin32 () {
|
|||||||
|
|
||||||
electron.autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'update-not-available',
|
'update-not-available',
|
||||||
() => log('Update not available')
|
() => log('No update available')
|
||||||
)
|
)
|
||||||
|
|
||||||
electron.autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ function setBounds (bounds, maximize) {
|
|||||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||||
}
|
}
|
||||||
main.win.setBounds(bounds, true)
|
// Resize the window's content area (so window border doesn't need to be taken
|
||||||
|
// into account)
|
||||||
|
main.win.setContentBounds(bounds, true)
|
||||||
} else {
|
} else {
|
||||||
log('setBounds: not setting bounds because of window maximization')
|
log('setBounds: not setting bounds because of window maximization')
|
||||||
}
|
}
|
||||||
@@ -204,13 +206,13 @@ function toggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function onWindowBlur () {
|
||||||
menu.onWindowBlur()
|
menu.setWindowFocus(false)
|
||||||
tray.onWindowBlur()
|
tray.setWindowFocus(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowFocus () {
|
function onWindowFocus () {
|
||||||
menu.onWindowFocus()
|
menu.setWindowFocus(true)
|
||||||
tray.onWindowFocus()
|
tray.setWindowFocus(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ module.exports = class PlaybackController {
|
|||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
||||||
} else {
|
} else {
|
||||||
@@ -242,7 +242,7 @@ module.exports = class PlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, play the video
|
// otherwise, play the video
|
||||||
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
|
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
// Controls the Preferences screen
|
// Controls the Preferences screen
|
||||||
module.exports = class PrefsController {
|
module.exports = class PrefsController {
|
||||||
@@ -15,11 +16,15 @@ module.exports = class PrefsController {
|
|||||||
url: 'preferences',
|
url: 'preferences',
|
||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
dispatch('setTitle', 'Preferences')
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||||
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
destroy: () => this.save()
|
destroy: () => {
|
||||||
|
ipcRenderer.send('setAllowNav', true)
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +46,11 @@ module.exports = class PrefsController {
|
|||||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
save () {
|
save () {
|
||||||
var state = this.state
|
var state = this.state
|
||||||
|
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
||||||
|
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
||||||
|
}
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
State.save(state)
|
State.save(state)
|
||||||
|
dispatch('checkDownloadPath')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ module.exports = class TorrentListController {
|
|||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow a instant.io link to be pasted
|
// Allow a instant.io link to be pasted
|
||||||
// TODO: remove this once support is added to webtorrent core
|
|
||||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
}
|
}
|
||||||
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
showCreateTorrent (files) {
|
showCreateTorrent (files) {
|
||||||
|
// You can only create torrents from the home screen.
|
||||||
|
if (this.state.location.url() !== 'home') {
|
||||||
|
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
|
||||||
|
}
|
||||||
|
|
||||||
// Files will either be an array of file objects, which we can send directly
|
// Files will either be an array of file objects, which we can send directly
|
||||||
// to the create-torrent screen
|
// to the create-torrent screen
|
||||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||||
this.state.location.go({
|
this.state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files,
|
||||||
|
setup: (cb) => {
|
||||||
|
this.state.window.title = 'Create New Torrent'
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,27 +76,29 @@ module.exports = class TorrentListController {
|
|||||||
var state = this.state
|
var state = this.state
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
state.location.backToFirst(function () {
|
state.location.cancel()
|
||||||
state.location.clearForward('create-torrent')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentSummary) {
|
startTorrentingSummary (torrentKey) {
|
||||||
var s = torrentSummary
|
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||||
// Backward compatibility for config files save before we had torrentKey
|
|
||||||
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// Use Downloads folder by default
|
||||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||||
s.torrentKey,
|
if (err) {
|
||||||
TorrentSummary.getTorrentID(s),
|
s.error = 'path-missing'
|
||||||
s.path,
|
return
|
||||||
s.fileModtimes,
|
}
|
||||||
s.selections)
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
|
s.torrentKey,
|
||||||
|
TorrentSummary.getTorrentID(s),
|
||||||
|
s.path,
|
||||||
|
s.fileModtimes,
|
||||||
|
s.selections)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
@@ -95,7 +106,7 @@ module.exports = class TorrentListController {
|
|||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
this.startTorrentingSummary(torrentSummary)
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||||
sound.play('ENABLE')
|
sound.play('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
@@ -271,6 +282,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||||
|
if (!savePath) return // They clicked Cancel
|
||||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ function run (state) {
|
|||||||
migrate_0_7_2(state.saved)
|
migrate_0_7_2(state.saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (semver.lt(version, '0.11.0')) {
|
||||||
|
migrate_0_11_0(state.saved)
|
||||||
|
}
|
||||||
|
|
||||||
// Config is now on the new version
|
// Config is now on the new version
|
||||||
state.saved.version = config.APP_VERSION
|
state.saved.version = config.APP_VERSION
|
||||||
}
|
}
|
||||||
@@ -93,3 +97,10 @@ function migrate_0_7_2 (saved) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_11_0 (saved) {
|
||||||
|
if (saved.prefs.isFileHandler === undefined) {
|
||||||
|
// The app used to make itself the default torrent file handler automatically
|
||||||
|
saved.prefs.isFileHandler = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ function save (state, cb) {
|
|||||||
if (key === 'playStatus') {
|
if (key === 'playStatus') {
|
||||||
continue // Don't save whether a torrent is playing / pending
|
continue // Don't save whether a torrent is playing / pending
|
||||||
}
|
}
|
||||||
|
if (key === 'error') {
|
||||||
|
continue // Don't save error states
|
||||||
|
}
|
||||||
torrent[key] = x[key]
|
torrent[key] = x[key]
|
||||||
}
|
}
|
||||||
return torrent
|
return torrent
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const dragDrop = require('drag-drop')
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const ReactDOM = require('react-dom')
|
const ReactDOM = require('react-dom')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const App = require('./views/app')
|
const App = require('./views/app')
|
||||||
@@ -77,21 +78,27 @@ function onState (err, _state) {
|
|||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
|
||||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
|
||||||
|
|
||||||
// Listen for messages from the main process
|
|
||||||
setupIpc()
|
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Calling update() updates the UI given the current state
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
// 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
|
// a progress bar and to keep the cursor in sync when playing a video
|
||||||
setInterval(update, 1000)
|
setInterval(update, 1000)
|
||||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||||
|
|
||||||
|
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||||
|
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
|
// Listen for messages from the main process
|
||||||
|
setupIpc()
|
||||||
|
|
||||||
|
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||||
|
checkDownloadPath()
|
||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...drag and drop a torrent or video file to play or seed
|
// ...drag and drop files/text to start torrenting or seeding
|
||||||
dragDrop('body', onOpen)
|
dragDrop('body', {
|
||||||
|
onDrop: onOpen,
|
||||||
|
onDropText: onOpen
|
||||||
|
})
|
||||||
|
|
||||||
// ...same thing if you paste a torrent
|
// ...same thing if you paste a torrent
|
||||||
document.addEventListener('paste', onPaste)
|
document.addEventListener('paste', onPaste)
|
||||||
@@ -172,8 +179,7 @@ const dispatchHandlers = {
|
|||||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||||
'startTorrentingSummary': (torrentSummary) =>
|
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||||
controllers.torrentList.startTorrentingSummary(torrentSummary),
|
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||||
@@ -209,6 +215,7 @@ const dispatchHandlers = {
|
|||||||
// Preferences screen
|
// Preferences screen
|
||||||
'preferences': () => controllers.prefs.show(),
|
'preferences': () => controllers.prefs.show(),
|
||||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
||||||
|
'checkDownloadPath': checkDownloadPath,
|
||||||
|
|
||||||
// Update (check for new versions on Linux, where there's no auto updater)
|
// Update (check for new versions on Linux, where there's no auto updater)
|
||||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
||||||
@@ -220,6 +227,7 @@ const dispatchHandlers = {
|
|||||||
'escapeBack': escapeBack,
|
'escapeBack': escapeBack,
|
||||||
'back': () => state.location.back(),
|
'back': () => state.location.back(),
|
||||||
'forward': () => state.location.forward(),
|
'forward': () => state.location.forward(),
|
||||||
|
'cancel': () => state.location.cancel(),
|
||||||
|
|
||||||
// Controlling the window
|
// Controlling the window
|
||||||
'setDimensions': setDimensions,
|
'setDimensions': setDimensions,
|
||||||
@@ -309,8 +317,14 @@ function escapeBack () {
|
|||||||
// Starts all torrents that aren't paused on program startup
|
// Starts all torrents that aren't paused on program startup
|
||||||
function resumeTorrents () {
|
function resumeTorrents () {
|
||||||
state.saved.torrents
|
state.saved.torrents
|
||||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
.map((torrentSummary) => {
|
||||||
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
|
// Torrent keys are ephemeral, reassigned each time the app runs.
|
||||||
|
// On startup, give all torrents a key, even the ones that are paused.
|
||||||
|
torrentSummary.torrentKey = state.nextTorrentKey++
|
||||||
|
return torrentSummary
|
||||||
|
})
|
||||||
|
.filter((s) => s.status !== 'paused')
|
||||||
|
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
@@ -357,25 +371,25 @@ function setDimensions (dimensions) {
|
|||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
if (state.modal) {
|
var url = state.location.url()
|
||||||
|
var allTorrents = files.every(TorrentPlayer.isTorrent)
|
||||||
|
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
|
||||||
|
|
||||||
|
if (allTorrents) {
|
||||||
|
// Drop torrents onto the app: go to home screen, add torrents, no matter what
|
||||||
|
dispatch('backToList')
|
||||||
|
// All .torrent files? Add them.
|
||||||
|
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
||||||
|
} else if (url === 'player' && allSubtitles) {
|
||||||
|
// Drop subtitles onto a playing video: add subtitles
|
||||||
|
controllers.subtitles.addSubtitles(files, true)
|
||||||
|
} else if (url === 'home') {
|
||||||
|
// Drop files onto home screen: show Create Torrent
|
||||||
state.modal = null
|
state.modal = null
|
||||||
}
|
controllers.torrentList.showCreateTorrent(files)
|
||||||
|
} else {
|
||||||
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
// Drop files onto any other screen: show error
|
||||||
|
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||||
if (state.location.url() === 'home' || subtitles.length === 0) {
|
|
||||||
if (files.every(TorrentPlayer.isTorrent)) {
|
|
||||||
if (state.location.url() !== 'home') {
|
|
||||||
dispatch('backToList')
|
|
||||||
}
|
|
||||||
// All .torrent files? Add them.
|
|
||||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
|
||||||
} else {
|
|
||||||
// Show the Create Torrent screen. Let's seed those files.
|
|
||||||
controllers.torrentList.showCreateTorrent(files)
|
|
||||||
}
|
|
||||||
} else if (state.location.url() === 'player') {
|
|
||||||
controllers.subtitles.addSubtitles(subtitles, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
@@ -429,3 +443,14 @@ function onFullscreenChanged (e, isFullScreen) {
|
|||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkDownloadPath () {
|
||||||
|
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||||
|
if (err) {
|
||||||
|
state.downloadPathStatus = 'missing'
|
||||||
|
return console.error(err)
|
||||||
|
}
|
||||||
|
if (stat.isDirectory()) state.downloadPathStatus = 'ok'
|
||||||
|
else state.downloadPathStatus = 'missing'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
|
|||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
<p className='float-right'>
|
<p className='float-right'>
|
||||||
<button className='button-flat light' onClick={dispatcher('back')}>
|
<button className='button-flat light' onClick={dispatcher('cancel')}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ module.exports = class CreateTorrentPage extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div key='trackers' className='torrent-attribute'>
|
<div key='trackers' className='torrent-attribute'>
|
||||||
<label>Trackers:</label>
|
<label>Trackers:</label>
|
||||||
<textarea className='torrent-attribute torrent-trackers' value={trackers}></textarea>
|
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers}></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div key='private' className='torrent-attribute'>
|
<div key='private' className='torrent-attribute'>
|
||||||
<label>Private:</label>
|
<label>Private:</label>
|
||||||
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div key='buttons' className='float-right'>
|
<div key='buttons' className='float-right'>
|
||||||
<button key='cancel' className='button-flat light' onClick={dispatcher('back')}>Cancel</button>
|
<button key='cancel' className='button-flat light' onClick={dispatcher('cancel')}>Cancel</button>
|
||||||
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ function renderGeneralSection (state) {
|
|||||||
description: '',
|
description: '',
|
||||||
icon: 'settings'
|
icon: 'settings'
|
||||||
}, [
|
}, [
|
||||||
renderDownloadDirSelector(state)
|
renderDownloadPathSelector(state),
|
||||||
|
renderFileHandlers(state)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDownloadDirSelector (state) {
|
function renderDownloadPathSelector (state) {
|
||||||
return renderFileSelector({
|
return renderFileSelector({
|
||||||
key: 'download-path',
|
key: 'download-path',
|
||||||
label: 'Download Path',
|
label: 'Download Path',
|
||||||
@@ -39,10 +40,33 @@ function renderDownloadDirSelector (state) {
|
|||||||
},
|
},
|
||||||
state.unsaved.prefs.downloadPath,
|
state.unsaved.prefs.downloadPath,
|
||||||
function (filePath) {
|
function (filePath) {
|
||||||
setStateValue('downloadPath', filePath)
|
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFileHandlers (state) {
|
||||||
|
var definition = {
|
||||||
|
key: 'file-handlers',
|
||||||
|
label: 'Handle Torrent Files'
|
||||||
|
}
|
||||||
|
var buttonText = state.unsaved.prefs.isFileHandler
|
||||||
|
? 'Remove default app for torrent files'
|
||||||
|
: 'Make WebTorrent the default app for torrent files'
|
||||||
|
var controls = [(
|
||||||
|
<button key='toggle-handlers'
|
||||||
|
className='btn'
|
||||||
|
onClick={toggleFileHandlers}>
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
)]
|
||||||
|
return renderControlGroup(definition, controls)
|
||||||
|
|
||||||
|
function toggleFileHandlers () {
|
||||||
|
var isFileHandler = state.unsaved.prefs.isFileHandler
|
||||||
|
dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Renders a prefs section.
|
// Renders a prefs section.
|
||||||
// - definition should be {icon, title, description}
|
// - definition should be {icon, title, description}
|
||||||
// - controls should be an array of vdom elements
|
// - controls should be an array of vdom elements
|
||||||
@@ -73,25 +97,24 @@ function renderSection (definition, controls) {
|
|||||||
// - value should be the current pref, a file or folder path
|
// - value should be the current pref, a file or folder path
|
||||||
// - callback takes a new file or folder path
|
// - callback takes a new file or folder path
|
||||||
function renderFileSelector (definition, value, callback) {
|
function renderFileSelector (definition, value, callback) {
|
||||||
return (
|
var controls = [(
|
||||||
<div key={definition.key} className='control-group'>
|
<input
|
||||||
<div className='controls'>
|
type='text'
|
||||||
<label className='control-label'>
|
className='file-picker-text'
|
||||||
<div className='preference-title'>{definition.label}</div>
|
key={definition.property}
|
||||||
<div className='preference-description'>{definition.description}</div>
|
id={definition.property}
|
||||||
</label>
|
disabled='disabled'
|
||||||
<div className='controls'>
|
value={value} />
|
||||||
<input type='text' className='file-picker-text'
|
), (
|
||||||
id={definition.property}
|
<button
|
||||||
disabled='disabled'
|
key={definition.property + '-btn'}
|
||||||
value={value} />
|
className='btn'
|
||||||
<button className='btn' onClick={handleClick}>
|
onClick={handleClick}>
|
||||||
<i className='icon'>folder_open</i>
|
<i className='icon'>folder_open</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)]
|
||||||
</div>
|
return renderControlGroup(definition, controls)
|
||||||
</div>
|
|
||||||
)
|
|
||||||
function handleClick () {
|
function handleClick () {
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
@@ -100,6 +123,18 @@ function renderFileSelector (definition, value, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStateValue (property, value) {
|
function renderControlGroup (definition, controls) {
|
||||||
dispatch('updatePreferences', property, value)
|
return (
|
||||||
|
<div key={definition.key} className='control-group'>
|
||||||
|
<div className='controls'>
|
||||||
|
<label className='control-label'>
|
||||||
|
<div className='preference-title'>{definition.label}</div>
|
||||||
|
<div className='preference-description'>{definition.description}</div>
|
||||||
|
</label>
|
||||||
|
<div className='controls'>
|
||||||
|
{controls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,32 @@ const {dispatcher} = require('../lib/dispatcher')
|
|||||||
module.exports = class TorrentList extends React.Component {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
var state = this.props.state
|
||||||
var torrentRows = state.saved.torrents.map(
|
|
||||||
|
var contents = []
|
||||||
|
if (state.downloadPathStatus === 'missing') {
|
||||||
|
contents.push(
|
||||||
|
<div key='torrent-missing-path'>
|
||||||
|
<p>Download path missing: {state.saved.prefs.downloadPath}</p>
|
||||||
|
<p>Check that all drives are connected?</p>
|
||||||
|
<p>Alternatively, choose a new download path
|
||||||
|
in <a href='#' onClick={dispatcher('preferences')}>Preferences</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var torrentElems = state.saved.torrents.map(
|
||||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||||
)
|
)
|
||||||
|
contents.push(...torrentElems)
|
||||||
|
contents.push(
|
||||||
|
<div key='torrent-placeholder' className='torrent-placeholder'>
|
||||||
|
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='torrent-list' className='torrent-list'>
|
<div key='torrent-list' className='torrent-list'>
|
||||||
{torrentRows}
|
{contents}
|
||||||
<div key='torrent-placeholder' className='torrent-placeholder'>
|
|
||||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,6 +60,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||||
if (isSelected) classes.push('selected')
|
if (isSelected) classes.push('selected')
|
||||||
if (!infoHash) classes.push('disabled')
|
if (!infoHash) classes.push('disabled')
|
||||||
|
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
@@ -67,8 +84,14 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
var prog = torrentSummary.progress
|
||||||
if (torrentSummary.status !== 'paused' && prog) {
|
if (torrentSummary.error) {
|
||||||
elements.push((
|
elements.push(
|
||||||
|
<div key='progress-info' className='ellipsis'>
|
||||||
|
{getErrorMessage(torrentSummary)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||||
|
elements.push(
|
||||||
<div key='progress-info' className='ellipsis'>
|
<div key='progress-info' className='ellipsis'>
|
||||||
{renderPercentProgress()}
|
{renderPercentProgress()}
|
||||||
{renderTotalProgress()}
|
{renderTotalProgress()}
|
||||||
@@ -77,7 +100,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
{renderUploadSpeed()}
|
{renderUploadSpeed()}
|
||||||
{renderEta()}
|
{renderEta()}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||||
@@ -174,8 +197,9 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton, downloadButton
|
||||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
var noErrors = !torrentSummary.error
|
||||||
|
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
playButton = (
|
playButton = (
|
||||||
<i
|
<i
|
||||||
key='play-button'
|
key='play-button'
|
||||||
@@ -186,11 +210,8 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (noErrors) {
|
||||||
return (
|
downloadButton = (
|
||||||
<div key='buttons' className='buttons'>
|
|
||||||
{positionElem}
|
|
||||||
{playButton}
|
|
||||||
<i
|
<i
|
||||||
key='download-button'
|
key='download-button'
|
||||||
className={'button-round icon download ' + torrentSummary.status}
|
className={'button-round icon download ' + torrentSummary.status}
|
||||||
@@ -198,6 +219,14 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||||
{downloadIcon}
|
{downloadIcon}
|
||||||
</i>
|
</i>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key='buttons' className='buttons'>
|
||||||
|
{positionElem}
|
||||||
|
{playButton}
|
||||||
|
{downloadButton}
|
||||||
<i
|
<i
|
||||||
key='delete-button'
|
key='delete-button'
|
||||||
className='icon delete'
|
className='icon delete'
|
||||||
@@ -212,12 +241,26 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
renderTorrentDetails (torrentSummary) {
|
renderTorrentDetails (torrentSummary) {
|
||||||
var filesElement
|
var filesElement
|
||||||
if (!torrentSummary.files) {
|
if (torrentSummary.error || !torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
var message = ''
|
||||||
var message = torrentSummary.status === 'paused'
|
if (torrentSummary.error === 'path-missing') {
|
||||||
? 'Failed to load torrent info. Click the download button to try again...'
|
// Special case error: this torrent's download dir or file is missing
|
||||||
: 'Downloading torrent info...'
|
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
filesElement = (<div key='files' className='files warning'>{message}</div>)
|
} else if (torrentSummary.error) {
|
||||||
|
// General error for this torrent: just show the message
|
||||||
|
message = torrentSummary.error.message || torrentSummary.error
|
||||||
|
} else if (torrentSummary.status === 'paused') {
|
||||||
|
// No file info, no infohash, and we're not trying to download from the DHT
|
||||||
|
message = 'Failed to load torrent info. Click the download button to try again...'
|
||||||
|
} else {
|
||||||
|
// No file info, no infohash, trying to load from the DHT
|
||||||
|
message = 'Downloading torrent info...'
|
||||||
|
}
|
||||||
|
filesElement = (
|
||||||
|
<div key='files' className='files warning'>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files
|
var fileRows = torrentSummary.files
|
||||||
@@ -255,7 +298,8 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||||
var isDone = false // Are we finished torrenting it?
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files &&
|
||||||
|
torrentSummary.progress.files[index]) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
var fileProg = torrentSummary.progress.files[index]
|
||||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||||
@@ -327,3 +371,16 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getErrorMessage (torrentSummary) {
|
||||||
|
var err = torrentSummary.error
|
||||||
|
if (err === 'path-missing') {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Path missing.<br />
|
||||||
|
Fix and restart the app, or delete the torrent.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return 'Error'
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ module.exports = class UpdateAvailableModal extends React.Component {
|
|||||||
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
||||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
||||||
<p className='float-right'>
|
<p className='float-right'>
|
||||||
<button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
|
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
|
||||||
<button className='button button-raised' onClick={handleOK}>Show Download Page</button>
|
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleOK () {
|
function handleShow () {
|
||||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleSkip () {
|
||||||
dispatch('skipVersion', state.modal.version)
|
dispatch('skipVersion', state.modal.version)
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -551,6 +551,19 @@ input[type='text'] {
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TORRENT LIST: ERRORS
|
||||||
|
*/
|
||||||
|
|
||||||
|
.torrent-list p {
|
||||||
|
margin: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-list a {
|
||||||
|
color: #99f;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TORRENT LIST: DRAG-DROP TARGET
|
* TORRENT LIST: DRAG-DROP TARGET
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user