« get me outta code hell

WIP - support multiple players at once - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/ui.js
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-09-20 16:54:13 -0300
committerFlorrie <towerofnix@gmail.com>2019-09-20 16:54:13 -0300
commit6bad90e8e0db9c9273de984be53a1ca61b4d8a24 (patch)
tree45e5f2a74a4fe9f6d939590a5d547be939071596 /ui.js
parenta5d3b710eb46e58708b8dbb51f5231ba534561fd (diff)
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!
Diffstat (limited to 'ui.js')
-rw-r--r--ui.js230
1 files changed, 157 insertions, 73 deletions
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(' ')