Compare commits

..

36 Commits

Author SHA1 Message Date
DC
0a15db2892 0.6.0 2016-05-24 03:05:31 -07:00
DC
63dda10380 changelog 2016-05-24 03:03:42 -07:00
DC
6e651df083 authors 2016-05-24 02:42:29 -07:00
DC
3a8fe24eec Fix scrub cursor 2016-05-24 02:39:28 -07:00
DC
918a35e091 Fix scrub button position 2016-05-24 02:09:17 -07:00
DC
c76abeb8c0 Remove cursor:pointer 2016-05-24 01:59:51 -07:00
DC
d389b8ab38 Bugfix: window title shouldn't be stuck on 'Preferences' 2016-05-24 01:56:39 -07:00
DC
a59faacbd7 Simplify prefs window 2016-05-24 01:52:31 -07:00
grunjol
12f9709601 Add preferences page
* For now, the prefs page has just a single option, Downloads Folder
* For now, you can't type in a folder, you must use the chooser
* Further fixes coming om master
* Written by @ChrisMorrisOrg and @grunjol, rebased by @dcposch
2016-05-23 22:31:09 -07:00
Feross Aboukhadijeh
455c9c02b9 Reduce startup jank, improve startup time (#568)
* Reduce jank on app startup

This feels a lot better on my 12" macbook (underpowered machine)

* Defer loading iso-639-1 and simple-concat
2016-05-23 22:12:04 -07:00
Feross Aboukhadijeh
1b49c6568b Cleanup unsupported codec detection (pt 2) (#570) 2016-05-23 22:03:38 -07:00
Feross Aboukhadijeh
30e81c7699 Cleanup for PR #571 2016-05-23 15:15:52 -07:00
Feross Aboukhadijeh
2dafc68301 Merge pull request #571 from Lurk/242
#242 add sort by file name
2016-05-23 15:08:31 -07:00
Feross Aboukhadijeh
c310222af2 Merge pull request #566 from feross/ui-improvements
More consistent controls
2016-05-23 14:49:49 -07:00
Feross Aboukhadijeh
b4bb9a6603 Fix rate UX for new design 2016-05-23 14:47:02 -07:00
Feross Aboukhadijeh
279c621d23 More consistent controls, delete verbose css 2016-05-23 14:16:51 -07:00
DC
eb11dbdcbd Fix error handling in dispatch('play') 2016-05-23 06:17:17 -07:00
DC
8dfdb34d31 Bugfix: default file to play on default torrents 2016-05-23 05:42:37 -07:00
Sergey Bargamon
fc9a73d67f #242 add sort by file name 2016-05-23 10:43:55 +03:00
DC
4b5b84a0fc Resume playback from saved position, even if we had to restart the torrent 2016-05-23 00:33:17 -07:00
DC
327c95d754 Show video position on circular progress bars 2016-05-23 00:33:17 -07:00
DC
6e969e5d07 Cleaner look for the torrent file list 2016-05-23 00:33:17 -07:00
DC
ca7c872420 Save video position 2016-05-23 00:33:17 -07:00
Sergey Bargamon
8af4f42c42 Add additional video player keyboard shortcuts (#275)
*  Skip forward 10 seconds ((CMD OR CTRL) ALT right)
 Skip back 10 seconds ((CMD OR CTRL) ALT left)
 Increase video speed ((CMD OR CTRL) +)
 Decrease video speed ((CMD OR CTRL) -)

* Codestyle fix

* The 'steps' should be implemented in base2, standard players use 1x, 2x, 4x, 8x, 16x

fixed bug with shift + "=" which is "+"

* resolve conflicts

* remove ide specific data
make playback rate more granular
add to menu skip and speed entries

* intendation fix

* conflict resolve

* rename setPlaybackRate to changePlaybackRate
setRate return boolean depending on whether this cast target supports setting the playback rate.
if setRate returns false - don`t change state
redundant else if statement in changePlaybackRate function
2016-05-23 00:15:57 -07:00
Feross Aboukhadijeh
ffce76a9b1 Cleanup unsupported codec detection (#569)
Review: @dcposch
2016-05-22 23:35:29 -07:00
Feross Aboukhadijeh
fca1d9dae4 Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
Closes #539.
2016-05-22 23:20:30 -07:00
Feross Aboukhadijeh
eba09430e3 Merge pull request #563 from mathiasvr/patch
Handle unsupported video codec (e.g. H.265)
2016-05-22 23:13:50 -07:00
Feross Aboukhadijeh
6bc8de7625 Merge pull request #562 from demoneaux/video-progress
Add support for video progress time in player controls.
2016-05-22 19:51:59 -07:00
Benjamin Tan
8a08ed8538 Add support for video progress time in player controls.
Closes #351.
2016-05-23 10:39:58 +08:00
Mathias Rasmussen
56d802f741 Handle unsupported video codec (e.g. H.265) 2016-05-22 23:23:45 +02:00
Benjamin Tan
f7b46336fd Use poster.jpg file as the poster image if available. (#558)
Closes #501.
2016-05-22 02:14:48 -07:00
Feross Aboukhadijeh
510187c2ae electron-prebuilt@1.1.1 (#555) 2016-05-21 17:00:46 -07:00
Feross Aboukhadijeh
ff6ff8db00 Fewer click handlers (#552)
One more bit of cleanup for PR #529.

We can register `handleClick` once on the `<tr>` tag and just have the
onclick handler on `col-select` too. Because dispatcher calls
`event.stopPropagation()` we don't need to worry about event bubbling
up to the `<tr>`.
2016-05-21 16:50:18 -07:00
Feross Aboukhadijeh
014017604d Improve subtitle positioning (#551)
Before this commit, we tweaked the subtitle position by modifying the
VTT file, line by line with a regex because I did not know it was
possible to use CSS for it.

But apparently there are Shadow DOM elements that we can use instead.

This new approach improves:

- Wrapping long lines. Before, the text would go off the edge of the
screen. Now it wraps intelligently.

- The subtitles move up to get out of the way of the controls when
those are visible.
2016-05-21 16:49:30 -07:00
Feross Aboukhadijeh
8cf544d54f Associate .torrent files to WebTorrent Desktop (OS X) (#553)
Before this change, .torrent files would only be associated to
WebTorrent Desktop if another torrent client (like Transmission) was
installed on the system.

That's because one of the user's apps needs to define
"UTExportedTypeDeclarations".

On a fresh machine, without Transmission, WebTorrent Desktop now
associates .torrent files correctly.

So it will appear in the "Open With..." menu and the webtorrent
.torrent icon will be used for .torrent files.

Closes #542.
2016-05-21 16:47:57 -07:00
Benjamin Tan
870dd893fc Add support for pasting a instant.io link. (#559)
Closes #547.
2016-05-21 16:23:16 -07:00
19 changed files with 946 additions and 301 deletions

2
.gitignore vendored
View File

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

View File

@@ -18,5 +18,8 @@
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com>
- 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.

View File

@@ -1,5 +1,34 @@
# 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
### Fixed

View File

@@ -103,7 +103,7 @@ var all = {
var darwin = {
// Build for OS X
platform: 'mas',
platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
@@ -182,8 +182,6 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist')
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 = [
{
CFBundleTypeExtensions: [ 'torrent' ],
@@ -211,7 +209,24 @@ function buildDarwin (cb) {
}
]
infoPlist.ElectronTeamID = '5MAMC8G3L8'
infoPlist.UTExportedTypeDeclarations = [
{
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))
@@ -250,9 +265,8 @@ function buildDarwin (cb) {
*/
var signOpts = {
app: appPath,
entitlements: path.join(config.STATIC_PATH, 'parent.entitlements'),
'entitlements-inherit': path.join(config.STATIC_PATH, 'child.entitlements'),
platform: 'mas'
platform: 'darwin',
verbose: true
}
console.log('OS X: Signing app...')

View File

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

View File

@@ -92,6 +92,35 @@ 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 () {
log('onWindowShow')
getMenuItem('Full Screen').enabled = true
@@ -110,6 +139,10 @@ function onPlayerOpen () {
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').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 () {
@@ -118,6 +151,10 @@ function onPlayerClose () {
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').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) {
@@ -237,6 +274,14 @@ function getAppMenuTemplate () {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
}
]
},
@@ -307,6 +352,36 @@ function getAppMenuTemplate () {
{
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...',
click: openSubtitles,
@@ -349,6 +424,14 @@ function getAppMenuTemplate () {
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',

View File

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

View File

@@ -50,19 +50,22 @@ table {
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.app {
-webkit-user-select: none;
-webkit-app-region: drag;
height: 100%;
display: flex;
flex-flow: column;
animation: fadein 0.3s;
background: rgb(40, 40, 40);
animation: fadein 1s;
}
.app:not(.is-focused) {
@@ -94,11 +97,20 @@ table {
word-wrap: normal;
white-space: nowrap;
direction: ltr;
opacity: 0.85;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.icon.disabled {
opacity: 0.3;
}
.icon:not(.disabled):hover {
opacity: 1;
}
/*
* UTILITY CLASSES
*/
@@ -109,8 +121,8 @@ table {
white-space: nowrap;
}
.disabled {
opacity: 0.3;
.float-left {
float: left;
}
.float-right {
@@ -144,8 +156,8 @@ table {
.header {
background: rgb(40, 40, 40);
border-bottom: 1px solid rgb(20, 20, 20);
height: 37px; /* vertically center OS menu buttons (OS X) */
padding-top: 6px;
height: 38px; /* vertically center OS menu buttons (OS X) */
padding-top: 7px;
overflow: hidden;
flex: 0 1 auto;
opacity: 1;
@@ -164,7 +176,13 @@ table {
}
.app.view-player .header {
opacity: 0.8;
background: rgba(40, 40, 40, 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 {
@@ -172,12 +190,8 @@ table {
cursor: none;
}
.app.hide-header .header {
display: none;
}
.header .title {
opacity: 0.6;
opacity: 0.7;
position: absolute;
margin-top: 1px;
padding: 0 150px 0 150px;
@@ -188,35 +202,22 @@ table {
.header .nav {
font-weight: bold;
margin-right: 9px;
}
.header .nav.left {
float: left;
margin-left: 10px;
}
.header .nav.right {
margin-right: 10px;
}
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
margin-left: 78px;
}
.header .nav.right {
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 {
.header .back,
.header .forward {
font-size: 30px;
margin-top: -3px;
}
@@ -535,6 +536,11 @@ input {
}
}
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete {
opacity: 0.5;
}
@@ -543,6 +549,10 @@ input {
opacity: 0.7;
}
.torrent .buttons .radial-progress {
position: absolute;
}
.torrent .name {
font-size: 18px;
font-weight: bold;
@@ -592,7 +602,7 @@ body.drag .app::after {
}
.torrent-details {
padding: 8em 12px 20px 20px;
padding: 8em 0 20px 0;
}
.torrent-details table {
@@ -617,7 +627,7 @@ body.drag .app::after {
.torrent-details td {
overflow: hidden;
padding: 0;
vertical-align: bottom;
vertical-align: middle;
}
.torrent-details td .icon {
@@ -627,7 +637,14 @@ body.drag .app::after {
}
.torrent-details td.col-icon {
width: 2em;
width: 3em;
padding-left: 16px;
}
.torrent-details td.col-icon .radial-progress {
position: absolute;
margin-top: 4px;
margin-left: 0.5px;
}
.torrent-details td.col-name {
@@ -646,7 +663,8 @@ body.drag .app::after {
}
.torrent-details td.col-select {
width: 2em;
width: 3em;
padding-right: 13px;
text-align: right;
}
@@ -678,7 +696,7 @@ body.drag .app::after {
* PLAYER CONTROLS
*/
.player-controls {
.player .controls {
position: fixed;
background: rgba(40, 40, 40, 0.8);
width: 100%;
@@ -687,7 +705,63 @@ body.drag .app::after {
transition: opacity 0.15s ease-out;
}
.app.hide-video-controls .player-controls {
.player .controls .icon {
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;
}
@@ -695,13 +769,16 @@ body.drag .app::after {
cursor: none;
}
.app.hide-video-controls .player .player-controls:hover {
/* TODO: find better way to handle this (that also
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1;
cursor: default;
}
/* invisible click target for scrubbing */
.player-controls .scrub-bar {
.player .controls .scrub-bar {
position: absolute;
width: 100%;
height: 23px; /* 3px .loading-bar plus 10px above and below */
@@ -710,7 +787,7 @@ body.drag .app::after {
-webkit-app-region: no-drag;
}
.player-controls .loading-bar {
.player .controls .loading-bar {
position: relative;
width: 100%;
top: -3px;
@@ -720,14 +797,14 @@ body.drag .app::after {
position: absolute;
}
.player-controls .loading-bar-part {
.player .controls .loading-bar-part {
position: absolute;
background-color: #dd0000;
top: 0;
height: 100%;
}
.player-controls .playback-cursor {
.player .controls .playback-cursor {
position: absolute;
top: -3px;
background-color: #FFF;
@@ -736,94 +813,26 @@ body.drag .app::after {
border-radius: 50%;
margin-top: 0;
margin-left: 0;
transition-property: width, height, border-radius, margin-top, margin-left;
transition-property: width, height, top, margin-left;
transition-duration: 0.1s;
transition-timing-function: ease-out;
}
.player-controls .play-pause {
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 {
.player .controls .closed-captions.active,
.player .controls .device.active {
color: #9af;
}
.player-controls .volume {
display: block;
width: 90px;
}
.player-controls .volume-icon {
float: left;
margin-right: 0px;
}
.player-controls .volume-slider {
.player .controls .volume-slider::-webkit-slider-thumb {
-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;
width: 13px;
height: 13px;
border-radius: 50%;
-webkit-app-region: no-drag;
}
.player-controls .volume-slider:focus {
.player .controls .volume-slider:focus {
outline: none;
}
@@ -833,19 +842,27 @@ body.drag .app::after {
.player .playback-bar:hover .playback-cursor {
top: -8px;
margin-left: -5px;
width: 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 {
background: none;
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;
}
/*
* CHROMECAST / AIRPLAY CONTROLS
*/
@@ -895,6 +912,173 @@ body.drag .app::after {
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
*/
@@ -963,10 +1147,6 @@ body.drag .app::after {
z-index: 1;
}
.app.hide-header .error-popover {
top: 0px;
}
.error-popover.hidden {
display: none;
}
@@ -994,3 +1174,66 @@ body.drag .app::after {
.error-text {
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,10 +14,8 @@ var ipcRenderer = electron.ipcRenderer
setupIpc()
var appConfig = require('application-config')('WebTorrent')
var concat = require('simple-concat')
var dragDrop = require('drag-drop')
var fs = require('fs-extra')
var iso639 = require('iso-639-1')
var mainLoop = require('main-loop')
var parallel = require('run-parallel')
var path = require('path')
@@ -42,18 +40,29 @@ appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
// This dependency is the slowest-loading, so we lazy load it
var Cast = null
// For easy debugging in Developer Tools
var state = global.state = State.getInitialState()
// Push the first page into the location history
state.location.go({ url: 'home' })
var vdomLoop
var state = State.getInitialState()
state.location.go({ url: 'home' }) // Add first page to location history
// 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.
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.)
* Connects to the torrent networks, sets up the UI and OS integrations like
@@ -183,7 +192,7 @@ function render (state) {
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
showOrHidePlayerControls()
vdomLoop.update(state)
if (vdomLoop) vdomLoop.update(state)
updateElectron()
}
@@ -273,18 +282,17 @@ function dispatch (action, ...args) {
playPause()
}
if (action === 'play') {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
play()
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
playFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'playbackJump') {
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') {
changeVolume(args[0] /* increase */)
}
@@ -338,6 +346,26 @@ function dispatch (action, ...args) {
if (action === 'exitModal') {
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') {
updateAvailable(args[0] /* version */)
}
@@ -399,7 +427,26 @@ function 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) {
// change volume with delta value
setVolume(state.playing.volume + delta)
@@ -489,22 +536,6 @@ function setupIpc () {
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
function resumeTorrents () {
state.saved.torrents
@@ -512,6 +543,27 @@ function resumeTorrents () {
.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
function saveStateThrottled () {
if (state.saveStateTimeout) return
@@ -537,7 +589,7 @@ function saveState () {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus' && x.playStatus !== 'unplayable') {
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
torrent[key] = x[key]
@@ -605,14 +657,19 @@ function getTorrentSummary (torrentKey) {
// 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-
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
function addTorrent (torrentId) {
backToList()
var torrentKey = state.nextTorrentKey++
var path = state.saved.downloadPath
var path = state.saved.prefs.downloadPath
if (torrentId.path) {
// Use path string instead of W3C File object
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)
}
@@ -647,8 +704,9 @@ function addSubtitles (files, autoSelect) {
}
function loadSubtitle (file, cb) {
var srtToVtt = require('srt-to-vtt')
var concat = require('simple-concat')
var LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file
@@ -664,12 +722,8 @@ function loadSubtitle (file, cb) {
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
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 = {
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath: filePath
@@ -686,6 +740,7 @@ function selectSubtitle (ix) {
// Checks whether a language name like "English" or "German" matches the system
// language, aka the current locale
function isSystemLanguage (language) {
var iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
var langIso = iso639.getCode(language) // eg "de" if language is "German"
return langIso === osLangISO
@@ -728,7 +783,7 @@ function startTorrentingSummary (torrentSummary) {
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
// Use Downloads folder by default
var path = s.path || state.saved.downloadPath
var path = s.path || state.saved.prefs.downloadPath
var torrentID
if (s.torrentFileName) { // Load torrent file from disk
@@ -847,11 +902,17 @@ function torrentMetadata (torrentKey, torrentInfo) {
torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path
torrentSummary.files = torrentInfo.files
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) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
update()
// Save the .torrent file, if it hasn't been saved already
@@ -960,11 +1021,25 @@ function pickFileToPlay (files) {
return undefined
}
// Opens the video player
function playFile (infoHash, index) {
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) {
var torrentSummary = getTorrentSummary(infoHash)
// 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) return cb(new errors.UnplayableError())
@@ -976,7 +1051,7 @@ function openPlayer (infoHash, index, cb) {
var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR')
cb(new Error('playback timed out'))
cb(new Error('Playback timed out. Try again.'))
update()
}, 10000) /* give it a few seconds */
@@ -999,6 +1074,15 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: '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 (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
@@ -1148,7 +1232,7 @@ function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(state.saved.downloadPath, newFileName),
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }

View File

@@ -8,7 +8,8 @@ module.exports = {
play,
pause,
seek,
setVolume
setVolume,
setRate
}
var airplay = require('airplay-js')
@@ -344,6 +345,22 @@ 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) {
var device = getDevice()
if (device) {

View File

@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
var path = require('path')
function torrentPoster (torrent, cb) {
// First, try to use the largest video file
// First, try to use a poster image if available
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
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Second, try to use the largest image file
// Third, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)

View File

@@ -9,8 +9,7 @@ var LocationHistory = require('./lib/location-history')
module.exports = {
getInitialState,
getDefaultPlayState,
getDefaultSavedState,
getPlayingTorrentSummary
getDefaultSavedState
}
function getInitialState () {
@@ -63,7 +62,8 @@ function getInitialState () {
/*
* Getters, for convenience
*/
getPlayingTorrentSummary
getPlayingTorrentSummary,
getPlayingFileSummary
}
}
@@ -80,6 +80,7 @@ function getDefaultPlayState () {
isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */
selectedIndex: -1, /* current subtitle track */
@@ -265,9 +266,11 @@ function getDefaultSavedState () {
]
}
],
downloadPath: config.IS_PORTABLE
? path.join(config.CONFIG_PATH, 'Downloads')
: remote.app.getPath('downloads')
prefs: {
downloadPath: config.IS_PORTABLE
? path.join(config.CONFIG_PATH, 'Downloads')
: remote.app.getPath('downloads')
}
}
}
@@ -275,3 +278,9 @@ function getPlayingTorrentSummary () {
var infoHash = this.playing.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,7 +8,8 @@ var Header = require('./header')
var Views = {
'home': require('./torrent-list'),
'player': require('./player'),
'create-torrent': require('./create-torrent-page')
'create-torrent': require('./create-torrent-page'),
'preferences': require('./preferences')
}
var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'),
@@ -26,11 +27,8 @@ function App (state) {
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
// Hide the header on Windows/Linux when in the player
// On OSX, the header appears as part of the title bar
var hideHeader = process.platform !== 'darwin' && state.location.url() === 'player'
state.playing.location === 'local' &&
state.playing.playbackRate === 1
var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */
@@ -39,7 +37,6 @@ function App (state) {
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
if (hideHeader) cls.push('hide-header')
return hx`
<div class='app ${cls.join(' ')}'>

View File

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

View File

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

@@ -0,0 +1,104 @@
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,12 +118,7 @@ function TorrentList (state) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
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') {
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
@@ -143,6 +138,18 @@ function TorrentList (state) {
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
var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
@@ -158,6 +165,7 @@ function TorrentList (state) {
return hx`
<div class='buttons'>
${positionElem}
${playButton}
<i.button-round.icon.download
class=${torrentSummary.status}
@@ -186,11 +194,16 @@ function TorrentList (state) {
filesElement = hx`<div class='files warning'>${message}</div>`
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files.map(
(file, index) => renderFileRow(torrentSummary, file, index))
var fileRows = torrentSummary.files
.sort(function (a, b) {
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`
<div class='files'>
<strong>Files</strong>
<table>
${fileRows}
</table>
@@ -217,7 +230,14 @@ function TorrentList (state) {
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
}
// Second, render the file as a table row
// Second, for media files where we saved our position, show how far we got
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 infoHash = torrentSummary.infoHash
var icon
@@ -233,17 +253,18 @@ function TorrentList (state) {
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx`
<tr>
<td class='col-icon ${rowClass}' onclick=${handleClick}>
<tr onclick=${handleClick}>
<td class='col-icon ${rowClass}'>
${positionElem}
<i class='icon'>${icon}</i>
</td>
<td class='col-name ${rowClass}' onclick=${handleClick}>
<td class='col-name ${rowClass}'>
${file.name}
</td>
<td class='col-progress ${rowClass}' onclick=${handleClick}>
<td class='col-progress ${rowClass}'>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}' onclick=${handleClick}>
<td class='col-size ${rowClass}'>
${prettyBytes(file.length)}
</td>
<td class='col-select'
@@ -254,3 +275,24 @@ 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>
`
}

View File

@@ -1,10 +0,0 @@
<?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

@@ -1,20 +0,0 @@
<?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>