From 6bad90e8e0db9c9273de984be53a1ca61b4d8a24 Mon Sep 17 00:00:00 2001 From: Florrie Date: Fri, 20 Sep 2019 16:54:13 -0300 Subject: WIP - support multiple players at once Currently bug-free and doesn't change anything about existing mtui behavior! Meta N to create a new player, meta up/down to switch between which one you're interacting with. Each player has its own queue. Eventually (soon(TM)) there'll be much better UI to go with all this! --- ui.js | 230 +++++++++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 157 insertions(+), 73 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index aafe064..de3ab98 100644 --- a/ui.js +++ b/ui.js @@ -84,6 +84,9 @@ const keyBindings = [ ['isFocusLabels', 'L', {caseless: false}], // todo: better key? to let isToggleLoop be caseless again ['isSelectUp', telc.isShiftUp], ['isSelectDown', telc.isShiftDown], + ['isPreviousPlayer', telc.isMetaUp], + ['isNextPlayer', telc.isMetaDown], + ['isNewPlayer', keyBuf => keyBuf.equals(Buffer.from([0x1b, 0x6e]))], // Number pad ['isUp', '8'], @@ -150,11 +153,12 @@ class AppElement extends FocusElement { this.isPartyHost = false this.bindListeners() - this.attachListeners() + this.initialAttachListeners() this.config = Object.assign({ canControlPlayback: true, canControlQueue: true, + canControlQueuePlayers: true, canProcessMetadata: true, canSuspend: true, menubarColor: 4, // blue @@ -198,7 +202,6 @@ class AppElement extends FocusElement { this.queueListingElement = new QueueListingElement(this) this.setupCommonGrouplikeListingEvents(this.queueListingElement) - this.queueListingElement.loadGrouplike(this.backend.queueGrouplike) this.paneRight.addChild(this.queueListingElement) this.queueLengthLabel = new Label('') @@ -220,9 +223,9 @@ class AppElement extends FocusElement { this.playbackInfoElement = new PlaybackInfoElement() this.playbackPane.addChild(this.playbackInfoElement) - this.playbackInfoElement.on('seek back', () => this.backend.seekBack(5)) - this.playbackInfoElement.on('seek ahead', () => this.backend.seekAhead(5)) - this.playbackInfoElement.on('toggle pause', () => this.backend.togglePause()) + this.playbackInfoElement.on('seek back', () => this.SQP.seekBack(5)) + this.playbackInfoElement.on('seek ahead', () => this.SQP.seekAhead(5)) + this.playbackInfoElement.on('toggle pause', () => this.SQP.togglePause()) this.partyTop = new DisplayElement() this.partyBottom = new DisplayElement() @@ -277,8 +280,8 @@ class AppElement extends FocusElement { this.config.canSuspend && {label: 'Suspend', action: () => this.suspend()} ]}, {text: 'Playback', menuFn: () => { - const { playingTrack } = this.backend - const { items } = this.backend.queueGrouplike + const { playingTrack } = this.SQP + const { items } = this.SQP.queueGrouplike const curIndex = items.indexOf(playingTrack) const next = (curIndex >= 0) && items[curIndex + 1] const previous = (curIndex >= 0) && items[curIndex - 1] @@ -291,13 +294,13 @@ class AppElement extends FocusElement { {element: this.pauseNextControl}, {element: this.volumeSlider}, {divider: true}, - previous && {label: `Previous (${previous.name})`, action: () => this.backend.playPrevious(playingTrack)}, - next && {label: `Next (${next.name})`, action: () => this.backend.playNext(playingTrack)}, + previous && {label: `Previous (${previous.name})`, action: () => this.SQP.playPrevious(playingTrack)}, + next && {label: `Next (${next.name})`, action: () => this.SQP.playNext(playingTrack)}, next && {label: '- Play later', action: () => this.playLater(next)} ] }}, {text: 'Queue', menuFn: () => { - const { items } = this.backend.queueGrouplike + const { items } = this.SQP.queueGrouplike const curIndex = items.indexOf(this.playingTrack) return [ @@ -310,49 +313,82 @@ class AppElement extends FocusElement { ]) this.playingControl = new ToggleControl('Pause?', { - setValue: val => this.backend.setPause(val), - getValue: () => this.backend.player.isPaused, + setValue: val => this.SQP.setPause(val), + getValue: () => this.SQP.player.isPaused, getEnabled: () => this.config.canControlPlayback }) this.loopingControl = new ToggleControl('Loop current track?', { - setValue: val => this.backend.setLoop(val), - getValue: () => this.backend.player.isLooping, + setValue: val => this.SQP.setLoop(val), + getValue: () => this.SQP.player.isLooping, getEnabled: () => this.config.canControlPlayback }) this.pauseNextControl = new ToggleControl('Pause when this track ends?', { - setValue: val => this.backend.setPauseNextTrack(val), - getValue: () => this.backend.pauseNextTrack, + setValue: val => this.SQP.setPauseNextTrack(val), + getValue: () => this.SQP.pauseNextTrack, getEnabled: () => this.config.canControlPlayback }) this.volumeSlider = new SliderElement('Volume', { - setValue: val => this.backend.setVolume(val), - getValue: () => this.backend.player.volume, + setValue: val => this.SQP.setVolume(val), + getValue: () => this.SQP.player.volume, getEnabled: () => this.config.canControlPlayback }) + + this.selectQueuePlayer(this.backend.queuePlayers[0]) } bindListeners() { - this.handlePlaying = this.handlePlaying.bind(this) - this.handlePrintStatusLine = this.handlePrintStatusLine.bind(this) - this.handleProcessMetadataProgress = this.handleProcessMetadataProgress.bind(this) - this.handleQueueUpdated = this.handleQueueUpdated.bind(this) + for (const key of [ + 'handlePlaying', + 'handleReceivedTimeData', + 'handleProcessMetadataProgress', + 'handleQueueUpdated', + 'handleAddedQueuePlayer' + ]) { + this[key] = this[key].bind(this) + } } - attachListeners() { - this.backend.on('playing', this.handlePlaying) - this.backend.on('printStatusLine', this.handlePrintStatusLine) - this.backend.on('processMetadata progress', this.handleProcessMetadataProgress) - this.backend.on('queue updated', this.handleQueueUpdated) + initialAttachListeners() { + this.attachBackendListeners() + for (const queuePlayer of this.backend.queuePlayers) { + this.attachQueuePlayerListeners(queuePlayer) + } } removeListeners() { - this.backend.removeListener('playing', this.handlePlaying) - this.backend.removeListener('printStatusLine', this.handlePrintStatusLine) + this.removeBackendListeners() + for (const queuePlayer of this.backend.queuePlayers) { + this.removeQueuePlayerListeners(queuePlayer) + } + } + + attachQueuePlayerListeners(queuePlayer) { + queuePlayer.on('received time data', this.handleReceivedTimeData) + queuePlayer.on('playing', this.handlePlaying) + queuePlayer.on('queue updated', this.handleQueueUpdated) + } + + removeQueuePlayerListeners(queuePlayer) { + queuePlayer.removeListener('receivedTimeData', this.handleReceivedTimeData) + queuePlayer.removeListener('playing', this.handlePlaying) + queuePlayer.removeListener('queue updated', this.handleQueueUpdated) + } + + attachBackendListeners() { + this.backend.on('processMetadata progress', this.handleProcessMetadataProgress) + this.backend.on('added queue player', this.handleAddedQueuePlayer) + } + + removeBackendListeners() { this.backend.removeListener('processMetadata progress', this.handleProcessMetadataProgress) - this.backend.removeListener('queue updated', this.handleQueueUpdated) + this.backend.removeListener('added queue player', this.handleAddedQueuePlayer) + } + + handleAddedQueuePlayer(queuePlayer) { + this.attachQueuePlayerListeners(queuePlayer) } handlePlaying(track, oldTrack) { @@ -367,9 +403,11 @@ class AppElement extends FocusElement { this.updateQueueLengthLabel() } - handlePrintStatusLine(data) { - this.playbackInfoElement.updateProgress(data, this.backend.player) - this.updateQueueLengthLabel() + handleReceivedTimeData(data, queuePlayer) { + if (queuePlayer === this.SQP) { + this.playbackInfoElement.updateProgress(data, queuePlayer.player) + this.updateQueueLengthLabel() + } } handleProcessMetadataProgress(remaining) { @@ -380,6 +418,45 @@ class AppElement extends FocusElement { this.queueListingElement.buildItems() } + selectQueuePlayer(queuePlayer) { + // You can use this.SQP as a shorthand to get this. + this.selectedQueuePlayer = queuePlayer + + this.playbackInfoElement.updateTrack(queuePlayer.playingTrack) + if (queuePlayer.timeData) { + this.playbackInfoElement.updateProgress(queuePlayer.timeData, queuePlayer.player) + } + + this.queueListingElement.loadGrouplike(queuePlayer.queueGrouplike) + } + + selectNextQueuePlayer() { + const { queuePlayers } = this.backend + let index = queuePlayers.indexOf(this.SQP) + 1 + if (index >= queuePlayers.length) { + index = 0 + } + this.selectQueuePlayer(queuePlayers[index]) + } + + selectPreviousQueuePlayer() { + const { queuePlayers } = this.backend + let index = queuePlayers.indexOf(this.SQP) - 1 + if (index <= -1) { + index = queuePlayers.length - 1 + } + this.selectQueuePlayer(queuePlayers[index]) + } + + async addQueuePlayer() { + if (!this.config.canControlQueuePlayers) { + return false + } + + const queuePlayer = await this.backend.addQueuePlayer() + this.selectQueuePlayer(queuePlayer) + } + selected() { if (this.paneLeft.visible) { this.root.select(this.tabber) @@ -397,7 +474,7 @@ class AppElement extends FocusElement { this.tabber.addTab(grouplikeListing) this.tabber.selectTab(grouplikeListing) - grouplikeListing.on('download', item => this.backend.download(item)) + grouplikeListing.on('download', item => this.SQP.download(item)) grouplikeListing.on('browse', item => grouplikeListing.loadGrouplike(item)) grouplikeListing.on('queue', (item, opts) => this.handleQueueOptions(item, opts)) @@ -530,7 +607,7 @@ class AppElement extends FocusElement { return } - this.backend.play(item) + this.SQP.play(item) } unqueue(item) { @@ -539,7 +616,7 @@ class AppElement extends FocusElement { } let focusItem = this.queueListingElement.currentItem - focusItem = this.backend.unqueue(item, focusItem) + focusItem = this.SQP.unqueue(item, focusItem) this.queueListingElement.buildItems() this.updateQueueLengthLabel() @@ -554,7 +631,7 @@ class AppElement extends FocusElement { return } - this.backend.playSooner(item) + this.SQP.playSooner(item) // It may not have queued as soon as the user wants; in that case, they'll // want to queue it sooner again. Automatically reselect the track so that // this they don't have to navigate back to it by hand. @@ -566,7 +643,7 @@ class AppElement extends FocusElement { return } - this.backend.playLater(item) + this.SQP.playLater(item) // Just for consistency with playSooner (you can press ^-L to quickly get // back to the current track). this.queueListingElement.selectAndShow(item) @@ -577,7 +654,7 @@ class AppElement extends FocusElement { return } - this.backend.clearQueuePast(item) + this.SQP.clearQueuePast(item) this.queueListingElement.selectAndShow(item) } @@ -586,7 +663,7 @@ class AppElement extends FocusElement { return } - this.backend.clearQueueUpTo(item) + this.SQP.clearQueueUpTo(item) this.queueListingElement.selectAndShow(item) } @@ -740,7 +817,7 @@ class AppElement extends FocusElement { async shutdown() { if (this.config.stopPlayingUponQuit) { - await this.backend.stopPlaying() + await this.backend.stopPlayingAll() } this.emit('quitRequested') @@ -878,23 +955,23 @@ class AppElement extends FocusElement { if ((telc.isLeft(keyBuf) || telc.isRight(keyBuf)) && this.menubar.isSelected) { return // le sigh } else if (input.isRight(keyBuf)) { - this.backend.seekAhead(10) + this.SQP.seekAhead(10) } else if (input.isLeft(keyBuf)) { - this.backend.seekBack(10) + this.SQP.seekBack(10) } else if (input.isTogglePause(keyBuf)) { - this.backend.togglePause() + this.SQP.togglePause() } else if (input.isToggleLoop(keyBuf)) { - this.backend.toggleLoop() + this.SQP.toggleLoop() } else if (input.isVolumeUp(keyBuf)) { - this.backend.volUp() + this.SQP.volUp() } else if (input.isVolumeDown(keyBuf)) { - this.backend.volDown() + this.SQP.volDown() } else if (input.isStop(keyBuf)) { - this.backend.stopPlaying() + this.SQP.stopPlaying() } else if (input.isSkipBack(keyBuf)) { - this.backend.playPrevious(this.backend.playingTrack, true) + this.SQP.playPrevious(this.SQP.playingTrack, true) } else if (input.isSkipAhead(keyBuf)) { - this.backend.playNext(this.backend.playingTrack, true) + this.SQP.playNext(this.SQP.playingTrack, true) } } @@ -928,6 +1005,12 @@ class AppElement extends FocusElement { this.tabber.nextTab() } else if (this.tabber.isSelected && keyBuf.equals(Buffer.from(['T'.charCodeAt(0)]))) { this.tabber.previousTab() + } else if (input.isPreviousPlayer(keyBuf)) { + this.selectPreviousQueuePlayer() + } else if (input.isNextPlayer(keyBuf)) { + this.selectNextQueuePlayer() + } else if (input.isNewPlayer(keyBuf)) { + this.addQueuePlayer() } else { super.keyPressed(keyBuf) } @@ -963,11 +1046,11 @@ class AppElement extends FocusElement { } shuffleQueue() { - this.backend.shuffleQueue() + this.SQP.shuffleQueue() } clearQueue() { - this.backend.clearQueue() + this.SQP.clearQueue() this.queueListingElement.selectNone() this.updateQueueLengthLabel() @@ -980,12 +1063,16 @@ class AppElement extends FocusElement { // just directly moved from the old event listener on grouplikeListings for // 'queue'. handleQueueOptions(item, {where = 'end', order = 'normal', play = false, skip = false} = {}) { + if (!this.config.canControlQueue) { + return + } + const passedItem = item - let { playingTrack } = this.backend + let { playingTrack } = this.SQP if (skip && playingTrack === item) { - this.backend.playNext(playingTrack) + this.SQP.playNext(playingTrack) } if (isGroup(item)) { @@ -1011,7 +1098,7 @@ class AppElement extends FocusElement { afterItem = this.queueListingElement.currentItem } - this.backend.queue(item, afterItem, { + this.SQP.queue(item, afterItem, { movePlayingTrack: order === 'normal' }) @@ -1019,7 +1106,7 @@ class AppElement extends FocusElement { this.queueListingElement.selectAndShow(passedItem) } } else if (where.startsWith('distribute-')) { - this.backend.distributeQueue(item, { + this.SQP.distributeQueue(item, { how: where.slice('distribute-'.length) }) } @@ -1058,8 +1145,8 @@ class AppElement extends FocusElement { } updateQueueLengthLabel() { - const { playingTrack } = this.backend - const { items } = this.backend.queueGrouplike + const { playingTrack } = this.SQP + const { items } = this.SQP.queueGrouplike let trackRemainSec = 0 @@ -1091,7 +1178,7 @@ class AppElement extends FocusElement { const { duration } = getTimeStringsFromSec(0, totalRemainSec) this.queueLengthLabel.text = (playingTrack && items.includes(playingTrack) - ? `(${this.playSymbol} ${index} / ${items.length})` + ? `(${this.SQP.playSymbol} ${index} / ${items.length})` : `(${items.length})`) this.queueTimeLabel.text = `(${duration + approxSymbol})` @@ -1102,18 +1189,13 @@ class AppElement extends FocusElement { this.queueTimeLabel.y = this.paneRight.contentH - 1 } - get playSymbol() { - const { player, playingTrack } = this.backend - if (player && playingTrack) { - if (player.isPaused) { - return '⏸' - } else { - return '▶' - } - } else { - return '.' - } + get SQP() { + // Just a convenient shorthand. + return this.selectedQueuePlayer } + + get selectedQueuePlayer() { return this.getDep('selectedQueuePlayer') } + set selectedQueuePlayer(v) { return this.setDep('selectedQueuePlayer', v) } } class GrouplikeListingElement extends Form { @@ -1232,7 +1314,7 @@ class GrouplikeListingElement extends Form { this.form.selectAndShow(this.grouplike.items[this.grouplike.items.length - 1]) } else if (keyBuf[0] === 12) { // ctrl-L if (this.grouplike.isTheQueue) { - this.form.selectAndShow(this.app.backend.playingTrack) + this.form.selectAndShow(this.app.SQP.playingTrack) } else { this.toggleExpandLabels() } @@ -1437,7 +1519,9 @@ class GrouplikeListingElement extends Form { this.form.scrollSelectedElementIntoView() } this.jumpElement.visible = false - this.root.select(this) + if (this.jumpElement.isSelected) { + this.root.select(this) + } this.fixLayout() } @@ -2345,7 +2429,7 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writable.write('G') } else if (record.downloading) { writable.write(braille[Math.floor(Date.now() / 250) % 6]) - } else if (this.app.backend.playingTrack === this.item) { + } else if (this.app.SQP.playingTrack === this.item) { writable.write('\u25B6') } else { writable.write(' ') -- cgit 1.3.0-6-gf8a5