« get me outta code hell

"Loop mode" option: no loop, loop, shuffle - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2021-10-10 10:46:23 -0300
committer(quasar) nebula <qznebula@protonmail.com>2021-10-10 10:48:58 -0300
commitec0b00d62f5fe02248de71552f5cd3d76589295c (patch)
tree6f96214fcc73d9bf5228ed530b4682bc67812b39
parent3fdb4b7961f55a6b0fa24a3f271c3c8090497856 (diff)
"Loop mode" option: no loop, loop, shuffle
This also reorganizes the menubar options a little.
-rw-r--r--backend.js18
-rw-r--r--todo.txt11
-rw-r--r--ui.js75
3 files changed, 92 insertions, 12 deletions
diff --git a/backend.js b/backend.js
index e38fe2f..41107d7 100644
--- a/backend.js
+++ b/backend.js
@@ -66,7 +66,7 @@ class QueuePlayer extends EventEmitter {
     this.playingTrack = null
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
     this.pauseNextTrack = false
-    this.loopQueueAtEnd = false
+    this.queueEndMode = 'end' // end, loop, shuffle
     this.playedTrackToEnd = false
     this.timeData = null
 
@@ -435,10 +435,18 @@ class QueuePlayer extends EventEmitter {
     if (playingThisTrack) {
       this.playedTrackToEnd = true
       if (!this.playNext(item)) {
-        if (this.loopQueueAtEnd) {
-          this.playFirst()
-        } else {
-          this.clearPlayingTrack()
+        switch (this.queueEndMode) {
+          case 'loop':
+            this.playFirst()
+            break
+          case 'shuffle':
+            this.clearPlayingTrack()
+            this.shuffleQueue()
+            this.playFirst()
+            break
+          case 'end':
+          default:
+            this.clearPlayingTrack()
         }
       }
     }
diff --git a/todo.txt b/todo.txt
index af31659..90ed41b 100644
--- a/todo.txt
+++ b/todo.txt
@@ -637,3 +637,14 @@ TODO: Timestamps which have a timestampEnd property (all of them I think?)
 
 TODO: Read timestamps as JSON when the file extension is .json. (Right now
       any .timestamps.json file is ignored!)
+
+TODO: "Remove from queue" seems to always restore the cursor to a non-timestamp
+      input. This might be an issue with other queue-modifying actions too!
+
+TODO: The "From: <downloaderArg>" text in the playback info element *does* cut
+      off its text in an attempt to not go outside the screen bounds... but it
+      goes over the info pane edges anyway, so there's probably a math issue
+      there.
+
+TODO: "Play later" has a slight chance of keeping the track in the same place,
+      which is accentuated when there's only a couple tracks left in the queue.
diff --git a/ui.js b/ui.js
index 1331344..bf67b8e 100644
--- a/ui.js
+++ b/ui.js
@@ -351,12 +351,13 @@ class AppElement extends FocusElement {
         return [
           {label: playingTrack ? `("${playingTrack.name}")` : '(No track playing.)'},
           {divider: true},
+          {element: this.loopModeControl},
+          {element: this.volumeSlider},
+          {divider: true},
           playingTrack && {element: this.playingControl},
-          {element: this.loopingControl},
-          {element: this.loopQueueControl},
-          {element: this.pauseNextControl},
+          playingTrack && {element: this.loopingControl},
+          playingTrack && {element: this.pauseNextControl},
           {element: this.autoDJControl},
-          {element: this.volumeSlider},
           {divider: true},
           previous && {label: `Previous (${previous.name})`, action: () => this.SQP.playPrevious(playingTrack)},
           next && {label: `Next (${next.name})`, action: () => this.SQP.playNext(playingTrack)},
@@ -403,6 +404,20 @@ class AppElement extends FocusElement {
       getEnabled: () => this.config.canControlPlayback
     })
 
+    this.loopModeControl = new InlineListPickerElement('Loop mode', [
+      {value: 'end', label: 'Don\'t loop'},
+      {value: 'loop', label: 'Loop (same order)'},
+      {value: 'shuffle', label: 'Loop (shuffle)'}
+    ], {
+      setValue: val => {
+        if (this.SQP) {
+          this.SQP.queueEndMode = val
+        }
+      },
+      getValue: () => this.SQP && this.SQP.queueEndMode,
+      showContextMenu: this.showContextMenu
+    })
+
     this.pauseNextControl = new ToggleControl('Pause when this track ends?', {
       setValue: val => this.SQP.setPauseNextTrack(val),
       getValue: () => this.SQP.pauseNextTrack,
@@ -3153,13 +3168,27 @@ class InlineListPickerElement extends FocusElement {
   // next or previous. (That's the point, it's inline.) This element is mainly
   // useful in forms or ContextMenus.
 
-  constructor(labelText, options, showContextMenu = null) {
+  constructor(labelText, options, optsOrShowContextMenu = null) {
     super()
+
     this.labelText = labelText
     this.options = options
-    this.showContextMenu = showContextMenu
-    this.curIndex = 0
+
+    if (typeof optsOrShowContextMenu === 'function') {
+      this.showContextMenu = optsOrShowContextMenu
+    }
+
+    if (typeof optsOrShowContextMenu === 'object') {
+      const opts = optsOrShowContextMenu
+      this.showContextMenu = opts.showContextMenu
+      this.getValue = opts.getValue
+      this.setValue = opts.setValue
+    }
+
     this.keyboardIdentifier = this.labelText
+
+    this.curIndex = 0
+    this.refreshValue()
   }
 
   fixLayout() {
@@ -3244,11 +3273,26 @@ class InlineListPickerElement extends FocusElement {
     return false
   }
 
+
+  refreshValue() {
+    if (this.getValue) {
+      const value = this.getValue()
+      const index = this.options.findIndex(opt => opt.value === value)
+      if (index >= 0) {
+        this.curIndex = index
+      }
+    }
+  }
+
   nextOption() {
     this.curIndex++
     if (this.curIndex === this.options.length) {
       this.curIndex = 0
     }
+
+    if (this.setValue) {
+      this.setValue(this.curValue)
+    }
   }
 
   previousOption() {
@@ -3256,6 +3300,10 @@ class InlineListPickerElement extends FocusElement {
     if (this.curIndex < 0) {
       this.curIndex = this.options.length - 1
     }
+
+    if (this.setValue) {
+      this.setValue(this.curValue)
+    }
   }
 
   get curValue() {
@@ -3453,6 +3501,9 @@ class ToggleControl extends FocusElement {
   }
 
 
+  // Note: ToggleControl doesn't specify refreshValue because it doesn't have an
+  // internal state for the current value. It sets and draws based on the value
+  // getter provided externally.
   toggle() {
     this.setValue(!this.getValue())
   }
@@ -4716,6 +4767,16 @@ class ContextMenu extends FocusElement {
       return
     }
 
+    // Call refreshValue() on any items before they're shown, for items that
+    // provide it. (This is handy when reusing the same input across a menu that
+    // might be shown under different contexts.)
+    for (const item of items) {
+      const el = item.element
+      if (!el) continue
+      if (!el.refreshValue) continue
+      el.refreshValue()
+    }
+
     if (!this.root.selectedElement.directAncestors.includes(this)) {
       this.selectedBefore = this.root.selectedElement
     }