Compare commits

...

4 Commits

Author SHA1 Message Date
Diego Rodríguez Baquero
f9e858d21a Merge branch 'master' into esm 2021-10-18 19:52:42 -05:00
Renovate Bot
2cc62e26b3 chore(deps): update dependency electron to v15.2.0 2021-10-15 22:43:17 +00:00
Diego Rodríguez Baquero
7e8078ed92 esm 2021-10-11 18:27:41 -05:00
Diego Rodríguez Baquero
e42a515199 fix: modernify code (#2068)
* fix: modernify code

* standard fix

* fixes
2021-10-11 18:23:09 -05:00
71 changed files with 1007 additions and 1308 deletions

1
electron.cjs Normal file
View File

@@ -0,0 +1 @@
module.exports = require('electron')

1
index.cjs Normal file
View File

@@ -0,0 +1 @@
import('./build/main/index.js')

View File

@@ -1 +0,0 @@
require('./build/main')

38
package-lock.json generated
View File

@@ -52,7 +52,7 @@
"@babel/plugin-transform-react-jsx": "7.14.9", "@babel/plugin-transform-react-jsx": "7.14.9",
"cross-zip": "4.0.0", "cross-zip": "4.0.0",
"depcheck": "1.4.2", "depcheck": "1.4.2",
"electron": "15.1.2", "electron": "15.2.0",
"electron-notarize": "1.1.1", "electron-notarize": "1.1.1",
"electron-osx-sign": "0.5.0", "electron-osx-sign": "0.5.0",
"electron-packager": "15.4.0", "electron-packager": "15.4.0",
@@ -4232,9 +4232,9 @@
"dev": true "dev": true
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "15.1.2", "version": "15.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-15.1.2.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-15.2.0.tgz",
"integrity": "sha512-cU5x1ZvhwcTtzClJrsxqbvCocs40uhuXcdXbXFMpe6XnbnjruTbwrB2V0OxN01pStDrYihnfKo2yhfEvhO3zmw==", "integrity": "sha512-kg0JdlsVbJgD/hO/A7o9VH8U44pQWkIsyt/sALxH6g8CiHQxMujLn2JfB2gyUfHXPT7m8vD4Z+CurS2KodEsWw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@electron/get": "^1.13.0", "@electron/get": "^1.13.0",
@@ -4860,9 +4860,9 @@
} }
}, },
"node_modules/electron/node_modules/@types/node": { "node_modules/electron/node_modules/@types/node": {
"version": "14.17.21", "version": "14.17.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.27.tgz",
"integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==" "integrity": "sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw=="
}, },
"node_modules/elementtree": { "node_modules/elementtree": {
"version": "0.1.7", "version": "0.1.7",
@@ -7079,9 +7079,9 @@
} }
}, },
"node_modules/global-agent/node_modules/core-js": { "node_modules/global-agent/node_modules/core-js": {
"version": "3.18.2", "version": "3.18.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.2.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz",
"integrity": "sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ==", "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"funding": { "funding": {
@@ -17882,9 +17882,9 @@
"dev": true "dev": true
}, },
"electron": { "electron": {
"version": "15.1.2", "version": "15.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-15.1.2.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-15.2.0.tgz",
"integrity": "sha512-cU5x1ZvhwcTtzClJrsxqbvCocs40uhuXcdXbXFMpe6XnbnjruTbwrB2V0OxN01pStDrYihnfKo2yhfEvhO3zmw==", "integrity": "sha512-kg0JdlsVbJgD/hO/A7o9VH8U44pQWkIsyt/sALxH6g8CiHQxMujLn2JfB2gyUfHXPT7m8vD4Z+CurS2KodEsWw==",
"requires": { "requires": {
"@electron/get": "^1.13.0", "@electron/get": "^1.13.0",
"@types/node": "^14.6.2", "@types/node": "^14.6.2",
@@ -17892,9 +17892,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "14.17.21", "version": "14.17.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.27.tgz",
"integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==" "integrity": "sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw=="
} }
} }
}, },
@@ -20075,9 +20075,9 @@
}, },
"dependencies": { "dependencies": {
"core-js": { "core-js": {
"version": "3.18.2", "version": "3.18.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.2.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz",
"integrity": "sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ==", "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==",
"optional": true "optional": true
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"type": "module",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.24.0", "version": "0.24.0",
"author": { "author": {
@@ -64,7 +65,7 @@
"@babel/plugin-transform-react-jsx": "7.14.9", "@babel/plugin-transform-react-jsx": "7.14.9",
"cross-zip": "4.0.0", "cross-zip": "4.0.0",
"depcheck": "1.4.2", "depcheck": "1.4.2",
"electron": "15.1.2", "electron": "15.2.0",
"electron-notarize": "1.1.1", "electron-notarize": "1.1.1",
"electron-osx-sign": "0.5.0", "electron-osx-sign": "0.5.0",
"electron-packager": "15.4.0", "electron-packager": "15.4.0",
@@ -96,7 +97,7 @@
"webtorrent" "webtorrent"
], ],
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.cjs",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.6.0", "appdmg": "^0.6.0",
"electron-installer-debian": "^3.1.0", "electron-installer-debian": "^3.1.0",

View File

@@ -1,12 +1,13 @@
const appConfig = require('application-config')('WebTorrent') import applicationConfig from 'application-config'
const path = require('path') import path from 'path'
const { app } = require('electron') import arch from 'arch'
const arch = require('arch') import fs from 'fs'
import electron from '../electron.cjs'
const appConfig = applicationConfig('WebTorrent')
const APP_NAME = 'WebTorrent' const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC' const APP_TEAM = 'WebTorrent, LLC'
const APP_VERSION = require('../package.json').version const APP_VERSION = JSON.parse(fs.readFileSync('package.json').toString()).version
const IS_TEST = isTest() const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST const PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest') ? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
@@ -17,93 +18,6 @@ const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38 const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100 const UI_TORRENT_HEIGHT = 100
module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME,
APP_TEAM,
APP_VERSION,
APP_WINDOW_TITLE: APP_NAME,
CONFIG_PATH: getConfigPath(),
DEFAULT_TORRENTS: [
{
testID: 'bbb',
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
testID: 'cosmos',
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
testID: 'sintel',
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
testID: 'tears',
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
],
DELAYED_INIT: 3000 /* 3 seconds */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/webtorrent/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/webtorrent/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master',
GITHUB_URL_RELEASES: 'https://github.com/webtorrent/webtorrent-desktop/releases',
HOME_PAGE_URL: 'https://webtorrent.io',
TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp',
IS_PORTABLE,
IS_PRODUCTION,
IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'),
STATIC_PATH: path.join(__dirname, '..', 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
WINDOW_INITIAL_BOUNDS: {
width: 500,
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
},
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
WINDOW_MIN_WIDTH: 425,
UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT
}
function getConfigPath () { function getConfigPath () {
if (IS_PORTABLE) { if (IS_PORTABLE) {
return PORTABLE_PATH return PORTABLE_PATH
@@ -121,16 +35,12 @@ function getDefaultDownloadPath () {
} }
function getPath (key) { function getPath (key) {
if (!process.versions.electron) { if (!process.versions.electron || process.type !== 'browser') {
// Node.js process // Node.js process
return '' return ''
} else if (process.type === 'renderer') {
// Electron renderer process
return require('@electron/remote').app.getPath(key)
} else {
// Electron main process
return app.getPath(key)
} }
// Electron main process
return electron.app.getPath(key)
} }
function isTest () { function isTest () {
@@ -147,8 +57,6 @@ function isPortable () {
return false return false
} }
const fs = require('fs')
try { try {
// This line throws if the "Portable Settings" folder does not exist, and does // This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise. // nothing otherwise.
@@ -174,3 +82,117 @@ function isProduction () {
return !/\/electron$/.test(process.execPath) return !/\/electron$/.test(process.execPath)
} }
} }
export const ANNOUNCEMENT_URL = 'https://webtorrent.io/desktop/announcement'
export const AUTO_UPDATE_URL = 'https://webtorrent.io/desktop/update'
export const CRASH_REPORT_URL = 'https://webtorrent.io/desktop/crash-report'
export const TELEMETRY_URL = 'https://webtorrent.io/desktop/telemetry'
export const APP_COPYRIGHT = `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`
export const APP_FILE_ICON = new URL('../static/WebTorrentFile', import.meta.url).pathname // path.join(__dirname, '..',
// 'static', 'WebTorrentFile')
export const APP_ICON = new URL('../static/WebTorrent', import.meta.url).pathname // path.join(__dirname, '..',
// 'static',
// 'WebTorrent')
export const CONFIG_PATH = getConfigPath()
export const DEFAULT_TORRENTS = [
{
testID: 'bbb',
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
testID: 'cosmos',
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
testID: 'sintel',
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
testID: 'tears',
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
]
export const DELAYED_INIT = 3000 /* 3 seconds */
export const DEFAULT_DOWNLOAD_PATH = getDefaultDownloadPath()
export const GITHUB_URL = 'https://github.com/webtorrent/webtorrent-desktop'
export const GITHUB_URL_ISSUES = 'https://github.com/webtorrent/webtorrent-desktop/issues'
export const GITHUB_URL_RAW = 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master'
export const GITHUB_URL_RELEASES = 'https://github.com/webtorrent/webtorrent-desktop/releases'
export const HOME_PAGE_URL = 'https://webtorrent.io'
export const TWITTER_PAGE_URL = 'https://twitter.com/WebTorrentApp'
export const OS_SYSARCH = arch() === 'x64' ? 'x64' : 'ia32'
export const POSTER_PATH = path.join(getConfigPath(), 'Posters')
export const ROOT_PATH = new URL('../', import.meta.url).pathname
export const STATIC_PATH = new URL('../static', import.meta.url).pathname
export const TORRENT_PATH = path.join(getConfigPath(), 'Torrents')
export const WINDOW_ABOUT = 'file://' + new URL('../static/about.html', import.meta.url).pathname
export const WINDOW_MAIN = 'file://' + new URL('../static/main.html', import.meta.url).pathname
export const WINDOW_WEBTORRENT = 'file://' + new URL('../static/webtorrent.html', import.meta.url).pathname
export const WINDOW_INITIAL_BOUNDS = {
width: 500,
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
}
export const WINDOW_MIN_HEIGHT = UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2)
export const WINDOW_MIN_WIDTH = 425
export { APP_NAME }
export { APP_TEAM }
export { APP_VERSION }
export { APP_NAME as APP_WINDOW_TITLE }
export { IS_PORTABLE }
export { IS_PRODUCTION }
export { IS_TEST }
export { UI_HEADER_HEIGHT }
export { UI_TORRENT_HEIGHT }
export default {
ANNOUNCEMENT_URL,
AUTO_UPDATE_URL,
CRASH_REPORT_URL,
TELEMETRY_URL,
APP_COPYRIGHT,
APP_FILE_ICON,
APP_ICON,
APP_NAME,
APP_TEAM,
APP_VERSION,
APP_WINDOW_TITLE: APP_NAME,
CONFIG_PATH,
DEFAULT_TORRENTS,
DELAYED_INIT,
DEFAULT_DOWNLOAD_PATH,
GITHUB_URL,
GITHUB_URL_ISSUES,
GITHUB_URL_RAW,
GITHUB_URL_RELEASES,
HOME_PAGE_URL,
TWITTER_PAGE_URL,
IS_PORTABLE,
IS_PRODUCTION,
IS_TEST,
OS_SYSARCH,
POSTER_PATH,
ROOT_PATH,
STATIC_PATH,
TORRENT_PATH,
WINDOW_ABOUT,
WINDOW_MAIN,
WINDOW_WEBTORRENT,
WINDOW_INITIAL_BOUNDS,
WINDOW_MIN_HEIGHT,
WINDOW_MIN_WIDTH,
UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT
}

View File

@@ -1,15 +1,13 @@
module.exports = { import electron from '../electron.cjs'
init
}
function init () { async function init () {
const config = require('./config') const config = await import('./config.js')
const { crashReporter } = require('electron') electron.crashReporter.start({
crashReporter.start({
productName: config.APP_NAME, productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL, submitURL: config.CRASH_REPORT_URL,
globalExtra: { _companyName: config.APP_NAME }, globalExtra: { _companyName: config.APP_NAME },
compress: true compress: true
}) })
} }
export default { init }

View File

@@ -1,11 +1,6 @@
module.exports = { import electron from '../../electron.cjs'
init import config from '../config.js'
} import log from './log.js'
const { dialog } = require('electron')
const config = require('../config')
const log = require('./log')
const ANNOUNCEMENT_URL = const ANNOUNCEMENT_URL =
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}` `${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
@@ -24,8 +19,8 @@ const ANNOUNCEMENT_URL =
* "detail": "Please update to v0.xx as soon as possible..." * "detail": "Please update to v0.xx as soon as possible..."
* } * }
*/ */
function init () { async function init () {
const get = require('simple-get') const { default: get } = await import('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse) get.concat(ANNOUNCEMENT_URL, onResponse)
} }
@@ -44,7 +39,7 @@ function onResponse (err, res, data) {
} }
} }
dialog.showMessageBox({ electron.dialog.showMessageBox({
type: 'info', type: 'info',
buttons: ['OK'], buttons: ['OK'],
title: data.title, title: data.title,
@@ -52,3 +47,5 @@ function onResponse (err, res, data) {
detail: data.detail detail: data.detail
}) })
} }
export default { init }

View File

@@ -1,20 +1,13 @@
module.exports = { import electron from '../../electron.cjs'
openSeedFile, import log from './log.js'
openSeedDirectory, import * as windows from './windows/index.js'
openTorrentFile,
openTorrentAddress,
openFiles
}
const { dialog } = require('electron') const { dialog } = electron
const log = require('./log')
const windows = require('./windows')
/** /**
* Show open dialog to create a single-file torrent. * Show open dialog to create a single-file torrent.
*/ */
function openSeedFile () { export function openSeedFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedFile') log('openSeedFile')
const opts = { const opts = {
@@ -29,7 +22,7 @@ function openSeedFile () {
* Windows and Linux, open dialogs are for files *or* directories only, not both, * Windows and Linux, open dialogs are for files *or* directories only, not both,
* so this function shows a directory dialog on those platforms. * so this function shows a directory dialog on those platforms.
*/ */
function openSeedDirectory () { export function openSeedDirectory () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedDirectory') log('openSeedDirectory')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
@@ -48,7 +41,7 @@ function openSeedDirectory () {
* Show flexible open dialog that supports selecting .torrent files to add, or * Show flexible open dialog that supports selecting .torrent files to add, or
* a file or folder to create a single-file or single-directory torrent. * a file or folder to create a single-file or single-directory torrent.
*/ */
function openFiles () { export function openFiles () {
if (!windows.main.win) return if (!windows.main.win) return
log('openFiles') log('openFiles')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
@@ -70,7 +63,7 @@ function openFiles () {
/* /*
* Show open dialog to open a .torrent file. * Show open dialog to open a .torrent file.
*/ */
function openTorrentFile () { export function openTorrentFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openTorrentFile') log('openTorrentFile')
const opts = { const opts = {
@@ -82,7 +75,7 @@ function openTorrentFile () {
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts) const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) { selectedPaths.forEach(selectedPath => {
windows.main.dispatch('addTorrent', selectedPath) windows.main.dispatch('addTorrent', selectedPath)
}) })
} }
@@ -90,7 +83,7 @@ function openTorrentFile () {
/* /*
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.) * Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
*/ */
function openTorrentAddress () { export function openTorrentAddress () {
log('openTorrentAddress') log('openTorrentAddress')
windows.main.dispatch('openTorrentAddress') windows.main.dispatch('openTorrentAddress')
} }

View File

@@ -1,13 +1,8 @@
module.exports = { import electron from '../../electron.cjs'
downloadFinished, import * as dialog from './dialog.js'
init, import log from './log.js'
setBadge
}
const { app, Menu } = require('electron') const { app, Menu } = electron
const dialog = require('./dialog')
const log = require('./log')
/** /**
* Add a right-click menu to the dock icon. (Mac) * Add a right-click menu to the dock icon. (Mac)
@@ -57,3 +52,5 @@ function getMenuTemplate () {
} }
] ]
} }
export default { downloadFinished, init, setBadge }

View File

@@ -1,27 +1,19 @@
module.exports = { import cp from 'child_process'
spawn, import path from 'path'
kill, import vlcCommand from 'vlc-command'
checkInstall import log from './log.js'
} import * as windows from './windows'
const cp = require('child_process')
const path = require('path')
const vlcCommand = require('vlc-command')
const log = require('./log')
const windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise // holds a ChildProcess while we're playing a video in an external player, null otherwise
let proc = null let proc = null
function checkInstall (playerPath, cb) { export function checkInstall (playerPath, cb) {
// check for VLC if external player has not been specified by the user // check for VLC if external player has not been specified by the user
// otherwise assume the player is installed // otherwise assume the player is installed
if (!playerPath) return vlcCommand(cb) if (!playerPath) return vlcCommand(cb)
process.nextTick(() => cb(null)) process.nextTick(() => cb(null))
} }
function spawn (playerPath, url, title) { export function spawn (playerPath, url, title) {
if (playerPath) return spawnExternal(playerPath, [url]) if (playerPath) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified // Try to find and use VLC if external player is not specified
@@ -37,7 +29,7 @@ function spawn (playerPath, url, title) {
}) })
} }
function kill () { export function kill () {
if (!proc) return if (!proc) return
log(`Killing external player, pid ${proc.pid}`) log(`Killing external player, pid ${proc.pid}`)
proc.kill('SIGKILL') // kill -9 proc.kill('SIGKILL') // kill -9

View File

@@ -1,7 +1,7 @@
const chokidar = require('chokidar') import * as chokidar from 'chokidar'
const log = require('./log') import log from './log.js'
class FolderWatcher { export class FolderWatcher {
constructor ({ window, state }) { constructor ({ window, state }) {
this.window = window this.window = window
this.state = state this.state = state
@@ -46,5 +46,3 @@ class FolderWatcher {
this.watching = false this.watching = false
} }
} }
module.exports = FolderWatcher

View File

@@ -1,12 +1,7 @@
module.exports = { import config from '../config.js'
install, import path from 'path'
uninstall
}
const config = require('../config') export function install () {
const path = require('path')
function install () {
switch (process.platform) { switch (process.platform) {
case 'darwin': installDarwin() case 'darwin': installDarwin()
break break
@@ -15,7 +10,7 @@ function install () {
} }
} }
function uninstall () { export function uninstall () {
switch (process.platform) { switch (process.platform) {
case 'darwin': uninstallDarwin() case 'darwin': uninstallDarwin()
break break
@@ -24,9 +19,8 @@ function uninstall () {
} }
} }
function installDarwin () { async function installDarwin () {
const { app } = require('electron') const { app } = await import('electron')
// On Mac, only protocols that are listed in `Info.plist` can be set as the // On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime. // default handler at runtime.
app.setAsDefaultProtocolClient('magnet') app.setAsDefaultProtocolClient('magnet')
@@ -43,33 +37,14 @@ if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH) EXEC_COMMAND.push(config.ROOT_PATH)
} }
function installWin32 () { async function installWin32 () {
const Registry = require('winreg') const Registry = await import('wingreg')
const log = await import('./log')
const log = require('./log') const iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
const iconPath = path.join( registerProtocolHandlerWin32('stream-magnet', 'URL:BitTorrent Stream-Magnet URL', iconPath, EXEC_COMMAND)
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico' registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
)
registerProtocolHandlerWin32(
'magnet',
'URL:BitTorrent Magnet URL',
iconPath,
EXEC_COMMAND
)
registerProtocolHandlerWin32(
'stream-magnet',
'URL:BitTorrent Stream-Magnet URL',
iconPath,
EXEC_COMMAND
)
registerFileHandlerWin32(
'.torrent',
'io.webtorrent.torrent',
'BitTorrent Document',
iconPath,
EXEC_COMMAND
)
/** /**
* To add a protocol handler, the following keys must be added to the Windows registry: * To add a protocol handler, the following keys must be added to the Windows registry:
@@ -197,9 +172,8 @@ function installWin32 () {
} }
} }
function uninstallWin32 () { async function uninstallWin32 () {
const Registry = require('winreg') const Registry = await import('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)

View File

@@ -1,22 +1,25 @@
/* eslint-disable import/first */
console.time('init') console.time('init')
require('@electron/remote/main').initialize() import * as RemoteMain from '@electron/remote/main/index.js'
const { app, ipcMain } = require('electron') RemoteMain.initialize()
import electron from '../../electron.cjs'
const { app, ipcMain } = electron
// Start crash reporter early, so it takes effect for child processes // Start crash reporter early, so it takes effect for child processes
const crashReporter = require('../crash-reporter') import crashReporter from '../crash-reporter.js'
crashReporter.init() crashReporter.init()
const parallel = require('run-parallel') import fs from 'fs'
import parallel from 'run-parallel'
import config from '../config.js'
import ipc from './ipc.js'
import log from './log.js'
import menu from './menu.js'
import State from '../renderer/lib/state.js'
import * as windows from './windows/index.js'
const config = require('../config') const WEBTORRENT_VERSION = JSON.parse(fs.readFileSync('node_modules/webtorrent/package.json').toString()).version
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const State = require('../renderer/lib/state')
const windows = require('./windows')
const WEBTORRENT_VERSION = require('webtorrent/package.json').version
let shouldQuit = false let shouldQuit = false
let argv = sliceArgv(process.argv) let argv = sliceArgv(process.argv)
@@ -54,13 +57,18 @@ if (!shouldQuit && !config.IS_PORTABLE) {
if (shouldQuit) { if (shouldQuit) {
app.quit() app.quit()
} else { } else {
init() init().catch(console.error)
} }
function init () { async function init () {
console.log('index init')
app.whenReady().then(() => {
console.log('readyyyy')
})
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine)) app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
if (config.IS_PORTABLE) { if (config.IS_PORTABLE) {
const path = require('path') console.log('is portable')
const path = await import('path')
// Put all user data into the "Portable Settings" folder // Put all user data into the "Portable Settings" folder
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder // Put Electron crash files, etc. into the "Portable Settings\Temp" folder
@@ -72,7 +80,7 @@ function init () {
app.isQuitting = false app.isQuitting = false
parallel({ parallel({
appReady: (cb) => app.on('ready', () => cb(null)), appReady: (cb) => app.whenReady().then(cb),
state: (cb) => State.load(cb) state: (cb) => State.load(cb)
}, onReady) }, onReady)
@@ -110,13 +118,13 @@ function init () {
ipc.init() ipc.init()
app.once('ipcReady', function () { app.once('ipcReady', () => {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init') console.timeEnd('init')
}) })
app.on('before-quit', function (e) { app.on('before-quit', e => {
if (app.isQuitting) return if (app.isQuitting) return
app.isQuitting = true app.isQuitting = true
@@ -129,18 +137,19 @@ function init () {
}, 4000) // quit after 4 secs, at most }, 4000) // quit after 4 secs, at most
}) })
app.on('activate', function () { app.on('activate', () => {
console.log('activate')
if (isReady) windows.main.show() if (isReady) windows.main.show()
}) })
} }
function delayedInit (state) { async function delayedInit (state) {
if (app.isQuitting) return if (app.isQuitting) return
const announcement = require('./announcement') const { default: announcement } = await import('./announcement.js')
const dock = require('./dock') const { default: dock } = await import('./dock.js')
const updater = require('./updater') const { default: updater } = await import('./updater.js')
const FolderWatcher = require('./folder-watcher') const { FolderWatcher } = await import('./folder-watcher.js')
const folderWatcher = new FolderWatcher({ window: windows.main, state }) const folderWatcher = new FolderWatcher({ window: windows.main, state })
announcement.init() announcement.init()
@@ -153,13 +162,13 @@ function delayedInit (state) {
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
const userTasks = require('./user-tasks') const userTasks = await import('./user-tasks.js')
userTasks.init() userTasks.init()
} }
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const tray = require('./tray') const { init: trayInit } = await import('./tray.js')
tray.init() trayInit()
} }
} }
@@ -205,12 +214,12 @@ function sliceArgv (argv) {
) )
} }
function processArgv (argv) { async function processArgv (argv) {
const torrentIds = [] const torrentIds = []
argv.forEach(function (arg) { await Promise.all(argv.forEach(async arg => {
if (arg === '-n' || arg === '-o' || arg === '-u') { if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed // Critical path: Only load the 'dialog' package if it is needed
const dialog = require('./dialog') const dialog = await import('./dialog')
if (arg === '-n') { if (arg === '-n') {
dialog.openSeedDirectory() dialog.openSeedDirectory()
} else if (arg === '-o') { } else if (arg === '-o') {
@@ -233,7 +242,7 @@ function processArgv (argv) {
// running. // running.
torrentIds.push(arg) torrentIds.push(arg)
} }
}) }))
if (torrentIds.length > 0) { if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds) windows.main.dispatch('onOpen', torrentIds)
} }

View File

@@ -1,13 +1,10 @@
module.exports = { import electron from '../../electron.cjs'
init,
setModule
}
const { app, ipcMain } = require('electron') import log from './log.js'
import menu from './menu.js'
import * as windows from './windows/index.js'
const log = require('./log') const { app, ipcMain } = electron
const menu = require('./menu')
const windows = require('./windows')
// Messages from the main process, to be sent once the WebTorrent process starts // Messages from the main process, to be sent once the WebTorrent process starts
const messageQueueMainToWebTorrent = [] const messageQueueMainToWebTorrent = []
@@ -21,16 +18,16 @@ function setModule (name, module) {
} }
function init () { function init () {
ipcMain.once('ipcReady', function (e) { ipcMain.once('ipcReady', e => {
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
}) })
ipcMain.once('ipcReadyWebTorrent', function (e) { ipcMain.once('ipcReadyWebTorrent', e => {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length) messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(function (message) { messageQueueMainToWebTorrent.forEach(message => {
windows.webtorrent.send(message.name, ...message.args) windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name) log('webtorrent: sent queued %s', message.name)
}) })
@@ -40,12 +37,12 @@ function init () {
* Dialog * Dialog
*/ */
ipcMain.on('openTorrentFile', () => { ipcMain.on('openTorrentFile', async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentFile() dialog.openTorrentFile()
}) })
ipcMain.on('openFiles', () => { ipcMain.on('openFiles', async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openFiles() dialog.openFiles()
}) })
@@ -53,12 +50,12 @@ function init () {
* Dock * Dock
*/ */
ipcMain.on('setBadge', (e, ...args) => { ipcMain.on('setBadge', async (e, ...args) => {
const dock = require('./dock') const dock = await import('./dock')
dock.setBadge(...args) dock.setBadge(...args)
}) })
ipcMain.on('downloadFinished', (e, ...args) => { ipcMain.on('downloadFinished', async (e, ...args) => {
const dock = require('./dock') const dock = await import('./dock')
dock.downloadFinished(...args) dock.downloadFinished(...args)
}) })
@@ -66,10 +63,10 @@ function init () {
* Player Events * Player Events
*/ */
ipcMain.on('onPlayerOpen', function () { ipcMain.on('onPlayerOpen', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = await import('./shortcuts')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.togglePlaybackControls(true) menu.togglePlaybackControls(true)
powerSaveBlocker.enable() powerSaveBlocker.enable()
@@ -77,17 +74,17 @@ function init () {
thumbar.enable() thumbar.enable()
}) })
ipcMain.on('onPlayerUpdate', function (e, ...args) { ipcMain.on('onPlayerUpdate', async (e, ...args) => {
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.onPlayerUpdate(...args) menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args) thumbar.onPlayerUpdate(...args)
}) })
ipcMain.on('onPlayerClose', function () { ipcMain.on('onPlayerClose', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = await import('./shortcuts')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.togglePlaybackControls(false) menu.togglePlaybackControls(false)
powerSaveBlocker.disable() powerSaveBlocker.disable()
@@ -95,17 +92,17 @@ function init () {
thumbar.disable() thumbar.disable()
}) })
ipcMain.on('onPlayerPlay', function () { ipcMain.on('onPlayerPlay', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
powerSaveBlocker.enable() powerSaveBlocker.enable()
thumbar.onPlayerPlay() thumbar.onPlayerPlay()
}) })
ipcMain.on('onPlayerPause', function () { ipcMain.on('onPlayerPause', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
powerSaveBlocker.disable() powerSaveBlocker.disable()
thumbar.onPlayerPause() thumbar.onPlayerPause()
@@ -115,7 +112,7 @@ function init () {
* Folder Watcher Events * Folder Watcher Events
*/ */
ipcMain.on('startFolderWatcher', function () { ipcMain.on('startFolderWatcher', () => {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -124,7 +121,7 @@ function init () {
modules.folderWatcher.start() modules.folderWatcher.start()
}) })
ipcMain.on('stopFolderWatcher', function () { ipcMain.on('stopFolderWatcher', () => {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -137,16 +134,16 @@ function init () {
* Shell * Shell
*/ */
ipcMain.on('openPath', (e, ...args) => { ipcMain.on('openPath', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.openPath(...args) shell.openPath(...args)
}) })
ipcMain.on('showItemInFolder', (e, ...args) => { ipcMain.on('showItemInFolder', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.showItemInFolder(...args) shell.showItemInFolder(...args)
}) })
ipcMain.on('moveItemToTrash', (e, ...args) => { ipcMain.on('moveItemToTrash', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.moveItemToTrash(...args) shell.moveItemToTrash(...args)
}) })
@@ -154,8 +151,8 @@ function init () {
* File handlers * File handlers
*/ */
ipcMain.on('setDefaultFileHandler', (e, flag) => { ipcMain.on('setDefaultFileHandler', async (e, flag) => {
const handlers = require('./handlers') const handlers = await import('./handlers')
if (flag) handlers.install() if (flag) handlers.install()
else handlers.uninstall() else handlers.uninstall()
@@ -165,8 +162,8 @@ function init () {
* Auto start on login * Auto start on login
*/ */
ipcMain.on('setStartup', (e, flag) => { ipcMain.on('setStartup', async (e, flag) => {
const startup = require('./startup') const startup = await import('./startup')
if (flag) startup.install() if (flag) startup.install()
else startup.uninstall() else startup.uninstall()
@@ -190,18 +187,18 @@ function init () {
* External Media Player * External Media Player
*/ */
ipcMain.on('checkForExternalPlayer', function (e, path) { ipcMain.on('checkForExternalPlayer', async (e, path) => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
externalPlayer.checkInstall(path, function (err) { externalPlayer.checkInstall(path, err => {
windows.main.send('checkForExternalPlayer', !err) windows.main.send('checkForExternalPlayer', !err)
}) })
}) })
ipcMain.on('openExternalPlayer', (e, ...args) => { ipcMain.on('openExternalPlayer', async (e, ...args) => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
const shortcuts = require('./shortcuts') const shortcuts = await import('./shortcuts')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.togglePlaybackControls(false) menu.togglePlaybackControls(false)
shortcuts.disable() shortcuts.disable()
@@ -209,8 +206,8 @@ function init () {
externalPlayer.spawn(...args) externalPlayer.spawn(...args)
}) })
ipcMain.on('quitExternalPlayer', () => { ipcMain.on('quitExternalPlayer', async () => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
externalPlayer.kill() externalPlayer.kill()
}) })
@@ -219,7 +216,7 @@ function init () {
*/ */
const oldEmit = ipcMain.emit const oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) { ipcMain.emit = (name, e, ...args) => {
// Relay messages between the main window and the WebTorrent hidden window // Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) { if (name.startsWith('wt-') && !app.isQuitting) {
console.dir(e.sender.getTitle()) console.dir(e.sender.getTitle())
@@ -246,3 +243,10 @@ function init () {
oldEmit.call(ipcMain, name, e, ...args) oldEmit.call(ipcMain, name, e, ...args)
} }
} }
export { init }
export { setModule }
export default {
init,
setModule
}

View File

@@ -1,5 +1,7 @@
module.exports = log import electron from '../../electron.cjs'
module.exports.error = error import * as windows from './windows/index.js'
const { app } = electron
/** /**
* In the main electron process, we do not use console.log() statements because they do * In the main electron process, we do not use console.log() statements because they do
@@ -7,11 +9,7 @@ module.exports.error = error
* version of the app. Instead use this module, which sends the logs to the main window * version of the app. Instead use this module, which sends the logs to the main window
* where they can be viewed in Developer Tools. * where they can be viewed in Developer Tools.
*/ */
export default function log (...args) {
const { app } = require('electron')
const windows = require('./windows')
function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('log', ...args) windows.main.send('log', ...args)
} else { } else {
@@ -19,7 +17,7 @@ function log (...args) {
} }
} }
function error (...args) { export function error (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('error', ...args) windows.main.send('error', ...args)
} else { } else {

View File

@@ -1,23 +1,14 @@
module.exports = { import electron from '../../electron.cjs'
init, import config from '../config.js'
togglePlaybackControls, import * as windows from './windows/index.js'
setWindowFocus, import shell from './shell.js'
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}
const { app, Menu } = require('electron')
const config = require('../config')
const windows = require('./windows')
let menu = null let menu = null
function init () { function init () {
menu = Menu.buildFromTemplate(getMenuTemplate()) console.log('menu init')
Menu.setApplicationMenu(menu) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
} }
function togglePlaybackControls (flag) { function togglePlaybackControls (flag) {
@@ -68,11 +59,9 @@ function onToggleFullScreen (flag) {
} }
function getMenuItem (label) { function getMenuItem (label) {
for (let i = 0; i < menu.items.length; i++) { for (const menuItem of menu.items) {
const menuItem = menu.items[i].submenu.items.find(function (item) { const submenuItem = menuItem.submenu.items.find(item => item.label === label)
return item.label === label if (submenuItem) return submenuItem
})
if (menuItem) return menuItem
} }
return {} return {}
} }
@@ -87,24 +76,24 @@ function getMenuTemplate () {
? 'Create New Torrent...' ? 'Create New Torrent...'
: 'Create New Torrent from Folder...', : 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N', accelerator: 'CmdOrCtrl+N',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openSeedDirectory() dialog.openSeedDirectory()
} }
}, },
{ {
label: 'Open Torrent File...', label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O', accelerator: 'CmdOrCtrl+O',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentFile() dialog.openTorrentFile()
} }
}, },
{ {
label: 'Open Torrent Address...', label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U', accelerator: 'CmdOrCtrl+U',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentAddress() dialog.openTorrentAddress()
} }
}, },
@@ -305,21 +294,18 @@ function getMenuTemplate () {
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.HOME_PAGE_URL) shell.openExternal(config.HOME_PAGE_URL)
} }
}, },
{ {
label: 'Release Notes', label: 'Release Notes',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_RELEASES) shell.openExternal(config.GITHUB_URL_RELEASES)
} }
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL) shell.openExternal(config.GITHUB_URL)
} }
}, },
@@ -329,21 +315,18 @@ function getMenuTemplate () {
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES) shell.openExternal(config.GITHUB_URL_ISSUES)
} }
}, },
{ {
label: 'Follow us on Twitter', label: 'Follow us on Twitter',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.TWITTER_PAGE_URL) shell.openExternal(config.TWITTER_PAGE_URL)
} }
} }
] ]
} }
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// WebTorrent menu (Mac) // WebTorrent menu (Mac)
template.unshift({ template.unshift({
@@ -428,8 +411,8 @@ function getMenuTemplate () {
// File menu (Windows, Linux) // File menu (Windows, Linux)
template[0].submenu.unshift({ template[0].submenu.unshift({
label: 'Create New Torrent from File...', label: 'Create New Torrent from File...',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openSeedFile() dialog.openSeedFile()
} }
}) })
@@ -462,9 +445,26 @@ function getMenuTemplate () {
// File menu (Linux) // File menu (Linux)
template[0].submenu.push({ template[0].submenu.push({
label: 'Quit', label: 'Quit',
click: () => app.quit() click: () => electron.app.quit()
}) })
} }
return template return template
} }
export { init }
export { togglePlaybackControls }
export { setWindowFocus }
export { setAllowNav }
export { onPlayerUpdate }
export { onToggleAlwaysOnTop }
export { onToggleFullScreen }
export default {
init,
togglePlaybackControls,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}

