diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2024-05-16 22:41:33 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2024-05-16 22:41:33 -0300 |
commit | cd91ccbea9e76ee1de1e3cbcfe17d8a2cb6ef9b4 (patch) | |
tree | d98e0b2a100170c7f913da57fb8e010c4b581603 /ui.js | |
parent | 2a270f20cc8e2f2a0754d28d6892bb1bd27e45ce (diff) | |
parent | eb6a997df675fc8176fb15f34450a0c1b8416edc (diff) |
Merge branch 'main' into merge-socket-mtui merge-socket-mtui
So we made this merge commit *after* going through a whole fidanglin' bunch of steps to flat-out rebase socket-mtui... but of course, that was pretty hopeless from the start to get just quite right. After all, we didn't know the exact point of each commit and how to test out the changes, so we couldn't make sure all our rebased commits were working the same way. (This is maybe the single coolest reason to make sure that automated tests *pass with 100% coverage at every commit*, but obviously this project doesn't have that. Alas!) This merge commit follows up a different merge commit we actually made almost exactly one year ago to this day (also: our birthday LOL). We figure the testing done at that point was quite a bit more thorough than we'd do today, and anyway there's little reason to repeat the work we did in that commit. Comparatively, this merge commit is way smaller! It was still fun to go through the whole rebasing process, even if it didn't practically bring us anywhere. You know, assuming the merge commit from last year didn't accidentally destroy any code or todos lol........ *prays* ^____^
Diffstat (limited to 'ui.js')
-rw-r--r-- | ui.js | 220 |
1 files changed, 165 insertions, 55 deletions
diff --git a/ui.js b/ui.js index 6cabb32..cb38990 100644 --- a/ui.js +++ b/ui.js @@ -49,6 +49,7 @@ import { parentSymbol, reverseOrderOfGroups, searchForItem, + selectTracks, shuffleOrderOfGroups, } from './playlist-utils.js' @@ -210,6 +211,8 @@ export default class AppElement extends FocusElement { this.timestampDictionary = new WeakMap() + this.itemMenuPage = 'cursor' + // We add this is a child later (so that it's on top of every element). this.menuLayer = new DisplayElement() this.menuLayer.clickThrough = true @@ -344,6 +347,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)'}, @@ -980,32 +992,95 @@ 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 + } + + // If this is the first addition (starting from empty), switch the + // remembered context menu page so that the next context menu will show + // the marked items automatically. + if (!mark.length) { + this.itemMenuPage = 'mark' + } + + 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) + } + + // If this is the last removal (going to empty), switch the remembered + // context menu page so that the next context menu will show the usual + // controls for the item under the cursor. This isn't exactly necessary + // since various fallbacks will handle this value pointing to a page that + // doesn't exist anymore, but it's nice for consistency. + if (!mark.length) { + this.itemMenuPage = 'cursor' + } + + this.emitMarkChanged() } getMarkStatus(item) { @@ -1466,6 +1541,9 @@ export default class AppElement extends FocusElement { } } + // TODO: Implement this! :P + // const isMarked = false + // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing) ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)} @@ -1519,50 +1597,66 @@ export default class AppElement extends FocusElement { ...((this.config.showPartyControls && !rootGroup.isPartySources) ? [ - {label: 'Share with party', action: () => this.shareWithParty(item)}, - {divider: true} - ] + {label: 'Share with party', action: () => this.shareWithParty(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}, - 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)}, - isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)}, - {divider: true}, - - timestampsItem, - ...(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)} - ]) - ]) + 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}, + 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)}, + isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)}, + {divider: true}, + + timestampsItem, + ...(item === this.markGrouplike + ? [{label: 'Deselect all', action: () => this.unmarkAll()}] + : [ + isGroup(item) && {element: this.selectGrouplikeItemsControl}, + this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item)}, + this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item)}, + ]) + ]) ] } } - const pages = [ - this.markGrouplike.items.length && generatePageForItem(this.markGrouplike), - el.item && generatePageForItem(el.item) + const pageInfo = [ + this.markGrouplike.items.length && { + id: 'mark', + page: generatePageForItem(this.markGrouplike), + }, + + el.item && { + id: 'cursor', + page: generatePageForItem(el.item), + } ].filter(Boolean) - // TODO: Implement this! :P - // const isMarked = false + const pages = pageInfo.map(({ page }) => page) - this.showContextMenu({ + let pageNum = pageInfo.findIndex(({ id }) => id === this.itemMenuPage) + if (pageNum === -1) pageNum = pageInfo.findIndex(({ id }) => id === 'cursor') + if (pageNum === -1) pageNum = 0 + + const menu = this.showContextMenu({ x: el.absLeft, y: el.absTop + 1, - pages + pages, + pageNum, + }) + + menu.on('page changed', pageNum => { + this.itemMenuPage = pageInfo[pageNum].id }) } @@ -4906,6 +5000,7 @@ class ContextMenu extends FocusElement { } this.close(false) this.show({x, y, pages, pageNum}) + this.emit('page changed', pageNum) } } @@ -4917,6 +5012,7 @@ class ContextMenu extends FocusElement { } this.close(false) this.show({x, y, pages, pageNum}) + this.emit('page changed', pageNum) } } @@ -4928,7 +5024,7 @@ class ContextMenu extends FocusElement { if (pages.length === 0) { return } - itemsArg = pages[pageNum] + itemsArg = pages[Math.min(pages.length - 1, pageNum)] } let items = (typeof itemsArg === 'function') ? itemsArg() : itemsArg @@ -5133,16 +5229,30 @@ class ContextMenu extends FocusElement { width += 2 // Space for the pane border height += 2 // Space for the pane border - if (this.form.scrollBarShown) width++ + + // Hilarity time: at THIS point, we can't identify if the form has its + // scroll bar visible, because we haven't constrained the height of the + // form. We're going to apply all the dimensions leading to the form twice, + // adapting the second time to be one wider if the form shows its scroll + // bar the first time. + this.w = width this.h = height - this.fitToParent() this.pane.fillParent() this.form.fillParent() this.form.fixLayout() + // We only need to update our own (and the descendants') width this time, + // because height won't have changed from the earlier measurement. + if (this.form.scrollBarShown) width++ + this.w = width + + this.pane.fillParent() + this.form.fillParent() + this.form.fixLayout() + // 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 |