diff --git a/src/main/menu.js b/src/main/menu.js index 2e4d6373..551aba8f 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 b7880fe6..b9778d4c 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 dfc97c31..3906aad3 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 a7a92c04..723d14c9 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 67c54d54..effb89a2 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 ae5c6aac..380f8116 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; }