From b8f6f9f3ebb1b607973719efb25282bc6d276af5 Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Tue, 14 Jun 2016 11:50:26 +0200 Subject: [PATCH 1/7] Open multi-file torrents as playlists --- src/main/ipc.js | 4 + src/main/menu.js | 28 +++ src/main/shortcuts.js | 10 + src/renderer/controllers/media-controller.js | 3 +- .../controllers/playback-controller.js | 176 ++++++++++++------ src/renderer/lib/cast.js | 6 +- src/renderer/lib/errors.js | 13 +- src/renderer/lib/playlist.js | 78 ++++++++ src/renderer/lib/state.js | 1 + src/renderer/main.js | 2 + src/renderer/pages/PlayerPage.js | 26 ++- src/renderer/webtorrent.js | 16 +- static/main.css | 20 +- 13 files changed, 309 insertions(+), 74 deletions(-) create mode 100644 src/renderer/lib/playlist.js diff --git a/src/main/ipc.js b/src/main/ipc.js index ec6f4b1b8d..0ba4cad774 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -64,6 +64,10 @@ function init () { thumbar.enable() }) + ipc.on('onPlayerUpdate', function (e, ...args) { + menu.onPlayerUpdate(...args) + }) + ipc.on('onPlayerClose', function () { menu.setPlayerOpen(false) powerSaveBlocker.disable() diff --git a/src/main/menu.js b/src/main/menu.js index 74067fa349..2e4d6373f5 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -3,6 +3,7 @@ module.exports = { setPlayerOpen, setWindowFocus, setAllowNav, + onPlayerUpdate, onToggleAlwaysOnTop, onToggleFullScreen } @@ -25,6 +26,8 @@ function init () { function setPlayerOpen (flag) { getMenuItem('Play/Pause').enabled = flag + getMenuItem('Skip Next').enabled = flag + getMenuItem('Skip Previous').enabled = flag getMenuItem('Increase Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag getMenuItem('Step Forward').enabled = flag @@ -32,6 +35,16 @@ function setPlayerOpen (flag) { getMenuItem('Increase Speed').enabled = flag getMenuItem('Decrease Speed').enabled = flag getMenuItem('Add Subtitles File...').enabled = flag + + if (flag === false) { + getMenuItem('Skip Next').enabled = false + getMenuItem('Skip Previous').enabled = false + } +} + +function onPlayerUpdate (hasNext, hasPrevious) { + getMenuItem('Skip Next').enabled = hasNext + getMenuItem('Skip Previous').enabled = hasPrevious } function setWindowFocus (flag) { @@ -187,6 +200,21 @@ function getMenuTemplate () { { type: 'separator' }, + { + label: 'Skip Next', + accelerator: 'N', + click: () => windows.main.dispatch('nextTrack'), + enabled: false + }, + { + label: 'Skip Previous', + accelerator: 'P', + click: () => windows.main.dispatch('previousTrack'), + enabled: false + }, + { + type: 'separator' + }, { label: 'Increase Volume', accelerator: 'CmdOrCtrl+Up', diff --git a/src/main/shortcuts.js b/src/main/shortcuts.js index 02bc2320de..9fd71e8a70 100644 --- a/src/main/shortcuts.js +++ b/src/main/shortcuts.js @@ -12,9 +12,19 @@ function enable () { 'MediaPlayPause', () => windows.main.dispatch('playPause') ) + electron.globalShortcut.register( + 'MediaNextTrack', + () => windows.main.dispatch('nextTrack') + ) + electron.globalShortcut.register( + 'MediaPreviousTrack', + () => windows.main.dispatch('previousTrack') + ) } function disable () { // Return the media key to the OS, so other apps can use it. electron.globalShortcut.unregister('MediaPlayPause') + electron.globalShortcut.unregister('MediaNextTrack') + electron.globalShortcut.unregister('MediaPreviousTrack') } diff --git a/src/renderer/controllers/media-controller.js b/src/renderer/controllers/media-controller.js index a3821683eb..a68ac90153 100644 --- a/src/renderer/controllers/media-controller.js +++ b/src/renderer/controllers/media-controller.js @@ -44,7 +44,8 @@ module.exports = class MediaController { openExternalPlayer () { var state = this.state - ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title) + var mediaURL = state.server.localURL + '/' + state.playlist.getCurrent().fileIndex + ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, mediaURL, state.window.title) state.playing.location = 'external' } diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index d1c38153d2..b7880fe6a0 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -8,6 +8,7 @@ const errors = require('../lib/errors') const sound = require('../lib/sound') const TorrentPlayer = require('../lib/torrent-player') const TorrentSummary = require('../lib/torrent-summary') +const Playlist = require('../lib/playlist') const State = require('../lib/state') const ipcRenderer = electron.ipcRenderer @@ -26,16 +27,37 @@ module.exports = class PlaybackController { // * Stream, if not already fully downloaded // * If no file index is provided, pick the default file to play playFile (infoHash, index /* optional */) { - this.state.location.go({ - url: 'player', - setup: (cb) => { - this.play() - this.openPlayer(infoHash, index, cb) - }, - destroy: () => this.closePlayer() - }, (err) => { - if (err) dispatch('error', err) - }) + var state = this.state + if (state.location.url() === 'player') { + this.play() + state.playlist.jumpToFile(infoHash, index) + this.updatePlayer(false, callback) + } else { + var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) + var playlist = new Playlist(torrentSummary) + + // automatically choose which file in the torrent to play, if necessary + if (index === undefined) index = torrentSummary.defaultPlayFileIndex + if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) + if (index === undefined) return this.onError(new errors.UnplayableError()) + + playlist.jumpToFile(infoHash, index) + + state.location.go({ + url: 'player', + setup: (cb) => { + this.play() + this.openPlayer(playlist, cb) + }, + destroy: () => this.closePlayer() + }, (err) => { + if (err) dispatch('error', err) + }) + } + + function callback (err) { + if (err) this.onError(err) + } } // Open a file in OS default app. @@ -64,6 +86,30 @@ module.exports = class PlaybackController { else this.pause() } + // Play next file in list (if any) + nextTrack () { + var state = this.state + if (state.playlist && state.playlist.hasNext()) { + state.playlist.next() + this.updatePlayer(false, (err) => { + if (err) this.onError(err) + else this.play() + }) + } + } + + // Play previous track in list (if any) + previousTrack () { + var state = this.state + if (state.playlist && state.playlist.hasPrevious()) { + state.playlist.previous() + this.updatePlayer(false, (err) => { + if (err) this.onError(err) + else this.play() + }) + } + } + // Play (unpause) the current media play () { var state = this.state @@ -167,14 +213,16 @@ module.exports = class PlaybackController { return false } - // Opens the video player to a specific torrent - openPlayer (infoHash, index, cb) { - var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) + // Opens the video player to a specific playlist + openPlayer (playlist, cb) { + var state = this.state + state.playlist = playlist + + var track = playlist.getCurrent() + if (track === undefined) return cb(new errors.UnplayableError()) - // automatically choose which file in the torrent to play, if necessary - if (index === undefined) index = torrentSummary.defaultPlayFileIndex - if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) - if (index === undefined) return cb(new errors.UnplayableError()) + var torrentSummary = TorrentSummary.getByKey(state, state.playlist.getInfoHash()) + state.playing.infoHash = torrentSummary.infoHash // update UI to show pending playback if (torrentSummary.progress !== 1) sound.play('PLAY') @@ -191,38 +239,68 @@ module.exports = class PlaybackController { this.update() }, 10000) /* give it a few seconds */ + this.startServer(torrentSummary, () => { + clearTimeout(timeout) + + // if we timed out (user clicked play a long time ago), don't autoplay + var timedOut = torrentSummary.playStatus === 'timeout' + delete torrentSummary.playStatus + if (timedOut) { + ipcRenderer.send('wt-stop-server') + return this.update() + } + + ipcRenderer.send('onPlayerOpen') + this.updatePlayer(true, cb) + }) + } + + // Starts WebTorrent server for media streaming + startServer (torrentSummary, cb) { if (torrentSummary.status === 'paused') { dispatch('startTorrentingSummary', torrentSummary.torrentKey) ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, - () => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)) + () => onTorrentReady()) } else { - this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb) + onTorrentReady() + } + + function onTorrentReady () { + ipcRenderer.send('wt-start-server', torrentSummary.infoHash) + ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb()) } } - openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) { - var fileSummary = torrentSummary.files[index] + // Called each time the playlist state changes + updatePlayer (resume, cb) { + var state = this.state + var track = state.playlist.getCurrent() + + if (track === undefined) { + return cb(new Error('Can\'t play that file')) + } + + var torrentSummary = TorrentSummary.getByKey(this.state, state.playlist.getInfoHash()) + var fileSummary = torrentSummary.files[track.fileIndex] // update state - var state = this.state - state.playing.infoHash = torrentSummary.infoHash - state.playing.fileIndex = index - state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video' - : TorrentPlayer.isAudio(fileSummary) ? 'audio' - : 'other' + state.playing.fileIndex = track.fileIndex + state.playing.type = track.type // pick up where we left off - if (fileSummary.currentTime) { + var jumpToTime = 0 + if (resume && 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 + jumpToTime = fileSummary.currentTime } } + state.playing.jumpToTime = jumpToTime // 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) + ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, track.fileIndex) } // if it's video, check for subtitles files that are done downloading @@ -233,34 +311,21 @@ module.exports = class PlaybackController { dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) } - ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) - ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => { - clearTimeout(timeout) + state.window.title = fileSummary.name - // if we timed out (user clicked play a long time ago), don't autoplay - var timedOut = torrentSummary.playStatus === 'timeout' - delete torrentSummary.playStatus - if (timedOut) { - ipcRenderer.send('wt-stop-server') - return this.update() - } - - state.window.title = torrentSummary.files[state.playing.fileIndex].name - - // play in VLC if set as default player (Preferences / Playback / Play in VLC) - if (this.state.saved.prefs.openExternalPlayer) { - dispatch('openExternalPlayer') - this.update() - cb() - return - } - - // otherwise, play the video + // play in VLC if set as default player (Preferences / Playback / Play in VLC) + if (this.state.saved.prefs.openExternalPlayer) { + dispatch('openExternalPlayer') this.update() - - ipcRenderer.send('onPlayerOpen') cb() - }) + return + } + + // otherwise, play the video + this.update() + + ipcRenderer.send('onPlayerUpdate', state.playlist.hasNext(), state.playlist.hasPrevious()) + cb() } closePlayer () { @@ -287,6 +352,7 @@ module.exports = class PlaybackController { // Reset the window contents back to the home screen state.playing = State.getDefaultPlayState() + state.playlist = null state.server = null // Reset the window size and location back to where it was diff --git a/src/renderer/lib/cast.js b/src/renderer/lib/cast.js index 180c833e00..8fdf6884ee 100644 --- a/src/renderer/lib/cast.js +++ b/src/renderer/lib/cast.js @@ -96,7 +96,7 @@ function chromecastPlayer () { function open () { var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) - ret.device.play(state.server.networkURL, { + ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { type: 'video/mp4', title: config.APP_NAME + ' - ' + torrentSummary.name }, function (err) { @@ -183,7 +183,7 @@ function airplayPlayer () { } function open () { - ret.device.play(state.server.networkURL, function (err, res) { + ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) { if (err) { state.playing.location = 'local' state.errors.push({ @@ -275,7 +275,7 @@ function dlnaPlayer (player) { function open () { var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) - ret.device.play(state.server.networkURL, { + ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { type: 'video/mp4', title: config.APP_NAME + ' - ' + torrentSummary.name, seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 diff --git a/src/renderer/lib/errors.js b/src/renderer/lib/errors.js index 2bdfdfe605..a69c1ead9a 100644 --- a/src/renderer/lib/errors.js +++ b/src/renderer/lib/errors.js @@ -1,8 +1,15 @@ module.exports = { - UnplayableError + UnplayableTorrentError, + UnplayableFileError } -function UnplayableError () { +function UnplayableTorrentError () { this.message = 'Can\'t play any files in torrent' } -UnplayableError.prototype = Error + +function UnplayableFileError () { + this.message = 'Can\'t play that file' +} + +UnplayableTorrentError.prototype = Error +UnplayableFileError.prototype = Error diff --git a/src/renderer/lib/playlist.js b/src/renderer/lib/playlist.js new file mode 100644 index 0000000000..dfc97c31b9 --- /dev/null +++ b/src/renderer/lib/playlist.js @@ -0,0 +1,78 @@ +var TorrentPlayer = require('./torrent-player') + +module.exports = Playlist + +function Playlist (torrentSummary) { + this._infoHash = torrentSummary.infoHash + this._position = 0 + this._tracks = extractTracks(torrentSummary) +} + +Playlist.prototype.getInfoHash = function () { + return this._infoHash +} + +Playlist.prototype.getTracks = function () { + return this._tracks +} + +Playlist.prototype.hasNext = function () { + return this._position + 1 < this._tracks.length +} + +Playlist.prototype.hasPrevious = function () { + return this._position > 0 +} + +Playlist.prototype.next = function () { + if (this.hasNext()) { + this._position++ + return this.getCurrent() + } +} + +Playlist.prototype.previous = function () { + if (this.hasPrevious()) { + this._position-- + return this.getCurrent() + } +} + +Playlist.prototype.jumpToFile = function (infoHash, fileIndex) { + this.setPosition(this._tracks.findIndex( + (track) => track.infoHash === infoHash && track.fileIndex === fileIndex + )) + return this.getCurrent() +} + +Playlist.prototype.getCurrent = function () { + var position = this.getPosition() + return position === undefined ? undefined : this._tracks[position] +} + +Playlist.prototype.getPosition = function () { + if (this._position >= 0 && this._position < this._tracks.length) { + return this._position + } else return undefined +} + +Playlist.prototype.setPosition = function (position) { + this._position = position +} + +function extractTracks (torrentSummary) { + return torrentSummary.files.map((file, index) => ({ file, index })) + .filter((object) => TorrentPlayer.isPlayable(object.file)) + .sort(function (a, b) { + if (a.file.name < b.file.name) return -1 + if (b.file.name < a.file.name) return 1 + return 0 + }) + .map((object) => ({ + infoHash: torrentSummary.infoHash, + fileIndex: object.index, + type: TorrentPlayer.isVideo(object.file) ? 'video' + : TorrentPlayer.isAudio(object.file) ? 'audio' + : 'other' + })) +} diff --git a/src/renderer/lib/state.js b/src/renderer/lib/state.js index d366f61965..53bfecb592 100644 --- a/src/renderer/lib/state.js +++ b/src/renderer/lib/state.js @@ -39,6 +39,7 @@ function getDefaultState () { selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */ playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */ devices: {}, /* playback devices like Chromecast and AppleTV */ + playlist: null, dock: { badge: 0, progress: 0 diff --git a/src/renderer/main.js b/src/renderer/main.js index e9ed6cee96..a7a92c0454 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -188,6 +188,8 @@ const dispatchHandlers = { // Playback 'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playPause': () => controllers.playback.playPause(), + 'nextTrack': () => controllers.playback.nextTrack(), + 'previousTrack': () => controllers.playback.previousTrack(), 'skip': (time) => controllers.playback.skip(time), 'skipTo': (time) => controllers.playback.skipTo(time), 'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), diff --git a/src/renderer/pages/PlayerPage.js b/src/renderer/pages/PlayerPage.js index 44db7c81b9..67c54d54a0 100644 --- a/src/renderer/pages/PlayerPage.js +++ b/src/renderer/pages/PlayerPage.js @@ -109,7 +109,7 @@ function renderMedia (state) { var MediaTagName = state.playing.type var mediaTag = ( = 0 ? 'active' : '' + var prevClass = state.playlist.hasPrevious() ? '' : 'disabled' + var nextClass = state.playlist.hasNext() ? '' : 'disabled' var elements = [
@@ -397,6 +403,13 @@ function renderPlayerControls (state) { />
, + + skip_previous + , + , + + skip_next + , + getAudioMetadata(infoHash, index)) - ipc.on('wt-start-server', (e, infoHash, index) => - startServer(infoHash, index)) + ipc.on('wt-start-server', (e, infoHash) => + startServer(infoHash)) ipc.on('wt-stop-server', (e) => stopServer()) ipc.on('wt-select-files', (e, infoHash, selections) => @@ -301,20 +301,20 @@ function getTorrentProgress () { } } -function startServer (infoHash, index) { +function startServer (infoHash) { var torrent = client.get(infoHash) - if (torrent.ready) startServerFromReadyTorrent(torrent, index) - else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index)) + if (torrent.ready) startServerFromReadyTorrent(torrent) + else torrent.once('ready', () => startServerFromReadyTorrent(torrent)) } -function startServerFromReadyTorrent (torrent, index, cb) { +function startServerFromReadyTorrent (torrent, cb) { if (server) return // start the streaming torrent-to-http server server = torrent.createServer() server.listen(0, function () { var port = server.address().port - var urlSuffix = ':' + port + '/' + index + var urlSuffix = ':' + port var info = { torrentKey: torrent.key, localURL: 'http://localhost' + urlSuffix, @@ -322,7 +322,7 @@ function startServerFromReadyTorrent (torrent, index, cb) { } ipc.send('wt-server-running', info) - ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack + ipc.send('wt-server-' + torrent.infoHash, info) }) } diff --git a/static/main.css b/static/main.css index bb9e5cf6ed..ae5c6aac8b 100644 --- a/static/main.css +++ b/static/main.css @@ -629,7 +629,25 @@ body.drag .app::after { opacity: 1; } -.player .controls .play-pause { +.player .controls .icon.disabled { + opacity: 0.3; +} + +.player .controls .icon.skip-previous { + font-size: 28px; + margin-top: 5px; + margin-right: 10px; + margin-left: 15px; +} + +.player .controls .icon.play-pause { + font-size: 28px; + margin-top: 5px; + margin-right: 10px; + margin-left: 15px; +} + +.player .controls .icon.skip-next { font-size: 28px; margin-top: 5px; margin-right: 10px; From 9284122461fdb57a3884e97f43495b2167a259dd Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Fri, 17 Jun 2016 16:55:36 +0200 Subject: [PATCH 2/7] Add `repeat` and `shuffle` options to the player --- src/main/menu.js | 29 +++++- .../controllers/playback-controller.js | 22 ++++- src/renderer/lib/playlist.js | 99 +++++++++++++++++-- src/renderer/main.js | 2 + src/renderer/pages/PlayerPage.js | 18 ++++ static/main.css | 2 + 6 files changed, 160 insertions(+), 12 deletions(-) diff --git a/src/main/menu.js b/src/main/menu.js index 2e4d6373f5..551aba8f53 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -28,6 +28,8 @@ function setPlayerOpen (flag) { getMenuItem('Play/Pause').enabled = flag getMenuItem('Skip Next').enabled = flag getMenuItem('Skip Previous').enabled = flag + getMenuItem('Enable Shuffle').enabled = flag + getMenuItem('Enable Repeat').enabled = flag getMenuItem('Increase Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag getMenuItem('Step Forward').enabled = flag @@ -39,12 +41,16 @@ function setPlayerOpen (flag) { if (flag === false) { getMenuItem('Skip Next').enabled = false getMenuItem('Skip Previous').enabled = false + getMenuItem('Enable Shuffle').checked = false + getMenuItem('Enable Repeat').checked = false } } -function onPlayerUpdate (hasNext, hasPrevious) { - getMenuItem('Skip Next').enabled = hasNext - getMenuItem('Skip Previous').enabled = hasPrevious +function onPlayerUpdate (state) { + getMenuItem('Skip Next').enabled = state.hasNext + getMenuItem('Skip Previous').enabled = state.hasPrevious + getMenuItem('Enable Shuffle').checked = state.shuffle + getMenuItem('Enable Repeat').checked = state.repeat } function setWindowFocus (flag) { @@ -215,6 +221,23 @@ function getMenuTemplate () { { type: 'separator' }, + { + label: 'Enable Shuffle', + type: 'checkbox', + checked: false, + click: () => windows.main.dispatch('toggleShuffle'), + enabled: false + }, + { + label: 'Enable Repeat', + type: 'checkbox', + checked: false, + click: () => windows.main.dispatch('toggleRepeat'), + enabled: false + }, + { + type: 'separator' + }, { label: 'Increase Volume', accelerator: 'CmdOrCtrl+Up', diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index b7880fe6a0..b9778d4c07 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -110,6 +110,24 @@ module.exports = class PlaybackController { } } + // Enable or disable playlist shuffle + toggleShuffle (flag) { + var playlist = this.state.playlist + if (playlist) { + playlist.toggleShuffle(flag) + ipcRenderer.send('onPlayerUpdate', playlist.getState()) + } + } + + // Enable or disable repetition of the entire playlist + toggleRepeat (flag) { + var playlist = this.state.playlist + if (playlist) { + playlist.toggleRepeat(flag) + ipcRenderer.send('onPlayerUpdate', playlist.getState()) + } + } + // Play (unpause) the current media play () { var state = this.state @@ -313,6 +331,8 @@ module.exports = class PlaybackController { state.window.title = fileSummary.name + ipcRenderer.send('onPlayerUpdate', state.playlist.getState()) + // play in VLC if set as default player (Preferences / Playback / Play in VLC) if (this.state.saved.prefs.openExternalPlayer) { dispatch('openExternalPlayer') @@ -324,7 +344,7 @@ module.exports = class PlaybackController { // otherwise, play the video this.update() - ipcRenderer.send('onPlayerUpdate', state.playlist.hasNext(), state.playlist.hasPrevious()) + ipcRenderer.send('onPlayerUpdate', state.playlist.getState()) cb() } diff --git a/src/renderer/lib/playlist.js b/src/renderer/lib/playlist.js index dfc97c31b9..3906aad378 100644 --- a/src/renderer/lib/playlist.js +++ b/src/renderer/lib/playlist.js @@ -6,8 +6,16 @@ function Playlist (torrentSummary) { this._infoHash = torrentSummary.infoHash this._position = 0 this._tracks = extractTracks(torrentSummary) + this._order = range(0, this._tracks.length) + + this._repeat = false + this._shuffled = false } +// ============================================================================= +// Public methods +// ============================================================================= + Playlist.prototype.getInfoHash = function () { return this._infoHash } @@ -17,37 +25,61 @@ Playlist.prototype.getTracks = function () { } Playlist.prototype.hasNext = function () { - return this._position + 1 < this._tracks.length + return !this._tracks.length ? false + : this._repeat ? true + : this._position + 1 < this._tracks.length } Playlist.prototype.hasPrevious = function () { - return this._position > 0 + return !this._tracks.length ? false + : this._repeat ? true + : this._position > 0 } Playlist.prototype.next = function () { if (this.hasNext()) { - this._position++ + this._position = mod(this._position + 1, this._tracks.length) return this.getCurrent() } } Playlist.prototype.previous = function () { if (this.hasPrevious()) { - this._position-- + this._position = mod(this._position - 1, this._tracks.length) return this.getCurrent() } } +Playlist.prototype.shuffleEnabled = function () { + return this._shuffled +} + +Playlist.prototype.toggleShuffle = function (value) { + this._shuffled = (value === undefined ? !this._shuffled : value) + this._shuffled ? this._shuffle() : this._unshuffle() +} + +Playlist.prototype.repeatEnabled = function () { + return this._repeat +} + +Playlist.prototype.toggleRepeat = function (value) { + this._repeat = (value === undefined ? !this._repeat : value) +} + Playlist.prototype.jumpToFile = function (infoHash, fileIndex) { - this.setPosition(this._tracks.findIndex( - (track) => track.infoHash === infoHash && track.fileIndex === fileIndex - )) + this.setPosition(this._order.findIndex((i) => { + let track = this._tracks[i] + return track.infoHash === infoHash && track.fileIndex === fileIndex + })) return this.getCurrent() } Playlist.prototype.getCurrent = function () { var position = this.getPosition() - return position === undefined ? undefined : this._tracks[position] + + return position === undefined ? undefined + : this._tracks[this._order[position]] } Playlist.prototype.getPosition = function () { @@ -60,6 +92,43 @@ Playlist.prototype.setPosition = function (position) { this._position = position } +Playlist.prototype.getState = function () { + return { + hasNext: this.hasNext(), + hasPrevious: this.hasPrevious(), + shuffle: this.shuffleEnabled(), + repeat: this.repeatEnabled() + } +} + +// ============================================================================= +// Private methods +// ============================================================================= + +Playlist.prototype._shuffle = function () { + let order = this._order + if (!order.length) return + + // Move the current track to the beggining of the playlist + swap(order, 0, this._position) + this._position = 0 + + // Shuffle the rest of the tracks with Fisher-Yates Shuffle + for (let i = order.length - 1; i > 0; --i) { + let j = Math.floor(Math.random() * i) + 1 + swap(order, i, j) + } +} + +Playlist.prototype._unshuffle = function () { + this._position = this._order[this._position] + this._order = range(0, this._order.length) +} + +// ============================================================================= +// Utility fuctions +// ============================================================================= + function extractTracks (torrentSummary) { return torrentSummary.files.map((file, index) => ({ file, index })) .filter((object) => TorrentPlayer.isPlayable(object.file)) @@ -76,3 +145,17 @@ function extractTracks (torrentSummary) { : 'other' })) } + +function range (begin, end) { + return Array.apply(null, {length: end - begin}).map((v, i) => begin + i) +} + +function swap (array, i, j) { + let temp = array[i] + array[i] = array[j] + array[j] = temp +} + +function mod (a, b) { + return ((a % b) + b) % b +} diff --git a/src/renderer/main.js b/src/renderer/main.js index a7a92c0454..723d14c9f3 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -190,6 +190,8 @@ const dispatchHandlers = { 'playPause': () => controllers.playback.playPause(), 'nextTrack': () => controllers.playback.nextTrack(), 'previousTrack': () => controllers.playback.previousTrack(), + 'toggleShuffle': (flag) => controllers.playback.toggleShuffle(flag), + 'toggleRepeat': (flag) => controllers.playback.toggleRepeat(flag), 'skip': (time) => controllers.playback.skip(time), 'skipTo': (time) => controllers.playback.skipTo(time), 'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), diff --git a/src/renderer/pages/PlayerPage.js b/src/renderer/pages/PlayerPage.js index 67c54d54a0..effb89a273 100644 --- a/src/renderer/pages/PlayerPage.js +++ b/src/renderer/pages/PlayerPage.js @@ -384,6 +384,8 @@ function renderPlayerControls (state) { : '' var prevClass = state.playlist.hasPrevious() ? '' : 'disabled' var nextClass = state.playlist.hasNext() ? '' : 'disabled' + var repeatClass = state.playlist.repeatEnabled() ? 'active' : '' + var shuffleClass = state.playlist.shuffleEnabled() ? 'active' : '' var elements = [
@@ -444,6 +446,22 @@ function renderPlayerControls (state) { )) } + elements.push( + + repeat + , + + + shuffle + + ) + // If we've detected a Chromecast or AppleTV, the user can play video there var castTypes = ['chromecast', 'airplay', 'dlna'] var isCastingAnywhere = castTypes.some( diff --git a/static/main.css b/static/main.css index ae5c6aac8b..380f8116b8 100644 --- a/static/main.css +++ b/static/main.css @@ -743,6 +743,8 @@ body.drag .app::after { } .player .controls .closed-caption.active, +.player .controls .repeat.active, +.player .controls .shuffle.active, .player .controls .device.active { color: #9af; } From 964aedab5269e30b12a08e3d2258d47772da8ab6 Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Thu, 1 Sep 2016 20:28:18 +0200 Subject: [PATCH 3/7] Autoplay first file in torrent * replaces `pickFileToPlay` feature * when reopening player, restores the most recently viewed file --- .../controllers/playback-controller.js | 12 ++++----- .../controllers/torrent-controller.js | 2 -- src/renderer/lib/torrent-player.js | 25 +------------------ src/renderer/pages/TorrentListPage.js | 8 +++--- 4 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index b9778d4c07..801ce837d6 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -6,7 +6,6 @@ const {dispatch} = require('../lib/dispatcher') const telemetry = require('../lib/telemetry') const errors = require('../lib/errors') const sound = require('../lib/sound') -const TorrentPlayer = require('../lib/torrent-player') const TorrentSummary = require('../lib/torrent-summary') const Playlist = require('../lib/playlist') const State = require('../lib/state') @@ -25,7 +24,7 @@ module.exports = class PlaybackController { // Play a file in a torrent. // * Start torrenting, if necessary // * Stream, if not already fully downloaded - // * If no file index is provided, pick the default file to play + // * If no file index is provided, restore the most recently viewed file or autoplay the first playFile (infoHash, index /* optional */) { var state = this.state if (state.location.url() === 'player') { @@ -37,11 +36,8 @@ module.exports = class PlaybackController { var playlist = new Playlist(torrentSummary) // automatically choose which file in the torrent to play, if necessary - if (index === undefined) index = torrentSummary.defaultPlayFileIndex - if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files) - if (index === undefined) return this.onError(new errors.UnplayableError()) - - playlist.jumpToFile(infoHash, index) + if (index === undefined) index = torrentSummary.mostRecentFileIndex + if (index !== undefined) playlist.jumpToFile(infoHash, index) state.location.go({ url: 'player', @@ -301,6 +297,8 @@ module.exports = class PlaybackController { var torrentSummary = TorrentSummary.getByKey(this.state, state.playlist.getInfoHash()) var fileSummary = torrentSummary.files[track.fileIndex] + torrentSummary.mostRecentFileIndex = track.fileIndex + // update state state.playing.fileIndex = track.fileIndex state.playing.type = track.type diff --git a/src/renderer/controllers/torrent-controller.js b/src/renderer/controllers/torrent-controller.js index 4d71f05beb..90aec3c4d6 100644 --- a/src/renderer/controllers/torrent-controller.js +++ b/src/renderer/controllers/torrent-controller.js @@ -2,7 +2,6 @@ const path = require('path') const ipcRenderer = require('electron').ipcRenderer const TorrentSummary = require('../lib/torrent-summary') -const TorrentPlayer = require('../lib/torrent-player') const sound = require('../lib/sound') const {dispatch} = require('../lib/dispatcher') @@ -73,7 +72,6 @@ module.exports = class TorrentController { if (!torrentSummary.selections) { torrentSummary.selections = torrentSummary.files.map((x) => true) } - torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files) dispatch('update') // Save the .torrent file, if it hasn't been saved already diff --git a/src/renderer/lib/torrent-player.js b/src/renderer/lib/torrent-player.js index 10d9341180..d3ecceedcc 100644 --- a/src/renderer/lib/torrent-player.js +++ b/src/renderer/lib/torrent-player.js @@ -3,8 +3,7 @@ module.exports = { isVideo, isAudio, isTorrent, - isPlayableTorrentSummary, - pickFileToPlay + isPlayableTorrentSummary } var path = require('path') @@ -60,25 +59,3 @@ function getFileExtension (file) { function isPlayableTorrentSummary (torrentSummary) { return torrentSummary.files && torrentSummary.files.some(isPlayable) } - -// Picks the default file to play from a list of torrent or torrentSummary files -// Returns an index or undefined, if no files are playable -function pickFileToPlay (files) { - // first, try to find the biggest video file - var videoFiles = files.filter(isVideo) - if (videoFiles.length > 0) { - var largestVideoFile = videoFiles.reduce(function (a, b) { - return a.length > b.length ? a : b - }) - return files.indexOf(largestVideoFile) - } - - // if there are no videos, play the first audio file - var audioFiles = files.filter(isAudio) - if (audioFiles.length > 0) { - return files.indexOf(audioFiles[0]) - } - - // no video or audio means nothing is playable - return undefined -} diff --git a/src/renderer/pages/TorrentListPage.js b/src/renderer/pages/TorrentListPage.js index 868cbc3034..b9790cd78a 100644 --- a/src/renderer/pages/TorrentListPage.js +++ b/src/renderer/pages/TorrentListPage.js @@ -201,10 +201,10 @@ module.exports = class TorrentList extends React.Component { // 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 willShowSpinner = torrentSummary.playStatus === 'requested' - var defaultFile = torrentSummary.files && - torrentSummary.files[torrentSummary.defaultPlayFileIndex] - if (defaultFile && defaultFile.currentTime && !willShowSpinner) { - var fraction = defaultFile.currentTime / defaultFile.duration + var mostRecentFile = torrentSummary.files && + torrentSummary.files[torrentSummary.mostRecentFileIndex] + if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) { + var fraction = mostRecentFile.currentTime / mostRecentFile.duration positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large') playClass = 'resume-position' } From 9bcf8d4221bef08a94b775da62cdfaa5c963fa51 Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Thu, 1 Sep 2016 22:26:39 +0200 Subject: [PATCH 4/7] Add playlist navigation buttons to Windows thumbar --- src/main/ipc.js | 1 + src/main/thumbar.js | 69 ++++++++++++++++----- static/NextTrackThumbnailBarButton.png | Bin 0 -> 225 bytes static/PreviousTrackThumbnailBarButton.png | Bin 0 -> 233 bytes 4 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 static/NextTrackThumbnailBarButton.png create mode 100644 static/PreviousTrackThumbnailBarButton.png diff --git a/src/main/ipc.js b/src/main/ipc.js index 0ba4cad774..2e9ffc3331 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -66,6 +66,7 @@ function init () { ipc.on('onPlayerUpdate', function (e, ...args) { menu.onPlayerUpdate(...args) + thumbar.onPlayerUpdate(...args) }) ipc.on('onPlayerClose', function () { diff --git a/src/main/thumbar.js b/src/main/thumbar.js index 749aa1a378..6793cfae65 100644 --- a/src/main/thumbar.js +++ b/src/main/thumbar.js @@ -2,7 +2,8 @@ module.exports = { disable, enable, onPlayerPause, - onPlayerPlay + onPlayerPlay, + onPlayerUpdate } /** @@ -16,39 +17,75 @@ var config = require('../config') var windows = require('./windows') +const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png') +const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png') +const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png') +const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png') + +// Array indices for each button +const PREV = 0 +const PLAY_PAUSE = 1 +const NEXT = 2 + +var buttons = [] + /** * Show the Windows thumbnail toolbar buttons. */ function enable () { - update(false) + buttons = [ + { + tooltip: 'Previous Track', + icon: PREV_ICON, + click: () => windows.main.dispatch('previousTrack') + }, + { + tooltip: 'Pause', + icon: PAUSE_ICON, + click: () => windows.main.dispatch('playPause') + }, + { + tooltip: 'Next Track', + icon: NEXT_ICON, + click: () => windows.main.dispatch('nextTrack') + } + ] + update() } /** * Hide the Windows thumbnail toolbar buttons. */ function disable () { - windows.main.win.setThumbarButtons([]) + buttons = [] + update() } function onPlayerPause () { - update(true) + if (!isEnabled()) return + buttons[PLAY_PAUSE].tooltip = 'Play' + buttons[PLAY_PAUSE].icon = PLAY_ICON + update() } function onPlayerPlay () { - update(false) + if (!isEnabled()) return + buttons[PLAY_PAUSE].tooltip = 'Pause' + buttons[PLAY_PAUSE].icon = PAUSE_ICON + update() } -function update (isPaused) { - var icon = isPaused - ? 'PlayThumbnailBarButton.png' - : 'PauseThumbnailBarButton.png' +function onPlayerUpdate (state) { + if (!isEnabled()) return + buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ] + buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ] + update() +} - var buttons = [ - { - tooltip: isPaused ? 'Play' : 'Pause', - icon: path.join(config.STATIC_PATH, icon), - click: () => windows.main.dispatch('playPause') - } - ] +function isEnabled () { + return buttons.length > 0 +} + +function update () { windows.main.win.setThumbarButtons(buttons) } diff --git a/static/NextTrackThumbnailBarButton.png b/static/NextTrackThumbnailBarButton.png new file mode 100644 index 0000000000000000000000000000000000000000..97a05c3ae613aae8f92f0a18d054a0c09a2a2731 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D6`n4RAr*{or)@lWh(Uy{UtTBZ zX5_7`xA%89NLw`eEs%b}w5w701%p*XX_W5UoJ%Y!pAIl?u`v1Yj?Kd3(Ce54VfR#Tqwz*{VY$x=7KTv!`@D5j_o383OYZ{nG?@WvNsl( z_hNSAhjUx5HZK0zktO!bTTgQ4{FiNuwk}V~m)qSn)7pZ$*eLv2N?glD^SbDL{*zQr Z3jcD_x?347v=HcK22WQ%mvv4FO#o`>SX=-A literal 0 HcmV?d00001 diff --git a/static/PreviousTrackThumbnailBarButton.png b/static/PreviousTrackThumbnailBarButton.png new file mode 100644 index 0000000000000000000000000000000000000000..b1bc9e3a9b4f69bdb32c41fc7467d9aa9aa83955 GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D4W2HJAr*{ouWsaJOq4kGv0iWI zvstgFY{{x(OJGuiA#Mo=-_?8kmQFaaxI+Ab&B^dR`=uYuzcYOgzvj{q6T$oTQRmJb zn4(_9n)R(_qPD@4wh7uBp7q8w%89qcG+N3Qu}bxE>|{!oJ0ZH_#wW*h2O6}U)*YB6 zUc@H$j^WGP{Td-TbNXf6?(AjEu&`iGbW7*`(H|ROd46ks0h6iq^OUyE;xEppJDZkH axy0Wuq4jaQc)S$Q?F^o-elF{r5}E+-a#&yh literal 0 HcmV?d00001 From 706484eb69f4ec1f966f1e74c32cf8d2574523f8 Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Fri, 2 Sep 2016 01:08:05 +0200 Subject: [PATCH 5/7] Remove `repeat` and `shuffle` options This reverts commit 9284122461fdb57a3884e97f43495b2167a259dd. --- src/main/menu.js | 29 +----- .../controllers/playback-controller.js | 22 +---- src/renderer/lib/playlist.js | 99 ++----------------- src/renderer/main.js | 2 - src/renderer/pages/PlayerPage.js | 18 ---- static/main.css | 2 - 6 files changed, 12 insertions(+), 160 deletions(-) diff --git a/src/main/menu.js b/src/main/menu.js index 551aba8f53..2e4d6373f5 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -28,8 +28,6 @@ function setPlayerOpen (flag) { getMenuItem('Play/Pause').enabled = flag getMenuItem('Skip Next').enabled = flag getMenuItem('Skip Previous').enabled = flag - getMenuItem('Enable Shuffle').enabled = flag - getMenuItem('Enable Repeat').enabled = flag getMenuItem('Increase Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag getMenuItem('Step Forward').enabled = flag @@ -41,16 +39,12 @@ function setPlayerOpen (flag) { if (flag === false) { getMenuItem('Skip Next').enabled = false getMenuItem('Skip Previous').enabled = false - getMenuItem('Enable Shuffle').checked = false - getMenuItem('Enable Repeat').checked = false } } -function onPlayerUpdate (state) { - getMenuItem('Skip Next').enabled = state.hasNext - getMenuItem('Skip Previous').enabled = state.hasPrevious - getMenuItem('Enable Shuffle').checked = state.shuffle - getMenuItem('Enable Repeat').checked = state.repeat +function onPlayerUpdate (hasNext, hasPrevious) { + getMenuItem('Skip Next').enabled = hasNext + getMenuItem('Skip Previous').enabled = hasPrevious } function setWindowFocus (flag) { @@ -221,23 +215,6 @@ function getMenuTemplate () { { type: 'separator' }, - { - label: 'Enable Shuffle', - type: 'checkbox', - checked: false, - click: () => windows.main.dispatch('toggleShuffle'), - enabled: false - }, - { - label: 'Enable Repeat', - type: 'checkbox', - checked: false, - click: () => windows.main.dispatch('toggleRepeat'), - enabled: false - }, - { - type: 'separator' - }, { label: 'Increase Volume', accelerator: 'CmdOrCtrl+Up', diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index 801ce837d6..ab1c1504ce 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -106,24 +106,6 @@ module.exports = class PlaybackController { } } - // Enable or disable playlist shuffle - toggleShuffle (flag) { - var playlist = this.state.playlist - if (playlist) { - playlist.toggleShuffle(flag) - ipcRenderer.send('onPlayerUpdate', playlist.getState()) - } - } - - // Enable or disable repetition of the entire playlist - toggleRepeat (flag) { - var playlist = this.state.playlist - if (playlist) { - playlist.toggleRepeat(flag) - ipcRenderer.send('onPlayerUpdate', playlist.getState()) - } - } - // Play (unpause) the current media play () { var state = this.state @@ -329,8 +311,6 @@ module.exports = class PlaybackController { state.window.title = fileSummary.name - ipcRenderer.send('onPlayerUpdate', state.playlist.getState()) - // play in VLC if set as default player (Preferences / Playback / Play in VLC) if (this.state.saved.prefs.openExternalPlayer) { dispatch('openExternalPlayer') @@ -342,7 +322,7 @@ module.exports = class PlaybackController { // otherwise, play the video this.update() - ipcRenderer.send('onPlayerUpdate', state.playlist.getState()) + ipcRenderer.send('onPlayerUpdate', state.playlist.hasNext(), state.playlist.hasPrevious()) cb() } diff --git a/src/renderer/lib/playlist.js b/src/renderer/lib/playlist.js index 3906aad378..dfc97c31b9 100644 --- a/src/renderer/lib/playlist.js +++ b/src/renderer/lib/playlist.js @@ -6,16 +6,8 @@ function Playlist (torrentSummary) { this._infoHash = torrentSummary.infoHash this._position = 0 this._tracks = extractTracks(torrentSummary) - this._order = range(0, this._tracks.length) - - this._repeat = false - this._shuffled = false } -// ============================================================================= -// Public methods -// ============================================================================= - Playlist.prototype.getInfoHash = function () { return this._infoHash } @@ -25,61 +17,37 @@ Playlist.prototype.getTracks = function () { } Playlist.prototype.hasNext = function () { - return !this._tracks.length ? false - : this._repeat ? true - : this._position + 1 < this._tracks.length + return this._position + 1 < this._tracks.length } Playlist.prototype.hasPrevious = function () { - return !this._tracks.length ? false - : this._repeat ? true - : this._position > 0 + return this._position > 0 } Playlist.prototype.next = function () { if (this.hasNext()) { - this._position = mod(this._position + 1, this._tracks.length) + this._position++ return this.getCurrent() } } Playlist.prototype.previous = function () { if (this.hasPrevious()) { - this._position = mod(this._position - 1, this._tracks.length) + this._position-- return this.getCurrent() } } -Playlist.prototype.shuffleEnabled = function () { - return this._shuffled -} - -Playlist.prototype.toggleShuffle = function (value) { - this._shuffled = (value === undefined ? !this._shuffled : value) - this._shuffled ? this._shuffle() : this._unshuffle() -} - -Playlist.prototype.repeatEnabled = function () { - return this._repeat -} - -Playlist.prototype.toggleRepeat = function (value) { - this._repeat = (value === undefined ? !this._repeat : value) -} - Playlist.prototype.jumpToFile = function (infoHash, fileIndex) { - this.setPosition(this._order.findIndex((i) => { - let track = this._tracks[i] - return track.infoHash === infoHash && track.fileIndex === fileIndex - })) + this.setPosition(this._tracks.findIndex( + (track) => track.infoHash === infoHash && track.fileIndex === fileIndex + )) return this.getCurrent() } Playlist.prototype.getCurrent = function () { var position = this.getPosition() - - return position === undefined ? undefined - : this._tracks[this._order[position]] + return position === undefined ? undefined : this._tracks[position] } Playlist.prototype.getPosition = function () { @@ -92,43 +60,6 @@ Playlist.prototype.setPosition = function (position) { this._position = position } -Playlist.prototype.getState = function () { - return { - hasNext: this.hasNext(), - hasPrevious: this.hasPrevious(), - shuffle: this.shuffleEnabled(), - repeat: this.repeatEnabled() - } -} - -// ============================================================================= -// Private methods -// ============================================================================= - -Playlist.prototype._shuffle = function () { - let order = this._order - if (!order.length) return - - // Move the current track to the beggining of the playlist - swap(order, 0, this._position) - this._position = 0 - - // Shuffle the rest of the tracks with Fisher-Yates Shuffle - for (let i = order.length - 1; i > 0; --i) { - let j = Math.floor(Math.random() * i) + 1 - swap(order, i, j) - } -} - -Playlist.prototype._unshuffle = function () { - this._position = this._order[this._position] - this._order = range(0, this._order.length) -} - -// ============================================================================= -// Utility fuctions -// ============================================================================= - function extractTracks (torrentSummary) { return torrentSummary.files.map((file, index) => ({ file, index })) .filter((object) => TorrentPlayer.isPlayable(object.file)) @@ -145,17 +76,3 @@ function extractTracks (torrentSummary) { : 'other' })) } - -function range (begin, end) { - return Array.apply(null, {length: end - begin}).map((v, i) => begin + i) -} - -function swap (array, i, j) { - let temp = array[i] - array[i] = array[j] - array[j] = temp -} - -function mod (a, b) { - return ((a % b) + b) % b -} diff --git a/src/renderer/main.js b/src/renderer/main.js index 723d14c9f3..a7a92c0454 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -190,8 +190,6 @@ const dispatchHandlers = { 'playPause': () => controllers.playback.playPause(), 'nextTrack': () => controllers.playback.nextTrack(), 'previousTrack': () => controllers.playback.previousTrack(), - 'toggleShuffle': (flag) => controllers.playback.toggleShuffle(flag), - 'toggleRepeat': (flag) => controllers.playback.toggleRepeat(flag), 'skip': (time) => controllers.playback.skip(time), 'skipTo': (time) => controllers.playback.skipTo(time), 'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), diff --git a/src/renderer/pages/PlayerPage.js b/src/renderer/pages/PlayerPage.js index effb89a273..67c54d54a0 100644 --- a/src/renderer/pages/PlayerPage.js +++ b/src/renderer/pages/PlayerPage.js @@ -384,8 +384,6 @@ function renderPlayerControls (state) { : '' var prevClass = state.playlist.hasPrevious() ? '' : 'disabled' var nextClass = state.playlist.hasNext() ? '' : 'disabled' - var repeatClass = state.playlist.repeatEnabled() ? 'active' : '' - var shuffleClass = state.playlist.shuffleEnabled() ? 'active' : '' var elements = [
@@ -446,22 +444,6 @@ function renderPlayerControls (state) { )) } - elements.push( - - repeat - , - - - shuffle - - ) - // If we've detected a Chromecast or AppleTV, the user can play video there var castTypes = ['chromecast', 'airplay', 'dlna'] var isCastingAnywhere = castTypes.some( diff --git a/static/main.css b/static/main.css index 380f8116b8..ae5c6aac8b 100644 --- a/static/main.css +++ b/static/main.css @@ -743,8 +743,6 @@ body.drag .app::after { } .player .controls .closed-caption.active, -.player .controls .repeat.active, -.player .controls .shuffle.active, .player .controls .device.active { color: #9af; } From 7aa7875e2f95b387e7147d968361a6aebe321afe Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Fri, 2 Sep 2016 03:55:12 +0200 Subject: [PATCH 6/7] Play files in order they appear in torrent --- src/renderer/lib/playlist.js | 5 ----- src/renderer/pages/TorrentListPage.js | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/renderer/lib/playlist.js b/src/renderer/lib/playlist.js index dfc97c31b9..a90a931b8c 100644 --- a/src/renderer/lib/playlist.js +++ b/src/renderer/lib/playlist.js @@ -63,11 +63,6 @@ Playlist.prototype.setPosition = function (position) { function extractTracks (torrentSummary) { return torrentSummary.files.map((file, index) => ({ file, index })) .filter((object) => TorrentPlayer.isPlayable(object.file)) - .sort(function (a, b) { - if (a.file.name < b.file.name) return -1 - if (b.file.name < a.file.name) return 1 - return 0 - }) .map((object) => ({ infoHash: torrentSummary.infoHash, fileIndex: object.index, diff --git a/src/renderer/pages/TorrentListPage.js b/src/renderer/pages/TorrentListPage.js index b9790cd78a..a57439cdd1 100644 --- a/src/renderer/pages/TorrentListPage.js +++ b/src/renderer/pages/TorrentListPage.js @@ -267,11 +267,6 @@ module.exports = class TorrentList extends React.Component { var fileRows = torrentSummary.files .filter((file) => !file.path.includes('/.____padding_file/')) .map((file, index) => ({ file, index })) - .sort(function (a, b) { - if (a.file.name < b.file.name) return -1 - if (b.file.name < a.file.name) return 1 - return 0 - }) .map((object) => this.renderFileRow(torrentSummary, object.file, object.index)) filesElement = ( From 17b31f0fed999ada2c28cf67a0b732dcddf3f6a9 Mon Sep 17 00:00:00 2001 From: Adam Gotlib Date: Fri, 2 Sep 2016 04:03:36 +0200 Subject: [PATCH 7/7] Clean up playlists code --- src/renderer/controllers/media-controller.js | 4 +- .../controllers/playback-controller.js | 90 +++++++------- src/renderer/lib/playlist.js | 116 ++++++++++-------- src/renderer/lib/state.js | 1 - src/renderer/pages/PlayerPage.js | 9 +- 5 files changed, 115 insertions(+), 105 deletions(-) diff --git a/src/renderer/controllers/media-controller.js b/src/renderer/controllers/media-controller.js index a68ac90153..128878a27d 100644 --- a/src/renderer/controllers/media-controller.js +++ b/src/renderer/controllers/media-controller.js @@ -2,6 +2,8 @@ const electron = require('electron') const ipcRenderer = electron.ipcRenderer +const Playlist = require('../lib/playlist') + // Controls local play back: the