« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rwxr-xr-xindex.js23
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--todo.txt5
m---------tui-lib0
-rw-r--r--ui.js166
7 files changed, 182 insertions, 19 deletions
diff --git a/README.md b/README.md
index b5bbc47..d233c74 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ playlist.json file (usually generated by http-music or downloaded from online).
 * <kbd>Escape</kbd> - stop playing the current track
 * <kbd>Right</kbd> - seek ahead
 * <kbd>Left</kbd> - seek back
+* <kbd><kbd>Ctrl</kbd>+<kbd>O</kbd></kbd> - open a playlist from a source (like a /path/to/a/folder or a YouTube playlist URL) (you can also just pass this source to `mtui`)
 * **In the main listing:**
   * <kbd>Enter</kbd> - if the selected item is a group, enter it; otherwise play it
   * <kbd>Backspace</kbd> - leave the current group (if in one)
diff --git a/index.js b/index.js
index 59c91ec..694a384 100755
--- a/index.js
+++ b/index.js
@@ -65,31 +65,16 @@ async function main() {
     source: ['crawl-local', process.env.HOME + '/Music']
   }
 
-  if (process.argv[2]) {
-    console.log('Opening playlist...')
-
-    const crawlers = getAllCrawlersForArg(process.argv[2])
-
-    if (crawlers.length === 0) {
-      console.error(`No suitable playlist crawler for "${process.argv[2]}".`)
-      process.exit(1)
-    }
-
-    const crawler = crawlers[0]
-
-    if (crawlers.length > 1) {
-      console.warn(`More than one suitable crawler for "${process.argv[2]}" - using ${crawler.name}.`)
-    }
-
-    grouplike = await crawler(process.argv[2])
-  }
-
   grouplike = await processSmartPlaylist(grouplike)
 
   appElement.grouplikeListingElement.loadGrouplike(grouplike)
 
   root.select(appElement.form)
 
+  if (process.argv[2]) {
+    appElement.handlePlaylistSource(process.argv[2])
+  }
+
   const flushable = new Flushable(process.stdout, true)
   flushable.resizeScreen(size)
   flushable.shouldShowCompressionStatistics = process.argv.includes('--show-ansi-stats')
diff --git a/package-lock.json b/package-lock.json
index 4352577..efb723d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,11 @@
       "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-3.2.0.tgz",
       "integrity": "sha1-5WfP3LMk1OeuWSKjcAraXeh5oMo="
     },
+    "expand-home-dir": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/expand-home-dir/-/expand-home-dir-0.0.3.tgz",
+      "integrity": "sha1-ct6KBIbMKKO71wRjU5iCW1tign0="
+    },
     "fifo-js": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fifo-js/-/fifo-js-2.1.0.tgz",