View File

@@ -1,10 +1,5 @@
module.exports = { import { powerSaveBlocker } from 'electron'
enable, import log from './log.js'
disable
}
const { powerSaveBlocker } = require('electron')
const log = require('./log')
let blockId = 0 let blockId = 0
@@ -12,7 +7,7 @@ let blockId = 0
* Block the system from entering low-power (sleep) mode or turning off the * Block the system from entering low-power (sleep) mode or turning off the
* display. * display.
*/ */
function enable () { export function enable () {
if (powerSaveBlocker.isStarted(blockId)) { if (powerSaveBlocker.isStarted(blockId)) {
// If a power saver block already exists, do nothing. // If a power saver block already exists, do nothing.
return return
@@ -24,7 +19,7 @@ function enable () {
/** /**
* Stop blocking the system from entering low-power mode. * Stop blocking the system from entering low-power mode.
*/ */
function disable () { export function disable () {
if (!powerSaveBlocker.isStarted(blockId)) { if (!powerSaveBlocker.isStarted(blockId)) {
// If a power saver block does not exist, do nothing. // If a power saver block does not exist, do nothing.
return return

View File

@@ -1,12 +1,7 @@
module.exports = { import electron from '../../electron.cjs'
openExternal, import log from './log.js'
openPath,
showItemInFolder,
moveItemToTrash
}
const { shell } = require('electron') const { shell } = electron
const log = require('./log')
/** /**
* Open the given external protocol URL in the desktops default manner. * Open the given external protocol URL in the desktops default manner.
@@ -19,7 +14,6 @@ function openExternal (url) {
/** /**
* Open the given file in the desktops default manner. * Open the given file in the desktops default manner.
*/ */
function openPath (path) { function openPath (path) {
log(`openPath: ${path}`) log(`openPath: ${path}`)
shell.openPath(path) shell.openPath(path)
@@ -38,5 +32,12 @@ function showItemInFolder (path) {
*/ */
function moveItemToTrash (path) { function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`) log(`moveItemToTrash: ${path}`)
shell.moveItemToTrash(path) shell.trashItem(path)
}
export default {
openExternal,
openPath,
showItemInFolder,
moveItemToTrash
} }

View File

@@ -1,12 +1,7 @@
module.exports = { import { globalShortcut } from 'electron'
disable, import * as windows from './windows'
enable
}
const { globalShortcut } = require('electron') export function enable () {
const windows = require('./windows')
function enable () {
// Register play/pause media key, available on some keyboards. // Register play/pause media key, available on some keyboards.
globalShortcut.register( globalShortcut.register(
'MediaPlayPause', 'MediaPlayPause',
@@ -22,7 +17,7 @@ function enable () {
) )
} }
function disable () { export function disable () {
// Return the media key to the OS, so other apps can use it. // Return the media key to the OS, so other apps can use it.
globalShortcut.unregister('MediaPlayPause') globalShortcut.unregister('MediaPlayPause')
globalShortcut.unregister('MediaNextTrack') globalShortcut.unregister('MediaNextTrack')

View File

@@ -1,23 +1,16 @@
module.exports = { import { app } from 'electron'
handleEvent import path from 'path'
} import { spawn } from 'child_process'
import handlers from './handlers.js'
const { app } = require('electron')
const path = require('path')
const spawn = require('child_process').spawn
const handlers = require('./handlers')
const EXE_NAME = path.basename(process.execPath) const EXE_NAME = path.basename(process.execPath)
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
const run = function (args, done) { const run = (args, done) => {
spawn(UPDATE_EXE, args, { detached: true }) spawn(UPDATE_EXE, args, { detached: true }).on('close', done)
.on('close', done)
} }
function handleEvent (cmd) { export function handleEvent (cmd) {
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') { if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
run([`--createShortcut=${EXE_NAME}`], app.quit) run([`--createShortcut=${EXE_NAME}`], app.quit)
return true return true

View File

@@ -1,17 +1,12 @@
module.exports = { import { APP_NAME } from '../config.js'
install, import AutoLaunch from 'auto-launch'
uninstall
}
const { APP_NAME } = require('../config')
const AutoLaunch = require('auto-launch')
const appLauncher = new AutoLaunch({ const appLauncher = new AutoLaunch({
name: APP_NAME, name: APP_NAME,
isHidden: true isHidden: true
}) })
function install () { export function install () {
return appLauncher return appLauncher
.isEnabled() .isEnabled()
.then(enabled => { .then(enabled => {
@@ -19,7 +14,7 @@ function install () {
}) })
} }
function uninstall () { export function uninstall () {
return appLauncher return appLauncher
.isEnabled() .isEnabled()
.then(enabled => { .then(enabled => {

View File

@@ -1,21 +1,6 @@
module.exports = { import path from 'path'
disable, import config from '../config.js'
enable, import * as windows from './windows'
onPlayerPause,
onPlayerPlay,
onPlayerUpdate
}
/**
* On Windows, add a "thumbnail toolbar" with a play/pause button in the taskbar.
* This provides users a way to access play/pause functionality without restoring
* or activating the window.
*/
const path = require('path')
const config = require('../config')
const windows = require('./windows')
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png') const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png') const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
@@ -32,7 +17,7 @@ let buttons = []
/** /**
* Show the Windows thumbnail toolbar buttons. * Show the Windows thumbnail toolbar buttons.
*/ */
function enable () { export function enable () {
buttons = [ buttons = [
{ {
tooltip: 'Previous Track', tooltip: 'Previous Track',
@@ -56,26 +41,26 @@ function enable () {
/** /**
* Hide the Windows thumbnail toolbar buttons. * Hide the Windows thumbnail toolbar buttons.
*/ */
function disable () { export function disable () {
buttons = [] buttons = []
update() update()
} }
function onPlayerPause () { export function onPlayerPause () {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Play' buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON buttons[PLAY_PAUSE].icon = PLAY_ICON
update() update()
} }
function onPlayerPlay () { export function onPlayerPlay () {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Pause' buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON buttons[PLAY_PAUSE].icon = PAUSE_ICON
update() update()
} }
function onPlayerUpdate (state) { export function onPlayerUpdate (state) {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled'] buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled'] buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']

View File

@@ -1,17 +1,10 @@
module.exports = { import { app, Tray, Menu } from 'electron'
hasTray, import config from '../config.js'
init, import * as windows from './windows'
setWindowFocus
}
const { app, Tray, Menu } = require('electron')
const config = require('../config')
const windows = require('./windows')
let tray let tray
function init () { export function init () {
if (process.platform === 'linux') { if (process.platform === 'linux') {
initLinux() initLinux()
} }
@@ -24,17 +17,17 @@ function init () {
/** /**
* Returns true if there a tray icon is active. * Returns true if there a tray icon is active.
*/ */
function hasTray () { export function hasTray () {
return !!tray return !!tray
} }
function setWindowFocus (flag) { export function setWindowFocus (flag) {
if (!tray) return if (!tray) return
updateTrayMenu() updateTrayMenu()
} }
function initLinux () { function initLinux () {
checkLinuxTraySupport(function (err) { checkLinuxTraySupport(err => {
if (!err) createTray() if (!err) createTray()
}) })
} }
@@ -46,11 +39,11 @@ function initWin32 () {
/** /**
* Check for libappindicator support before creating tray icon. * Check for libappindicator support before creating tray icon.
*/ */
function checkLinuxTraySupport (cb) { async function checkLinuxTraySupport (cb) {
const cp = require('child_process') const cp = await import('child_process')
// Check that libappindicator libraries are installed in system. // Check that libappindicator libraries are installed in system.
cp.exec('ldconfig -p | grep libappindicator', function (err, stdout) { cp.exec('ldconfig -p | grep libappindicator', (err, stdout) => {
if (err) return cb(err) if (err) return cb(err)
cb(null) cb(null)
}) })

View File

@@ -1,13 +1,10 @@
module.exports = { import electron from '../../electron.cjs'
init import get from 'simple-get'
} import config from '../config.js'
import log from './log.js'
import * as windows from './windows/index.js'
const { autoUpdater } = require('electron') const { autoUpdater } = electron
const get = require('simple-get')
const config = require('../config')
const log = require('./log')
const windows = require('./windows')
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL + const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION + '?version=' + config.APP_VERSION +
@@ -22,6 +19,8 @@ function init () {
} }
} }
export default { init }
// The Electron auto-updater does not support Linux yet, so manually check for // The Electron auto-updater does not support Linux yet, so manually check for
// updates and show the user a modal notification. // updates and show the user a modal notification.
function initLinux () { function initLinux () {
@@ -49,7 +48,7 @@ function onResponse (err, res, data) {
function initDarwinWin32 () { function initDarwinWin32 () {
autoUpdater.on( autoUpdater.on(
'error', 'error',
(err) => log.error(`Update error: ${err.message}`) (err) => log(`Update error: ${err.message}`)
) )
autoUpdater.on( autoUpdater.on(

View File

@@ -1,13 +1,9 @@
module.exports = { import { app } from 'electron'
init
}
const { app } = require('electron')
/** /**
* Add a user task menu to the app icon on right-click. (Windows) * Add a user task menu to the app icon on right-click. (Windows)
*/ */
function init () { export function init () {
if (process.platform !== 'win32') return if (process.platform !== 'win32') return
app.setUserTasks(getUserTasks()) app.setUserTasks(getUserTasks())
} }

View File

@@ -1,17 +1,18 @@
const about = module.exports = { import config from '../../config.js'
import electron from '../../../electron.cjs'
import * as RemoteMain from '@electron/remote/main/index.js'
export const about = {
init, init,
win: null win: null
} }
const config = require('../../config')
const { BrowserWindow } = require('electron')
function init () { function init () {
if (about.win) { if (about.win) {
return about.win.show() return about.win.show()
} }
const win = about.win = new BrowserWindow({ const win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC', backgroundColor: '#ECECEC',
center: true, center: true,
fullscreen: false, fullscreen: false,
@@ -33,11 +34,10 @@ function init () {
}, },
width: 300 width: 300
}) })
require('@electron/remote/main').enable(win.webContents) RemoteMain.enable(win.webContents)
win.loadURL(config.WINDOW_ABOUT) win.loadURL(config.WINDOW_ABOUT)
win.once('ready-to-show', function () { win.once('ready-to-show', () => {
win.show() win.show()
// No menu on the About window // No menu on the About window
// Hack: BrowserWindow removeMenu method not working on electron@7 // Hack: BrowserWindow removeMenu method not working on electron@7
@@ -45,7 +45,7 @@ function init () {
win.setMenuBarVisibility(false) win.setMenuBarVisibility(false)
}) })
win.once('closed', function () { win.once('closed', () => {
about.win = null about.win = null
}) })
} }

View File

@@ -1,3 +1,3 @@
exports.about = require('./about') export * from './about.js'
exports.main = require('./main') export * from './main.js'
exports.webtorrent = require('./webtorrent') export * from './webtorrent.js'

View File

@@ -1,4 +1,13 @@
const main = module.exports = { import debounce from 'debounce'
import electron from '../../../electron.cjs'
import config from '../../config.js'
import log from '../log.js'
import menu from '../menu.js'
import * as RemoteMain from '@electron/remote/main/index.js'
const { app, BrowserWindow, screen } = electron
export const main = {
dispatch, dispatch,
hide, hide,
init, init,
@@ -14,13 +23,6 @@ const main = module.exports = {
win: null win: null
} }
const { app, BrowserWindow, screen } = require('electron')
const debounce = require('debounce')
const config = require('../../config')
const log = require('../log')
const menu = require('../menu')
function init (state, options) { function init (state, options) {
if (main.win) { if (main.win) {
return main.win.show() return main.win.show()
@@ -50,8 +52,8 @@ function init (state, options) {
x: initialBounds.x, x: initialBounds.x,
y: initialBounds.y y: initialBounds.y
}) })
require('@electron/remote/main').enable(win.webContents) RemoteMain.enable(win.webContents)
console.log(config.WINDOW_MAIN)
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
win.once('ready-to-show', () => { win.once('ready-to-show', () => {
@@ -98,10 +100,10 @@ function init (state, options) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}, 1000)) }, 1000))
win.on('close', e => { win.on('close', async e => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const tray = require('../tray') const { hasTray } = await import('../tray.js')
if (!tray.hasTray()) { if (!hasTray()) {
app.quit() app.quit()
return return
} }

View File

@@ -1,4 +1,8 @@
const webtorrent = module.exports = { import electron from '../../../electron.cjs'
import config from '../../config.js'
import * as RemoteMain from '@electron/remote/main/index.js'
export const webtorrent = {
init, init,
send, send,
show, show,
@@ -6,12 +10,8 @@ const webtorrent = module.exports = {
win: null win: null
} }
const { app, BrowserWindow } = require('electron')
const config = require('../../config')
function init () { function init () {
const win = webtorrent.win = new BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
center: true, center: true,
fullscreen: false, fullscreen: false,
@@ -33,13 +33,11 @@ function init () {
}, },
width: 150 width: 150
}) })
require('@electron/remote/main').enable(win.webContents) RemoteMain.enable(win.webContents)
win.loadURL(config.WINDOW_WEBTORRENT) win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process // Prevent killing the WebTorrent process
win.on('close', function (e) { win.on('close', e => {
if (app.isQuitting) { if (electron.app.isQuitting) {
return return
} }
e.preventDefault() e.preventDefault()

View File

@@ -1,8 +1,7 @@
const React = require('react') import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const { dispatcher } = require('../lib/dispatcher') export default class CreateTorrentErrorPage extends React.Component {
module.exports = class CreateTorrentErrorPage extends React.Component {
render () { render () {
return ( return (
<div className='create-torrent'> <div className='create-torrent'>

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class DeleteAllTorrentsModal extends React.Component {
const { dispatch, dispatcher } = require('../lib/dispatcher')
module.exports = class DeleteAllTorrentsModal extends React.Component {
render () { render () {
const { state: { modal: { deleteData } } } = this.props const { state: { modal: { deleteData } } } = this.props
const message = deleteData const message = deleteData

View File

@@ -1,8 +1,7 @@
const React = require('react') import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const { dispatcher } = require('../lib/dispatcher') export default class Header extends React.Component {
class Header extends React.Component {
render () { render () {
const loc = this.props.state.location const loc = this.props.state.location
return ( return (
@@ -56,5 +55,3 @@ class Header extends React.Component {
) )
} }
} }
module.exports = Header

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
const colors = require('material-ui/styles/colors') export default class Heading extends React.Component {
class Heading extends React.Component {
static get propTypes () { static get propTypes () {
return { return {
level: PropTypes.number level: PropTypes.number
@@ -31,5 +30,3 @@ class Heading extends React.Component {
) )
} }
} }
module.exports = Heading

View File

@@ -1,8 +1,10 @@
const React = require('react') import * as React from 'react'
const FlatButton = require('material-ui/FlatButton').default import flatButton from 'material-ui/FlatButton'
const RaisedButton = require('material-ui/RaisedButton').default import raisedButton from 'material-ui/RaisedButton'
module.exports = class ModalOKCancel extends React.Component { const FlatButton = flatButton.default
const RaisedButton = raisedButton.default
export default class ModalOKCancel extends React.Component {
render () { render () {
const cancelStyle = { marginRight: 10, color: 'black' } const cancelStyle = { marginRight: 10, color: 'black' }
const { cancelText, onCancel, okText, onOK } = this.props const { cancelText, onCancel, okText, onOK } = this.props

View File

@@ -1,12 +1,14 @@
const React = require('react') import * as React from 'react'
const TextField = require('material-ui/TextField').default import textField from 'material-ui/TextField'
const { clipboard } = require('electron') import electron from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
import { isMagnetLink } from '../lib/torrent-player.js'
const ModalOKCancel = require('./modal-ok-cancel') const TextField = textField.default
const { dispatch, dispatcher } = require('../lib/dispatcher') const { clipboard } = electron
const { isMagnetLink } = require('../lib/torrent-player')
module.exports = class OpenTorrentAddressModal extends React.Component { export default class OpenTorrentAddressModal extends React.Component {
render () { render () {
return ( return (
<div className='open-torrent-address-modal'> <div className='open-torrent-address-modal'>

View File

@@ -1,17 +1,17 @@
const path = require('path') import path from 'path'
import * as colors from 'material-ui/styles/colors'
const colors = require('material-ui/styles/colors') import remote from '@electron/remote'
const remote = require('@electron/remote') import * as React from 'react'
const React = require('react') import PropTypes from 'prop-types'
const PropTypes = require('prop-types') import raisedButton from 'material-ui/RaisedButton'
import textField from 'material-ui/TextField'
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const RaisedButton = raisedButton.default
const TextField = textField.default
// Lets you pick a file or directory. // Lets you pick a file or directory.
// Uses the system Open File dialog. // Uses the system Open File dialog.
// You can't edit the text field directly. // You can't edit the text field directly.
class PathSelector extends React.Component { export default class PathSelector extends React.Component {
static propTypes () { static propTypes () {
return { return {
className: PropTypes.string, className: PropTypes.string,
@@ -81,5 +81,3 @@ class PathSelector extends React.Component {
) )
} }
} }
module.exports = PathSelector

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class RemoveTorrentModal extends React.Component {
const { dispatch, dispatcher } = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
const message = state.modal.deleteData const message = state.modal.deleteData

View File

@@ -1,9 +1,10 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import raisedButton from 'material-ui/RaisedButton'
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = raisedButton.default
class ShowMore extends React.Component { export default class ShowMore extends React.Component {
static get propTypes () { static get propTypes () {
return { return {
defaultExpanded: PropTypes.bool, defaultExpanded: PropTypes.bool,
@@ -21,11 +22,9 @@ class ShowMore extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
expanded: !!this.props.defaultExpanded expanded: !!this.props.defaultExpanded
} }
this.handleClick = this.handleClick.bind(this) this.handleClick = this.handleClick.bind(this)
} }
@@ -51,5 +50,3 @@ class ShowMore extends React.Component {
) )
} }
} }
module.exports = ShowMore

View File

@@ -1,10 +1,9 @@
const React = require('react') import * as React from 'react'
const { shell } = require('electron') import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class UnsupportedMediaModal extends React.Component {
const { dispatcher } = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
const err = state.modal.error const err = state.modal.error

View File

@@ -1,10 +1,9 @@
const React = require('react') import * as React from 'react'
const { shell } = require('electron') import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class UpdateAvailableModal extends React.Component {
const { dispatch } = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
return ( return (

View File

@@ -1,6 +1,6 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
module.exports = class AudioTracksController { export default class AudioTracksController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }

View File

@@ -1,6 +1,6 @@
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
module.exports = class FolderWatcherController { export default class FolderWatcherController {
start () { start () {
console.log('-- IPC: start folder watcher') console.log('-- IPC: start folder watcher')
ipcRenderer.send('startFolderWatcher') ipcRenderer.send('startFolderWatcher')

View File

@@ -1,10 +1,8 @@
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
const telemetry = require('../lib/telemetry') import telemetry from '../lib/telemetry.js'
const Playlist = require('../lib/playlist') import Playlist from '../lib/playlist.js'
// Controls local play back: the <video>/<audio> tag and VLC export default class MediaController {
// Does not control remote casting (Chromecast etc)
module.exports = class MediaController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -23,7 +21,7 @@ module.exports = class MediaController {
telemetry.logPlayAttempt('error') telemetry.logPlayAttempt('error')
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath) ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) { ipcRenderer.once('checkForExternalPlayer', (e, isInstalled) => {
state.modal = { state.modal = {
id: 'unsupported-media-modal', id: 'unsupported-media-modal',
error, error,
@@ -56,7 +54,7 @@ module.exports = class MediaController {
const state = this.state const state = this.state
state.playing.location = 'external' state.playing.location = 'external'
const onServerRunning = function () { const onServerRunning = () => {
state.playing.isReady = true state.playing.isReady = true
telemetry.logPlayAttempt('external') telemetry.logPlayAttempt('external')

View File

@@ -1,19 +1,29 @@
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
const path = require('path') import path from 'path'
import Cast from '../lib/cast.js'
const Cast = require('../lib/cast') import { dispatch } from '../lib/dispatcher.js'
const { dispatch } = require('../lib/dispatcher') import telemetry from '../lib/telemetry.js'
const telemetry = require('../lib/telemetry') import { UnplayableFileError, UnplayableTorrentError } from '../lib/errors.js'
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors') import sound from '../lib/sound.js'
const sound = require('../lib/sound') import TorrentPlayer from '../lib/torrent-player.js'
const TorrentPlayer = require('../lib/torrent-player') import TorrentSummary from '../lib/torrent-summary.js'
const TorrentSummary = require('../lib/torrent-summary') import Playlist from '../lib/playlist.js'
const Playlist = require('../lib/playlist') import State from '../lib/state.js'
const State = require('../lib/state') // Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
// Controls playback of torrents and files within torrents // or if we're trying to connect but haven't yet ('chromecast-pending', etc)
// both local (<video>,<audio>,external player) and remote (cast) function isCasting (state) {
module.exports = class PlaybackController { return state.playing.location === 'chromecast' ||
state.playing.location === 'airplay' ||
state.playing.location === 'dlna'
}
function restoreBounds (state) {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}
export default class PlaybackController {
constructor (state, config, update) { constructor (state, config, update) {
this.state = state this.state = state
this.config = config this.config = config
@@ -26,12 +36,10 @@ module.exports = class PlaybackController {
// * If no file index is provided, restore the most recently viewed file or autoplay the first // * If no file index is provided, restore the most recently viewed file or autoplay the first
playFile (infoHash, index /* optional */) { playFile (infoHash, index /* optional */) {
this.pauseActiveTorrents(infoHash) this.pauseActiveTorrents(infoHash)
const state = this.state const state = this.state
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
this.updatePlayer(infoHash, index, false, (err) => { this.updatePlayer(infoHash, index, false, (err) => {
if (err) dispatch('error', err) if (err) { dispatch('error', err) } else { this.play() }
else this.play()
}) })
} else { } else {
let initialized = false let initialized = false
@@ -39,13 +47,10 @@ module.exports = class PlaybackController {
url: 'player', url: 'player',
setup: (cb) => { setup: (cb) => {
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable) if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new UnplayableTorrentError()) if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true initialized = true
this.openPlayer(infoHash, index, (err) => { this.openPlayer(infoHash, index, (err) => {
if (!err) this.play() if (!err) this.play()
cb(err) cb(err)
@@ -53,7 +58,7 @@ module.exports = class PlaybackController {
}, },
destroy: () => this.closePlayer() destroy: () => this.closePlayer()
}, (err) => { }, (err) => {
if (err) dispatch('error', err) if (err) { dispatch('error', err) }
}) })
} }
} }
@@ -61,9 +66,7 @@ module.exports = class PlaybackController {
// Open a file in OS default app. // Open a file in OS default app.
openPath (infoHash, index) { openPath (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join( const filePath = path.join(torrentSummary.path, torrentSummary.files[index].path)
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openPath', filePath) ipcRenderer.send('openPath', filePath)
} }
@@ -71,27 +74,21 @@ module.exports = class PlaybackController {
playPause () { playPause () {
const state = this.state const state = this.state
if (state.location.url() !== 'player') return if (state.location.url() !== 'player') return
// force rerendering if window is hidden, // force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately // in order to bypass `raf` and play/pause media immediately
const mediaTag = document.querySelector('video,audio') const mediaTag = document.querySelector('video,audio')
if (!state.window.isVisible && mediaTag) { if (!state.window.isVisible && mediaTag) {
if (state.playing.isPaused) mediaTag.play() if (state.playing.isPaused) { mediaTag.play() } else { mediaTag.pause() }
else mediaTag.pause()
} }
if (state.playing.isPaused) { this.play() } else { this.pause() }
if (state.playing.isPaused) this.play()
else this.pause()
} }
pauseActiveTorrents (infoHash) { pauseActiveTorrents (infoHash) {
// Playback Priority: pause all active torrents if needed. // Playback Priority: pause all active torrents if needed.
if (!this.state.saved.prefs.highestPlaybackPriority) return if (!this.state.saved.prefs.highestPlaybackPriority) return
// Do not pause active torrents if playing a fully downloaded torrent. // Do not pause active torrents if playing a fully downloaded torrent.
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'seeding') return if (torrentSummary.status === 'seeding') return
dispatch('prioritizeTorrent', infoHash) dispatch('prioritizeTorrent', infoHash)
} }
@@ -99,11 +96,9 @@ module.exports = class PlaybackController {
nextTrack () { nextTrack () {
const state = this.state const state = this.state
if (Playlist.hasNext(state) && state.playing.location !== 'external') { if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer( this.updatePlayer(state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => { if (err) { dispatch('error', err) } else { this.play() }
if (err) dispatch('error', err) })
else this.play()
})
} }
} }
@@ -111,11 +106,9 @@ module.exports = class PlaybackController {
previousTrack () { previousTrack () {
const state = this.state const state = this.state
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') { if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer( this.updatePlayer(state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => { if (err) { dispatch('error', err) } else { this.play() }
if (err) dispatch('error', err) })
else this.play()
})
} }
} }
@@ -152,8 +145,7 @@ module.exports = class PlaybackController {
console.error('Tried to skip to a non-finite time ' + time) console.error('Tried to skip to a non-finite time ' + time)
return console.trace() return console.trace()
} }
if (isCasting(this.state)) Cast.seek(time) if (isCasting(this.state)) { Cast.seek(time) } else { this.state.playing.jumpToTime = time }
else this.state.playing.jumpToTime = time
} }
// Show video preview // Show video preview
@@ -205,7 +197,6 @@ module.exports = class PlaybackController {
setVolume (volume) { setVolume (volume) {
// check if its in [0.0 - 1.0] range // check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume)) volume = Math.max(0, Math.min(1, volume))
const state = this.state const state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.setVolume(volume) Cast.setVolume(volume)
@@ -222,7 +213,6 @@ module.exports = class PlaybackController {
showOrHidePlayerControls () { showOrHidePlayerControls () {
const state = this.state const state = this.state
const hideControls = state.shouldHidePlayerControls() const hideControls = state.shouldHidePlayerControls()
if (hideControls !== state.playing.hideControls) { if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls state.playing.hideControls = hideControls
return true return true
@@ -234,13 +224,10 @@ module.exports = class PlaybackController {
openPlayer (infoHash, index, cb) { openPlayer (infoHash, index, cb) {
const state = this.state const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
state.playing.infoHash = torrentSummary.infoHash state.playing.infoHash = torrentSummary.infoHash
state.playing.isReady = false state.playing.isReady = false
// update UI to show pending playback // update UI to show pending playback
sound.play('PLAY') sound.play('PLAY')
this.startServer(torrentSummary) this.startServer(torrentSummary)
ipcRenderer.send('onPlayerOpen') ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb) this.updatePlayer(infoHash, index, true, cb)
@@ -249,15 +236,12 @@ module.exports = class PlaybackController {
// Starts WebTorrent server for media streaming // Starts WebTorrent server for media streaming
startServer (torrentSummary) { startServer (torrentSummary) {
const state = this.state const state = this.state
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey) dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, () => onTorrentReady())
() => onTorrentReady())
} else { } else {
onTorrentReady() onTorrentReady()
} }
function onTorrentReady () { function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash) ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true }) ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
@@ -267,17 +251,13 @@ module.exports = class PlaybackController {
// Called each time the current file changes // Called each time the current file changes
updatePlayer (infoHash, index, resume, cb) { updatePlayer (infoHash, index, resume, cb) {
const state = this.state const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
const fileSummary = torrentSummary.files[index] const fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) { if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined torrentSummary.mostRecentFileIndex = undefined
return cb(new UnplayableFileError()) return cb(new UnplayableFileError())
} }
torrentSummary.mostRecentFileIndex = index torrentSummary.mostRecentFileIndex = index
// update state // update state
state.playing.infoHash = infoHash state.playing.infoHash = infoHash
state.playing.fileIndex = index state.playing.fileIndex = index
@@ -287,8 +267,7 @@ module.exports = class PlaybackController {
: TorrentPlayer.isAudio(fileSummary) : TorrentPlayer.isAudio(fileSummary)
? 'audio' ? 'audio'
: 'other' : 'other'
// pick up where we left off
// pick up where we left off
let jumpToTime = 0 let jumpToTime = 0
if (resume && fileSummary.currentTime) { if (resume && fileSummary.currentTime) {
const fraction = fileSummary.currentTime / fileSummary.duration const fraction = fileSummary.currentTime / fileSummary.duration
@@ -298,30 +277,24 @@ module.exports = class PlaybackController {
} }
} }
state.playing.jumpToTime = jumpToTime state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata) ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
} else { } else {
getAudioMetadata() getAudioMetadata()
} }
function getAudioMetadata () { function getAudioMetadata () {
if (state.playing.type === 'audio') { if (state.playing.type === 'audio') {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
} }
} }
// if it's video, check for subtitles files that are done downloading // if it's video, check for subtitles files that are done downloading
dispatch('checkForSubtitles') dispatch('checkForSubtitles')
// enable previously selected subtitle track // enable previously selected subtitle track
if (fileSummary.selectedSubtitle) { if (fileSummary.selectedSubtitle) {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
} }
state.window.title = fileSummary.name state.window.title = fileSummary.name
// play in VLC if set as default player (Preferences / Playback / Play in VLC) // play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) { if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer') dispatch('openExternalPlayer')
@@ -329,10 +302,8 @@ module.exports = class PlaybackController {
cb() cb()
return return
} }
// otherwise, play the video // otherwise, play the video
this.update() this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state)) ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb() cb()
} }
@@ -363,33 +334,13 @@ module.exports = class PlaybackController {
dispatch('toggleFullScreen', false) dispatch('toggleFullScreen', false)
} }
restoreBounds(state) restoreBounds(state)
// Tell the WebTorrent process to kill the torrent-to-HTTP server // Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server') ipcRenderer.send('wt-stop-server')
ipcRenderer.send('onPlayerClose') ipcRenderer.send('onPlayerClose')
// Playback Priority: resume previously paused downloads. // Playback Priority: resume previously paused downloads.
if (this.state.saved.prefs.highestPlaybackPriority) { if (this.state.saved.prefs.highestPlaybackPriority) {
dispatch('resumePausedTorrents') dispatch('resumePausedTorrents')
} }
this.update() this.update()
} }
} }
// Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting (state) {
return state.playing.location === 'chromecast' ||
state.playing.location === 'airplay' ||
state.playing.location === 'dlna'
}
function restoreBounds (state) {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}

View File

@@ -1,8 +1,7 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
// Controls the Preferences screen export default class PrefsController {
module.exports = class PrefsController {
constructor (state, config) { constructor (state, config) {
this.state = state this.state = state
this.config = config this.config = config
@@ -13,7 +12,7 @@ module.exports = class PrefsController {
const state = this.state const state = this.state
state.location.go({ state.location.go({
url: 'preferences', url: 'preferences',
setup: function (cb) { setup (cb) {
// initialize preferences // initialize preferences
state.window.title = 'Preferences' state.window.title = 'Preferences'
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)

View File

@@ -1,11 +1,10 @@
const remote = require('@electron/remote') import remote from '@electron/remote'
const fs = require('fs') import fs from 'fs'
const path = require('path') import path from 'path'
const parallel = require('run-parallel') import parallel from 'run-parallel'
import { dispatch } from '../lib/dispatcher.js'
const { dispatch } = require('../lib/dispatcher') export default class SubtitlesController {
module.exports = class SubtitlesController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -37,12 +36,11 @@ module.exports = class SubtitlesController {
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) { parallel(tasks, (err, tracks) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
for (let i = 0; i < tracks.length; i++) { // No dupes allowed
// No dupes allowed tracks.forEach((track, i) => {
const track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) => let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath) track.filePath === t.filePath)
@@ -55,7 +53,7 @@ module.exports = class SubtitlesController {
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) { if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
subtitles.selectedIndex = trackIndex subtitles.selectedIndex = trackIndex
} }
} })
// Finally, make sure no two tracks have the same label // Finally, make sure no two tracks have the same label
relabelSubtitles(subtitles) relabelSubtitles(subtitles)
@@ -83,18 +81,18 @@ module.exports = class SubtitlesController {
} }
} }
function loadSubtitle (file, cb) { async function loadSubtitle (file, cb) {
// Lazy load to keep startup fast // Lazy load to keep startup fast
const concat = require('simple-concat') const concat = await import('simple-concat')
const LanguageDetect = require('languagedetect') const LanguageDetect = await import('languagedetect')
const srtToVtt = require('srt-to-vtt') const srtToVtt = await import('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track // Read the .SRT or .VTT file, parse it, add subtitle track
const filePath = file.path || file const filePath = file.path || file
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt()) const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, function (err, buf) { concat(vttStream, (err, buf) => {
if (err) return dispatch('error', 'Can\'t parse subtitles file.') if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in // Detect what language the subtitles are in
@@ -116,8 +114,8 @@ function loadSubtitle (file, cb) {
// Checks whether a language name like 'English' or 'German' matches the system // Checks whether a language name like 'English' or 'German' matches the system
// language, aka the current locale // language, aka the current locale
function isSystemLanguage (language) { async function isSystemLanguage (language) {
const iso639 = require('iso-639-1') const iso639 = await import('iso-639-1')
const osLangISO = window.navigator.language.split('-')[0] // eg 'en' const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
const langIso = iso639.getCode(language) // eg 'de' if language is 'German' const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO return langIso === osLangISO
@@ -127,7 +125,7 @@ function isSystemLanguage (language) {
// Labels each track by language, eg 'German', 'English', 'English 2', ... // Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) { function relabelSubtitles (subtitles) {
const counts = {} const counts = {}
subtitles.tracks.forEach(function (track) { subtitles.tracks.forEach(track => {
const lang = track.language const lang = track.language
counts[lang] = (counts[lang] || 0) + 1 counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang

View File

@@ -1,11 +1,10 @@
const path = require('path') import path from 'path'
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
import TorrentSummary from '../lib/torrent-summary.js'
import sound from '../lib/sound.js'
import { dispatch } from '../lib/dispatcher.js'
const TorrentSummary = require('../lib/torrent-summary') export default class TorrentController {
const sound = require('../lib/sound')
const { dispatch } = require('../lib/dispatcher')
module.exports = class TorrentController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -177,7 +176,7 @@ function showDoneNotification (torrent) {
silent: true silent: true
}) })
notif.onclick = function () { notif.onclick = () => {
ipcRenderer.send('show') ipcRenderer.send('show')
} }

View File

@@ -1,17 +1,18 @@
const fs = require('fs') import fs from 'fs'
const path = require('path') import path from 'path'
const { ipcRenderer, clipboard } = require('electron') import electron from 'electron'
const remote = require('@electron/remote') import remote from '@electron/remote'
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
const { TorrentKeyNotFoundError } = require('../lib/errors') import { TorrentKeyNotFoundError } from '../lib/errors.js'
const sound = require('../lib/sound') import sound from '../lib/sound.js'
const TorrentSummary = require('../lib/torrent-summary') import TorrentSummary from '../lib/torrent-summary.js'
const { ipcRenderer, clipboard } = electron
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/ const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
// Controls the torrent list: creating, adding, deleting, & manipulating torrents export default class TorrentListController {
module.exports = class TorrentListController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -94,7 +95,7 @@ module.exports = class TorrentListController {
if (!fileOrFolder) return start() if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there // Existing torrent: check that the path is still there
fs.stat(fileOrFolder, function (err) { fs.stat(fileOrFolder, err => {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
dispatch('backToList') dispatch('backToList')
@@ -156,9 +157,7 @@ module.exports = class TorrentListController {
prioritizeTorrent (infoHash) { prioritizeTorrent (infoHash) {
this.state.saved.torrents this.state.saved.torrents
.filter((torrent) => { // We're interested in active torrents only. .filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only.
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
})
.forEach((torrent) => { // Pause all active torrents except the one that started playing. .forEach((torrent) => { // Pause all active torrents except the one that started playing.
if (infoHash === torrent.infoHash) return if (infoHash === torrent.infoHash) return
@@ -331,9 +330,9 @@ module.exports = class TorrentListController {
if (!savePath) return // They clicked Cancel if (!savePath) return // They clicked Cancel
console.log('Saving torrent ' + torrentKey + ' to ' + savePath) console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary) const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, (err, torrentFile) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) { fs.writeFile(savePath, torrentFile, err => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
}) })
}) })
@@ -346,8 +345,8 @@ function findFilesRecursive (paths, cb_) {
if (paths.length > 1) { if (paths.length > 1) {
let numComplete = 0 let numComplete = 0
const ret = [] const ret = []
paths.forEach(function (path) { paths.forEach(path => {
findFilesRecursive([path], function (fileObjs) { findFilesRecursive([path], fileObjs => {
ret.push(...fileObjs) ret.push(...fileObjs)
if (++numComplete === paths.length) { if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path)) ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path))
@@ -359,7 +358,7 @@ function findFilesRecursive (paths, cb_) {
} }
const fileOrFolder = paths[0] const fileOrFolder = paths[0]
fs.stat(fileOrFolder, function (err, stat) { fs.stat(fileOrFolder, (err, stat) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
// Files: return name, path, and size // Files: return name, path, and size
@@ -374,7 +373,7 @@ function findFilesRecursive (paths, cb_) {
// Folders: recurse, make a list of all the files // Folders: recurse, make a list of all the files
const folderPath = fileOrFolder const folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) { fs.readdir(folderPath, (err, fileNames) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName)) const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb_) findFilesRecursive(paths, cb_)
@@ -384,7 +383,7 @@ function findFilesRecursive (paths, cb_) {
function deleteFile (path) { function deleteFile (path) {
if (!path) return if (!path) return
fs.unlink(path, function (err) { fs.unlink(path, err => {
if (err) dispatch('error', err) if (err) dispatch('error', err)
}) })
} }

View File

@@ -1,7 +1,6 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
// Controls the UI checking for new versions of the app, prompting install export default class UpdateController {
module.exports = class UpdateController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }

View File

@@ -1,40 +1,18 @@
// The Cast module talks to Airplay and Chromecast import http from 'http'
// * Modifies state when things change import config from '../src/config.js'
// * Starts and stops casting, provides remote video controls import { CastingError } from './errors.js'
module.exports = {
init,
toggleMenu,
selectDevice,
stop,
play,
pause,
seek,
setVolume,
setRate
}
const http = require('http')
const config = require('../../config')
const { CastingError } = require('./errors')
// Lazy load these for a ~300ms improvement in startup time // Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts let airplayer, chromecasts, dlnacasts
// App state. Cast modifies state.playing and state.errors in response to events // App state. Cast modifies state.playing and state.errors in response to events
let state let state
// Callback to notify module users when state has changed // Callback to notify module users when state has changed
let update let update
// setInterval() for updating cast status // setInterval() for updating cast status
let statusInterval = null let statusInterval = null
// Start looking for cast devices on the local network // Start looking for cast devices on the local network
function init (appState, callback) { function init (appState, callback) {
state = appState state = appState
update = callback update = callback
// Don't actually cast during integration tests // Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.) // (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) { if (config.IS_TEST) {
@@ -43,32 +21,26 @@ function init (appState, callback) {
state.devices.dlna = testPlayer('dlna') state.devices.dlna = testPlayer('dlna')
return return
} }
// Load modules, scan the network for devices // Load modules, scan the network for devices
airplayer = require('airplayer')() airplayer = require('airplayer')()
chromecasts = require('chromecasts')() chromecasts = require('chromecasts')()
dlnacasts = require('dlnacasts')() dlnacasts = require('dlnacasts')()
state.devices.chromecast = chromecastPlayer() state.devices.chromecast = chromecastPlayer()
state.devices.dlna = dlnaPlayer() state.devices.dlna = dlnaPlayer()
state.devices.airplay = airplayPlayer() state.devices.airplay = airplayPlayer()
// Listen for devices: Chromecast, DLNA and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (device) { chromecasts.on('update', device => {
// TODO: how do we tell if there are *no longer* any Chromecasts available? // TODO: how do we tell if there are *no longer* any Chromecasts available?
// From looking at the code, chromecasts.players only grows, never shrinks // From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device) state.devices.chromecast.addDevice(device)
}) })
dlnacasts.on('update', device => {
dlnacasts.on('update', function (device) {
state.devices.dlna.addDevice(device) state.devices.dlna.addDevice(device)
}) })
airplayer.on('update', device => {
airplayer.on('update', function (device) {
state.devices.airplay.addDevice(device) state.devices.airplay.addDevice(device)
}) })
} }
// integration test player implementation // integration test player implementation
function testPlayer (type) { function testPlayer (type) {
return { return {
@@ -81,20 +53,17 @@ function testPlayer (type) {
seek, seek,
volume volume
} }
function getDevices () { function getDevices () {
return [{ name: type + '-1' }, { name: type + '-2' }] return [{ name: type + '-1' }, { name: type + '-2' }]
} }
function open () { }
function open () {} function play () { }
function play () {} function pause () { }
function pause () {} function stop () { }
function stop () {} function status () { }
function status () {} function seek () { }
function seek () {} function volume () { }
function volume () {}
} }
// chromecast player implementation // chromecast player implementation
function chromecastPlayer () { function chromecastPlayer () {
const ret = { const ret = {
@@ -110,13 +79,11 @@ function chromecastPlayer () {
volume volume
} }
return ret return ret
function getDevices () { function getDevices () {
return chromecasts.players return chromecasts.players
} }
function addDevice (device) { function addDevice (device) {
device.on('error', function (err) { device.on('error', err => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -125,13 +92,12 @@ function chromecastPlayer () {
}) })
update() update()
}) })
device.on('disconnect', function () { device.on('disconnect', () => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
} }
function serveSubtitles (callback) { function serveSubtitles (callback) {
const subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex] const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
@@ -145,23 +111,22 @@ function chromecastPlayer () {
'Transfer-Encoding': 'chunked' 'Transfer-Encoding': 'chunked'
}) })
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64')) res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, function () { }).listen(0, () => {
const port = ret.subServer.address().port const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/' const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl) callback(subtitlesUrl)
}) })
} }
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
serveSubtitles(function (subtitlesUrl) { serveSubtitles(subtitlesUrl => {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
subtitles: subtitlesUrl ? [subtitlesUrl] : [], subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl autoSubtitles: !!subtitlesUrl
}, function (err) { }, err => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -175,35 +140,28 @@ function chromecastPlayer () {
}) })
}) })
} }
function play (callback) { function play (callback) {
ret.device.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
if (ret.subServer) { if (ret.subServer) {
ret.subServer.close() ret.subServer.close()
} }
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(handleStatus)
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
ret.device.volume(volume, callback) ret.device.volume(volume, callback)
} }
} }
// airplay player implementation // airplay player implementation
function airplayPlayer () { function airplayPlayer () {
const ret = { const ret = {
@@ -219,9 +177,8 @@ function airplayPlayer () {
volume volume
} }
return ret return ret
function addDevice (player) { function addDevice (player) {
player.on('event', function (event) { player.on('event', event => {
switch (event.state) { switch (event.state) {
case 'loading': case 'loading':
break break
@@ -237,13 +194,11 @@ function airplayPlayer () {
update() update()
}) })
} }
function getDevices () { function getDevices () {
return airplayer.players return airplayer.players
} }
function open () { function open () {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, (err, res) => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -256,21 +211,17 @@ function airplayPlayer () {
update() update()
}) })
} }
function play (callback) { function play (callback) {
ret.device.resume(callback) ret.device.resume(callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
ret.device.playbackInfo(function (err, res, status) { ret.device.playbackInfo((err, res, status) => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -284,18 +235,15 @@ function airplayPlayer () {
} }
}) })
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.scrub(time, callback) ret.device.scrub(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
// AirPlay doesn't support volume // AirPlay doesn't support volume
// TODO: We should just disable the volume slider // TODO: We should just disable the volume slider
state.playing.volume = volume state.playing.volume = volume
} }
} }
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
const ret = { const ret = {
@@ -311,13 +259,11 @@ function dlnaPlayer (player) {
volume volume
} }
return ret return ret
function getDevices () { function getDevices () {
return dlnacasts.players return dlnacasts.players
} }
function addDevice (device) { function addDevice (device) {
device.on('error', function (err) { device.on('error', err => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -326,20 +272,19 @@ function dlnaPlayer (player) {
}) })
update() update()
}) })
device.on('disconnect', function () { device.on('disconnect', () => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
}, function (err) { }, err => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -352,56 +297,45 @@ function dlnaPlayer (player) {
update() update()
}) })
} }
function play (callback) { function play (callback) {
ret.device.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(handleStatus)
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
ret.device.volume(volume, function (err) { ret.device.volume(volume, err => {
// quick volume update // quick volume update
state.playing.volume = volume state.playing.volume = volume
callback(err) callback(err)
}) })
} }
} }
function handleStatus (err, status) { function handleStatus (err, status) {
if (err || !status) { if (err || !status) {
return console.log('error getting %s status: %o', return console.log('error getting %s status: %o', state.playing.location, err || 'missing response')
state.playing.location,
err || 'missing response')
} }
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level state.playing.volume = status.volume.muted ? 0 : status.volume.level
update() update()
} }
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(function () { statusInterval = setInterval(() => {
const player = getPlayer() const player = getPlayer()
if (player) player.status() if (player) player.status()
}, 1000) }, 1000)
} }
/* /*
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc) * Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
* The menu lists eg. all Chromecasts detected; the user can click one to cast. * The menu lists eg. all Chromecasts detected; the user can click one to cast.
@@ -413,48 +347,38 @@ function toggleMenu (location) {
state.devices.castMenu = null state.devices.castMenu = null
return return
} }
// Never cast to two devices at the same time // Never cast to two devices at the same time
if (state.playing.location !== 'local') { if (state.playing.location !== 'local') {
throw new CastingError( throw new CastingError(`You can't connect to ${location} when already connected to another device`)
`You can't connect to ${location} when already connected to another device`
)
} }
// Find all cast devices of the given type // Find all cast devices of the given type
const player = getPlayer(location) const player = getPlayer(location)
const devices = player ? player.getDevices() : [] const devices = player ? player.getDevices() : []
if (devices.length === 0) { if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`) throw new CastingError(`No ${location} devices available`)
} }
// Show a menu // Show a menu
state.devices.castMenu = { location, devices } state.devices.castMenu = { location, devices }
} }
function selectDevice (index) { function selectDevice (index) {
const { location, devices } = state.devices.castMenu const { location, devices } = state.devices.castMenu
// Start casting // Start casting
const player = getPlayer(location) const player = getPlayer(location)
player.device = devices[index] player.device = devices[index]
player.open() player.open()
// Poll the casting device's status every few seconds // Poll the casting device's status every few seconds
startStatusInterval() startStatusInterval()
// Show the Connecting... screen // Show the Connecting... screen
state.devices.castMenu = null state.devices.castMenu = null
state.playing.castName = devices[index].name state.playing.castName = devices[index].name
state.playing.location = location + '-pending' state.playing.location = location + '-pending'
update() update()
} }
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function stop () { function stop () {
const player = getPlayer() const player = getPlayer()
if (player) { if (player) {
player.stop(function () { player.stop(() => {
player.device = null player.device = null
stoppedCasting() stoppedCasting()
}) })
@@ -463,7 +387,6 @@ function stop () {
stoppedCasting() stoppedCasting()
} }
} }
function stoppedCasting () { function stoppedCasting () {
state.playing.location = 'local' state.playing.location = 'local'
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime) state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
@@ -471,7 +394,6 @@ function stoppedCasting () {
: 0 : 0
update() update()
} }
function getPlayer (location) { function getPlayer (location) {
if (location) { if (location) {
return state.devices[location] return state.devices[location]
@@ -485,17 +407,14 @@ function getPlayer (location) {
return null return null
} }
} }
function play () { function play () {
const player = getPlayer() const player = getPlayer()
if (player) player.play(castCallback) if (player) player.play(castCallback)
} }
function pause () { function pause () {
const player = getPlayer() const player = getPlayer()
if (player) player.pause(castCallback) if (player) player.pause(castCallback)
} }
function setRate (rate) { function setRate (rate) {
let player let player
let result = true let result = true
@@ -511,17 +430,34 @@ function setRate (rate) {
} }
return result return result
} }
function seek (time) { function seek (time) {
const player = getPlayer() const player = getPlayer()
if (player) player.seek(time, castCallback) if (player) { player.seek(time, castCallback) }
} }
function setVolume (volume) { function setVolume (volume) {
const player = getPlayer() const player = getPlayer()
if (player) player.volume(volume, castCallback) if (player) { player.volume(volume, castCallback) }
} }
function castCallback (...args) {
function castCallback () { console.log('%s callback: %o', state.playing.location, args)
console.log('%s callback: %o', state.playing.location, arguments) }
export { init }
export { toggleMenu }
export { selectDevice }
export { stop }
export { play }
export { pause }
export { seek }
export { setVolume }
export { setRate }
export default {
init,
toggleMenu,
selectDevice,
stop,
play,
pause,
seek,
setVolume,
setRate
} }

View File

@@ -1,20 +1,11 @@
module.exports = {
dispatch,
dispatcher,
setDispatch
}
const dispatchers = {} const dispatchers = {}
let _dispatch = function () {} let _dispatch = () => { }
function setDispatch (dispatch) { function setDispatch (dispatch) {
_dispatch = dispatch _dispatch = dispatch
} }
function dispatch (...args) { function dispatch (...args) {
_dispatch(...args) _dispatch(...args)
} }
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`. // Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler // For these, `dispatcher(<args>)` is preferred because it memoizes the handler
// function. This prevents React from updating the listener functions on // function. This prevents React from updating the listener functions on
@@ -23,17 +14,23 @@ function dispatcher (...args) {
const str = JSON.stringify(args) const str = JSON.stringify(args)
let handler = dispatchers[str] let handler = dispatchers[str]
if (!handler) { if (!handler) {
handler = dispatchers[str] = function (e) { handler = dispatchers[str] = e => {
// Do not propagate click to elements below the button // Do not propagate click to elements below the button
e.stopPropagation() e.stopPropagation()
if (e.currentTarget.classList.contains('disabled')) { if (e.currentTarget.classList.contains('disabled')) {
// Ignore clicks on disabled elements // Ignore clicks on disabled elements
return return
} }
dispatch(...args) dispatch(...args)
} }
} }
return handler return handler
} }
export { dispatch }
export { dispatcher }
export { setDispatch }
export default {
dispatch,
dispatcher,
setDispatch
}

View File

@@ -1,35 +1,37 @@
const ExtendableError = require('es6-error') import ExtendableError from 'es6-error'
/* Generic errors */ /* Generic errors */
class CastingError extends ExtendableError {
class CastingError extends ExtendableError {} }
class PlaybackError extends ExtendableError {} class PlaybackError extends ExtendableError {
class SoundError extends ExtendableError {} }
class TorrentError extends ExtendableError {} class SoundError extends ExtendableError {
}
class TorrentError extends ExtendableError {
}
/* Playback */ /* Playback */
class UnplayableTorrentError extends PlaybackError { class UnplayableTorrentError extends PlaybackError {
constructor () { super('Can\'t play any files in torrent') } constructor () { super('Can\'t play any files in torrent') }
} }
class UnplayableFileError extends PlaybackError { class UnplayableFileError extends PlaybackError {
constructor () { super('Can\'t play that file') } constructor () { super('Can\'t play that file') }
} }
/* Sound */ /* Sound */
class InvalidSoundNameError extends SoundError { class InvalidSoundNameError extends SoundError {
constructor (name) { super(`Invalid sound name: ${name}`) } constructor (name) { super(`Invalid sound name: ${name}`) }
} }
/* Torrent */ /* Torrent */
class TorrentKeyNotFoundError extends TorrentError { class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) } constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
} }
export { CastingError }
module.exports = { export { PlaybackError }
export { SoundError }
export { TorrentError }
export { UnplayableTorrentError }
export { UnplayableFileError }
export { InvalidSoundNameError }
export { TorrentKeyNotFoundError }
export default {
CastingError, CastingError,
PlaybackError, PlaybackError,
SoundError, SoundError,

View File

@@ -2,11 +2,12 @@ const mediaExtensions = {
audio: [ audio: [
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a', '.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
'.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga', '.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga',
'.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'], '.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'
],
video: [ video: [
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg', '.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
'.ogv', '.webm', '.wmv', '.m2ts'], '.ogv', '.webm', '.wmv', '.m2ts'
],
image: ['.gif', '.jpg', '.jpeg', '.png'] image: ['.gif', '.jpg', '.jpeg', '.png']
} }
export default mediaExtensions
module.exports = mediaExtensions

View File

@@ -1,14 +1,8 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import fs, { copyFileSync } from 'fs'
module.exports = { import path from 'path'
run import semver from 'semver'
} import config from '../../config.js'
const fs = require('fs')
const path = require('path')
const semver = require('semver')
const config = require('../../config')
// Change `state.saved` (which will be saved back to config.json on exit) as // Change `state.saved` (which will be saved back to config.json on exit) as
// needed, for example to deal with config.json format changes across versions // needed, for example to deal with config.json format changes across versions
@@ -41,18 +35,15 @@ function run (state) {
// Whenever the app is updated, re-install default handlers if the user has // Whenever the app is updated, re-install default handlers if the user has
// enabled them. // enabled them.
function installHandlers (saved) { async function installHandlers (saved) {
if (saved.prefs.isFileHandler) { if (saved.prefs.isFileHandler) {
const ipcRenderer = require('electron').ipcRenderer const { ipcRenderer } = await import('electron')
ipcRenderer.send('setDefaultFileHandler', true) ipcRenderer.send('setDefaultFileHandler', true)
} }
} }
function migrate_0_7_0 (saved) { function migrate_0_7_0 (saved) {
const { copyFileSync } = require('fs') saved.torrents.forEach(ts => {
const path = require('path')
saved.torrents.forEach(function (ts) {
const infoHash = ts.infoHash const infoHash = ts.infoHash
// Replace torrentPath with torrentFileName // Replace torrentPath with torrentFileName
@@ -91,7 +82,6 @@ function migrate_0_7_0 (saved) {
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension ts.posterFileName = infoHash + extension
} }
// Fix exception caused by incorrect file ordering. // Fix exception caused by incorrect file ordering.
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214 // https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
delete ts.defaultPlayFileIndex delete ts.defaultPlayFileIndex
@@ -116,8 +106,8 @@ function migrate_0_11_0 (saved) {
} }
} }
function migrate_0_12_0 (saved) { async function migrate_0_12_0 (saved) {
const TorrentSummary = require('./torrent-summary') const TorrentSummary = await import('./torrent-summary')
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) { if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc saved.prefs.openExternalPlayer = saved.prefs.playInVlc
@@ -134,7 +124,7 @@ function migrate_0_12_0 (saved) {
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent', '02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent' '3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
] ]
saved.torrents.forEach(function (torrentSummary) { saved.torrents.forEach(torrentSummary => {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary) const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return if (!fileOrFolder) return
@@ -148,16 +138,16 @@ function migrate_0_12_0 (saved) {
} }
function migrate_0_14_0 (saved) { function migrate_0_14_0 (saved) {
saved.torrents.forEach(function (ts) { saved.torrents.forEach(ts => {
delete ts.defaultPlayFileIndex delete ts.defaultPlayFileIndex
}) })
} }
function migrate_0_17_0 (saved) { function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files // Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(function (ts) { saved.torrents.forEach(ts => {
if (!ts.files) return if (!ts.files) return
ts.files.forEach(function (file) { ts.files.forEach(file => {
if (!file.audioInfo || !file.audioInfo.picture) return if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON // This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture delete file.audioInfo.picture
@@ -169,19 +159,13 @@ function migrate_0_17_2 (saved) {
// Remove the trailing dot (.) from the Wired CD torrent name, since // Remove the trailing dot (.) from the Wired CD torrent name, since
// folders/files that end in a trailing dot (.) or space are not deletable from // folders/files that end in a trailing dot (.) or space are not deletable from
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905 // Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
const { copyFileSync } = require('fs')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.' const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share' const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d' const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3' const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
const ts = saved.torrents.find((ts) => { const ts = saved.torrents.find(ts => ts.infoHash === OLD_HASH)
return ts.infoHash === OLD_HASH
})
if (!ts) return // Wired CD torrent does not exist if (!ts) return // Wired CD torrent does not exist
@@ -204,7 +188,7 @@ function migrate_0_17_2 (saved) {
} catch (err) {} } catch (err) {}
ts.posterFileName = NEW_HASH + '.jpg' ts.posterFileName = NEW_HASH + '.jpg'
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName)) fs.rmdirSync(path.join(config.TORRENT_PATH, ts.torrentFileName))
copyFileSync( copyFileSync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'), path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent') path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
@@ -234,3 +218,7 @@ function migrate_0_22_0 (saved) {
saved.prefs.externalPlayerPath = '' saved.prefs.externalPlayerPath = ''
} }
} }
export default {
run
}

View File

@@ -1,52 +1,36 @@
module.exports = { import TorrentSummary from './torrent-summary.js'
hasNext, import TorrentPlayer from './torrent-player.js'
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}
const TorrentSummary = require('./torrent-summary')
const TorrentPlayer = require('./torrent-player')
const cache = { const cache = {
infoHash: null, infoHash: null,
previousIndex: null, previousIndex: null,
currentIndex: null, currentIndex: null,
nextIndex: null nextIndex: null
} }
function hasNext (state) { function hasNext (state) {
updateCache(state) updateCache(state)
return cache.nextIndex !== null return cache.nextIndex !== null
} }
function getNextIndex (state) { function getNextIndex (state) {
updateCache(state) updateCache(state)
return cache.nextIndex return cache.nextIndex
} }
function hasPrevious (state) { function hasPrevious (state) {
updateCache(state) updateCache(state)
return cache.previousIndex !== null return cache.previousIndex !== null
} }
function getPreviousIndex (state) { function getPreviousIndex (state) {
updateCache(state) updateCache(state)
return cache.previousIndex return cache.previousIndex
} }
function getCurrentLocalURL (state) { function getCurrentLocalURL (state) {
return state.server return state.server
? state.server.localURL + '/' + state.playing.fileIndex + '/' + ? state.server.localURL + '/' + state.playing.fileIndex + '/' +
encodeURIComponent(state.playing.fileName) encodeURIComponent(state.playing.fileName)
: '' : ''
} }
function updateCache (state) { function updateCache (state) {
const infoHash = state.playing.infoHash const infoHash = state.playing.infoHash
const fileIndex = state.playing.fileIndex const fileIndex = state.playing.fileIndex
if (infoHash === cache.infoHash) { if (infoHash === cache.infoHash) {
switch (fileIndex) { switch (fileIndex) {
case cache.currentIndex: case cache.currentIndex:
@@ -65,12 +49,10 @@ function updateCache (state) {
} else { } else {
cache.infoHash = infoHash cache.infoHash = infoHash
} }
cache.previousIndex = findPreviousIndex(state) cache.previousIndex = findPreviousIndex(state)
cache.currentIndex = fileIndex cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state) cache.nextIndex = findNextIndex(state)
} }
function findPreviousIndex (state) { function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex - 1; i >= 0; i--) { for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
@@ -78,7 +60,6 @@ function findPreviousIndex (state) {
} }
return null return null
} }
function findNextIndex (state) { function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex + 1; i < files.length; i++) { for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
@@ -86,3 +67,15 @@ function findNextIndex (state) {
} }
return null return null
} }
export { hasNext }
export { getNextIndex }
export { hasPrevious }
export { getPreviousIndex }
export { getCurrentLocalURL }
export default {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}

View File

@@ -1,20 +1,11 @@
module.exports = { import config from '../src/config.js'
init, import { InvalidSoundNameError } from './errors.js'
play import path from 'path'
}
const config = require('../../config')
const { InvalidSoundNameError } = require('./errors')
const path = require('path')
const VOLUME = 0.25 const VOLUME = 0.25
// App state to access the soundNotifications preference // App state to access the soundNotifications preference
let state let state
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
const cache = {} const cache = {}
const sounds = { const sounds = {
ADD: { ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
@@ -49,20 +40,16 @@ const sounds = {
volume: VOLUME volume: VOLUME
} }
} }
function init (appState) { function init (appState) {
state = appState state = appState
} }
function play (name) { function play (name) {
if (state == null) { if (state == null) {
return return
} }
if (!state.saved.prefs.soundNotifications) { if (!state.saved.prefs.soundNotifications) {
return return
} }
let audio = cache[name] let audio = cache[name]
if (!audio) { if (!audio) {
const sound = sounds[name] const sound = sounds[name]
@@ -76,3 +63,9 @@ function play (name) {
audio.currentTime = 0 audio.currentTime = 0
audio.play() audio.play()
} }
export { init }
export { play }
export default {
init,
play
}

View File

@@ -1,30 +1,33 @@
const appConfig = require('application-config')('WebTorrent') import applicationConfig from 'application-config'
const path = require('path') import path from 'path'
const { EventEmitter } = require('events') import { EventEmitter } from 'events'
const config = require('../../config') import config from '../../config.js'
const appConfig = applicationConfig('WebTorrent')
const SAVE_DEBOUNCE_INTERVAL = 1000 const SAVE_DEBOUNCE_INTERVAL = 1000
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json') appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
const State = module.exports = Object.assign(new EventEmitter(), { const State = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit. // state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save: function () { async save (...args) {
// Perf optimization: Lazy-require debounce (and it's dependencies) // Perf optimization: Lazy-require debounce (and it's dependencies)
const debounce = require('debounce') const debounce = await import('debounce')
// After first State.save() invokation, future calls go straight to the // After first State.save() invokation, future calls go straight to the
// debounced function // debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL) State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...arguments) State.save(...args)
}, },
saveImmediate saveImmediate
}) })
function getDefaultState () { export default State
const LocationHistory = require('location-history')
async function getDefaultState () {
const { default: LocationHistory } = await import('location-history')
return { return {
/* /*
@@ -115,9 +118,9 @@ function getDefaultPlayState () {
} }
/* If the saved state file doesn't exist yet, here's what we use instead */ /* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved () { async function setupStateSaved () {
const { copyFileSync, mkdirSync, readFileSync } = require('fs') const { copyFileSync, mkdirSync, readFileSync } = await import('fs')
const parseTorrent = require('parse-torrent') const { default: parseTorrent } = await import('parse-torrent')
const saved = { const saved = {
prefs: { prefs: {
@@ -203,12 +206,13 @@ function shouldHidePlayerControls () {
} }
async function load (cb) { async function load (cb) {
console.log('state load')
let saved = await appConfig.read() let saved = await appConfig.read()
if (!saved || !saved.version) { if (!saved || !saved.version) {
console.log('Missing config file: Creating new one') console.log('Missing config file: Creating new one')
try { try {
saved = setupStateSaved() saved = await setupStateSaved()
} catch (err) { } catch (err) {
onSavedState(err) onSavedState(err)
return return
@@ -217,9 +221,9 @@ async function load (cb) {
onSavedState(null, saved) onSavedState(null, saved)
function onSavedState (err, saved) { async function onSavedState (err, saved) {
if (err) return cb(err) if (err) return cb(err)
const state = getDefaultState() const state = await getDefaultState()
state.saved = saved state.saved = saved
if (process.type === 'renderer') { if (process.type === 'renderer') {
@@ -228,6 +232,7 @@ async function load (cb) {
migrations.run(state) migrations.run(state)
} }
console.log('calling cb')
cb(null, state) cb(null, state)
} }
} }
@@ -242,7 +247,7 @@ async function saveImmediate (state, cb) {
// reading the torrent file or file(s) to seed & don't have an infohash // reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents copy.torrents = copy.torrents
.filter((x) => x.infoHash) .filter((x) => x.infoHash)
.map(function (x) { .map(x => {
const torrent = {} const torrent = {}
for (const key in x) { for (const key in x) {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {

View File

@@ -1,24 +1,14 @@
// Collects anonymous usage stats and uncaught errors import remote from '@electron/remote'
// Reports back so that we can improve WebTorrent Desktop import config from '../../config.js'
module.exports = {
init,
send,
logUncaughtError,
logPlayAttempt
}
const remote = require('@electron/remote')
const config = require('../../config')
let telemetry let telemetry
function init (state) { async function init (state) {
telemetry = state.saved.telemetry telemetry = state.saved.telemetry
// First app run // First app run
if (!telemetry) { if (!telemetry) {
const crypto = require('crypto') const crypto = await import('crypto')
telemetry = state.saved.telemetry = { telemetry = state.saved.telemetry = {
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
} }
@@ -26,7 +16,7 @@ function init (state) {
} }
} }
function send (state) { async function send (state) {
const now = new Date() const now = new Date()
telemetry.version = config.APP_VERSION telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString() telemetry.timestamp = now.toISOString()
@@ -42,7 +32,7 @@ function send (state) {
return reset() return reset()
} }
const get = require('simple-get') const get = await import('simple-get')
const opts = { const opts = {
url: config.TELEMETRY_URL, url: config.TELEMETRY_URL,
@@ -50,7 +40,7 @@ function send (state) {
json: true json: true
} }
get.post(opts, function (err, res) { get.post(opts, (err, res) => {
if (err) return console.error('Error sending telemetry', err) if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`) return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
@@ -82,8 +72,8 @@ function getScreenInfo () {
} }
// Track basic system info like OS version and amount of RAM // Track basic system info like OS version and amount of RAM
function getSystemInfo () { async function getSystemInfo () {
const os = require('os') const os = await import('os')
return { return {
osPlatform: process.platform, osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(), osRelease: os.type() + ' ' + os.release(),
@@ -106,15 +96,14 @@ function getTorrentStats (state) {
} }
// First, count torrents & total file size // First, count torrents & total file size
for (let i = 0; i < count; i++) { for (const torrent of state.saved.torrents) {
const t = state.saved.torrents[i] const stat = byStatus[torrent.status]
const stat = byStatus[t.status] if (!torrent || !torrent.files || !stat) continue
if (!t || !t.files || !stat) continue
stat.count++ stat.count++
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j] for (const file of torrent.files) {
if (!f || !f.length) continue if (!file || !file.length) continue
const fileSizeMB = f.length / (1 << 20) const fileSizeMB = file.length / (1 << 20)
sizeMB += fileSizeMB sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB stat.sizeMB += fileSizeMB
} }
@@ -145,7 +134,7 @@ function roundPow2 (n) {
if (n <= 0) return 0 if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space // Otherwise, return 1, 2, 4, 8, etc by rounding in log space
const log2 = Math.log(n) / Math.log(2) const log2 = Math.log(n) / Math.log(2)
return Math.pow(2, Math.round(log2)) return 2 ** Math.round(log2)
} }
// An uncaught error happened in the main process or in one of the windows // An uncaught error happened in the main process or in one of the windows
@@ -221,3 +210,14 @@ function logPlayAttempt (result) {
attempts.total = (attempts.total || 0) + 1 attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1 attempts[result] = (attempts[result] || 0) + 1
} }
export { init }
export { send }
export { logUncaughtError }
export { logPlayAttempt }
export default {
init,
send,
logUncaughtError,
logPlayAttempt
}

View File

@@ -1,20 +1,17 @@
module.exports = {
calculateEta
}
function calculateEta (missing, downloadSpeed) { function calculateEta (missing, downloadSpeed) {
if (downloadSpeed === 0 || missing === 0) return if (downloadSpeed === 0 || missing === 0) return
const rawEta = missing / downloadSpeed const rawEta = missing / downloadSpeed
const hours = Math.floor(rawEta / 3600) % 24 const hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60 const minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60) const seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always // Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed // display minutes if hours is being displayed
const hoursStr = hours ? hours + ' h' : '' const hoursStr = hours ? hours + ' h' : ''
const minutesStr = (hours || minutes) ? minutes + ' min' : '' const minutesStr = (hours || minutes) ? minutes + ' min' : ''
const secondsStr = seconds + ' s' const secondsStr = seconds + ' s'
return `${hoursStr} ${minutesStr} ${secondsStr} remaining` return `${hoursStr} ${minutesStr} ${secondsStr} remaining`
} }
export { calculateEta }
export default {
calculateEta
}

View File

@@ -1,32 +1,18 @@
module.exports = { import path from 'path'
isPlayable, import mediaExtensions from './media-extensions.js'
isVideo,
isAudio,
isTorrent,
isMagnetLink,
isPlayableTorrentSummary
}
const path = require('path')
const mediaExtensions = require('./media-extensions')
// Checks whether a fileSummary or file path is audio/video that we can play, // Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension // based on the file extension
function isPlayable (file) { function isPlayable (file) {
return isVideo(file) || isAudio(file) return isVideo(file) || isAudio(file)
} }
// Checks whether a fileSummary or file path is playable video // Checks whether a fileSummary or file path is playable video
function isVideo (file) { function isVideo (file) {
return mediaExtensions.video.includes(getFileExtension(file)) return mediaExtensions.video.includes(getFileExtension(file))
} }
// Checks whether a fileSummary or file path is playable audio // Checks whether a fileSummary or file path is playable audio
function isAudio (file) { function isAudio (file) {
return mediaExtensions.audio.includes(getFileExtension(file)) return mediaExtensions.audio.includes(getFileExtension(file))
} }
// Checks if the argument is either: // Checks if the argument is either:
// - a string that's a valid filename ending in .torrent // - a string that's a valid filename ending in .torrent
// - a file object where obj.name is ends in .torrent // - a file object where obj.name is ends in .torrent
@@ -34,20 +20,30 @@ function isAudio (file) {
function isTorrent (file) { function isTorrent (file) {
return isTorrentFile(file) || isMagnetLink(file) return isTorrentFile(file) || isMagnetLink(file)
} }
function isTorrentFile (file) { function isTorrentFile (file) {
return getFileExtension(file) === '.torrent' return getFileExtension(file) === '.torrent'
} }
function isMagnetLink (link) { function isMagnetLink (link) {
return typeof link === 'string' && /^(stream-)?magnet:/.test(link) return typeof link === 'string' && /^(stream-)?magnet:/.test(link)
} }
function getFileExtension (file) { function getFileExtension (file) {
const name = typeof file === 'string' ? file : file.name const name = typeof file === 'string' ? file : file.name
return path.extname(name).toLowerCase() return path.extname(name).toLowerCase()
} }
function isPlayableTorrentSummary (torrentSummary) { function isPlayableTorrentSummary (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable) return torrentSummary.files && torrentSummary.files.some(isPlayable)
} }
export { isPlayable }
export { isVideo }
export { isAudio }
export { isTorrent }
export { isMagnetLink }
export { isPlayableTorrentSummary }
export default {
isPlayable,
isVideo,
isAudio,
isTorrent,
isMagnetLink,
isPlayableTorrentSummary
}

View File

@@ -1,34 +1,20 @@
module.exports = torrentPoster import captureFrame from 'capture-frame'
import path from 'path'
const captureFrame = require('capture-frame') import mediaExtensions from './media-extensions.js'
const path = require('path')
const mediaExtensions = require('./media-extensions')
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent' const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) { const posterFile = torrent.files.filter(file => /^poster\.(jpg|png|gif)$/.test(file.name))[0]
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return extractPoster(posterFile, cb) if (posterFile) return extractPoster(posterFile, cb)
// 'score' each media type based on total size present in torrent // 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => { const bestScore = ['audio', 'video', 'image'].map(mediaType => ({
return { type: mediaType,
type: mediaType, size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType]) })).sort((a, b) => b.size - a.size)[0] // sort descending on size
}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
if (bestScore.size === 0) { if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence // Admit defeat, no video, audio or image had a significant presence
return cb(new Error(msgNoSuitablePoster)) return cb(new Error(msgNoSuitablePoster))
} }
// Based on which media type is dominant we select the corresponding poster function // Based on which media type is dominant we select the corresponding poster function
switch (bestScore.type) { switch (bestScore.type) {
case 'audio': case 'audio':
@@ -39,7 +25,6 @@ function torrentPoster (torrent, cb) {
return torrentPosterFromVideo(torrent, cb) return torrentPosterFromVideo(torrent, cb)
} }
} }
/** /**
* Calculate the total data size of file matching one of the provided extensions * Calculate the total data size of file matching one of the provided extensions
* @param torrent * @param torrent
@@ -51,11 +36,8 @@ function calculateDataLengthByExtension (torrent, extensions) {
if (files.length === 0) return 0 if (files.length === 0) return 0
return files return files
.map(file => file.length) .map(file => file.length)
.reduce((a, b) => { .reduce((a, b) => a + b)
return a + b
})
} }
/** /**
* Get the largest file of a given torrent, filtered by provided extension * Get the largest file of a given torrent, filtered by provided extension
* @param torrent Torrent to search in * @param torrent Torrent to search in
@@ -65,11 +47,8 @@ function calculateDataLengthByExtension (torrent, extensions) {
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions) const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return undefined if (files.length === 0) return undefined
return files.reduce((a, b) => { return files.reduce((a, b) => a.length > b.length ? a : b)
return a.length > b.length ? a : b
})
} }
/** /**
* Filter file on a list extension, can be used to find al image files * Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from * @param torrent Torrent to filter files from
@@ -82,7 +61,6 @@ function filterOnExtension (torrent, extensions) {
return extensions.indexOf(extname) !== -1 return extensions.indexOf(extname) !== -1
}) })
} }
/** /**
* Returns a score how likely the file is suitable as a poster * Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image * @param imgFile File object of an image
@@ -98,7 +76,6 @@ function scoreAudioCoverFile (imgFile) {
back: 20, back: 20,
spectrogram: -80 spectrogram: -80
} }
for (const keyword in relevanceScore) { for (const keyword in relevanceScore) {
if (fileName === keyword) { if (fileName === keyword) {
return relevanceScore[keyword] return relevanceScore[keyword]
@@ -109,18 +86,13 @@ function scoreAudioCoverFile (imgFile) {
} }
return 0 return 0
} }
function torrentPosterFromAudio (torrent, cb) { function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image) const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster)) if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => ({
const bestCover = imageFiles.map(file => { file,
return { score: scoreAudioCoverFile(file)
file, })).reduce((a, b) => {
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) { if (a.score > b.score) {
return a return a
} }
@@ -133,62 +105,47 @@ function torrentPosterFromAudio (torrent, cb) {
} }
return b return b
}) })
const extname = path.extname(bestCover.file.name) const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname)) bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
} }
function torrentPosterFromVideo (torrent, cb) { function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video) const file = getLargestFileByExtension(torrent, mediaExtensions.video)
const index = torrent.files.indexOf(file) const index = torrent.files.indexOf(file)
const server = torrent.createServer(0) const server = torrent.createServer(0)
server.listen(0, onListening) server.listen(0, onListening)
function onListening () { function onListening () {
const port = server.address().port const port = server.address().port
const url = 'http://localhost:' + port + '/' + index const url = 'http://localhost:' + port + '/' + index
const video = document.createElement('video') const video = document.createElement('video')
video.addEventListener('canplay', onCanPlay) video.addEventListener('canplay', onCanPlay)
video.volume = 0 video.volume = 0
video.src = url video.src = url
video.play() video.play()
function onCanPlay () { function onCanPlay () {
video.removeEventListener('canplay', onCanPlay) video.removeEventListener('canplay', onCanPlay)
video.addEventListener('seeked', onSeeked) video.addEventListener('seeked', onSeeked)
video.currentTime = Math.min((video.duration || 600) * 0.03, 60) video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
} }
function onSeeked () { function onSeeked () {
video.removeEventListener('seeked', onSeeked) video.removeEventListener('seeked', onSeeked)
const frame = captureFrame(video) const frame = captureFrame(video)
const buf = frame && frame.image const buf = frame && frame.image
// unload video element // unload video element
video.pause() video.pause()
video.src = '' video.src = ''
video.load() video.load()
server.destroy() server.destroy()
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster)) if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
cb(null, buf, '.jpg') cb(null, buf, '.jpg')
} }
} }
} }
function torrentPosterFromImage (torrent, cb) { function torrentPosterFromImage (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image) const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb) extractPoster(file, cb)
} }
function extractPoster (file, cb) { function extractPoster (file, cb) {
const extname = path.extname(file.name) const extname = path.extname(file.name)
file.getBuffer((err, buf) => { return cb(err, buf, extname) }) file.getBuffer((err, buf) => cb(err, buf, extname))
} }
export default torrentPoster

View File

@@ -1,21 +1,11 @@
module.exports = { import path from 'path'
getPosterPath, import config from '../src/config.js'
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}
const path = require('path')
const config = require('../../config')
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable // Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) { function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName) return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
} }
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable // Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) { function getPosterPath (torrentSummary) {
@@ -25,7 +15,6 @@ function getPosterPath (torrentSummary) {
// Backslashes in URLS in CSS cause bizarre string encoding issues // Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/') return posterPath.replace(/\\/g, '/')
} }
// Expects a torrentSummary // Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash // Returns a torrentID: filename, magnet URI, or infohash
function getTorrentId (torrentSummary) { function getTorrentId (torrentSummary) {
@@ -36,15 +25,12 @@ function getTorrentId (torrentSummary) {
return s.magnetURI || s.infoHash return s.magnetURI || s.infoHash
} }
} }
// Expects a torrentKey or infoHash // Expects a torrentKey or infoHash
// Returns the corresponding torrentSummary, or undefined // Returns the corresponding torrentSummary, or undefined
function getByKey (state, torrentKey) { function getByKey (state, torrentKey) {
if (!torrentKey) return undefined if (!torrentKey) return undefined
return state.saved.torrents.find((x) => return state.saved.torrents.find((x) => x.torrentKey === torrentKey || x.infoHash === torrentKey)
x.torrentKey === torrentKey || x.infoHash === torrentKey)
} }
// Returns the path to either the file (in a single-file torrent) or the root // Returns the path to either the file (in a single-file torrent) or the root
// folder (in multi-file torrent) // folder (in multi-file torrent)
// WARNING: assumes that multi-file torrents consist of a SINGLE folder. // WARNING: assumes that multi-file torrents consist of a SINGLE folder.
@@ -56,3 +42,15 @@ function getFileOrFolder (torrentSummary) {
const dirname = ts.files[0].path.split(path.sep)[0] const dirname = ts.files[0].path.split(path.sep)[0]
return path.join(ts.path, dirname) return path.join(ts.path, dirname)
} }
export { getPosterPath }
export { getTorrentPath }
export { getByKey }
export { getTorrentId }
export { getFileOrFolder }
export default {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}

View File

@@ -1,51 +1,43 @@
/** /* eslint-disable import/first */
* Perf optimization: Hook into require() to modify how certain modules load:
*
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
* actually used because auto-prefixing is disabled with
* `darkBaseTheme.userAgent = false`. Return a fake object.
*/
const Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = function (id) {
if (id === 'inline-style-prefixer') return {}
return _require.apply(this, arguments)
}
console.time('init') console.time('init')
// Perf optimization: Start asynchronously read on config file before all the // Perf optimization: Start asynchronously read on config file before all the
// blocking require() calls below. // blocking import() calls below.
const State = require('./lib/state') import State from './lib/state'
State.load(onState) State.load(onState)
const createGetter = require('fn-getter') import createGetter from 'fn-getter'
const debounce = require('debounce') import remote from '@electron/remote'
const dragDrop = require('drag-drop') import debounce from 'debounce'
const electron = require('electron') import dragDrop from 'drag-drop'
const fs = require('fs') import electron from 'electron'
const React = require('react') import fs from 'fs'
const ReactDOM = require('react-dom') import * as React from 'react'
import * as ReactDOM from 'react-dom'
const config = require('../config') import config from '../config.js'
const telemetry = require('./lib/telemetry') import telemetry from './lib/telemetry.js'
const sound = require('./lib/sound') import sound from './lib/sound.js'
const TorrentPlayer = require('./lib/torrent-player') import TorrentPlayer from './lib/torrent-player.js'
import App from './pages/app.js'
import { setDispatch } from './lib/dispatcher.js'
// Perf optimization: Needed immediately, so do not lazy load it below import TorrentController from './controllers/torrent-controller.js'
const TorrentListController = require('./controllers/torrent-list-controller') import AudioTracksController from './controllers/audio-tracks-controller.js'
import SubtitlesController from './controllers/subtitles-controller.js'
import PrefsController from './controllers/prefs-controller.js'
import PlaybackController from './controllers/playback-controller.js'
import MediaController from './controllers/media-controller.js'
import TorrentListController from './controllers/torrent-list-controller.js'
import UpdateController from './controllers/update-controller.js'
import FolderWatcherController from './controllers/folder-watcher-controller.js'
const App = require('./pages/app') const { clipboard, ipcRenderer } = electron
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
const ipcRenderer = electron.ipcRenderer
// Yo-yo pattern: state object lives here and percolates down thru all the views. // Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...) // Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch) setDispatch(dispatch)
// From dispatch(...), events are sent to one of the controllers // From dispatch(...), events are sent to one of the controllers
let controllers = null let controllers = null
@@ -80,41 +72,15 @@ function onState (err, _state) {
// Create controllers // Create controllers
controllers = { controllers = {
media: createGetter(() => { media: createGetter(() => new MediaController(state)),
const MediaController = require('./controllers/media-controller') playback: createGetter(() => new PlaybackController(state, config, update)),
return new MediaController(state) prefs: createGetter(() => new PrefsController(state, config)),
}), subtitles: createGetter(() => new SubtitlesController(state)),
playback: createGetter(() => { audioTracks: createGetter(() => new AudioTracksController(state)),
const PlaybackController = require('./controllers/playback-controller') torrent: createGetter(() => new TorrentController(state)),
return new PlaybackController(state, config, update) torrentList: createGetter(() => new TorrentListController(state)),
}), update: createGetter(() => new UpdateController(state)),
prefs: createGetter(() => { folderWatcher: createGetter(() => new FolderWatcherController())
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
audioTracks: createGetter(() => {
const AudioTracksController = require('./controllers/audio-tracks-controller')
return new AudioTracksController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
}),
folderWatcher: createGetter(() => {
const FolderWatcherController = require('./controllers/folder-watcher-controller')
return new FolderWatcherController()
})
} }
// Add first page to location history // Add first page to location history
@@ -155,11 +121,11 @@ function onState (err, _state) {
// Add YouTube style hotkey shortcuts // Add YouTube style hotkey shortcuts
window.addEventListener('keydown', onKeydown) window.addEventListener('keydown', onKeydown)
const debouncedFullscreenToggle = debounce(function () { const debouncedFullscreenToggle = debounce(() => {
dispatch('toggleFullScreen') dispatch('toggleFullScreen')
}, 1000, true) }, 1000, true)
document.addEventListener('wheel', function (event) { document.addEventListener('wheel', event => {
// ctrlKey detects pinch to zoom, http://crbug.com/289887 // ctrlKey detects pinch to zoom, http://crbug.com/289887
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault() event.preventDefault()
@@ -171,7 +137,7 @@ function onState (err, _state) {
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
if (electron.remote.getCurrentWindow().isVisible()) { if (remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP') sound.play('STARTUP')
} }
@@ -201,9 +167,9 @@ function delayedInit () {
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
function lazyLoadCast () { async function lazyLoadCast () {
if (!Cast) { if (!Cast) {
Cast = require('./lib/cast') Cast = (await import('./lib/cast')).default
Cast.init(state, update) // Search the local network for Chromecast and Airplays Cast.init(state, update) // Search the local network for Chromecast and Airplays
} }
return Cast return Cast
@@ -400,7 +366,7 @@ function setupIpc () {
function backToList () { function backToList () {
// Exit any modals and screens with a back button // Exit any modals and screens with a back button
state.modal = null state.modal = null
state.location.backToFirst(function () { state.location.backToFirst(() => {
// If we were already on the torrent list, scroll to the top // If we were already on the torrent list, scroll to the top
const contentTag = document.querySelector('.content') const contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 if (contentTag) contentTag.scrollTop = 0
@@ -434,7 +400,7 @@ function resumeTorrents () {
// Set window dimensions to match video dimensions or fill the screen // Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) { function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized // Don't modify the window size if it's already maximized
if (electron.remote.getCurrentWindow().isMaximized()) { if (remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null state.window.bounds = null
return return
} }
@@ -446,7 +412,7 @@ function setDimensions (dimensions) {
width: window.outerWidth, width: window.outerWidth,
height: window.outerHeight height: window.outerHeight
} }
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized state.window.wasMaximized = remote.getCurrentWindow().isMaximized
// Limit window size to screen size // Limit window size to screen size
const screenWidth = window.screen.width const screenWidth = window.screen.width
@@ -521,7 +487,7 @@ const editableHtmlTags = new Set(['input', 'textarea'])
function onPaste (e) { function onPaste (e) {
if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(electron.clipboard.readText()) controllers.torrentList().addTorrent(clipboard.readText())
update() update()
} }
@@ -590,7 +556,7 @@ function onWindowBoundsChanged (e, newBounds) {
} }
function checkDownloadPath () { function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) { fs.stat(state.saved.prefs.downloadPath, (err, stat) => {
if (err) { if (err) {
state.downloadPathStatus = 'missing' state.downloadPathStatus = 'missing'
return console.error(err) return console.error(err)

View File

@@ -1,32 +1,35 @@
const colors = require('material-ui/styles/colors') import * as colors from 'material-ui/styles/colors'
const createGetter = require('fn-getter') import createGetter from 'fn-getter'
const React = require('react') import * as React from 'react'
import darkBaseTheme$0 from 'material-ui/styles/baseThemes/darkBaseTheme'
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default import getMuiTheme$0 from 'material-ui/styles/getMuiTheme'
const getMuiTheme = require('material-ui/styles/getMuiTheme').default import muiThemeProvider from 'material-ui/styles/MuiThemeProvider'
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default import Header from '../components/header.js'
import TorrentListPage from './torrent-list-page.js'
const Header = require('../components/header') import playerPage from './player-page.js'
import createTorrentPage from './create-torrent-page.js'
// Perf optimization: Needed immediately, so do not lazy load it below import preferencesPage from './preferences-page.js'
const TorrentListPage = require('./torrent-list-page') import openTorrentAddressModal from '../components/open-torrent-address-modal.js'
import removeTorrentModal from '../components/remove-torrent-modal.js'
import updateAvailableModal from '../components/update-available-modal.js'
import unsupportedMediaModal from '../components/unsupported-media-modal.js'
import deleteAllTorrentsModal from '../components/delete-all-torrents-modal.js'
const darkBaseTheme = { default: darkBaseTheme$0 }.default
const getMuiTheme = getMuiTheme$0.default
const MuiThemeProvider = muiThemeProvider.default
const Views = { const Views = {
home: createGetter(() => TorrentListPage), home: createGetter(() => TorrentListPage),
player: createGetter(() => require('./player-page')), player: createGetter(() => playerPage),
'create-torrent': createGetter(() => require('./create-torrent-page')), 'create-torrent': createGetter(() => createTorrentPage),
preferences: createGetter(() => require('./preferences-page')) preferences: createGetter(() => preferencesPage)
} }
const Modals = { const Modals = {
'open-torrent-address-modal': createGetter( 'open-torrent-address-modal': createGetter(() => openTorrentAddressModal),
() => require('../components/open-torrent-address-modal') 'remove-torrent-modal': createGetter(() => removeTorrentModal),
), 'update-available-modal': createGetter(() => updateAvailableModal),
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')), 'unsupported-media-modal': createGetter(() => unsupportedMediaModal),
'update-available-modal': createGetter(() => require('../components/update-available-modal')), 'delete-all-torrents-modal': createGetter(() => deleteAllTorrentsModal)
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal')),
'delete-all-torrents-modal':
createGetter(() => require('../components/delete-all-torrents-modal'))
} }
const fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
@@ -45,7 +48,7 @@ darkBaseTheme.palette.accent3Color = colors.redA100
let darkMuiTheme let darkMuiTheme
let lightMuiTheme let lightMuiTheme
class App extends React.Component { export default class App extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
@@ -86,9 +89,7 @@ class App extends React.Component {
const recentErrors = state.errors.filter((x) => now - x.time < 5000) const recentErrors = state.errors.filter((x) => now - x.time < 5000)
const hasErrors = recentErrors.length > 0 const hasErrors = recentErrors.length > 0
const errorElems = recentErrors.map(function (error, i) { const errorElems = recentErrors.map((error, i) => <div key={i} className='error'>{error.message}</div>)
return (<div key={i} className='error'>{error.message}</div>)
})
return ( return (
<div <div
key='errors' key='errors'
@@ -100,12 +101,12 @@ class App extends React.Component {
) )
} }
getModal () { async getModal () {
const state = this.props.state const state = this.props.state
if (!state.modal) return if (!state.modal) return
if (!lightMuiTheme) { if (!lightMuiTheme) {
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default const lightBaseTheme = (await import('material-ui/styles/baseThemes/lightBaseTheme')).default
lightBaseTheme.fontFamily = fontFamily lightBaseTheme.fontFamily = fontFamily
lightBaseTheme.userAgent = false lightBaseTheme.userAgent = false
lightMuiTheme = getMuiTheme(lightBaseTheme) lightMuiTheme = getMuiTheme(lightBaseTheme)
@@ -130,5 +131,3 @@ class App extends React.Component {
return (<View state={state} />) return (<View state={state} />)
} }
} }
module.exports = App

View File

@@ -1,22 +1,25 @@
const createTorrent = require('create-torrent') import createTorrent from 'create-torrent'
const path = require('path') import path from 'path'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
const React = require('react') import * as React from 'react'
import flatButton from 'material-ui/FlatButton'
import raisedButton from 'material-ui/RaisedButton'
import textField from 'material-ui/TextField'
import checkbox from 'material-ui/Checkbox'
const { dispatch, dispatcher } = require('../lib/dispatcher') import { dispatch, dispatcher } from '../lib/dispatcher.js'
import CreateTorrentErrorPage from '../components/create-torrent-error-page.js'
import Heading from '../components/heading.js'
import ShowMore from '../components/show-more.js'
const FlatButton = require('material-ui/FlatButton').default const FlatButton = flatButton.default
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = raisedButton.default
const TextField = require('material-ui/TextField').default const TextField = textField.default
const Checkbox = require('material-ui/Checkbox').default const Checkbox = checkbox.default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/heading')
const ShowMore = require('../components/show-more')
// Shows a basic UI to create a torrent, from an already-selected file or folder. // Shows a basic UI to create a torrent, from an already-selected file or folder.
// Includes a "Show Advanced..." button and more advanced UI. // Includes a "Show Advanced..." button and more advanced UI.
class CreateTorrentPage extends React.Component { export default class CreateTorrentPage extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -223,5 +226,3 @@ function containsDots (path, pathPrefix) {
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/') const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.') return ('/' + suffix).includes('/.')
} }
module.exports = CreateTorrentPage

View File

@@ -1,15 +1,16 @@
const React = require('react') import * as React from 'react'
const BitField = require('bitfield').default import bitfield from 'bitfield'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
const TorrentSummary = require('../lib/torrent-summary') import TorrentSummary from '../lib/torrent-summary.js'
const Playlist = require('../lib/playlist') import Playlist from '../lib/playlist.js'
const { dispatch, dispatcher } = require('../lib/dispatcher') import { dispatch, dispatcher } from '../lib/dispatcher.js'
const config = require('../../config') import config from '../../config.js'
const { calculateEta } = require('../lib/time') import { calculateEta } from '../lib/time.js'
// Shows a streaming video player. Standard features + Chromecast + Airplay const BitField = bitfield.default
module.exports = class Player extends React.Component {
export default class Player extends React.Component {
render () { render () {
// Show the video as large as will fit in the window, play immediately // Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead // If the video is on Chromecast or Airplay, show a title screen instead
@@ -109,8 +110,7 @@ function renderMedia (state) {
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
const trackTags = [] const trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) { if (state.playing.subtitles.selectedIndex >= 0) {
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) { state.playing.subtitles.tracks.forEach((track, i) => {
const track = state.playing.subtitles.tracks[i]
const isSelected = state.playing.subtitles.selectedIndex === i const isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push( trackTags.push(
<track <track
@@ -121,7 +121,7 @@ function renderMedia (state) {
src={track.buffer} src={track.buffer}
/> />
) )
} })
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
@@ -200,7 +200,7 @@ function renderMedia (state) {
} }
} }
function onEnded (e) { function onEnded () {
if (Playlist.hasNext(state)) { if (Playlist.hasNext(state)) {
dispatch('nextTrack') dispatch('nextTrack')
} else { } else {
@@ -504,7 +504,7 @@ function renderCastOptions (state) {
const { location, devices } = state.devices.castMenu const { location, devices } = state.devices.castMenu
const player = state.devices[location] const player = state.devices[location]
const items = devices.map(function (device, ix) { const items = devices.map((device, ix) => {
const isSelected = player.device === device const isSelected = player.device === device
const name = device.name const name = device.name
return ( return (
@@ -527,7 +527,7 @@ function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
const items = subtitles.tracks.map(function (track, ix) { const items = subtitles.tracks.map((track, ix) => {
const isSelected = state.playing.subtitles.selectedIndex === ix const isSelected = state.playing.subtitles.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}> <li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
@@ -554,7 +554,7 @@ function renderAudioTrackOptions (state) {
const audioTracks = state.playing.audioTracks const audioTracks = state.playing.audioTracks
if (!audioTracks.tracks.length || !audioTracks.showMenu) return if (!audioTracks.tracks.length || !audioTracks.showMenu) return
const items = audioTracks.tracks.map(function (track, ix) { const items = audioTracks.tracks.map((track, ix) => {
const isSelected = state.playing.audioTracks.selectedIndex === ix const isSelected = state.playing.audioTracks.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectAudioTrack', ix)}> <li key={ix} onClick={dispatcher('selectAudioTrack', ix)}>
@@ -672,7 +672,7 @@ function renderPlayerControls (state) {
airplay: { true: 'airplay', false: 'airplay' }, airplay: { true: 'airplay', false: 'airplay' },
dlna: { true: 'tv', false: 'tv' } dlna: { true: 'tv', false: 'tv' }
} }
castTypes.forEach(function (castType) { castTypes.forEach(castType => {
// Do we show this button (eg. the Chromecast button) at all? // Do we show this button (eg. the Chromecast button) at all?
const isCasting = state.playing.location.startsWith(castType) const isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType] const player = state.devices[castType]
@@ -779,7 +779,7 @@ function renderPlayerControls (state) {
dispatch('preview', e.clientX) dispatch('preview', e.clientX)
} }
function clearPreview (e) { function clearPreview () {
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
dispatch('clearPreview') dispatch('clearPreview')
} }
@@ -795,7 +795,7 @@ function renderPlayerControls (state) {
} }
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute (e) { function handleVolumeMute () {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {
dispatch('setVolume', 1.0) dispatch('setVolume', 1.0)
} else { } else {
@@ -817,7 +817,7 @@ function renderPlayerControls (state) {
} }
} }
function handleAudioTracks (e) { function handleAudioTracks () {
dispatch('toggleAudioTracksMenu') dispatch('toggleAudioTracksMenu')
} }
@@ -914,7 +914,7 @@ function renderLoadingBar (state) {
} }
// Output some bars to show which parts of the file are loaded // Output some bars to show which parts of the file are loaded
const loadingBarElems = parts.map(function (part, i) { const loadingBarElems = parts.map((part, i) => {
const style = { const style = {
left: (100 * part.start / fileProg.numPieces) + '%', left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%' width: (100 * part.count / fileProg.numPieces) + '%'

View File

@@ -1,16 +1,18 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
import checkbox from 'material-ui/Checkbox'
import raisedButton from 'material-ui/RaisedButton'
const colors = require('material-ui/styles/colors') import Heading from '../components/heading.js'
const Checkbox = require('material-ui/Checkbox').default import PathSelector from '../components/path-selector.js'
const RaisedButton = require('material-ui/RaisedButton').default import { dispatch } from '../lib/dispatcher.js'
const Heading = require('../components/heading') import config from '../../config.js'
const PathSelector = require('../components/path-selector')
const { dispatch } = require('../lib/dispatcher') const Checkbox = checkbox.default
const config = require('../../config') const RaisedButton = raisedButton.default
class PreferencesPage extends React.Component { export default class PreferencesPage extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -286,5 +288,3 @@ class Preference extends React.Component {
return (<div style={style}>{this.props.children}</div>) return (<div style={style}>{this.props.children}</div>)
} }
} }
module.exports = PreferencesPage

View File

@@ -1,15 +1,17 @@
const React = require('react') import * as React from 'react'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
import checkbox from 'material-ui/Checkbox'
import linearProgress from 'material-ui/LinearProgress'
const Checkbox = require('material-ui/Checkbox').default import TorrentSummary from '../lib/torrent-summary.js'
const LinearProgress = require('material-ui/LinearProgress').default import TorrentPlayer from '../lib/torrent-player.js'
import { dispatcher } from '../lib/dispatcher.js'
import { calculateEta } from '../lib/time.js'
const TorrentSummary = require('../lib/torrent-summary') const Checkbox = checkbox.default
const TorrentPlayer = require('../lib/torrent-player') const LinearProgress = linearProgress.default
const { dispatcher } = require('../lib/dispatcher')
const { calculateEta } = require('../lib/time')
module.exports = class TorrentList extends React.Component { export default class TorrentList extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state

View File

@@ -1,20 +1,23 @@
/* eslint-disable import/first */
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate // To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window. // process from the main window.
console.time('init') console.time('init')
const crypto = require('crypto') import crypto from 'crypto'
const util = require('util') import util from 'util'
const defaultAnnounceList = require('create-torrent').announceList import { announceList as defaultAnnounceList } from 'create-torrent'
const { ipcRenderer } = require('electron') import electron from 'electron'
const fs = require('fs') import fs from 'fs'
const mm = require('music-metadata') import * as mm from 'music-metadata'
const networkAddress = require('network-address') import networkAddress from 'network-address'
const path = require('path') import path from 'path'
const WebTorrent = require('webtorrent') import WebTorrent from 'webtorrent'
const config = require('../config') import config from '../config.js'
const { TorrentKeyNotFoundError } = require('./lib/errors') import { TorrentKeyNotFoundError } from './lib/errors.js'
const torrentPoster = require('./lib/torrent-poster') import torrentPoster from './lib/torrent-poster.js'
const { ipcRenderer } = electron
// Force use of webtorrent trackers on all torrents // Force use of webtorrent trackers on all torrents
globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList
@@ -24,7 +27,7 @@ globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList
/** /**
* WebTorrent version. * WebTorrent version.
*/ */
const VERSION = require('../../package.json').version const VERSION = JSON.parse(fs.readFileSync('package.json').toString()).version
/** /**
* Version number in Azureus-style. Generated from major and minor semver version. * Version number in Azureus-style. Generated from major and minor semver version.
@@ -79,7 +82,7 @@ function init () {
getAudioMetadata(infoHash, index)) getAudioMetadata(infoHash, index))
ipcRenderer.on('wt-start-server', (e, infoHash) => ipcRenderer.on('wt-start-server', (e, infoHash) =>
startServer(infoHash)) startServer(infoHash))
ipcRenderer.on('wt-stop-server', (e) => ipcRenderer.on('wt-stop-server', () =>
stopServer()) stopServer())
ipcRenderer.on('wt-select-files', (e, infoHash, selections) => ipcRenderer.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections)) selectFiles(infoHash, selections))
@@ -165,7 +168,7 @@ function addTorrentEvents (torrent) {
updateTorrentProgress() updateTorrentProgress()
torrent.getFileModtimes(function (err, fileModtimes) { torrent.getFileModtimes((err, fileModtimes) => {
if (err) return onError(err) if (err) return onError(err)
ipcRenderer.send('wt-file-modtimes', torrent.key, fileModtimes) ipcRenderer.send('wt-file-modtimes', torrent.key, fileModtimes)
}) })
@@ -200,7 +203,7 @@ function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent') const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, function (err) { fs.access(torrentPath, fs.constants.R_OK, err => {
const fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (!err) { if (!err) {
// We've already saved the file // We've already saved the file
@@ -208,8 +211,8 @@ function saveTorrentFile (torrentKey) {
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, { recursive: true }, function (_) { fs.mkdir(config.TORRENT_PATH, { recursive: true }, _ => {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, err => {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
return ipcRenderer.send('wt-file-saved', torrentKey, fileName) return ipcRenderer.send('wt-file-saved', torrentKey, fileName)
@@ -222,14 +225,14 @@ function saveTorrentFile (torrentKey) {
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, (err, buf, extension) => {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdir(config.POSTER_PATH, { recursive: true }, function (err) { fs.mkdir(config.POSTER_PATH, { recursive: true }, err => {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) { fs.writeFile(posterFilePath, buf, err => {
if (err) return console.log('error saving poster: %o', err) if (err) return console.log('error saving poster: %o', err)
// show the poster // show the poster
ipcRenderer.send('wt-poster', torrentKey, posterFileName) ipcRenderer.send('wt-poster', torrentKey, posterFileName)
@@ -251,15 +254,13 @@ function updateTorrentProgress () {
function getTorrentProgress () { function getTorrentProgress () {
// First, track overall progress // First, track overall progress
const progress = client.progress const progress = client.progress
const hasActiveTorrents = client.torrents.some(function (torrent) { const hasActiveTorrents = client.torrents.some(torrent => torrent.progress !== 1)
return torrent.progress !== 1
})
// Track progress for every file in each torrent // Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it // TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield // more efficiently than looping over torrent.bitfield
const torrentProg = client.torrents.map(function (torrent) { const torrentProg = client.torrents.map(torrent => {
const fileProg = torrent.files && torrent.files.map(function (file, index) { const fileProg = torrent.files && torrent.files.map(file => {
const numPieces = file._endPiece - file._startPiece + 1 const numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0 let numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) { for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
@@ -299,12 +300,12 @@ function startServer (infoHash) {
else torrent.once('ready', () => startServerFromReadyTorrent(torrent)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
} }
function startServerFromReadyTorrent (torrent, cb) { function startServerFromReadyTorrent (torrent) {
if (server) return if (server) return
// start the streaming torrent-to-http server // start the streaming torrent-to-http server
server = torrent.createServer() server = torrent.createServer()
server.listen(0, function () { server.listen(0, () => {
const port = server.address().port const port = server.address().port
const urlSuffix = ':' + port const urlSuffix = ':' + port
const info = { const info = {
@@ -339,7 +340,7 @@ function getAudioMetadata (infoHash, index) {
native: false, native: false,
skipCovers: true, skipCovers: true,
fileSize: file.length, fileSize: file.length,
observer: event => { observer: () => {
ipcRenderer.send('wt-audio-metadata', infoHash, index, { ipcRenderer.send('wt-audio-metadata', infoHash, index, {
common: metadata.common, common: metadata.common,
format: metadata.format format: metadata.format
@@ -384,7 +385,7 @@ function selectFiles (torrentOrInfoHash, selections) {
// selection with individual selections for each file, so we can // selection with individual selections for each file, so we can
// select/deselect files later on // select/deselect files later on
if (!selections) { if (!selections) {
selections = torrent.files.map((x) => true) selections = new Array(torrent.files.length).fill(true)
} }
// Selections specified incorrectly? // Selections specified incorrectly?
@@ -397,15 +398,15 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false) torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files) // Add selections (individual files)
for (let i = 0; i < selections.length; i++) { selections.forEach((selection, i) => {
const file = torrent.files[i] const file = torrent.files[i]
if (selections[i]) { if (selection) {
file.select() file.select()
} else { } else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name) console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect() file.deselect()
} }
} })
} }
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
@@ -423,7 +424,7 @@ function onError (err) {
// TODO: remove this once the following bugs are fixed: // TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143 // https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212 // https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () { window.testOfflineMode = () => {
console.log('Test, going OFFLINE') console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({ client = window.client = new WebTorrent({
peerId: PEER_ID, peerId: PEER_ID,

View File

@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id='body'></div> <div id='body'></div>
<script>require('../build/renderer/main.js')</script> <script type="module" src="../build/renderer/main.js"></script>
</body> </body>
</html> </html>