« get me outta code hell

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:
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(' ')