Add repeat and shuffle options to the player
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user