diff options
-rw-r--r-- | todo.txt | 10 | ||||
-rw-r--r-- | ui.js | 151 |
2 files changed, 99 insertions, 62 deletions
diff --git a/todo.txt b/todo.txt index 916ec66..dcd847f 100644 --- a/todo.txt +++ b/todo.txt @@ -136,4 +136,12 @@ TODO: Entering more than one key "at once" into a text input element will only TODO: Pressing space while an "Up (to <group>)" button is selected both activates the button and pauses music (because the app detects that the space key is pressed). This is definitely wrong (it should do one or the - other - I'm not too sure which, yet, but probably the latter). + other - I'm not too sure which, yet, but probably the latter). (Done!) + +TODO: If a track's file is a symlink, the "From:" label should show where it + links to. + +TODO: The "Up (to <group>)" and "(This group has no items)" elements are not + quite buttons nor grouplike items. They should be more consistent with + actual grouplike items (i.e. same behavior and appearance), and should + be less buggy. (Done!) diff --git a/ui.js b/ui.js index 04d5e3c..5ddba13 100644 --- a/ui.js +++ b/ui.js @@ -817,28 +817,24 @@ class GrouplikeListingElement extends Form { const parent = this.grouplike[parentSymbol] if (parent) { - const upButton = new Button('Up (to ' + (parent.name || 'unnamed group') + ')') + const upButton = new BasicGrouplikeItemElement(`Up (to ${parent.name || 'unnamed group'})`) upButton.on('pressed', () => this.loadParentGrouplike()) form.addInput(upButton) } - let itemElements = [] if (this.grouplike.items.length) { - itemElements = this.grouplike.items.map(item => new GrouplikeItemElement(item, this.recordStore)) - } else if (!this.grouplike.isTheQueue) { - const fakeItem = { - fake: true, - name: '(This group is empty)', - [parentSymbol]: this.grouplike - } - itemElements = [new GrouplikeItemElement(fakeItem, this.recordStore)] - } + const itemElements = this.grouplike.items.map(item => { + return new InteractiveGrouplikeItemElement(item, this.recordStore) + }) - for (const itemElement of itemElements) { - for (const evtName of ['download', 'remove', 'mark', 'paste', 'browse', 'queue', 'unqueue', 'menu']) { - itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data)) + for (const itemElement of itemElements) { + for (const evtName of ['download', 'remove', 'mark', 'paste', 'browse', 'queue', 'unqueue', 'menu']) { + itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data)) + } + form.addInput(itemElement) } - form.addInput(itemElement) + } else if (!this.grouplike.isTheQueue) { + form.addInput(new BasicGrouplikeItemElement('(This group is empty)')) } if (wasSelected) { @@ -956,7 +952,7 @@ class GrouplikeListingForm extends ListScrollForm { } get firstItemIndex() { - return Math.max(0, this.inputs.findIndex(el => el instanceof GrouplikeItemElement)) + return Math.max(0, this.inputs.findIndex(el => el instanceof InteractiveGrouplikeItemElement)) } selectAndShow(item) { @@ -971,12 +967,11 @@ class GrouplikeListingForm extends ListScrollForm { } } -class GrouplikeItemElement extends Button { - constructor(item, recordStore) { +class BasicGrouplikeItemElement extends Button { + constructor(text) { super() - this.item = item - this.recordStore = recordStore + this.text = text } fixLayout() { @@ -1000,60 +995,48 @@ class GrouplikeItemElement extends Button { writable.write(ansi.moveCursor(this.absTop, this.absLeft)) - if (isGroup(this.item)) { - writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) - } - this.drawX = this.x this.writeStatus(writable) - writable.write(this.item.name.slice(0, this.w - this.drawX)) - this.drawX += this.item.name.length + writable.write(this.text.slice(0, this.w - this.drawX)) + this.drawX += this.text.length writable.write(' '.repeat(Math.max(0, this.w - this.drawX))) writable.write(ansi.resetAttributes()) } writeStatus(writable) { - this.drawX += 3 - - const braille = '⠈⠐⠠⠄⠂⠁' - const brailleChar = braille[Math.floor(Date.now() / 250) % 6] - - const record = this.recordStore.getRecord(this.item) - - if (this.isMarked) { - writable.write('M') - } else { - writable.write(' ') - } - - if (isGroup(this.item)) { - writable.write('G') - } else if (record.downloading) { - writable.write(braille[Math.floor(Date.now() / 250) % 6]) - } else if (record.playing) { - writable.write('\u25B6') - } else { - writable.write(' ') - } - - writable.write(' ') - } - - get isMarked() { - return this.recordStore.app.editMode && this.recordStore.app.markGrouplike.items.includes(this.item) + // Add a couple spaces anyway. This is less than the padding of the tatus + // text of elements which represent real playlist items; that's to + // distinguish "fake" rows from actual playlist items. + writable.write(' ') + this.drawX += 2 } - get isReal() { - return !this.item.fake + keyPressed(keyBuf) { + // This function is overridden by InteractiveGrouplikeItemElement, but + // it's still specified here that only enter counts as an action key. + // By default for buttons, the space key also works, but since in this app + // space is generally bound to mean "pause" instead of "select", we don't + // check if space is pressed here. + if (telc.isEnter(keyBuf)) { + this.emit('pressed') + if (isGroup(this.item)) { + this.emit('browse') + } + } } +} - get isGroup() { - return isGroup(this.item) && this.isReal +class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { + constructor(item, recordStore) { + super(item.name) + this.item = item + this.recordStore = recordStore } - get isTrack() { - return isTrack(this.item) && this.isReal + drawTo(writable) { + this.text = this.item.name + super.drawTo(writable) } keyPressed(keyBuf) { @@ -1081,7 +1064,7 @@ class GrouplikeItemElement extends Button { editMode && this.isReal && {label: this.isMarked ? 'Unmark' : 'Mark', action: () => this.emit('mark')}, anyMarked && this.isReal && {label: 'Paste (above)', action: () => this.emit('paste', {where: 'above'})}, anyMarked && this.isReal && {label: 'Paste (below)', action: () => this.emit('paste', {where: 'below'})}, - anyMarked && !this.isReal && {label: 'Paste', action: () => this.emit('paste')}, // No "above" or "elow" in the label because the fake item will be replaced (it'll disappear, since there'll be an item in the group) + anyMarked && !this.isReal && {label: 'Paste', action: () => this.emit('paste')}, // No "above" or "elow" in the label because the "fake" item/row will be replaced (it'll disappear, since there'll be an item in the group) this.isReal && {label: 'Play', action: () => this.emit('queue', {where: 'next', play: true})}, this.isReal && {label: 'Play next', action: () => this.emit('queue', {where: 'next'})}, this.isReal && {label: 'Play at end', action: () => this.emit('queue', {where: 'end'})}, @@ -1092,6 +1075,52 @@ class GrouplikeItemElement extends Button { }) } } + + writeStatus(writable) { + if (isGroup(this.item)) { + // The ANSI attributes here will apply to the rest of the line, too. + // (We don't reset the active attributes until after drawing the rest of + // the line.) + writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) + } + + this.drawX += 3 + + const braille = '⠈⠐⠠⠄⠂⠁' + const brailleChar = braille[Math.floor(Date.now() / 250) % 6] + + const record = this.recordStore.getRecord(this.item) + + if (this.isMarked) { + writable.write('M') + } else { + writable.write(' ') + } + + if (isGroup(this.item)) { + writable.write('G') + } else if (record.downloading) { + writable.write(braille[Math.floor(Date.now() / 250) % 6]) + } else if (record.playing) { + writable.write('\u25B6') + } else { + writable.write(' ') + } + + writable.write(' ') + } + + get isMarked() { + return this.recordStore.app.editMode && this.recordStore.app.markGrouplike.items.includes(this.item) + } + + get isGroup() { + return isGroup(this.item) + } + + get isTrack() { + return isTrack(this.item) + } } class ListingJumpElement extends Form { |