diff --git a/package.json b/package.json
index fe701c3..b3855ac 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
   "license": "GPL-3.0",
   "dependencies": {
     "command-exists": "^1.2.6",
+    "expand-home-dir": "0.0.3",
     "fifo-js": "^2.1.0",
     "js-base64": "^2.4.5",
     "mkdirp": "^0.5.1",
diff --git a/todo.txt b/todo.txt
index c1f7b51..9ad8667 100644
--- a/todo.txt
+++ b/todo.txt
@@ -49,3 +49,8 @@ TODO: There's some weird glitch where, if downloaderArg is missing (=== ""),
       72_food (by Jake Chudnow) plays. (That's the first thing returned by
       readdir, I suppose.)
       (Done!)
+
+TODO: Mouse support, obviously.
+
+TODO: Ctrl-O'ing a playlist sets the left-pane's selected index to the second
+      item, for some reason. (Regardless of what you had selected before..)
diff --git a/tui-lib b/tui-lib
-Subproject 9743a9fcf7b8e5e5dd98cd1c74aefa36a3f70a9
+Subproject 1076bd5e65658a0e846a7892cee787ade7660bb
diff --git a/ui.js b/ui.js
index 4fe1d8a..8d912a2 100644
--- a/ui.js
+++ b/ui.js
@@ -1,9 +1,13 @@
+const { getAllCrawlersForArg } = require('./crawlers')
 const { getDownloaderFor } = require('./downloaders')
 const { getPlayer } = require('./players')
 const { parentSymbol, isGroup, isTrack, getItemPath, getItemPathString, flattenGrouplike } = require('./playlist-utils')
 const { shuffleArray } = require('./general-util')
+const processSmartPlaylist = require('./smart-playlist')
+
 const ansi = require('./tui-lib/util/ansi')
 const Button = require('./tui-lib/ui/form/Button')
+const Dialog = require('./tui-lib/ui/Dialog')
 const DisplayElement = require('./tui-lib/ui/DisplayElement')
 const FocusElement = require('./tui-lib/ui/form/FocusElement')
 const Form = require('./tui-lib/ui/form/Form')
@@ -11,6 +15,7 @@ const Label = require('./tui-lib/ui/Label')
 const ListScrollForm = require('./tui-lib/ui/form/ListScrollForm')
 const Pane = require('./tui-lib/ui/Pane')
 const RecordStore = require('./record-store')
+const TextInput = require('./tui-lib/ui/form/TextInput')
 const WrapLabel = require('./tui-lib/ui/WrapLabel')
 const telc = require('./tui-lib/util/telchars')
 const unic = require('./tui-lib/util/unichars')
@@ -92,6 +97,64 @@ class AppElement extends FocusElement {
 
     this.playbackInfoElement = new PlaybackInfoElement()
     this.playbackPane.addChild(this.playbackInfoElement)
+
+    // Dialogs
+
+    this.openPlaylistDialog = new OpenPlaylistDialog()
+    this.setupDialog(this.openPlaylistDialog)
+
+    this.openPlaylistDialog.on('source selected', source => this.handlePlaylistSource(source))
+
+    this.alertDialog = new AlertDialog()
+    this.setupDialog(this.alertDialog)
+  }
+
+  async handlePlaylistSource(source) {
+    this.openPlaylistDialog.close()
+    this.alertDialog.showMessage('Opening playlist...', false)
+
+    let grouplike
+    try {
+      grouplike = await this.openPlaylist(source)
+    } catch (error) {
+      if (error === 'unknown argument') {
+        this.alertDialog.showMessage('Could not figure out how to load a playlist from: ' + source)
+      } else if (typeof error === 'string') {
+        this.alertDialog.showMessage(error)
+      } else {
+        throw error
+      }
+
+      return
+    }
+
+    this.alertDialog.close()
+    this.root.select(this.form)
+
+    grouplike = await processSmartPlaylist(grouplike)
+    this.grouplikeListingElement.loadGrouplike(grouplike)
+  }
+
+  openPlaylist(arg) {
+    const crawlers = getAllCrawlersForArg(arg)
+
+    if (crawlers.length === 0) {
+      throw 'unknown argument'
+    }
+
+    const crawler = crawlers[0]
+
+    return crawler(arg)
+  }
+
+  setupDialog(dialog) {
+    dialog.visible = false
+    this.addChild(dialog)
+
+    dialog.on('cancelled', () => {
+      dialog.close()
+      this.root.select(this.form)
+    })
   }
 
   async setup() {
@@ -155,6 +218,8 @@ class AppElement extends FocusElement {
     } else if (telc.isCharacter(keyBuf, '2') && this.queueListingElement.selectable) {
       this.form.curIndex = this.form.inputs.indexOf(this.queueListingElement)
       this.form.updateSelectedElement()
+    } else if (keyBuf.equals(Buffer.from([15]))) { // ctrl-O
+      this.openPlaylistDialog.open()
     } else {
       super.keyPressed(keyBuf)
     }
@@ -843,4 +908,105 @@ class PlaybackInfoElement extends DisplayElement {
   }
 }
 
+class OpenPlaylistDialog extends Dialog {
+  constructor() {
+    super()
+
+    this.label = new Label('Enter a playlist source:')
+    this.pane.addChild(this.label)
+
+    this.form = new Form()
+    this.pane.addChild(this.form)
+
+    this.input = new TextInput()
+    this.form.addInput(this.input)
+
+    this.button = new Button('Open')
+    this.form.addInput(this.button)
+
+    this.button.on('pressed', () => {
+      if (this.input.value) {
+        this.emit('source selected', this.input.value)
+      }
+    })
+  }
+
+  opened() {
+    this.input.setValue('')
+    this.form.curIndex = 0
+    this.form.updateSelectedElement()
+  }
+
+  fixLayout() {
+    super.fixLayout()
+
+    this.pane.w = Math.min(60, this.contentW)
+    this.pane.h = 5
+    this.pane.centerInParent()
+
+    this.label.centerInParent()
+    this.label.y = 0
+
+    this.form.w = this.pane.contentW
+    this.form.h = 2
+    this.form.y = 1
+
+    this.input.w = this.form.contentW
+
+    this.button.centerInParent()
+    this.button.y = 1
+  }
+
+  focused() {
+    this.root.select(this.form)
+  }
+}
+
+class AlertDialog extends Dialog {
+  constructor() {
+    super()
+
+    this.label = new Label()
+    this.pane.addChild(this.label)
+
+    this.button = new Button('Close')
+    this.button.on('pressed', () => {
+      if (this.canClose) {
+        this.emit('cancelled')
+      }
+    })
+    this.pane.addChild(this.button)
+  }
+
+  focused() {
+    this.root.select(this.button)
+  }
+
+  showMessage(message, canClose = true) {
+    this.canClose = canClose
+    this.label.text = message
+    this.button.text = canClose ? 'Close' : '(Hold on...)'
+    this.open()
+  }
+
+  fixLayout() {
+    super.fixLayout()
+
+    this.pane.w = Math.min(this.label.w + 4, this.contentW)
+    this.pane.h = 4
+    this.pane.centerInParent()
+
+    this.label.centerInParent()
+    this.label.y = 0
+
+    this.button.fixLayout()
+    this.button.centerInParent()
+    this.button.y = 1
+  }
+
+  keyPressed() {
+    // Don't handle the escape key.
+  }
+}
+
 module.exports.AppElement = AppElement