« get me outta code hell

add/remove part of group to/from selection - 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 <qznebula@protonmail.com>2024-05-14 16:23:17 -0300
committer(quasar) nebula <qznebula@protonmail.com>2024-05-14 16:23:17 -0300
commit1b3584f7f658d3df61ae2c5711280236490dc55d (patch)
treeea9d1c4b8de20a8a9e6d5068b5a4edf351030dbf
parent6d92670f2dd5849e51dd98780d09952164dfe2da (diff)
add/remove part of group to/from selection
-rw-r--r--todo.txt6
-rw-r--r--ui.js100
2 files changed, 85 insertions, 21 deletions
diff --git a/todo.txt b/todo.txt
index 4c93789..bfb6e98 100644
--- a/todo.txt
+++ b/todo.txt
@@ -696,3 +696,9 @@ TODO: Pressing escape while you've got items selected should deselect those
       still handy to not be locked out of stopping playback altogether.
       Alternative: clear the selection (without stopping playback) only if the
       cursor is currently on a selected item.
+
+TODO: When you're navigating down (or up) a menu, if that menu's got a
+      scrollbar *and* is divided into sections, passing a divider line should
+      try to scroll the whole newly active section into view! This way you get
+      all the context and don't miss out on realizing there are more items
+      below. (This only applies for keyboard navigation, not wheel scrolling.)
diff --git a/ui.js b/ui.js
index f006a70..3e0098f 100644
--- a/ui.js
+++ b/ui.js
@@ -45,6 +45,7 @@ import {
   parentSymbol,
   reverseOrderOfGroups,
   searchForItem,
+  selectTracks,
   shuffleOrderOfGroups,
 } from './playlist-utils.js'
 
@@ -317,6 +318,15 @@ export default class AppElement extends FocusElement {
       {value: 'normal', label: 'In order'}
     ], this.showContextMenu)
 
+    this.selectGrouplikeItemsControl = new InlineListPickerElement('Which?', [
+      {value: 'all', label: 'all tracks'},
+      {value: '1', label: 'one track, randomly'},
+      {value: '4', label: 'four tracks, randomly'},
+      {value: '25%', label: '25% of the tracks, randomly'},
+      {value: '50%', label: '50% of the tracks, randomly'},
+      {value: '75%', label: '75% of the tracks, randomly'},
+    ], this.showContextMenu)
+
     this.menubar.buildItems([
       {text: 'mtui', menuItems: [
         {label: 'mtui (perpetual development)'},
@@ -914,32 +924,79 @@ export default class AppElement extends FocusElement {
     this.emitMarkChanged()
   }
 
-  markItem(item) {
-    if (isGroup(item)) {
-      for (const child of item.items) {
-        this.markItem(child)
+  selectTracksForMarking(item, forUnmarking = false, useGroupSelectionControl = false) {
+    let mode = 'all'
+    let modeValue = 0
+
+    if (useGroupSelectionControl) {
+      const value = this.selectGrouplikeItemsControl.curValue
+      if (value === 'all') {
+        // Do nothing, this is the default
+      } else if (value.endsWith('%')) {
+        mode = 'random'
+        modeValue = value
+      } else if (parseInt(value).toString() === value) {
+        mode = 'random'
+        modeValue = value
       }
+    }
+
+    const range = {
+      items:
+        (flattenGrouplike(item)
+          .items
+          .filter(isTrack)
+          .filter(track =>
+            this.markGrouplike.items.includes(track) === forUnmarking))
+    }
+
+    return selectTracks(range, mode, modeValue).items
+  }
+
+  markItem(item, useGroupSelectionControl = false) {
+    const { items: mark } = this.markGrouplike
+
+    let add
+    if (isGroup(item)) {
+      add = this.selectTracksForMarking(item, false, useGroupSelectionControl)
+    } else if (mark.includes(item)) {
+      add = []
     } else {
-      const { items } = this.markGrouplike
-      if (!items.includes(item)) {
-        items.push(item)
-        this.emitMarkChanged()
-      }
+      add = [item]
+    }
+
+    if (!add.length) {
+      return
     }
+
+    for (const track of add) {
+      mark.push(track)
+    }
+
+    this.emitMarkChanged()
   }
 
-  unmarkItem(item) {
+  unmarkItem(item, useGroupSelectionControl) {
+    const { items: mark } = this.markGrouplike
+
+    let remove
     if (isGroup(item)) {
-      for (const child of item.items) {
-        this.unmarkItem(child)
-      }
+      remove = this.selectTracksForMarking(item, true, useGroupSelectionControl)
+    } else if (mark.includes(item)) {
+      remove = [item]
     } else {
-      const { items } = this.markGrouplike
-      if (items.includes(item)) {
-        items.splice(items.indexOf(item), 1)
-        this.emitMarkChanged()
-      }
+      remove = []
     }
+
+    if (!remove.length) {
+      return
+    }
+
+    for (const track of remove) {
+      mark.splice(mark.indexOf(track), 1)
+    }
+
+    this.emitMarkChanged()
   }
 
   getMarkStatus(item) {
@@ -1442,9 +1499,10 @@ export default class AppElement extends FocusElement {
           ...(item === this.markGrouplike
             ? [{label: 'Deselect all', action: () => this.unmarkAll()}]
             : [
-              this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item)},
-              this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item)}
-            ])
+                isGroup(item) && {element: this.selectGrouplikeItemsControl},
+                this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item, true)},
+                this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item, true)},
+              ])
         ]
       }
     }