Compare commits

..

2 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
93a9ba11b6 App Sandboxing (OS X) 2016-05-21 01:08:42 -07:00
Feross Aboukhadijeh
b367c0b72e electron-prebuilt@1.1.1 2016-05-20 23:57:51 -07:00
19 changed files with 305 additions and 950 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
node_modules node_modules
dist dist

View File

@@ -18,8 +18,5 @@
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com> - Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com> - gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net> - Rolando Guedes <rolando.guedes@3gnt.net>
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,34 +1,5 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.6.0 - 2016-05-24
### Added
- Added Preferences page
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting a `instant.io` links (#559)
- Add announcement feature
### Changed
- Nicer player UI
- Reduce startup jank, improve startup time (#568)
- Cleanup unsupported codec detection (#569, #570)
- Cleaner look for the torrent file list
- Improve subtitle positioning (#551)
### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
- After deleting torrent, remove just the player from forward stack
- After creating torrent, remove create torrent page from forward stack
- Cancel button on create torrent page should only go back one page
## v0.5.1 - 2016-05-18 ## v0.5.1 - 2016-05-18
### Fixed ### Fixed

View File

@@ -103,7 +103,7 @@ var all = {
var darwin = { var darwin = {
// Build for OS X // Build for OS X
platform: 'darwin', platform: 'mas',
// Build 64 bit binaries only. // Build 64 bit binaries only.
arch: 'x64', arch: 'x64',
@@ -182,6 +182,8 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -209,24 +211,7 @@ function buildDarwin (cb) {
} }
] ]
infoPlist.UTExportedTypeDeclarations = [ infoPlist.ElectronTeamID = '5MAMC8G3L8'
{
UTTypeConformsTo: [
'public.data',
'public.item',
'com.bittorrent.torrent'
],
UTTypeDescription: 'BitTorrent Document',
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
UTTypeIdentifier: 'org.bittorrent.torrent',
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
}
]
fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
@@ -265,8 +250,9 @@ function buildDarwin (cb) {
*/ */
var signOpts = { var signOpts = {
app: appPath, app: appPath,
platform: 'darwin', entitlements: path.join(config.STATIC_PATH, 'parent.entitlements'),
verbose: true 'entitlements-inherit': path.join(config.STATIC_PATH, 'child.entitlements'),
platform: 'mas'
} }
console.log('OS X: Signing app...') console.log('OS X: Signing app...')

View File

@@ -26,14 +26,14 @@ if (process.platform === 'win32') {
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1) argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
} }
if (!shouldQuit) { // if (!shouldQuit) {
// Prevent multiple instances of app from running at same time. New instances signal // // Prevent multiple instances of app from running at same time. New instances signal
// this instance and quit. // // this instance and quit.
shouldQuit = app.makeSingleInstance(onAppOpen) // shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) { // if (shouldQuit) {
app.quit() // app.quit()
} // }
} // }
if (!shouldQuit) { if (!shouldQuit) {
init() init()

View File

@@ -92,35 +92,6 @@ function openSubtitles () {
} }
} }
function skipForward () {
if (windows.main) {
windows.main.send('dispatch', 'skip', 1)
}
}
function skipBack () {
if (windows.main) {
windows.main.send('dispatch', 'skip', -1)
}
}
function increasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', 1)
}
}
function decreasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', -1)
}
}
// Open the preferences window
function showPreferences () {
windows.main.send('dispatch', 'preferences')
}
function onWindowShow () { function onWindowShow () {
log('onWindowShow') log('onWindowShow')
getMenuItem('Full Screen').enabled = true getMenuItem('Full Screen').enabled = true
@@ -139,10 +110,6 @@ function onPlayerOpen () {
getMenuItem('Increase Volume').enabled = true getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true getMenuItem('Decrease Volume').enabled = true
getMenuItem('Add Subtitles File...').enabled = true getMenuItem('Add Subtitles File...').enabled = true
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
} }
function onPlayerClose () { function onPlayerClose () {
@@ -151,10 +118,6 @@ function onPlayerClose () {
getMenuItem('Increase Volume').enabled = false getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false getMenuItem('Add Subtitles File...').enabled = false
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
} }
function onToggleFullScreen (isFullScreen) { function onToggleFullScreen (isFullScreen) {
@@ -274,14 +237,6 @@ function getAppMenuTemplate () {
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
role: 'selectall' role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
} }
] ]
}, },
@@ -352,36 +307,6 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right',
click: skipForward,
enabled: false
},
{
label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left',
click: skipBack,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate,
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate,
enabled: false
},
{
type: 'separator'
},
{ {
label: 'Add Subtitles File...', label: 'Add Subtitles File...',
click: openSubtitles, click: openSubtitles,
@@ -424,14 +349,6 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
},
{
type: 'separator'
},
{ {
label: 'Services', label: 'Services',
role: 'services', role: 'services',

View File

@@ -1,7 +1,7 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.6.0", "version": "0.5.1",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@feross.org", "email": "feross@feross.org",
@@ -39,8 +39,7 @@
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"vlc-command": "^1.0.1", "vlc-command": "^1.0.1",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.2.0", "winreg": "^1.2.0"
"zero-fill": "^2.2.3"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",

View File

@@ -50,22 +50,19 @@ table {
} }
@keyframes fadein { @keyframes fadein {
from { from { opacity: 0; }
opacity: 0; to { opacity: 1; }
}
to {
opacity: 1;
}
} }
.app { .app {
-webkit-user-select: none; -webkit-user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
height: 100%; height: 100%;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
animation: fadein 0.3s;
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
animation: fadein 1s;
} }
.app:not(.is-focused) { .app:not(.is-focused) {
@@ -97,20 +94,11 @@ table {
word-wrap: normal; word-wrap: normal;
white-space: nowrap; white-space: nowrap;
direction: ltr; direction: ltr;
opacity: 0.85;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.icon.disabled {
opacity: 0.3;
}
.icon:not(.disabled):hover {
opacity: 1;
}
/* /*
* UTILITY CLASSES * UTILITY CLASSES
*/ */
@@ -121,8 +109,8 @@ table {
white-space: nowrap; white-space: nowrap;
} }
.float-left { .disabled {
float: left; opacity: 0.3;
} }
.float-right { .float-right {
@@ -156,8 +144,8 @@ table {
.header { .header {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
border-bottom: 1px solid rgb(20, 20, 20); border-bottom: 1px solid rgb(20, 20, 20);
height: 38px; /* vertically center OS menu buttons (OS X) */ height: 37px; /* vertically center OS menu buttons (OS X) */
padding-top: 7px; padding-top: 6px;
overflow: hidden; overflow: hidden;
flex: 0 1 auto; flex: 0 1 auto;
opacity: 1; opacity: 1;
@@ -176,13 +164,7 @@ table {
} }
.app.view-player .header { .app.view-player .header {
background: rgba(40, 40, 40, 0.8); opacity: 0.8;
border-bottom: none;
}
.app.view-player.is-win32 .header,
.app.view-player.is-linux .header {
background: none;
} }
.app.hide-video-controls.view-player .header { .app.hide-video-controls.view-player .header {
@@ -190,8 +172,12 @@ table {
cursor: none; cursor: none;
} }
.app.hide-header .header {
display: none;
}
.header .title { .header .title {
opacity: 0.7; opacity: 0.6;
position: absolute; position: absolute;
margin-top: 1px; margin-top: 1px;
padding: 0 150px 0 150px; padding: 0 150px 0 150px;
@@ -202,22 +188,35 @@ table {
.header .nav { .header .nav {
font-weight: bold; font-weight: bold;
margin-right: 9px;
} }
.header .nav.left { .header .nav.left {
margin-left: 10px; float: left;
}
.header .nav.right {
margin-right: 10px;
} }
.app.is-darwin:not(.is-fullscreen) .header .nav.left { .app.is-darwin:not(.is-fullscreen) .header .nav.left {
margin-left: 78px; margin-left: 78px;
} }
.header .back, .header .nav.right {
.header .forward { float: right;
}
.header .nav * {
opacity: 0.6;
}
.header .nav .disabled {
opacity: 0.1;
}
.header .nav *:not(.disabled):hover {
opacity: 1;
}
.header .nav .back,
.header .nav .forward {
font-size: 30px; font-size: 30px;
margin-top: -3px; margin-top: -3px;
} }
@@ -536,11 +535,6 @@ input {
} }
} }
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete { .torrent .buttons .delete {
opacity: 0.5; opacity: 0.5;
} }
@@ -549,10 +543,6 @@ input {
opacity: 0.7; opacity: 0.7;
} }
.torrent .buttons .radial-progress {
position: absolute;
}
.torrent .name { .torrent .name {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
@@ -602,7 +592,7 @@ body.drag .app::after {
} }
.torrent-details { .torrent-details {
padding: 8em 0 20px 0; padding: 8em 12px 20px 20px;
} }
.torrent-details table { .torrent-details table {
@@ -627,7 +617,7 @@ body.drag .app::after {
.torrent-details td { .torrent-details td {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
vertical-align: middle; vertical-align: bottom;
} }
.torrent-details td .icon { .torrent-details td .icon {
@@ -637,14 +627,7 @@ body.drag .app::after {
} }
.torrent-details td.col-icon { .torrent-details td.col-icon {
width: 3em; width: 2em;
padding-left: 16px;
}
.torrent-details td.col-icon .radial-progress {
position: absolute;
margin-top: 4px;
margin-left: 0.5px;
} }
.torrent-details td.col-name { .torrent-details td.col-name {
@@ -663,8 +646,7 @@ body.drag .app::after {
} }
.torrent-details td.col-select { .torrent-details td.col-select {
width: 3em; width: 2em;
padding-right: 13px;
text-align: right; text-align: right;
} }
@@ -696,7 +678,7 @@ body.drag .app::after {
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
.player .controls { .player-controls {
position: fixed; position: fixed;
background: rgba(40, 40, 40, 0.8); background: rgba(40, 40, 40, 0.8);
width: 100%; width: 100%;
@@ -705,63 +687,7 @@ body.drag .app::after {
transition: opacity 0.15s ease-out; transition: opacity 0.15s ease-out;
} }
.player .controls .icon { .app.hide-video-controls .player-controls {
display: block;
margin: 8px;
font-size: 22px;
opacity: 0.85;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 23px;
overflow: hidden;
}
.player .controls .icon:hover {
opacity: 1;
}
.player .controls .play-pause {
font-size: 28px;
margin-top: 5px;
margin-right: 10px;
margin-left: 15px;
}
.player .controls .volume-slider {
-webkit-appearance: none;
-webkit-app-region: no-drag;
width: 60px;
height: 3px;
margin: 18px 8px 8px 0;
border: none;
padding: 0;
opacity: 0.85;
vertical-align: sub;
}
.player .controls .time,
.player .controls .rate {
font-weight: 100;
font-size: 13px;
margin: 9px 8px 8px 8px;
opacity: 0.8;
}
.player .controls .icon.closed-captions {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen {
font-size: 26px;
margin-right: 15px;
margin-top: 6px;
}
.app.hide-video-controls .player .controls {
opacity: 0; opacity: 0;
} }
@@ -769,16 +695,13 @@ body.drag .app::after {
cursor: none; cursor: none;
} }
/* TODO: find better way to handle this (that also .app.hide-video-controls .player .player-controls:hover {
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1; opacity: 1;
cursor: default; cursor: default;
} }
/* invisible click target for scrubbing */ /* invisible click target for scrubbing */
.player .controls .scrub-bar { .player-controls .scrub-bar {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 23px; /* 3px .loading-bar plus 10px above and below */ height: 23px; /* 3px .loading-bar plus 10px above and below */
@@ -787,7 +710,7 @@ body.drag .app::after {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.player .controls .loading-bar { .player-controls .loading-bar {
position: relative; position: relative;
width: 100%; width: 100%;
top: -3px; top: -3px;
@@ -797,14 +720,14 @@ body.drag .app::after {
position: absolute; position: absolute;
} }
.player .controls .loading-bar-part { .player-controls .loading-bar-part {
position: absolute; position: absolute;
background-color: #dd0000; background-color: #dd0000;
top: 0; top: 0;
height: 100%; height: 100%;
} }
.player .controls .playback-cursor { .player-controls .playback-cursor {
position: absolute; position: absolute;
top: -3px; top: -3px;
background-color: #FFF; background-color: #FFF;
@@ -813,26 +736,94 @@ body.drag .app::after {
border-radius: 50%; border-radius: 50%;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
transition-property: width, height, top, margin-left; transition-property: width, height, border-radius, margin-top, margin-left;
transition-duration: 0.1s; transition-duration: 0.1s;
transition-timing-function: ease-out; transition-timing-function: ease-out;
} }
.player .controls .closed-captions.active, .player-controls .play-pause {
.player .controls .device.active { display: block;
width: 30px;
height: 30px;
padding: 5px;
margin: 0 auto;
}
.player-controls .device,
.player-controls .fullscreen,
.player-controls .closed-captions,
.player-controls .volume-icon,
.player-controls .back {
display: block;
height: 20px;
margin: 5px;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 22px;
overflow: hidden;
}
.player-controls .volume,
.player-controls .back {
float: left;
}
.player-controls .device,
.player-controls .closed-captions,
.player-controls .fullscreen {
float: right;
}
.player-controls .fullscreen {
margin-right: 15px;
}
.player-controls .volume-icon,
.player-controls .device {
font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important;
}
.player-controls .closed-captions.active,
.player-controls .device.active {
color: #9af; color: #9af;
} }
.player .controls .volume-slider::-webkit-slider-thumb { .player-controls .volume {
-webkit-appearance: none; display: block;
-webkit-app-region: no-drag; width: 90px;
background-color: #fff;
width: 13px;
height: 13px;
border-radius: 50%;
} }
.player .controls .volume-slider:focus { .player-controls .volume-icon {
float: left;
margin-right: 0px;
}
.player-controls .volume-slider {
-webkit-appearance: none;
width: 50px;
height: 3px;
border: none;
padding: 0;
vertical-align: sub;
-webkit-app-region: no-drag;
}
.player-controls .volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: #fff;
opacity: 1.0;
width: 10px;
height: 10px;
border: 1px solid #303233;
border-radius: 50%;
-webkit-app-region: no-drag;
}
.player-controls .volume-slider:focus {
outline: none; outline: none;
} }
@@ -842,27 +833,19 @@ body.drag .app::after {
.player .playback-bar:hover .playback-cursor { .player .playback-bar:hover .playback-cursor {
top: -8px; top: -8px;
margin-left: -5px;
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
/**
* Set the cue text position so it appears above the player controls.
*/
video::-webkit-media-text-track-container {
bottom: 60px;
transition: bottom 0.1s ease-out;
}
.app.hide-video-controls video::-webkit-media-text-track-container {
bottom: 30px;
}
::cue { ::cue {
background: none; background: none;
color: #FFF; color: #FFF;
font: 24px;
line-height: 1.3em;
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0; text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
} }
/* /*
* CHROMECAST / AIRPLAY CONTROLS * CHROMECAST / AIRPLAY CONTROLS
*/ */
@@ -912,173 +895,6 @@ video::-webkit-media-text-track-container {
margin-right: 4px !important; margin-right: 4px !important;
} }
/*
* Preferences page, based on Atom settings style
*/
.preferences {
font-size: 12px;
line-height: calc(10/7);
}
.preferences .text {
color: #a8a8a8;
}
.preferences .icon {
color: rgba(170, 170, 170, 0.6);
font-size: 16px;
margin-right: 0.2em;
}
.preferences .btn {
display: inline-block;
-webkit-appearance: button;
margin: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
border-color: #cccccc;
border-radius: 3px;
color: #9da5b4;
text-shadow: none;
border: 1px solid #181a1f;
background-color: #3d3d3d;
white-space: initial;
font-size: 0.889em;
line-height: 1;
padding: 0.5em 0.75em;
}
.preferences .btn .icon {
margin: 0;
color: #a8a8a8;
}
.preferences .help .icon {
vertical-align: sub;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .section {
padding: 20px;
border-top: 1px solid #181a1f;
}
.preferences .section:first {
border-top: 0px;
}
.preferences .section:first-child,
.preferences .section:last-child {
padding: 20px;
}
.preferences .section.section:empty {
padding: 0;
border-top: none;
}
.preferences .section-container {
width: 100%;
max-width: 800px;
}
.preferences section .section-heading,
.preferences .section .section-heading {
margin-bottom: 10px;
color: #dcdcdc;
font-size: 1.75em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
cursor: default;
}
.preferences .sub-section-heading.icon:before,
.preferences .section-heading.icon:before {
margin-right: 8px;
}
.preferences .section-heading-count {
margin-left: .5em;
}
.preferences .section-body {
margin-top: 20px;
}
.preferences .sub-section {
margin: 20px 0;
}
.preferences .sub-section .sub-section-heading {
color: #dcdcdc;
font-size: 1.4em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
}
.preferences .preferences-panel label {
color: #a8a8a8;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .preferences-panel .control-group .editor-container {
margin: 0;
}
.preferences .preference-title {
font-size: 1.2em;
-webkit-user-select: none;
display: inline-block;
}
.preferences .preference-description {
color: rgba(170, 170, 170, 0.6);
-webkit-user-select: none;
cursor: default;
}
.preferences input {
font-size: 1.1em;
line-height: 1.15em;
max-height: none;
width: 100%;
padding-left: 0.5em;
border-radius: 3px;
color: #a8a8a8;
border: 1px solid #181a1f;
background-color: #1b1d23;
}
.preferences input::-webkit-input-placeholder {
color: rgba(170, 170, 170, 0.6);
}
.preferences .control-group input {
margin-top: 0.2em;
}
.preferences .control-group input.file-picker-text {
width: calc(100% - 40px);
}
.preferences .control-group .checkbox .icon {
font-size: 1.5em;
margin: 0;
vertical-align: text-bottom;
}
/* /*
* MEDIA OVERLAY / AUDIO DETAILS * MEDIA OVERLAY / AUDIO DETAILS
*/ */
@@ -1147,6 +963,10 @@ video::-webkit-media-text-track-container {
z-index: 1; z-index: 1;
} }
.app.hide-header .error-popover {
top: 0px;
}
.error-popover.hidden { .error-popover.hidden {
display: none; display: none;
} }
@@ -1174,66 +994,3 @@ video::-webkit-media-text-track-container {
.error-text { .error-text {
color: #c44; color: #c44;
} }
/*
* RADIAL PROGRESS BAR
*/
.radial-progress {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #888;
}
.radial-progress .circle .mask,
.radial-progress .circle .fill {
width: 16px;
height: 16px;
position: absolute;
border-radius: 50%;
-webkit-backface-visibility: hidden;
}
.radial-progress .circle .mask {
clip: rect(0px, 16px, 16px, 8px);
}
.radial-progress .circle .fill {
clip: rect(0px, 8px, 16px, 0px);
background-color: white;
}
.radial-progress .inset {
position: absolute;
width: 12px;
height: 12px;
margin: 2px 0 0 2px;
border-radius: 50%;
background-color: black;
}
.radial-progress-large {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask,
.radial-progress-large .circle .fill {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask {
clip: rect(0px, 40px, 40px, 20px);
}
.radial-progress-large .circle .fill {
clip: rect(0px, 20px, 40px, 0px);
background-color: white;
}
.radial-progress-large .inset {
width: 32px;
height: 32px;
margin: 4px 0 0 4px;
}

View File

@@ -14,8 +14,10 @@ var ipcRenderer = electron.ipcRenderer
setupIpc() setupIpc()
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var concat = require('simple-concat')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var fs = require('fs-extra') var fs = require('fs-extra')
var iso639 = require('iso-639-1')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var parallel = require('run-parallel') var parallel = require('run-parallel')
var path = require('path') var path = require('path')
@@ -40,29 +42,18 @@ appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
var Cast = null var Cast = null
var vdomLoop // For easy debugging in Developer Tools
var state = global.state = State.getInitialState()
var state = State.getInitialState() // Push the first page into the location history
state.location.go({ url: 'home' }) // Add first page to location history state.location.go({ url: 'home' })
var vdomLoop
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
loadState(init) loadState(init)
function loadState (cb) {
appConfig.read(function (err, data) {
if (err) console.error(err)
// populate defaults if they're not there
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
state.saved.torrents.forEach(function (torrentSummary) {
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
})
if (cb) cb()
})
}
/** /**
* Called once when the application loads. (Not once per window.) * Called once when the application loads. (Not once per window.)
* Connects to the torrent networks, sets up the UI and OS integrations like * Connects to the torrent networks, sets up the UI and OS integrations like
@@ -192,7 +183,7 @@ function render (state) {
// Calls render() to go from state -> UI, then applies to vdom to the real DOM. // Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () { function update () {
showOrHidePlayerControls() showOrHidePlayerControls()
if (vdomLoop) vdomLoop.update(state) vdomLoop.update(state)
updateElectron() updateElectron()
} }
@@ -282,17 +273,18 @@ function dispatch (action, ...args) {
playPause() playPause()
} }
if (action === 'play') { if (action === 'play') {
playFile(args[0] /* infoHash */, args[1] /* index */) state.location.go({
url: 'player',
onbeforeload: function (cb) {
play()
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
} }
if (action === 'playbackJump') { if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */) jumpToTime(args[0] /* seconds */)
} }
if (action === 'skip') {
jumpToTime(state.playing.currentTime + (args[0] /* direction */ * 10))
}
if (action === 'changePlaybackRate') {
changePlaybackRate(args[0] /* direction */)
}
if (action === 'changeVolume') { if (action === 'changeVolume') {
changeVolume(args[0] /* increase */) changeVolume(args[0] /* increase */)
} }
@@ -346,26 +338,6 @@ function dispatch (action, ...args) {
if (action === 'exitModal') { if (action === 'exitModal') {
state.modal = null state.modal = null
} }
if (action === 'preferences') {
state.location.go({
url: 'preferences',
onbeforeload: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
cb()
},
onbeforeunload: function (cb) {
// save state after preferences
savePreferences()
state.window.title = config.APP_WINDOW_TITLE
cb()
}
})
}
if (action === 'updatePreferences') {
updatePreferences(args[0], args[1] /* property, value */)
}
if (action === 'updateAvailable') { if (action === 'updateAvailable') {
updateAvailable(args[0] /* version */) updateAvailable(args[0] /* version */)
} }
@@ -427,26 +399,7 @@ function jumpToTime (time) {
state.playing.jumpToTime = time state.playing.jumpToTime = time
} }
} }
function changePlaybackRate (direction) {
var rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) {
rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
rate = -1
} else if (direction > 0 && rate === -1) {
rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2
}
state.playing.playbackRate = rate
if (lazyLoadCast().isCasting() && !Cast.setRate(rate)) {
state.playing.playbackRate = 1
}
}
function changeVolume (delta) { function changeVolume (delta) {
// change volume with delta value // change volume with delta value
setVolume(state.playing.volume + delta) setVolume(state.playing.volume + delta)
@@ -536,6 +489,22 @@ function setupIpc () {
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
} }
// Load state.saved from the JSON state file
function loadState (cb) {
appConfig.read(function (err, data) {
if (err) console.error(err)
console.log('loaded state from ' + appConfig.filePath)
// populate defaults if they're not there
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
state.saved.torrents.forEach(function (torrentSummary) {
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
})
if (cb) cb()
})
}
// Starts all torrents that aren't paused on program startup // Starts all torrents that aren't paused on program startup
function resumeTorrents () { function resumeTorrents () {
state.saved.torrents state.saved.torrents
@@ -543,27 +512,6 @@ function resumeTorrents () {
.forEach((x) => startTorrentingSummary(x)) .forEach((x) => startTorrentingSummary(x))
} }
// Updates a single property in the UNSAVED prefs
// For example: updatePreferences("foo.bar", "baz")
// Call savePreferences to save to config.json
function updatePreferences (property, value) {
var path = property.split('.')
var key = state.unsaved.prefs
for (var i = 0; i < path.length - 1; i++) {
if (typeof key[path[i]] === 'undefined') {
key[path[i]] = {}
}
key = key[path[i]]
}
key[path[i]] = value
}
// All unsaved prefs take effect atomically, and are saved to config.json
function savePreferences () {
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
saveState()
}
// Don't write state.saved to file more than once a second // Don't write state.saved to file more than once a second
function saveStateThrottled () { function saveStateThrottled () {
if (state.saveStateTimeout) return if (state.saveStateTimeout) return
@@ -589,7 +537,7 @@ function saveState () {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process continue // Don't save progress info or key for the webtorrent process
} }
if (key === 'playStatus') { if (key === 'playStatus' && x.playStatus !== 'unplayable') {
continue // Don't save whether a torrent is playing / pending continue // Don't save whether a torrent is playing / pending
} }
torrent[key] = x[key] torrent[key] = x[key]
@@ -657,19 +605,14 @@ function getTorrentSummary (torrentKey) {
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
function addTorrent (torrentId) { function addTorrent (torrentId) {
backToList() backToList()
var torrentKey = state.nextTorrentKey++ var torrentKey = state.nextTorrentKey++
var path = state.saved.prefs.downloadPath var path = state.saved.downloadPath
if (torrentId.path) { if (torrentId.path) {
// Use path string instead of W3C File object // Use path string instead of W3C File object
torrentId = torrentId.path torrentId = torrentId.path
} }
// Allow a instant.io link to be pasted
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
} }
@@ -704,9 +647,8 @@ function addSubtitles (files, autoSelect) {
} }
function loadSubtitle (file, cb) { function loadSubtitle (file, cb) {
var concat = require('simple-concat')
var LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt') var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
// Read the .SRT or .VTT file, parse it, add subtitle track // Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file var filePath = file.path || file
@@ -722,8 +664,12 @@ function loadSubtitle (file, cb) {
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle' langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1) langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
// Set the cue text position so it appears above the player controls.
// The only way to change cue text position is by modifying the VTT. It is not
// possible via CSS.
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
var track = { var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'), buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
language: langDetected, language: langDetected,
label: langDetected, label: langDetected,
filePath: filePath filePath: filePath
@@ -740,7 +686,6 @@ function selectSubtitle (ix) {
// 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) { function isSystemLanguage (language) {
var iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg "en" var osLangISO = window.navigator.language.split('-')[0] // eg "en"
var langIso = iso639.getCode(language) // eg "de" if language is "German" var langIso = iso639.getCode(language) // eg "de" if language is "German"
return langIso === osLangISO return langIso === osLangISO
@@ -783,7 +728,7 @@ function startTorrentingSummary (torrentSummary) {
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++ if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
// Use Downloads folder by default // Use Downloads folder by default
var path = s.path || state.saved.prefs.downloadPath var path = s.path || state.saved.downloadPath
var torrentID var torrentID
if (s.torrentFileName) { // Load torrent file from disk if (s.torrentFileName) { // Load torrent file from disk
@@ -902,17 +847,11 @@ function torrentMetadata (torrentKey, torrentInfo) {
torrentSummary.status = 'downloading' torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path torrentSummary.path = torrentInfo.path
torrentSummary.files = torrentInfo.files
torrentSummary.magnetURI = torrentInfo.magnetURI torrentSummary.magnetURI = torrentInfo.magnetURI
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
// For now, check whether torrentSummary.files has already been set:
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
if (!hasDetailedFileInfo) {
torrentSummary.files = torrentInfo.files
}
if (!torrentSummary.selections) { if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true) torrentSummary.selections = torrentSummary.files.map((x) => true)
} }
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
update() update()
// Save the .torrent file, if it hasn't been saved already // Save the .torrent file, if it hasn't been saved already
@@ -1021,25 +960,11 @@ function pickFileToPlay (files) {
return undefined return undefined
} }
function playFile (infoHash, index) { // Opens the video player
state.location.go({
url: 'player',
onbeforeload: function (cb) {
play()
openPlayer(infoHash, index, cb)
},
onbeforeunload: closePlayer
}, function (err) {
if (err) onError(err)
})
}
// Opens the video player to a specific torrent
function openPlayer (infoHash, index, cb) { function openPlayer (infoHash, index, cb) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
// automatically choose which file in the torrent to play, if necessary // automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = pickFileToPlay(torrentSummary.files) if (index === undefined) index = pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError()) if (index === undefined) return cb(new errors.UnplayableError())
@@ -1051,7 +976,7 @@ function openPlayer (infoHash, index, cb) {
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('Playback timed out. Try again.')) cb(new Error('playback timed out'))
update() update()
}, 10000) /* give it a few seconds */ }, 10000) /* give it a few seconds */
@@ -1074,15 +999,6 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
: TorrentPlayer.isAudio(fileSummary) ? 'audio' : TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other' : 'other'
// pick up where we left off
if (fileSummary.currentTime) {
var fraction = fileSummary.currentTime / fileSummary.duration
var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
state.playing.jumpToTime = fileSummary.currentTime
}
}
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) { if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
@@ -1232,7 +1148,7 @@ function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent` var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = { var opts = {
title: 'Save Torrent File', title: 'Save Torrent File',
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName), defaultPath: path.join(state.saved.downloadPath, newFileName),
filters: [ filters: [
{ name: 'Torrent Files', extensions: ['torrent'] }, { name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }

View File

@@ -8,8 +8,7 @@ module.exports = {
play, play,
pause, pause,
seek, seek,
setVolume, setVolume
setRate
} }
var airplay = require('airplay-js') var airplay = require('airplay-js')
@@ -345,22 +344,6 @@ function pause () {
} }
} }
function setRate (rate) {
var device
var result = true
if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast
castCallback()
result = false
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay
device.rate(rate, castCallback)
} else {
result = false
}
return result
}
function seek (time) { function seek (time) {
var device = getDevice() var device = getDevice()
if (device) { if (device) {

View File

@@ -4,18 +4,12 @@ var captureVideoFrame = require('./capture-video-frame')
var path = require('path') var path = require('path')
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use the largest video file
var posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play // Filter out file formats that the <video> tag definitely can't play
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv']) var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, try to use the largest image file // Second, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png']) var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)

View File

@@ -9,7 +9,8 @@ var LocationHistory = require('./lib/location-history')
module.exports = { module.exports = {
getInitialState, getInitialState,
getDefaultPlayState, getDefaultPlayState,
getDefaultSavedState getDefaultSavedState,
getPlayingTorrentSummary
} }
function getInitialState () { function getInitialState () {
@@ -62,8 +63,7 @@ function getInitialState () {
/* /*
* Getters, for convenience * Getters, for convenience
*/ */
getPlayingTorrentSummary, getPlayingTorrentSummary
getPlayingFileSummary
} }
} }
@@ -80,7 +80,6 @@ function getDefaultPlayState () {
isStalled: false, isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */ lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */ mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: { subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */ tracks: [], /* subtitle tracks, each {label, language, ...} */
selectedIndex: -1, /* current subtitle track */ selectedIndex: -1, /* current subtitle track */
@@ -266,11 +265,9 @@ function getDefaultSavedState () {
] ]
} }
], ],
prefs: { downloadPath: config.IS_PORTABLE
downloadPath: config.IS_PORTABLE ? path.join(config.CONFIG_PATH, 'Downloads')
? path.join(config.CONFIG_PATH, 'Downloads') : remote.app.getPath('downloads')
: remote.app.getPath('downloads')
}
} }
} }
@@ -278,9 +275,3 @@ function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash) return this.saved.torrents.find((x) => x.infoHash === infoHash)
} }
function getPlayingFileSummary () {
var torrentSummary = this.getPlayingTorrentSummary()
if (!torrentSummary) return null
return torrentSummary.files[this.playing.fileIndex]
}

View File

@@ -8,8 +8,7 @@ var Header = require('./header')
var Views = { var Views = {
'home': require('./torrent-list'), 'home': require('./torrent-list'),
'player': require('./player'), 'player': require('./player'),
'create-torrent': require('./create-torrent-page'), 'create-torrent': require('./create-torrent-page')
'preferences': require('./preferences')
} }
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'), 'open-torrent-address-modal': require('./open-torrent-address-modal'),
@@ -27,8 +26,11 @@ function App (state) {
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
state.playing.location === 'local' && state.playing.location === 'local'
state.playing.playbackRate === 1
// Hide the header on Windows/Linux when in the player
// On OSX, the header appears as part of the title bar
var hideHeader = process.platform !== 'darwin' && state.location.url() === 'player'
var cls = [ var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */ 'view-' + state.location.url(), /* e.g. view-home, view-player */
@@ -37,6 +39,7 @@ function App (state) {
if (state.window.isFullScreen) cls.push('is-fullscreen') if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused') if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls') if (hideControls) cls.push('hide-video-controls')
if (hideHeader) cls.push('hide-header')
return hx` return hx`
<div class='app ${cls.join(' ')}'> <div class='app ${cls.join(' ')}'>

View File

@@ -10,7 +10,7 @@ function Header (state) {
return hx` return hx`
<div class='header'> <div class='header'>
${getTitle()} ${getTitle()}
<div class='nav left float-left'> <div class='nav left'>
<i.icon.back <i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'} class=${state.location.hasBack() ? '' : 'disabled'}
title='Back' title='Back'
@@ -24,7 +24,7 @@ function Header (state) {
chevron_right chevron_right
</i> </i>
</div> </div>
<div class='nav right float-right'> <div class='nav right'>
${getAddButton()} ${getAddButton()}
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@ function Header (state) {
} }
function getAddButton () { function getAddButton () {
if (state.location.url() === 'home') { if (state.location.url() !== 'player') {
return hx` return hx`
<i <i
class='icon add' class='icon add'

View File

@@ -4,9 +4,8 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill') var Bitfield = require('bitfield')
var TorrentSummary = require('../lib/torrent-summary') var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -49,9 +48,6 @@ function renderMedia (state) {
mediaElement.currentTime = state.playing.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null state.playing.jumpToTime = null
} }
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// Set volume // Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) { if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume mediaElement.volume = state.playing.setVolume
@@ -64,10 +60,8 @@ function renderMedia (state) {
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden' tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
} }
// Save video position state.playing.currentTime = mediaElement.currentTime
var file = state.getPlayingFileSummary() state.playing.duration = mediaElement.duration
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
@@ -131,13 +125,12 @@ function renderMedia (state) {
} }
function onCanPlay (e) { function onCanPlay (e) {
var elem = e.target var video = e.target
if (state.playing.type === 'video' && elem.webkitVideoDecodedByteCount === 0) { if (video.webkitVideoDecodedByteCount > 0 &&
dispatch('mediaError', 'Video codec unsupported') video.webkitAudioDecodedByteCount === 0) {
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
elem.play() video.play()
} }
} }
} }
@@ -168,7 +161,8 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var fileSummary = state.getPlayingFileSummary() var torrentSummary = state.getPlayingTorrentSummary()
var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo var info = fileSummary.audioInfo
@@ -298,7 +292,7 @@ function renderSubtitlesOptions (state) {
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.selectedIndex >= 0 : state.playing.subtitles.selectedIndex >= 0
@@ -309,27 +303,15 @@ function renderPlayerControls (state) {
hx` hx`
<div class='playback-bar'> <div class='playback-bar'>
${renderLoadingBar(state)} ${renderLoadingBar(state)}
<div <div class='playback-cursor' style=${playbackCursorStyle}></div>
class='playback-cursor' <div class='scrub-bar'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true' draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub}, onclick=${handleScrub},
ondrag=${handleScrub}> ondrag=${handleScrub}></div>
</div>
</div> </div>
`, `,
hx` hx`
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}> <i class='icon fullscreen'
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`,
hx`
<i
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}> onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i> </i>
@@ -339,7 +321,7 @@ function renderPlayerControls (state) {
if (state.playing.type === 'video') { if (state.playing.type === 'video') {
// show closed captions icon // show closed captions icon
elements.push(hx` elements.push(hx`
<i.icon.closed-captions.float-right <i.icon.closed-captions
class=${captionsClass} class=${captionsClass}
onclick=${handleSubtitles}> onclick=${handleSubtitles}>
closed_captions closed_captions
@@ -384,7 +366,7 @@ function renderPlayerControls (state) {
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast' var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx` elements.push(hx`
<i.icon.device.float-right <i.icon.device
class=${chromecastClass} class=${chromecastClass}
onclick=${chromecastHandler}> onclick=${chromecastHandler}>
${castIcon} ${castIcon}
@@ -393,7 +375,7 @@ function renderPlayerControls (state) {
} }
if (state.devices.airplay || isOnAirplay) { if (state.devices.airplay || isOnAirplay) {
elements.push(hx` elements.push(hx`
<i.icon.device.float-right <i.icon.device
class=${airplayClass} class=${airplayClass}
onclick=${airplayHandler}> onclick=${airplayHandler}>
airplay airplay
@@ -402,8 +384,7 @@ function renderPlayerControls (state) {
} }
if (state.devices.dlna || isOnDlna) { if (state.devices.dlna || isOnDlna) {
elements.push(hx` elements.push(hx`
<i <i.icon.device
class='icon device float-right'
class=${dlnaClass} class=${dlnaClass}
onclick=${dlnaHandler}> onclick=${dlnaHandler}>
tv tv
@@ -411,6 +392,17 @@ function renderPlayerControls (state) {
`) `)
} }
// On OSX, the back button is in the title bar of the window; see app.js
// On other platforms, we render one over the video on mouseover
if (process.platform !== 'darwin') {
elements.push(hx`
<i.icon.back
onclick=${dispatcher('back')}>
chevron_left
</i>
`)
}
// render volume // render volume
var volume = state.playing.volume var volume = state.playing.volume
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up') var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
@@ -420,54 +412,34 @@ function renderPlayerControls (state) {
} }
elements.push(hx` elements.push(hx`
<div class='volume float-left'> <div.volume>
<i <i.icon.volume-icon onmousedown=${handleVolumeMute}>
class='icon volume-icon float-left' ${volumeIcon}
onmousedown=${handleVolumeMute}> </i>
${volumeIcon} <input.volume-slider
</i> type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
<input onmousedown=${handleVolumeScrub}
class='volume-slider float-right' onmouseup=${handleVolumeScrub}
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume} onmousemove=${handleVolumeScrub}
onmousedown=${handleVolumeScrub} style=${volumeStyle}
onmouseup=${handleVolumeScrub} />
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
/>
</div> </div>
`) `)
// Show video playback progress // Finally, the big button in the center plays or pauses the video
elements.push(hx` elements.push(hx`
<span class='time float-left'> <i class='icon play-pause' onclick=${dispatcher('playPause')}>
${formatTime(state.playing.currentTime)} / ${formatTime(state.playing.duration)} ${state.playing.isPaused ? 'play_arrow' : 'pause'}
</span> </i>
`) `)
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx`
<span class='rate float-left'>
${state.playing.playbackRate}x
</span>
`)
}
return hx` return hx`
<div class='controls'> <div class='player-controls'>
${elements} ${elements}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
@@ -568,18 +540,3 @@ function cssBackgroundImageDarkGradient () {
return 'radial-gradient(circle at center, ' + return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' 'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
} }
function formatTime (time) {
if (typeof time !== 'number' || Number.isNaN(time)) {
return '0:00'
}
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
}

View File

@@ -1,104 +0,0 @@
module.exports = Preferences
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
var remote = require('electron').remote
var dialog = remote.dialog
function Preferences (state) {
return hx`
<div class='preferences'>
${renderGeneralSection(state)}
</div>
`
}
function renderGeneralSection (state) {
return renderSection({
title: 'General',
description: '',
icon: 'settings'
}, [
renderDownloadDirSelector(state)
])
}
function renderDownloadDirSelector (state) {
return renderFileSelector({
label: 'Download Path',
description: 'Data from torrents will be saved here',
property: 'downloadPath',
options: {
title: 'Select download directory',
properties: [ 'openDirectory' ]
}
},
state.unsaved.prefs.downloadPath,
function (filePath) {
setStateValue('downloadPath', filePath)
})
}
// Renders a prefs section.
// - definition should be {icon, title, description}
// - controls should be an array of vdom elements
function renderSection (definition, controls) {
var helpElem = !definition.description ? null : hx`
<div class='help text'>
<i.icon>help_outline</i>${definition.description}
</div>
`
return hx`
<section class='section preferences-panel'>
<div class='section-container'>
<div class='section-heading'>
<i.icon>${definition.icon}</i>${definition.title}
</div>
${helpElem}
<div class='section-body'>
${controls}
</div>
</div>
</section>
`
}
// Creates a file chooser
// - defition should be {label, description, options}
// options are passed to dialog.showOpenDialog
// - value should be the current pref, a file or folder path
// - callback takes a new file or folder path
function renderFileSelector (definition, value, callback) {
return hx`
<div class='control-group'>
<div class='controls'>
<label class='control-label'>
<div class='preference-title'>${definition.label}</div>
<div class='preference-description'>${definition.description}</div>
</label>
<div class='controls'>
<input type='text' class='file-picker-text'
id=${definition.property}
disabled='disabled'
value=${value} />
<button class='btn' onclick=${handleClick}>
<i.icon>folder_open</i>
</button>
</div>
</div>
</div>
`
function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return
callback(filenames[0])
})
}
}
function setStateValue (property, value) {
dispatch('updatePreferences', property, value)
}

View File

@@ -118,7 +118,12 @@ function TorrentList (state) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') { if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning' playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.' playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else { } else {
@@ -138,18 +143,6 @@ function TorrentList (state) {
downloadTooltip = 'Click to start torrenting.' downloadTooltip = 'Click to start torrenting.'
} }
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var positionElem
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
// Only show the play button for torrents that contain playable media // Only show the play button for torrents that contain playable media
var playButton var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) { if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
@@ -165,7 +158,6 @@ function TorrentList (state) {
return hx` return hx`
<div class='buttons'> <div class='buttons'>
${positionElem}
${playButton} ${playButton}
<i.button-round.icon.download <i.button-round.icon.download
class=${torrentSummary.status} class=${torrentSummary.status}
@@ -194,16 +186,11 @@ function TorrentList (state) {
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = hx`<div class='files warning'>${message}</div>`
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files var fileRows = torrentSummary.files.map(
.sort(function (a, b) { (file, index) => renderFileRow(torrentSummary, file, index))
if (a.name < b.name) return -1
if (b.name < a.name) return 1
return 0
})
.map((file, index) => renderFileRow(torrentSummary, file, index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -230,14 +217,7 @@ function TorrentList (state) {
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
} }
// Second, for media files where we saved our position, show how far we got // Second, render the file as a table row
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file) var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
@@ -253,18 +233,17 @@ function TorrentList (state) {
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx` return hx`
<tr onclick=${handleClick}> <tr>
<td class='col-icon ${rowClass}'> <td class='col-icon ${rowClass}' onclick=${handleClick}>
${positionElem}
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name ${rowClass}'> <td class='col-name ${rowClass}' onclick=${handleClick}>
${file.name} ${file.name}
</td> </td>
<td class='col-progress ${rowClass}'> <td class='col-progress ${rowClass}' onclick=${handleClick}>
${isSelected ? progress : ''} ${isSelected ? progress : ''}
</td> </td>
<td class='col-size ${rowClass}'> <td class='col-size ${rowClass}' onclick=${handleClick}>
${prettyBytes(file.length)} ${prettyBytes(file.length)}
</td> </td>
<td class='col-select' <td class='col-select'
@@ -275,24 +254,3 @@ function TorrentList (state) {
` `
} }
} }
function renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx`
<div class="radial-progress ${cssClass}">
<div class="circle">
<div class="mask full" style=${transformFill}>
<div class="fill" style=${transformFill}></div>
</div>
<div class="mask half">
<div class="fill" style=${transformFill}></div>
<div class="fill fix" style=${transformFix}></div>
</div>
</div>
<div class="inset"></div>
</div>
`
}

10
static/child.entitlements Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>5MAMC8G3L8.io.webtorrent.webtorrent</string>
</array>
</dict>
</plist>