From 0e0cb91ffc1e7a28a03428528d437e074145a72e Mon Sep 17 00:00:00 2001 From: Florrie Date: Tue, 7 Jul 2020 11:31:29 -0300 Subject: make the mtui menu work again! i forgot to implement menuItems. oops. :P --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 0bd512a..04c65c0 100644 --- a/ui.js +++ b/ui.js @@ -4175,7 +4175,7 @@ class Menubar extends ListScrollForm { button.on('pressed', () => { this.contextMenu = this.showContextMenu({ x: container.absLeft, y: container.absY + 1, - items: menuFn + items: menuFn || menuItems }) this.contextMenu.on('closed', () => { this.contextMenu = null -- cgit 1.3.0-6-gf8a5 From 8baf386abdf611acd855e61578f06cf7ac8f014f Mon Sep 17 00:00:00 2001 From: Florrie Date: Thu, 9 Jul 2020 14:40:25 -0300 Subject: rename handlePlaylistSource & let accept grouplike It's called loadPlaylistOrSource now, and can take a grouplike (which it will process with processSmartPlaylist as usual) instead of a URL to pass to a crawler. This is so that all functionality for loading a playlist can be collected in and accessed through one interface, so that modifications to the way playlists are loaded will be reflected across everywhere that loads a playlist. --- ui.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 04c65c0..41ab4cc 100644 --- a/ui.js +++ b/ui.js @@ -295,8 +295,8 @@ class AppElement extends FocusElement { this.openPlaylistDialog = new OpenPlaylistDialog() this.setupDialog(this.openPlaylistDialog) - this.openPlaylistDialog.on('source selected', source => this.handlePlaylistSource(source)) - this.openPlaylistDialog.on('source selected (new tab)', source => this.handlePlaylistSource(source, true)) + this.openPlaylistDialog.on('source selected', source => this.loadPlaylistOrSource(source)) + this.openPlaylistDialog.on('source selected (new tab)', source => this.loadPlaylistOrSource(source, true)) this.alertDialog = new AlertDialog() this.setupDialog(this.alertDialog) @@ -944,7 +944,7 @@ class AppElement extends FocusElement { openSpecialOrThroughSystem(item) { if (item.url.endsWith('.json')) { - return this.handlePlaylistSource(item.url, true) + return this.loadPlaylistOrSource(item.url, true) /* } else if (item.url.endsWith('.txt')) { if (this.textInfoPane.visible) { @@ -1094,7 +1094,7 @@ class AppElement extends FocusElement { }) } - async handlePlaylistSource(source, newTab = false) { + async loadPlaylistOrSource(sourceOrPlaylist, newTab = false) { if (this.openPlaylistDialog.visible) { this.openPlaylistDialog.close() } @@ -1102,18 +1102,22 @@ class AppElement extends FocusElement { 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 - } + if (typeof sourceOrPlaylist === 'object' && isGroup(sourceOrPlaylist) || sourceOrPlaylist.source) { + grouplike = sourceOrPlaylist + } else { + try { + grouplike = await this.openPlaylist(sourceOrPlaylist) + } catch (error) { + if (error === 'unknown argument') { + this.alertDialog.showMessage('Could not figure out how to load a playlist from: ' + sourceOrPlaylist) + } else if (typeof error === 'string') { + this.alertDialog.showMessage(error) + } else { + throw error + } - return + return + } } this.alertDialog.close() -- cgit 1.3.0-6-gf8a5 From befceed00ce677befe0ae9fa5122390e391207d4 Mon Sep 17 00:00:00 2001 From: Florrie Date: Thu, 9 Jul 2020 16:35:06 -0300 Subject: replace existing empty tab when loading playlist --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 41ab4cc..f333b7a 100644 --- a/ui.js +++ b/ui.js @@ -1124,7 +1124,7 @@ class AppElement extends FocusElement { grouplike = await processSmartPlaylist(grouplike) - if (newTab || !this.tabber.currentElement) { + if (!this.tabber.currentElement || newTab && this.tabber.currentElement.grouplike) { const grouplikeListing = this.newGrouplikeListing() grouplikeListing.loadGrouplike(grouplike) } else { -- cgit 1.3.0-6-gf8a5 From ca096c7a64b5098ac1548d0996e871f63934b7ae Mon Sep 17 00:00:00 2001 From: Florrie Date: Thu, 9 Jul 2020 17:09:02 -0300 Subject: don't show a divider as the first item in a menu --- ui.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index f333b7a..0499ff6 100644 --- a/ui.js +++ b/ui.js @@ -3836,12 +3836,16 @@ class ContextMenu extends FocusElement { let wantDivider = false const addDividerIfWanted = () => { if (wantDivider) { - const element = new HorizontalRule() - this.form.addInput(element) + if (!firstItem) { + const element = new HorizontalRule() + this.form.addInput(element) + } wantDivider = false } } + let firstItem = true + const keyElementMap = {} for (const item of items.filter(Boolean)) { @@ -3854,6 +3858,7 @@ class ContextMenu extends FocusElement { if (item.isDefault) { this.root.select(item.element) } + firstItem = false } else if (item.divider) { wantDivider = true } else { @@ -3875,6 +3880,7 @@ class ContextMenu extends FocusElement { if (item.isDefault) { this.root.select(button) } + firstItem = false } if (item.key) { keyElementMap[item.key] = focusEl -- cgit 1.3.0-6-gf8a5 From 20f8491545c3b598cc6bba893bc79db5cf6bcb1f Mon Sep 17 00:00:00 2001 From: Florrie Date: Thu, 9 Jul 2020 17:09:21 -0300 Subject: don't show Reveal option if track has no parent --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 0499ff6..de0b214 100644 --- a/ui.js +++ b/ui.js @@ -1024,7 +1024,7 @@ class AppElement extends FocusElement { let items; if (listing.grouplike.isTheQueue && isTrack(item)) { items = [ - this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, + item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, {divider: true}, canControlQueue && {label: 'Play later', action: () => this.playLater(item)}, canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)}, -- cgit 1.3.0-6-gf8a5 From 5d58951225dba66402fc113d390ec602043d263e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 15 Sep 2020 15:51:42 -0300 Subject: better hiding track numbering in queue --- ui.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index de0b214..5e6ef6e 100644 --- a/ui.js +++ b/ui.js @@ -60,6 +60,7 @@ const TuiTextEditor = require('tui-text-editor') const { promisify } = require('util') const { spawn } = require('child_process') +const { orderBy } = require('natural-orderby') const fs = require('fs') const open = require('open') const path = require('path') @@ -317,6 +318,7 @@ class AppElement extends FocusElement { {value: 'shuffle-groups', label: 'Shuffle order of groups'}, {value: 'reverse', label: 'Reverse all'}, {value: 'reverse-groups', label: 'Reverse order of groups'}, + {value: 'alphabetic', label: 'Alphabetically'}, {value: 'normal', label: 'In order'} ], this.showContextMenu) @@ -1475,6 +1477,11 @@ class AppElement extends FocusElement { item = {items: flattenGrouplike(item).items.reverse()} } else if (order === 'reverse-groups') { item = reverseOrderOfGroups(item) + } else if (order === 'alphabetic') { + item = { + name: `${oldName} (alphabetic)`, + items: orderBy(flattenGrouplike(item).items, getNameWithoutTrackNumber) + } } } else { // Make it into a grouplike that just contains itself. -- cgit 1.3.0-6-gf8a5 From 7c8cb5f3df821719a4187ca4c0fe3fb455f9529a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 15 Sep 2020 15:55:43 -0300 Subject: name reordered groups for queuing cherry-picked in part from 4b171a6a! --- ui.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 5e6ef6e..ae3cf32 100644 --- a/ui.js +++ b/ui.js @@ -1468,15 +1468,24 @@ class AppElement extends FocusElement { this.SQP.playNext(playingTrack) } + const oldName = item.name if (isGroup(item)) { if (order === 'shuffle') { - item = {items: shuffleArray(flattenGrouplike(item).items)} + item = { + name: `${oldName} (shuffled)`, + items: shuffleArray(flattenGrouplike(item).items) + } } else if (order === 'shuffle-groups') { item = shuffleOrderOfGroups(item) + item.name = `${oldName} (group order shuffled)` } else if (order === 'reverse') { - item = {items: flattenGrouplike(item).items.reverse()} + item = { + name: `${oldName} (reversed)`, + items: flattenGrouplike(item).items.reverse() + } } else if (order === 'reverse-groups') { item = reverseOrderOfGroups(item) + item.name = `${oldName} (group order reversed)` } else if (order === 'alphabetic') { item = { name: `${oldName} (alphabetic)`, @@ -1485,7 +1494,7 @@ class AppElement extends FocusElement { } } else { // Make it into a grouplike that just contains itself. - item = {items: [item]} + item = {name: oldName, items: [item]} } if (where === 'next' || where === 'next-selected' || where === 'end') { -- cgit 1.3.0-6-gf8a5 From 771d789e42a908a8b8e086c5a97bbf180538894e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 15 Sep 2020 16:53:59 -0300 Subject: show child when opening group from path element --- ui.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index ae3cf32..85cefcd 100644 --- a/ui.js +++ b/ui.js @@ -737,7 +737,7 @@ class AppElement extends FocusElement { // Sets up event listeners that are common to ordinary grouplike listings // (made by newGrouplikeListing) as well as the queue grouplike listing. - grouplikeListing.pathElement.on('select', item => this.reveal(item)) + grouplikeListing.pathElement.on('select', (item, child) => this.reveal(item, child)) grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing)) /* grouplikeListing.on('select', item => this.editNotesFile(item, false)) @@ -758,7 +758,7 @@ class AppElement extends FocusElement { return menu } - reveal(item) { + reveal(item, child) { if (!this.tabberPane.visible) { return } @@ -769,6 +769,9 @@ class AppElement extends FocusElement { const parent = item[parentSymbol] if (isGroup(item)) { tabberListing.loadGrouplike(item) + if (child) { + tabberListing.selectAndShow(child) + } } else if (parent) { if (tabberListing.grouplike !== parent) { tabberListing.loadGrouplike(parent) @@ -3080,10 +3083,12 @@ class PathElement extends ListScrollForm { const itemPath = getItemPath(item) const parentPath = itemPath.slice(0, -1) - for (const pathItem of parentPath) { - const isFirst = pathItem === parentPath[0] + for (let i = 0; i < parentPath.length; i++) { + const pathItem = parentPath[i] + const nextItem = itemPath[i + 1] + const isFirst = (i === 0) const element = new PathItemElement(pathItem, isFirst) - element.on('select', () => this.emit('select', pathItem)) + element.on('select', () => this.emit('select', pathItem, nextItem)) element.fixLayout() this.addInput(element) } -- cgit 1.3.0-6-gf8a5 From 90116fa6c67e4c3262d0e98079acdf5cd984f641 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 15 Sep 2020 16:59:33 -0300 Subject: "Before selected [track]" queue option note: this is actually "Before selected song" but the next commit changes much-outdated "song" terminology in the UI to "track" --- ui.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 85cefcd..0f20bc5 100644 --- a/ui.js +++ b/ui.js @@ -306,11 +306,12 @@ class AppElement extends FocusElement { this.addChild(this.menuLayer) this.whereControl = new InlineListPickerElement('Where?', [ - {value: 'next-selected', label: 'After selected song'}, + {value: 'after-selected', label: 'After selected song'}, {value: 'next', label: 'After current song'}, {value: 'end', label: 'At end of queue'}, {value: 'distribute-evenly', label: 'Distributed across queue evenly'}, - {value: 'distribute-randomly', label: 'Distributed across queue randomly'} + {value: 'distribute-randomly', label: 'Distributed across queue randomly'}, + {value: 'before-selected', label: 'Before selected song'} ], this.showContextMenu) this.orderControl = new InlineListPickerElement('Order?', [ @@ -1500,12 +1501,20 @@ class AppElement extends FocusElement { item = {name: oldName, items: [item]} } - if (where === 'next' || where === 'next-selected' || where === 'end') { + if (where === 'next' || where === 'after-selected' || where === 'before-selected' || where === 'end') { let afterItem = null if (where === 'next') { afterItem = playingTrack - } else if (where === 'next-selected') { + } else if (where === 'after-selected') { afterItem = this.queueListingElement.currentItem + } else if (where === 'before-selected') { + const { items } = this.SQP.queueGrouplike + const index = items.indexOf(this.queueListingElement.currentItem) + if (index === 0) { + afterItem = 'FRONT' + } else if (index > 0) { + afterItem = items[index - 1] + } } this.SQP.queue(item, afterItem, { -- cgit 1.3.0-6-gf8a5 From 05d29dff7b14bc1a1ea641bbb03314ec308610df Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 15 Sep 2020 17:01:58 -0300 Subject: rename "song" terminology to "track" We've always used "track" as the proper term, but these managed to slip by over time anyway. Oops! --- ui.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 0f20bc5..642a02a 100644 --- a/ui.js +++ b/ui.js @@ -306,12 +306,12 @@ class AppElement extends FocusElement { this.addChild(this.menuLayer) this.whereControl = new InlineListPickerElement('Where?', [ - {value: 'after-selected', label: 'After selected song'}, - {value: 'next', label: 'After current song'}, + {value: 'after-selected', label: 'After selected track'}, + {value: 'next', label: 'After current track'}, {value: 'end', label: 'At end of queue'}, {value: 'distribute-evenly', label: 'Distributed across queue evenly'}, {value: 'distribute-randomly', label: 'Distributed across queue randomly'}, - {value: 'before-selected', label: 'Before selected song'} + {value: 'before-selected', label: 'Before selected track'} ], this.showContextMenu) this.orderControl = new InlineListPickerElement('Order?', [ -- cgit 1.3.0-6-gf8a5 From 60f33193fb78c8154a1b112d0faf76af34288df8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 16 Sep 2020 12:52:40 -0300 Subject: cancel jump-to when selecting outside of listing --- ui.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 642a02a..5a801de 100644 --- a/ui.js +++ b/ui.js @@ -2070,6 +2070,10 @@ class GrouplikeListingElement extends Form { this.fixLayout() } + unselected() { + this.hideJumpElement(true) + } + get tabberLabel() { if (this.grouplike) { return this.grouplike.name || 'Unnamed group' -- cgit 1.3.0-6-gf8a5 From 82b243df1a54eaf4be83faa2ac6fbe2a33ffe4bf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 16 Sep 2020 13:28:28 -0300 Subject: fix jump-to cancel being very broken --- ui.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 5a801de..4b45bc9 100644 --- a/ui.js +++ b/ui.js @@ -2059,15 +2059,17 @@ class GrouplikeListingElement extends Form { } hideJumpElement(isCancel) { - if (isCancel) { - this.form.curIndex = this.oldFocusedIndex - this.form.scrollSelectedElementIntoView() - } - this.jumpElement.visible = false - if (this.jumpElement.isSelected) { - this.root.select(this) + if (this.jumpElement.visible) { + if (isCancel) { + this.form.curIndex = this.oldFocusedIndex + this.form.scrollSelectedElementIntoView() + } + this.jumpElement.visible = false + if (this.jumpElement.isSelected) { + this.root.select(this) + } + this.fixLayout() } - this.fixLayout() } unselected() { -- cgit 1.3.0-6-gf8a5 From a8b64d31462aeb65c9286140f37f5c71a9965917 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 16 Sep 2020 13:29:40 -0300 Subject: multi-page menu support --- ui.js | 216 +++++++++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 134 insertions(+), 82 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 4b45bc9..d948208 100644 --- a/ui.js +++ b/ui.js @@ -1004,99 +1004,99 @@ class AppElement extends FocusElement { } showMenuForItemElement(el, listing) { - const emitControls = play => () => { - this.handleQueueOptions(item, { - where: this.whereControl.curValue, - order: this.orderControl.curValue, - play: play - }) - } - - let item - if (this.markGrouplike.items.length) { - item = this.markGrouplike - } else { - item = el.item - } - - // TODO: Implement this! :P - const isMarked = false - const { editMode } = this const { canControlQueue, canProcessMetadata } = this.config const anyMarked = editMode && this.markGrouplike.items.length > 0 - const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') - let items; - if (listing.grouplike.isTheQueue && isTrack(item)) { - items = [ - item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, - {divider: true}, - canControlQueue && {label: 'Play later', action: () => this.playLater(item)}, - canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)}, - {divider: true}, - canControlQueue && {label: 'Clear past this track', action: () => this.clearQueuePast(item)}, - canControlQueue && {label: 'Clear up to this track', action: () => this.clearQueueUpTo(item)}, - {divider: true}, - {label: 'Autoscroll', action: () => listing.toggleAutoscroll()}, - {divider: true}, - canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)} - ] - } else { - const numTracks = countTotalTracks(item) - const { string: durationString } = this.backend.getDuration(item) - items = [ - // A label that just shows some brief information about the item. - {label: - `(${item.name ? `"${item.name}"` : 'Unnamed'} - ` + - (isGroup(item) ? ` ${numTracks} track${numTracks === 1 ? '' : 's'}, ` : '') + - durationString + - ')', - keyboardIdentifier: item.name - }, + const generatePageForItem = item => { + const emitControls = play => () => { + this.handleQueueOptions(item, { + where: this.whereControl.curValue, + order: this.orderControl.curValue, + play: play + }) + } - // The actual controls! - {divider: true}, + const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') + if (listing.grouplike.isTheQueue && isTrack(item)) { + return [ + item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, + {divider: true}, + canControlQueue && {label: 'Play later', action: () => this.playLater(item)}, + canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)}, + {divider: true}, + canControlQueue && {label: 'Clear past this track', action: () => this.clearQueuePast(item)}, + canControlQueue && {label: 'Clear up to this track', action: () => this.clearQueueUpTo(item)}, + {divider: true}, + {label: 'Autoscroll', action: () => listing.toggleAutoscroll()}, + {divider: true}, + canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)} + ] + } else { + const numTracks = countTotalTracks(item) + const { string: durationString } = this.backend.getDuration(item) + return [ + // A label that just shows some brief information about the item. + {label: + `(${item.name ? `"${item.name}"` : 'Unnamed'} - ` + + (isGroup(item) ? ` ${numTracks} track${numTracks === 1 ? '' : 's'}, ` : '') + + durationString + + ')', + keyboardIdentifier: item.name, + isPageSwitcher: true + }, - // TODO: Don't emit these on the element (and hence receive them from - // the listing) - instead, handle their behavior directly. We'll want - // to move the "mark"/"paste" (etc) code into separate functions, - // instead of just defining their behavior inside the listing event - // handlers. - /* - editMode && {label: isMarked ? 'Unmark' : 'Mark', action: () => el.emit('mark')}, - anyMarked && {label: 'Paste (above)', action: () => el.emit('paste', {where: 'above'})}, - anyMarked && {label: 'Paste (below)', action: () => el.emit('paste', {where: 'below'})}, - // 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) - {divider: true}, - */ + // The actual controls! + {divider: true}, - canControlQueue && isPlayable(item) && {element: this.whereControl}, - canControlQueue && isGroup(item) && {element: this.orderControl}, - canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)}, - canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)}, - {divider: true}, + // TODO: Don't emit these on the element (and hence receive them from + // the listing) - instead, handle their behavior directly. We'll want + // to move the "mark"/"paste" (etc) code into separate functions, + // instead of just defining their behavior inside the listing event + // handlers. + /* + editMode && {label: isMarked ? 'Unmark' : 'Mark', action: () => el.emit('mark')}, + anyMarked && {label: 'Paste (above)', action: () => el.emit('paste', {where: 'above'})}, + anyMarked && {label: 'Paste (below)', action: () => el.emit('paste', {where: 'below'})}, + // 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) + {divider: true}, + */ - canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))}, - canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))}, - canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))}, - isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)}, - isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)}, - /* - !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)}, - hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)}, - */ - canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, - {divider: true}, + canControlQueue && isPlayable(item) && {element: this.whereControl}, + canControlQueue && isGroup(item) && {element: this.orderControl}, + canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)}, + canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)}, + {divider: true}, - item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} - ] + canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))}, + canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))}, + canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))}, + isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)}, + isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)}, + /* + !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)}, + hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)}, + */ + canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, + {divider: true}, + + item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} + ] + } } + const pages = [ + this.markGrouplike.items.length && generatePageForItem(this.markGrouplike), + el.item && generatePageForItem(el.item) + ].filter(Boolean) + + // TODO: Implement this! :P + const isMarked = false + this.showContextMenu({ x: el.absLeft, y: el.absTop + 1, - items + pages }) } @@ -3842,7 +3842,7 @@ class ContextMenu extends FocusElement { this.submenu = null } - show({x = 0, y = 0, items: itemsArg, focusKey = null}) { + show({x = 0, y = 0, pages = null, items: itemsArg = null, focusKey = null, pageNum = 0}) { this.reload = () => { const els = [this.root.selectedElement, ...this.root.selectedElement.directAncestors] const focusKey = Object.keys(keyElementMap).find(key => els.includes(keyElementMap[key])) @@ -3850,6 +3850,39 @@ class ContextMenu extends FocusElement { this.show({x, y, items: itemsArg, focusKey}) } + this.nextPage = () => { + if (pages.length > 1) { + pageNum++ + if (pageNum === pages.length) { + pageNum = 0 + } + this.close(false) + this.show({x, y, pages, pageNum}) + } + } + + this.previousPage = () => { + if (pages.length > 1) { + pageNum-- + if (pageNum === -1) { + pageNum = pages.length - 1 + } + this.close(false) + this.show({x, y, pages, pageNum}) + } + } + + if (!pages && !itemsArg || pages && itemsArg) { + return + } + + if (pages) { + if (pages.length === 0) { + return + } + itemsArg = pages[pageNum] + } + let items = (typeof itemsArg === 'function') ? itemsArg() : itemsArg items = items.filter(Boolean) @@ -3899,8 +3932,12 @@ class ContextMenu extends FocusElement { wantDivider = true } else { addDividerIfWanted() - const button = new Button(item.label) - button.keyboardIdentifier = item.keyboardIdentifier || item.label + let label = item.label + if (item.isPageSwitcher && pages.length > 1) { + label = `\x1b[2m(${pageNum + 1}/${pages.length}) « \x1b[22m${label}\x1b[2m »\x1b[22m` + } + const button = new Button(label) + button.keyboardIdentifier = item.keyboardIdentifier || label if (item.action) { button.on('pressed', async () => { this.restoreSelection() @@ -3911,6 +3948,12 @@ class ContextMenu extends FocusElement { } }) } + if (item.isPageSwitcher) { + button.on('pressed', async () => { + this.nextPage() + }) + } + button.item = item focusEl = button this.form.addInput(button) if (item.isDefault) { @@ -3961,6 +4004,15 @@ class ContextMenu extends FocusElement { this.form.scrollToBeginning() } else if (input.isScrollToEnd(keyBuf)) { this.form.lastInput() + } else if (input.isLeft(keyBuf) || input.isRight(keyBuf)) { + if (this.form.inputs[this.form.curIndex].item.isPageSwitcher) { + if (input.isLeft(keyBuf)) { + this.previousPage() + } else { + this.nextPage() + } + return false + } } else { return super.keyPressed(keyBuf) } -- cgit 1.3.0-6-gf8a5 From 3233c5ba82b89a3aae68081dd8cf9f8fa4282b60 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 16 Sep 2020 14:32:39 -0300 Subject: rework mark/selection system No more issues with duplicate tracks, and way more power to the user regardless of the interface they use or their experience with the mark/selection system! --- ui.js | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 125 insertions(+), 37 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index d948208..c734a74 100644 --- a/ui.js +++ b/ui.js @@ -204,6 +204,7 @@ class AppElement extends FocusElement { // TODO: Move edit mode stuff to the backend! this.undoManager = new UndoManager() this.markGrouplike = {name: 'Selected Items', items: []} + this.cachedMarkStatuses = new Map() this.editMode = false // We add this is a child later (so that it's on top of every element). @@ -846,8 +847,73 @@ class AppElement extends FocusElement { this.queueListingElement.selectAndShow(item) } - deselectAll() { - this.markGrouplike.items.splice(0) + replaceMark(items) { + this.markGrouplike.items = items.slice(0) // Don't share the array! :) + this.emitMarkChanged() + } + + unmarkAll() { + this.markGrouplike.items = [] + this.emitMarkChanged() + } + + markItem(item) { + if (isGroup(item)) { + for (const child of item.items) { + this.markItem(child) + } + } else { + const { items } = this.markGrouplike + if (!items.includes(item)) { + items.push(item) + this.emitMarkChanged() + } + } + } + + unmarkItem(item) { + if (isGroup(item)) { + for (const child of item.items) { + this.unmarkItem(child) + } + } else { + const { items } = this.markGrouplike + if (items.includes(item)) { + items.splice(items.indexOf(item), 1) + this.emitMarkChanged() + } + } + } + + getMarkStatus(item) { + if (!this.cachedMarkStatuses.get(item)) { + const { items } = this.markGrouplike + let status + if (isGroup(item)) { + const tracks = flattenGrouplike(item).items + if (tracks.every(track => items.includes(track))) { + status = 'marked' + } else if (tracks.some(track => items.includes(track))) { + status = 'partial' + } else { + status = 'unmarked' + } + } else { + if (items.includes(item)) { + status = 'marked' + } else { + status = 'unmarked' + } + } + this.cachedMarkStatuses.set(item, status) + } + return this.cachedMarkStatuses.get(item) + } + + emitMarkChanged() { + this.emit('mark changed') + this.cachedMarkStatuses = new Map() + this.scheduleDrawWithoutPropertyChange() } pauseAll() { @@ -1080,7 +1146,12 @@ class AppElement extends FocusElement { canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, {divider: true}, - item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} + ...(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)} + ]) ] } } @@ -1802,7 +1873,7 @@ class GrouplikeListingElement extends Form { */ } } else if (keyBuf[0] === 1) { // ctrl-A - this.toggleSelectAll() + this.toggleMarkAll() } else { return super.keyPressed(keyBuf) } @@ -1843,14 +1914,34 @@ class GrouplikeListingElement extends Form { this.form.scrollItems = 0 } - toggleSelectAll() { + toggleMarkAll() { const { items } = this.grouplike - if (items.every(item => this.app.markGrouplike.items.includes(item))) { - this.app.markGrouplike.items = [] + const actions = [] + const tracks = flattenGrouplike(this.grouplike).items + if (items.every(item => this.app.getMarkStatus(item) !== 'unmarked')) { + if (this.app.markGrouplike.items.length > tracks.length) { + actions.push({label: 'Remove from selection', action: () => this.app.unmarkItem(this.grouplike)}) + } + actions.push({label: 'Clear selection', action: () => this.app.unmarkAll()}) } else { - this.app.markGrouplike.items = items.slice(0) // Don't share the array! :) + actions.push({label: 'Add to selection', action: () => this.app.markItem(this.grouplike)}) + if (this.app.markGrouplike.items.some(item => !tracks.includes(item))) { + actions.push({label: 'Replace selection', action: () => { + this.app.unmarkAll() + this.app.markItem(this.grouplike) + }}) + } + } + if (actions.length === 1) { + actions[0].action() + } else { + const el = this.form.inputs[this.form.curIndex] + this.app.showContextMenu({ + x: el.absLeft, + y: el.absTop + 1, + items: actions + }) } - this.scheduleDrawWithoutPropertyChange() } /* @@ -2153,13 +2244,13 @@ class GrouplikeListingForm extends ListScrollForm { return } const { item } = input - if (this.app.markGrouplike.items.includes(item)) { - this.selectMode = 'deselect' - } else { + if (this.app.getMarkStatus(item) === 'unmarked') { if (!ctrl) { - this.app.markGrouplike.items = [] + this.app.unmarkAll() } this.selectMode = 'select' + } else { + this.selectMode = 'deselect' } if (ctrl) { this.dragInputs = [item] @@ -2196,27 +2287,22 @@ class GrouplikeListingForm extends ListScrollForm { } dragEnteredRange(item) { - const { items } = this.app.markGrouplike if (this.selectMode === 'select') { - if (!items.includes(item)) { - items.push(item) - } + this.app.markItem(item) } else if (this.selectMode === 'deselect') { - if (items.includes(item)) { - items.splice(items.indexOf(item), 1) - } + this.app.unmarkItem(item) } } dragLeftRange(item) { const { items } = this.app.markGrouplike if (this.selectMode === 'select') { - if (items.includes(item) && !this.oldMarkedItems.includes(item)) { - items.splice(items.indexOf(item), 1) + if (!this.oldMarkedItems.includes(item)) { + this.app.unmarkItem(item) } } else if (this.selectMode === 'deselect') { - if (!items.includes(item) && this.oldMarkedItems.includes(item)) { - items.push(item) + if (this.oldMarkedItems.includes(item)) { + this.app.markItem(item) } } } @@ -2255,11 +2341,13 @@ class GrouplikeListingForm extends ListScrollForm { return } this.keyboardDragDirection = direction - this.oldMarkedItems = this.app.markGrouplike.items.slice() - if (this.app.markGrouplike.items.includes(item)) { - this.selectMode = 'deselect' - } else { + this.oldMarkedItems = (this.inputs + .filter(input => input.item && this.app.getMarkStatus(input.item) !== 'unmarked') + .map(input => input.item)) + if (this.app.getMarkStatus(item) === 'unmarked') { this.selectMode = 'select' + } else { + this.selectMode = 'deselect' } this.dragEnteredRange(item) } @@ -2972,21 +3060,23 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } writeStatus(writable) { + const markStatus = this.app.getMarkStatus(this.item) + if (this.isGroup) { // 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.) - if (this.isMarked) { + if (markStatus === 'marked' || markStatus === 'partial') { writable.write(ansi.setAttributes([ansi.C_BLUE + 10])) } else { writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) } } else if (this.isTrack) { - if (this.isMarked) { + if (markStatus === 'marked') { writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) } } else if (!this.isPlayable) { - if (this.isMarked) { + if (markStatus === 'marked') { writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) } else { writable.write(ansi.setAttributes([ansi.A_DIM])) @@ -3000,8 +3090,10 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { const record = this.app.backend.getRecordFor(this.item) - if (this.isMarked) { - writable.write('>') + if (markStatus === 'marked') { + writable.write('+') + } else if (markStatus === 'partial') { + writable.write('*') } else { writable.write(' ') } @@ -3021,10 +3113,6 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writable.write(' ') } - get isMarked() { - return this.app.markGrouplike.items.includes(this.item) - } - get isGroup() { return isGroup(this.item) } -- cgit 1.3.0-6-gf8a5 From 942c36453fc9317056791acade02067f01c1464d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 16 Feb 2021 20:12:55 -0400 Subject: loop queue --- ui.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index c734a74..5a8e0ad 100644 --- a/ui.js +++ b/ui.js @@ -343,6 +343,7 @@ class AppElement extends FocusElement { {divider: true}, playingTrack && {element: this.playingControl}, {element: this.loopingControl}, + {element: this.loopQueueControl}, {element: this.pauseNextControl}, {element: this.autoDJControl}, {element: this.volumeSlider}, @@ -398,6 +399,12 @@ class AppElement extends FocusElement { getEnabled: () => this.config.canControlPlayback }) + this.loopQueueControl = new ToggleControl('Loop queue when it ends?', { + setValue: val => this.SQP.setLoopQueueAtEnd(val), + getValue: () => this.SQP.loopQueueAtEnd, + getEnabled: () => this.config.canControlPlayback + }) + this.volumeSlider = new SliderElement('Volume', { setValue: val => this.SQP.setVolume(val), getValue: () => this.SQP.player.volume, -- cgit 1.3.0-6-gf8a5 From 9ab44f1d8c0986986ac5d5df102d69aa21040f39 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 25 Feb 2021 11:51:34 -0400 Subject: loop queue "when it ends" is unnecessary lol --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 5a8e0ad..5e9e6f9 100644 --- a/ui.js +++ b/ui.js @@ -399,7 +399,7 @@ class AppElement extends FocusElement { getEnabled: () => this.config.canControlPlayback }) - this.loopQueueControl = new ToggleControl('Loop queue when it ends?', { + this.loopQueueControl = new ToggleControl('Loop queue?', { setValue: val => this.SQP.setLoopQueueAtEnd(val), getValue: () => this.SQP.loopQueueAtEnd, getEnabled: () => this.config.canControlPlayback -- cgit 1.3.0-6-gf8a5 From 1fb903a227094b5995d807f7bfc1c3bb414b37e5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 15 Mar 2021 20:34:32 -0300 Subject: count only alphanumeric symbols in alphabetic sort --- ui.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 5e9e6f9..8945ce0 100644 --- a/ui.js +++ b/ui.js @@ -1571,7 +1571,10 @@ class AppElement extends FocusElement { } else if (order === 'alphabetic') { item = { name: `${oldName} (alphabetic)`, - items: orderBy(flattenGrouplike(item).items, getNameWithoutTrackNumber) + items: orderBy( + flattenGrouplike(item).items, + t => getNameWithoutTrackNumber(t).replace(/[^a-zA-Z0-9]/g, '') + ) } } } else { -- cgit 1.3.0-6-gf8a5 From c542bc1a2acca7c62f9556499945794878789e3e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 15 Mar 2021 20:35:31 -0300 Subject: queue sorting fixes re: selected track not 100% sure what these are for since i wrote this patch ages ago! sorry :3 --- ui.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 8945ce0..20cfd99 100644 --- a/ui.js +++ b/ui.js @@ -1583,14 +1583,15 @@ class AppElement extends FocusElement { } if (where === 'next' || where === 'after-selected' || where === 'before-selected' || where === 'end') { + const selected = this.queueListingElement.currentItem let afterItem = null if (where === 'next') { afterItem = playingTrack } else if (where === 'after-selected') { - afterItem = this.queueListingElement.currentItem + afterItem = selected } else if (where === 'before-selected') { const { items } = this.SQP.queueGrouplike - const index = items.indexOf(this.queueListingElement.currentItem) + const index = items.indexOf(selected) if (index === 0) { afterItem = 'FRONT' } else if (index > 0) { @@ -1599,11 +1600,13 @@ class AppElement extends FocusElement { } this.SQP.queue(item, afterItem, { - movePlayingTrack: order === 'normal' + movePlayingTrack: order === 'normal' || order === 'alphabetic' }) if (isTrack(passedItem)) { this.queueListingElement.selectAndShow(passedItem) + } else { + this.queueListingElement.selectAndShow(selected) } } else if (where.startsWith('distribute-')) { this.SQP.distributeQueue(item, { -- cgit 1.3.0-6-gf8a5 From 45450549c34ecfdcb6082eeb11f18b5a005d3eb4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 15 Mar 2021 20:36:10 -0300 Subject: add (0) key for opening menu (for numpad use) --- ui.js | 1 + 1 file changed, 1 insertion(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 20cfd99..de73a81 100644 --- a/ui.js +++ b/ui.js @@ -131,6 +131,7 @@ const keyBindings = [ ['isTogglePause', '5'], ['isBackspace', '.'], ['isMenu', '+'], + ['isMenu', '0'], ['isSkipBack', '1'], ['isSkipAhead', '3'], // Disabled because this is the jump key! Oops. -- cgit 1.3.0-6-gf8a5