Add repeat and shuffle options to the player

This commit is contained in:
Adam Gotlib
2016-06-17 16:55:36 +02:00
parent b8f6f9f3eb
commit 9284122461
6 changed files with 160 additions and 12 deletions

View File

@@ -28,6 +28,8 @@ function setPlayerOpen (flag) {
getMenuItem('Play/Pause').enabled = flag getMenuItem('Play/Pause').enabled = flag
getMenuItem('Skip Next').enabled = flag getMenuItem('Skip Next').enabled = flag
getMenuItem('Skip Previous').enabled = flag getMenuItem('Skip Previous').enabled = flag
getMenuItem('Enable Shuffle').enabled = flag
getMenuItem('Enable Repeat').enabled = flag
getMenuItem('Increase Volume').enabled = flag getMenuItem('Increase Volume').enabled = flag
getMenuItem('Decrease Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag
getMenuItem('Step Forward').enabled = flag getMenuItem('Step Forward').enabled = flag
@@ -39,12 +41,16 @@ function setPlayerOpen (flag) {
if (flag === false) { if (flag === false) {
getMenuItem('Skip Next').enabled = false getMenuItem('Skip Next').enabled = false
getMenuItem('Skip Previous').enabled = false getMenuItem('Skip Previous').enabled = false
getMenuItem('Enable Shuffle').checked = false
getMenuItem('Enable Repeat').checked = false
} }
} }
function onPlayerUpdate (hasNext, hasPrevious) { function onPlayerUpdate (state) {
getMenuItem('Skip Next').enabled = hasNext getMenuItem('Skip Next').enabled = state.hasNext
getMenuItem('Skip Previous').enabled = hasPrevious getMenuItem('Skip Previous').enabled = state.hasPrevious
getMenuItem('Enable Shuffle').checked = state.shuffle
getMenuItem('Enable Repeat').checked = state.repeat
} }
function setWindowFocus (flag) { function setWindowFocus (flag) {
@@ -215,6 +221,23 @@ function getMenuTemplate () {
{ {
type: 'separator' 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', label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up', accelerator: 'CmdOrCtrl+Up',

View File

@@ -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 (unpause) the current media
play () { play () {
var state = this.state var state = this.state
@@ -313,6 +331,8 @@ module.exports = class PlaybackController {
state.window.title = fileSummary.name state.window.title = fileSummary.name
ipcRenderer.send('onPlayerUpdate', state.playlist.getState())
// play in VLC if set as default player (Preferences / Playback / Play in VLC) // play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) { if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer') dispatch('openExternalPlayer')
@@ -324,7 +344,7 @@ module.exports = class PlaybackController {
// otherwise, play the video // otherwise, play the video
this.update() this.update()
ipcRenderer.send('onPlayerUpdate', state.playlist.hasNext(), state.playlist.hasPrevious()) ipcRenderer.send('onPlayerUpdate', state.playlist.getState())
cb() cb()
} }

View File

@@ -6,8 +6,16 @@ function Playlist (torrentSummary) {
this._infoHash = torrentSummary.infoHash this._infoHash = torrentSummary.infoHash
this._position = 0 this._position = 0
this._tracks = extractTracks(torrentSummary) this._tracks = extractTracks(torrentSummary)
this._order = range(0, this._tracks.length)
this._repeat = false
this._shuffled = false
} }
// =============================================================================
// Public methods
// =============================================================================
Playlist.prototype.getInfoHash = function () { Playlist.prototype.getInfoHash = function () {
return this._infoHash return this._infoHash
} }
@@ -17,37 +25,61 @@ Playlist.prototype.getTracks = function () {
} }
Playlist.prototype.hasNext = 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 () { Playlist.prototype.hasPrevious = function () {
return this._position > 0 return !this._tracks.length ? false
: this._repeat ? true
: this._position > 0
} }
Playlist.prototype.next = function () { Playlist.prototype.next = function () {
if (this.hasNext()) { if (this.hasNext()) {
this._position++ this._position = mod(this._position + 1, this._tracks.length)
return this.getCurrent() return this.getCurrent()
} }
} }
Playlist.prototype.previous = function () { Playlist.prototype.previous = function () {
if (this.hasPrevious()) { if (this.hasPrevious()) {
this._position-- this._position = mod(this._position - 1, this._tracks.length)
return this.getCurrent() 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) { Playlist.prototype.jumpToFile = function (infoHash, fileIndex) {
this.setPosition(this._tracks.findIndex( this.setPosition(this._order.findIndex((i) => {
(track) => track.infoHash === infoHash && track.fileIndex === fileIndex let track = this._tracks[i]
)) return track.infoHash === infoHash && track.fileIndex === fileIndex
}))
return this.getCurrent() return this.getCurrent()
} }
Playlist.prototype.getCurrent = function () { Playlist.prototype.getCurrent = function () {
var position = this.getPosition() var position = this.getPosition()
return position === undefined ? undefined : this._tracks[position]
return position === undefined ? undefined
: this._tracks[this._order[position]]
} }
Playlist.prototype.getPosition = function () { Playlist.prototype.getPosition = function () {
@@ -60,6 +92,43 @@ Playlist.prototype.setPosition = function (position) {
this._position = 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) { function extractTracks (torrentSummary) {
return torrentSummary.files.map((file, index) => ({ file, index })) return torrentSummary.files.map((file, index) => ({ file, index }))
.filter((object) => TorrentPlayer.isPlayable(object.file)) .filter((object) => TorrentPlayer.isPlayable(object.file))
@@ -76,3 +145,17 @@ function extractTracks (torrentSummary) {
: 'other' : '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
}

View File

@@ -190,6 +190,8 @@ const dispatchHandlers = {
'playPause': () => controllers.playback.playPause(), 'playPause': () => controllers.playback.playPause(),
'nextTrack': () => controllers.playback.nextTrack(), 'nextTrack': () => controllers.playback.nextTrack(),
'previousTrack': () => controllers.playback.previousTrack(), 'previousTrack': () => controllers.playback.previousTrack(),
'toggleShuffle': (flag) => controllers.playback.toggleShuffle(flag),
'toggleRepeat': (flag) => controllers.playback.toggleRepeat(flag),
'skip': (time) => controllers.playback.skip(time), 'skip': (time) => controllers.playback.skip(time),
'skipTo': (time) => controllers.playback.skipTo(time), 'skipTo': (time) => controllers.playback.skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), 'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),

View File

@@ -384,6 +384,8 @@ function renderPlayerControls (state) {
: '' : ''
var prevClass = state.playlist.hasPrevious() ? '' : 'disabled' var prevClass = state.playlist.hasPrevious() ? '' : 'disabled'
var nextClass = state.playlist.hasNext() ? '' : 'disabled' var nextClass = state.playlist.hasNext() ? '' : 'disabled'
var repeatClass = state.playlist.repeatEnabled() ? 'active' : ''
var shuffleClass = state.playlist.shuffleEnabled() ? 'active' : ''
var elements = [ var elements = [
<div key='playback-bar' className='playback-bar'> <div key='playback-bar' className='playback-bar'>
@@ -444,6 +446,22 @@ function renderPlayerControls (state) {
)) ))
} }
elements.push(
<i
key='repeat'
className={'icon repeat float-right ' + repeatClass}
onClick={dispatcher('toggleRepeat')}>
repeat
</i>,
<i
key='shuffle'
className={'icon shuffle float-right ' + shuffleClass}
onClick={dispatcher('toggleShuffle')}>
shuffle
</i>
)
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var castTypes = ['chromecast', 'airplay', 'dlna'] var castTypes = ['chromecast', 'airplay', 'dlna']
var isCastingAnywhere = castTypes.some( var isCastingAnywhere = castTypes.some(

View File

@@ -743,6 +743,8 @@ body.drag .app::after {
} }
.player .controls .closed-caption.active, .player .controls .closed-caption.active,
.player .controls .repeat.active,
.player .controls .shuffle.active,
.player .controls .device.active { .player .controls .device.active {
color: #9af; color: #9af;
} }