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! --- backend.js | 321 ++++++++++++++++++++++++++++++++----------------------- telnet-server.js | 1 + todo.txt | 4 + tui-lib | 2 +- ui.js | 230 ++++++++++++++++++++++++++------------- 5 files changed, 351 insertions(+), 207 deletions(-) diff --git a/backend.js b/backend.js index 57910e9..e8601c5 100644 --- a/backend.js +++ b/backend.js @@ -28,24 +28,22 @@ const fs = require('fs') const writeFile = promisify(fs.writeFile) const readFile = promisify(fs.readFile) -class Backend extends EventEmitter { - constructor() { +class QueuePlayer extends EventEmitter { + constructor({ + getRecordFor + }) { super() this.player = null this.playingTrack = null - this.recordStore = new RecordStore() - this.throttleMetadata = throttlePromise(10) - this.metadataDictionary = {} this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []} this.pauseNextTrack = false this.playedTrackToEnd = false + this.timeData = null - this.rootDirectory = process.env.HOME + '/.mtui' - this.metadataPath = this.rootDirectory + '/track-metadata.json' + this.getRecordFor = getRecordFor } - async setup() { this.player = await getPlayer() @@ -55,79 +53,16 @@ class Backend extends EventEmitter { } } - await this.loadMetadata() - this.player.on('printStatusLine', data => { if (this.playingTrack) { - this.emit('printStatusLine', data) + this.timeData = data + this.emit('received time data', data, this) } }) return true } - - async readMetadata() { - try { - return JSON.parse(await readFile(this.metadataPath)) - } catch (error) { - // Just stop. It's okay to fail to load metadata. - return null - } - } - - async loadMetadata() { - Object.assign(this.metadataDictionary, await this.readMetadata()) - } - - async saveMetadata() { - const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary) - await writeFile(this.metadataPath, JSON.stringify(newData)) - } - - getMetadataFor(item) { - const key = this.metadataDictionary[item.downloaderArg] - return this.metadataDictionary[key] || null - } - - async processMetadata(item, reprocess = false, top = true) { - let counter = 0 - - if (isGroup(item)) { - const results = await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false))) - counter += results.reduce((acc, n) => acc + n, 0) - } else process: { - if (!reprocess && this.getMetadataFor(item)) { - break process - } - - await this.throttleMetadata(async () => { - const filePath = await this.download(item) - const metadataReader = getMetadataReaderFor(filePath) - const data = await metadataReader(filePath) - - this.metadataDictionary[item.downloaderArg] = filePath - this.metadataDictionary[filePath] = data - }) - - this.emit('processMetadata progress', this.throttleMetadata.queue.length) - - counter++ - } - - if (top) { - await this.saveMetadata() - } - - return counter - } - - - getRecordFor(item) { - return this.recordStore.getRecord(item) - } - - queue(topItem, afterItem = null, {movePlayingTrack = true} = {}) { const { items } = this.queueGrouplike const newTrackIndex = items.length @@ -389,49 +324,9 @@ class Backend extends EventEmitter { this.emit('queue updated') } - seekAhead(seconds) { - this.player.seekAhead(seconds) - } - - seekBack(seconds) { - this.player.seekBack(seconds) - } - - togglePause() { - this.player.togglePause() - } - - setPause(value) { - this.player.setPause(value) - } - - toggleLoop() { - this.player.toggleLoop() - } - - setLoop(value) { - this.player.setLoop(value) - } - - volUp(amount = 10) { - this.player.volUp(amount) - } - - volDown(amount = 10) { - this.player.volDown(amount) - } - - setVolume(value) { - this.player.setVolume(value) - } - - setPauseNextTrack(value) { - this.pauseNextTrack = !!value - } - async stopPlaying() { - // We emit this so playTrack doesn't immediately start a new track. - // We aren't *actually* about to play a new track. + // We emit this so the active play() call doesn't immediately start a new + // track. We aren't *actually* about to play a new track. this.emit('playing new track') await this.player.kill() this.clearPlayingTrack() @@ -559,10 +454,185 @@ class Backend extends EventEmitter { if (this.playingTrack !== null) { const oldTrack = this.playingTrack this.playingTrack = null + this.timeData = null this.emit('playing', null, oldTrack) } } + async download(item) { + if (isGroup(item)) { + // TODO: Download all children (recursively), show a confirmation prompt + // if there are a lot of items (remember to flatten). + return + } + + // Don't start downloading an item if we're already downloading it! + if (this.getRecordFor(item).downloading) { + return + } + + const arg = item.downloaderArg + this.getRecordFor(item).downloading = true + try { + return await getDownloaderFor(arg)(arg) + } finally { + this.getRecordFor(item).downloading = false + } + } + + seekAhead(seconds) { + this.player.seekAhead(seconds) + } + + seekBack(seconds) { + this.player.seekBack(seconds) + } + + togglePause() { + this.player.togglePause() + } + + setPause(value) { + this.player.setPause(value) + } + + toggleLoop() { + this.player.toggleLoop() + } + + setLoop(value) { + this.player.setLoop(value) + } + + volUp(amount = 10) { + this.player.volUp(amount) + } + + volDown(amount = 10) { + this.player.volDown(amount) + } + + setVolume(value) { + this.player.setVolume(value) + } + + setPauseNextTrack(value) { + this.pauseNextTrack = !!value + } + + get playSymbol() { + if (this.player && this.playingTrack) { + if (this.player.isPaused) { + return '⏸' + } else { + return '▶' + } + } else { + return '.' + } + } +} + +class Backend extends EventEmitter { + constructor() { + super() + + this.queuePlayers = [] + + this.recordStore = new RecordStore() + this.throttleMetadata = throttlePromise(10) + this.metadataDictionary = {} + + this.rootDirectory = process.env.HOME + '/.mtui' + this.metadataPath = this.rootDirectory + '/track-metadata.json' + } + + async setup() { + const error = await this.addQueuePlayer() + if (error.error) { + return error + } + + await this.loadMetadata() + + return true + } + + async addQueuePlayer() { + const queuePlayer = new QueuePlayer({ + getRecordFor: item => this.getRecordFor(item) + }) + + const error = await queuePlayer.setup() + if (error.error) { + return error + } + + this.queuePlayers.push(queuePlayer) + this.emit('added queue player', queuePlayer) + + return queuePlayer + } + + async readMetadata() { + try { + return JSON.parse(await readFile(this.metadataPath)) + } catch (error) { + // Just stop. It's okay to fail to load metadata. + return null + } + } + + async loadMetadata() { + Object.assign(this.metadataDictionary, await this.readMetadata()) + } + + async saveMetadata() { + const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary) + await writeFile(this.metadataPath, JSON.stringify(newData)) + } + + getMetadataFor(item) { + const key = this.metadataDictionary[item.downloaderArg] + return this.metadataDictionary[key] || null + } + + async processMetadata(item, reprocess = false, top = true) { + let counter = 0 + + if (isGroup(item)) { + const results = await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false))) + counter += results.reduce((acc, n) => acc + n, 0) + } else process: { + if (!reprocess && this.getMetadataFor(item)) { + break process + } + + await this.throttleMetadata(async () => { + const filePath = await this.download(item) + const metadataReader = getMetadataReaderFor(filePath) + const data = await metadataReader(filePath) + + this.metadataDictionary[item.downloaderArg] = filePath + this.metadataDictionary[filePath] = data + }) + + this.emit('processMetadata progress', this.throttleMetadata.queue.length) + + counter++ + } + + if (top) { + await this.saveMetadata() + } + + return counter + } + + getRecordFor(item) { + return this.recordStore.getRecord(item) + } + getDuration(item) { let noticedMissingMetadata = false @@ -588,24 +658,9 @@ class Backend extends EventEmitter { return {seconds, string, noticedMissingMetadata, approxSymbol} } - async download(item) { - if (isGroup(item)) { - // TODO: Download all children (recursively), show a confirmation prompt - // if there are a lot of items (remember to flatten). - return - } - - // Don't start downloading an item if we're already downloading it! - if (this.getRecordFor(item).downloading) { - return - } - - const arg = item.downloaderArg - this.getRecordFor(item).downloading = true - try { - return await getDownloaderFor(arg)(arg) - } finally { - this.getRecordFor(item).downloading = false + async stopPlayingAll() { + for (const queuePlayer of this.queuePlayers) { + await queuePlayer.stopPlaying() } } } diff --git a/telnet-server.js b/telnet-server.js index 64304c6..b4422f5 100644 --- a/telnet-server.js +++ b/telnet-server.js @@ -32,6 +32,7 @@ class TelnetServer extends EventEmitter { appConfig: { canControlPlayback: false, canControlQueue: true, + canControlQueuePlayers: false, canProcessMetadata: false, canSuspend: false, showLeftPane: true, diff --git a/todo.txt b/todo.txt index 3470204..9e03156 100644 --- a/todo.txt +++ b/todo.txt @@ -403,3 +403,7 @@ TODO: Investigate why reveal() has distinct support for grouplikes as well as flattened children) after all. But if you could -- I think reveal should work the same for groups as it does for tracks, i.e. opening the parent and then selecting the item. + +TODO: Make the menubar work like context menus for keyboard selection, e.g. + pressing P should select 'Playback' (not bubble to the app element and + rewind to the previous track). diff --git a/tui-lib b/tui-lib index e4ae178..e69d506 160000 --- a/tui-lib +++ b/tui-lib @@ -1 +1 @@ -Subproject commit e4ae17895cd673bdc8a8a2a060b835b0492daeb1 +Subproject commit e69d506fdc2d0238cb592256aca5353ecb292816 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