diff options
-rw-r--r-- | players.js | 3 | ||||
-rw-r--r-- | todo.txt | 6 | ||||
-rw-r--r-- | ui.js | 246 |
3 files changed, 239 insertions, 16 deletions
diff --git a/players.js b/players.js index 98fff09..a1a3a13 100644 --- a/players.js +++ b/players.js @@ -62,6 +62,9 @@ module.exports.MPVPlayer = class extends Player { if (this.isLooping) { opts.unshift('--loop') } + if (this.isPaused) { + opts.unshift('--pause') + } opts.unshift('--volume', this.volume) return opts } diff --git a/todo.txt b/todo.txt index fc21476..84b149d 100644 --- a/todo.txt +++ b/todo.txt @@ -241,3 +241,9 @@ TODO: An indicator for the number of tracks in the queue! TODO: "Reveal" option in queue listing context menu. (Done!) + +TODO: A menubar! + (Done!) + +TODO: Make pressing the de-focus menubar key restore the selection even if you + selected the menubar by clicking on it. diff --git a/ui.js b/ui.js index 253a9d9..033e0e3 100644 --- a/ui.js +++ b/ui.js @@ -65,6 +65,7 @@ const keyBindings = [ ['isQueueAtStart', 'Q', {caseless: false}], ['isShuffleQueue', 's'], ['isClearQueue', 'c'], + ['isFocusMenubar', ';'], // Number pad ['isUp', '8'], @@ -139,6 +140,12 @@ class AppElement extends FocusElement { this.loadMetadata() + // We add this is a child later (so that it's on top of every element). + this.menu = new ContextMenu() + + this.menubar = new Menubar(this.menu) + this.addChild(this.menubar) + this.paneLeft = new Pane() this.addChild(this.paneLeft) @@ -193,14 +200,7 @@ class AppElement extends FocusElement { this.alertDialog = new AlertDialog() this.setupDialog(this.alertDialog) - /* Ignore this comment mostly :) (Because menu isn't a child of pane, - so we can append it to the app right away. Helps w/ handling ^C and - stuff too.) - // If the program were embedded, this.menu should probably be set to the - // global menu object for that app (and everything should work fine). - // As is, remember to append app.menu to root. - */ - this.menu = new ContextMenu() + // Should be placed on top of everything else! this.addChild(this.menu) this.whereControl = new InlineListPickerElement('Where?', [ @@ -215,6 +215,53 @@ class AppElement extends FocusElement { {value: 'shuffle-groups', label: 'Shuffle order of groups'}, {value: 'normal', label: 'In order'} ]) + + this.menubar.buildItems([ + {text: 'mtui', menuItems: [ + {label: 'mtui (perpetual development)'}, + {divider: true}, + {label: 'Quit', action: () => this.shutdown()}, + {label: 'Suspend', action: () => this.suspend()} + ]}, + {text: 'Playback', menuFn: () => { + const { items } = this.queueGrouplike + const curIndex = items.indexOf(this.playingTrack) + const next = (curIndex >= 0) && items[curIndex + 1] + const previous = (curIndex >= 0) && items[curIndex - 1] + + return [ + {label: this.playingTrack ? `("${this.playingTrack.name}")` : '(No track playing.)'}, + {divider: true}, + {element: this.playingControl}, + {element: this.loopingControl}, + (next || previous) && {divider: true}, + previous && {label: `Previous (${previous.name})`, action: () => this.playPreviousTrack(this.playingTrack)}, + next && {label: `Next (${next.name})`, action: () => this.playNextTrack(this.playingTrack)}, + next && {label: '(...Play Later)', action: () => this.playLater(next)} + ] + }}, + {text: 'Queue', menuFn: () => { + const { items } = this.queueGrouplike + const curIndex = items.indexOf(this.playingTrack) + + return [ + {label: `(Queue - ${curIndex >= 0 ? `${curIndex + 1}/` : ''}${items.length} items.)`}, + items.length && {divider: true}, + items.length && {label: 'Shuffle', action: () => this.shuffleQueue()}, + items.length && {label: 'Clear', action: () => this.clearQueue()} + ] + }} + ]) + + this.playingControl = new ToggleControl('Pause?', { + setValue: val => this.setPause(val), + getValue: () => this.player.isPaused + }) + + this.loopingControl = new ToggleControl('Loop current track?', { + setValue: val => this.setLoop(val), + getValue: () => this.player.isLooping + }) } selected() { @@ -336,6 +383,13 @@ class AppElement extends FocusElement { } } + playLater(item) { + this.handleQueueOptions(item, { + where: 'distribute-randomly', + skip: true + }) + } + showMenuForItemElement(el, listing) { const emitControls = play => () => { this.handleQueueOptions(item, { @@ -354,10 +408,7 @@ class AppElement extends FocusElement { items = [ {label: 'Reveal', action: () => this.reveal(item)}, {divider: true}, - {label: 'Play later', action: () => this.handleQueueOptions(item, { - where: 'distribute-randomly', - skip: true - })}, + {label: 'Play later', action: () => this.playLater(item)}, {label: 'Remove from queue', action: () => this.unqueueGrouplikeItem(item)} ] } else { @@ -485,15 +536,23 @@ class AppElement extends FocusElement { this.emit('quitRequested') } + suspend() { + this.emit('suspendRequested') + } + fixLayout() { this.w = this.parent.contentW this.h = this.parent.contentH + this.menubar.fixLayout() + this.paneLeft.w = Math.max(Math.floor(0.8 * this.contentW), this.contentW - 80) - this.paneLeft.h = this.contentH - 5 + this.paneLeft.h = this.contentH - 6 + this.paneLeft.y = 1 this.paneRight.x = this.paneLeft.right this.paneRight.w = this.contentW - this.paneLeft.right this.paneRight.h = this.paneLeft.h + this.paneRight.y = 1 this.playbackPane.y = this.paneLeft.bottom this.playbackPane.w = this.contentW this.playbackPane.h = this.contentH - this.playbackPane.y @@ -520,11 +579,15 @@ class AppElement extends FocusElement { this.shutdown() return } else if (keyBuf[0] === 0x1a) { // Ctrl-Z - this.emit('suspendRequested') + this.suspend() return } - if (input.isRight(keyBuf)) { + if ((telc.isEscape(keyBuf) || telc.isBackspace(keyBuf)) && this.menubar.isSelected) { + this.menubar.restoreSelection() + } else if ((telc.isLeft(keyBuf) || telc.isRight(keyBuf)) && this.menubar.isSelected) { + return // le sigh + } else if (input.isRight(keyBuf)) { this.seekAhead(10) } else if (input.isLeft(keyBuf)) { this.seekBack(10) @@ -546,6 +609,12 @@ class AppElement extends FocusElement { this.root.select(this.tabber) } else if (input.isFocusQueue(keyBuf) && this.queueListingElement.selectable) { this.root.select(this.queueListingElement) + } else if (input.isFocusMenubar(keyBuf)) { + if (this.menubar.isSelected) { + this.menubar.restoreSelection() + } else { + this.menubar.select() + } } else if (keyBuf.equals(Buffer.from([5]))) { // Ctrl-E this.editMode = !this.editMode } else if (this.editMode && keyBuf.equals(Buffer.from([14]))) { // ctrl-N @@ -630,10 +699,18 @@ class AppElement extends FocusElement { this.player.togglePause() } + setPause(value) { + this.player.setPause(value) + } + toggleLoop() { this.player.toggleLoop() } + setLoop(value) { + this.player.setLoop(value) + } + volUp(amount = 10) { this.player.volUp(amount) } @@ -1724,6 +1801,63 @@ class InlineListPickerElement extends FocusElement { } } +class ToggleControl extends FocusElement { + constructor(labelText, {setValue, getValue}) { + super() + this.labelText = labelText + this.setValue = setValue + this.getValue = getValue + this.keyboardIdentifier = this.labelText + } + + keyPressed(keyBuf) { + if (input.isSelect(keyBuf)) { + this.toggle() + } + } + + clicked(button) { + if (button === 'left') { + if (this.isSelected) { + this.toggle() + } else { + this.root.select(this) + } + } else if (button === 'scroll-up' || button === 'scroll-down') { + this.toggle() + } else { + return true + } + return false + } + + + toggle() { + this.setValue(!this.getValue()) + } + + fixLayout() { + // Same general principle as ToggleControl - fill the parent, but always + // fit ourselves! + this.w = Math.max(this.parent.contentW, this.labelText.length + 5) + this.h = 1 + } + + drawTo(writable) { + if (this.isSelected) { + writable.write(ansi.invert()) + } + + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + + writable.write(this.getValue() ? '[X] ' : '[.] ') + writable.write(this.labelText) + writable.write(' '.repeat(this.w - (this.labelText.length + 4))) + + writable.write(ansi.resetAttributes()) + } +} + class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { constructor(item, app) { super(item.name) @@ -2504,7 +2638,7 @@ class ContextMenu extends FocusElement { this.pane.fillParent() this.form.fillParent() - // After everything else, do a second pass to apply the decided width + // After everything else, do a second pass to apply the decided width // to every element, so that they expand to all be the same width. // In order to change the width of a button (which is what these elements // are), we need to append space characters. @@ -2624,4 +2758,84 @@ class KeyboardSelector { } } +class Menubar extends ListScrollForm { + constructor(contextMenu) { + super('horizontal') + + this.contextMenu = contextMenu + } + + select() { + // The context menu disappears when it's deselected, so we really want to + // use whatever element was selected before the menu was opened. + if (this.contextMenu.isSelected) { + // ...Unless it was the menubar that was already selected. + if (!this.contextMenu.selectedBefore.directAncestors.includes(this)) { + this.selectedBefore = this.contextMenu.selectedBefore + } + } else { + this.selectedBefore = this.root.selectedElement + } + + this.root.select(this) + } + + selected() { + super.selected() + } + + keyPressed(keyBuf) { + super.keyPressed(keyBuf) + + // Don't pause the music from the menubar! + if (telc.isSpace(keyBuf)) { + return false + } + } + + restoreSelection() { + if (this.selectedBefore) { + this.root.select(this.selectedBefore) + this.selectedBefore = null + } + } + + buildItems(array) { + for (const {text, menuItems, menuFn} of array) { + const button = new Button(` ${text} `) + + const container = new FocusElement() + container.addChild(button) + button.x = 1 + container.w = button.w + 2 + container.h = 1 + container.selected = () => this.root.select(button) + + button.on('pressed', () => { + this.contextMenu.show({ + x: container.absLeft, y: container.absY + 1, + items: menuFn ? menuFn() : menuItems + }) + }) + + this.addInput(container) + } + } + + fixLayout() { + this.x = 0 + this.y = 0 + this.w = this.parent.contentW + this.h = 1 + super.fixLayout() + } + + drawTo(writable) { + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_DIM, ansi.A_INVERT, ansi.C_WHITE + 10])) + writable.write(' '.repeat(this.w)) + writable.write(ansi.resetAttributes()) + } +} + module.exports.AppElement = AppElement |