From 382d5afc7e2ac24f67b7c891328b8b9bb7e91058 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 13 Jul 2021 23:14:20 -0300 Subject: timestamp files!!! --- ui.js | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 3 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index a167dab..75dfe41 100644 --- a/ui.js +++ b/ui.js @@ -8,6 +8,7 @@ const UndoManager = require('./undo-manager') const { commandExists, + getSecFromTimestamp, getTimeStringsFromSec, promisifyProcess, shuffleArray @@ -208,6 +209,8 @@ class AppElement extends FocusElement { this.cachedMarkStatuses = new Map() this.editMode = false + this.timestampDictionary = new WeakMap() + // We add this is a child later (so that it's on top of every element). this.menuLayer = new DisplayElement() this.menuLayer.clickThrough = true @@ -658,7 +661,7 @@ class AppElement extends FocusElement { this.tabber.addTab(grouplikeListing) this.tabber.selectTab(grouplikeListing) - grouplikeListing.on('browse', item => grouplikeListing.loadGrouplike(item)) + grouplikeListing.on('browse', item => this.browse(grouplikeListing, item)) grouplikeListing.on('download', item => this.SQP.download(item)) grouplikeListing.on('open', item => this.openSpecialOrThroughSystem(item)) grouplikeListing.on('queue', (item, opts) => this.handleQueueOptions(item, opts)) @@ -666,7 +669,7 @@ class AppElement extends FocusElement { const updateListingsFor = item => { for (const grouplikeListing of this.tabber.tabberElements) { if (grouplikeListing.grouplike === item) { - grouplikeListing.loadGrouplike(item, false) + this.browse(grouplikeListing, item, false) } } } @@ -754,6 +757,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.on('timestamp', (item, time) => this.playOrSeek(item, time)) grouplikeListing.pathElement.on('select', (item, child) => this.reveal(item, child)) grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing)) /* @@ -775,6 +779,11 @@ class AppElement extends FocusElement { return menu } + browse(grouplikeListing, grouplike, ...args) { + this.loadTimestampDataInGrouplike(grouplike) + grouplikeListing.loadGrouplike(grouplike, ...args) + } + reveal(item, child) { if (!this.tabberPane.visible) { return @@ -805,6 +814,14 @@ class AppElement extends FocusElement { this.SQP.play(item) } + playOrSeek(item, time) { + if (!this.config.canControlQueue || !this.config.canControlPlayback) { + return + } + + this.SQP.playOrSeek(item, time) + } + unqueue(item) { if (!this.config.canControlQueue) { return @@ -1029,6 +1046,106 @@ class AppElement extends FocusElement { } */ + expandTimestamps(item, listing) { + listing.expandTimestamps(item) + } + + collapseTimestamps(item, listing) { + listing.collapseTimestamps(item) + } + + toggleTimestamps(item, listing) { + listing.toggleTimestamps(item) + } + + timestampsExpanded(item, listing) { + return listing.timestampsExpanded(item) + } + + hasTimestampsFile(item) { + return !!this.getTimestampsFile(item) + } + + getTimestampsFile(item) { + // Only tracks have timestamp files! + if (!isTrack(item)) { + return false + } + + return getCorrespondingFileForItem(item, '.timestamps.txt') + } + + async loadTimestampDataInGrouplike(grouplike) { + // Only load data for a grouplike once. + if (this.timestampDictionary.has(grouplike)) { + return + } + + this.timestampDictionary.set(grouplike, true) + + // There's no parallelization here, but like, whateeeever. + for (const item of grouplike.items) { + if (this.timestampDictionary.has(item)) { + continue + } + + if (!this.hasTimestampsFile(item)) { + this.timestampDictionary.set(item, false) + continue + } + + this.timestampDictionary.set(item, null) + const data = await this.readTimestampData(item) + this.timestampDictionary.set(item, data) + } + } + + getTimestampData(item) { + return this.timestampDictionary.get(item) || null + } + + async readTimestampData(item) { + const file = this.getTimestampsFile(item) + + if (!file) { + return null + } + + let filePath + try { + filePath = url.fileURLToPath(new URL(file.url)) + } catch (error) { + return null + } + + let contents + try { + contents = (await readFile(filePath)).toString() + } catch (error) { + return null + } + + if (contents.startsWith('{')) { + try { + return JSON.parse(contents) + } catch (error) { + return null + } + } + + const lines = contents.split('\n') + .filter(line => !line.startsWith('#')) + .filter(line => line) + + const data = lines + .map(line => line.match(/^\s*([0-9:]+)\s*(\S.*)\s*$/)) + .filter(match => match) + .map(match => ({timestamp: getSecFromTimestamp(match[1]), comment: match[2]})) + .filter(({ timestamp: sec }) => !isNaN(sec)) + + return data + } + openSpecialOrThroughSystem(item) { if (item.url.endsWith('.json')) { return this.loadPlaylistOrSource(item.url, true) @@ -1099,9 +1216,15 @@ class AppElement extends FocusElement { } const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') + const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing) + ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)} + : {label: 'Expand saved timestamps', action: () => this.expandTimestamps(item, listing)} + ) + if (listing.grouplike.isTheQueue && isTrack(item)) { return [ item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, + timestampsItem, {divider: true}, canControlQueue && {label: 'Play later', action: () => this.playLater(item)}, canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)}, @@ -1161,6 +1284,7 @@ class AppElement extends FocusElement { canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, {divider: true}, + timestampsItem, ...(item === this.markGrouplike ? [{label: 'Deselect all', action: () => this.unmarkAll()}] : [ @@ -1832,6 +1956,7 @@ class GrouplikeListingElement extends Form { this.grouplikeData = new WeakMap() this.autoscrollOffset = null + this.expandedTimestamps = [] } getNewForm() { @@ -1920,7 +2045,8 @@ class GrouplikeListingElement extends Form { if (isGroup(this.grouplike)) { this.grouplikeData.set(this.grouplike, { scrollItems: this.form.scrollItems, - currentItem: this.currentItem + currentItem: this.currentItem, + expandedTimestamps: this.expandedTimestamps }) } } @@ -1931,6 +2057,8 @@ class GrouplikeListingElement extends Form { this.form.scrollItems = data.scrollItems this.form.selectAndShow(data.currentItem) this.form.fixLayout() + this.expandedTimestamps = data.expandedTimestamps + this.buildTimestampItems() } } @@ -2006,6 +2134,109 @@ class GrouplikeListingElement extends Form { } } + expandTimestamps(item) { + if (this.grouplike && this.grouplike.items.includes(item)) { + this.expandedTimestamps.push(item) + this.buildTimestampItems() + } + } + + collapseTimestamps(item) { + const ET = this.expandedTimestamps // :alien: + if (ET.includes(item)) { + ET.splice(ET.indexOf(item), 1) + this.buildTimestampItems() + } + } + + toggleTimestamps(item) { + if (this.timestampsExpanded(item)) { + this.collapseTimestamps(item) + } else { + this.expandTimestamps(item) + } + } + + timestampsExpanded(item) { + this.updateTimestamps() + return this.expandedTimestamps.includes(item) + } + + updateTimestamps() { + const ET = this.expandedTimestamps + if (ET) { + this.expandedTimestamps = ET.filter(item => this.grouplike.items.includes(item)) + } + } + + buildTimestampItems(item) { + const form = this.form + + // We're going to restore this selection later. It's kinda hacky and won't + // work if the selected input was itself a timestamp item, but that + // [extremely RFC voice] hopefully won't happen! + const selectedInput = this.form.inputs[this.form.curIndex] + + // Clear up any existing timestamp items, since we're about to generate new + // ones! + form.children = form.children.filter(child => !(child instanceof TimestampGrouplikeItemElement)) + form.inputs = form.inputs.filter(child => !(child instanceof TimestampGrouplikeItemElement)) + + this.updateTimestamps() + + if (!this.expandedTimestamps) { + // Well that's going to have obvious consequences. + return + } + + for (const item of this.expandedTimestamps) { + // Find the main item element. The items we're about to generate will be + // inserted after it. + const mainElementIndex = form.inputs.findIndex(el => ( + el instanceof InteractiveGrouplikeItemElement && + el.item === item + )) + + const timestampData = this.app.getTimestampData(item) + + // Oh no. + // TODO: This should probably error report lol. + if (!timestampData) { + continue + } + + // Generate some items! Just go over the data list and generate one for + // each timestamp. + const tsElements = timestampData.map(ts => { + const el = new TimestampGrouplikeItemElement(item, ts.timestamp, ts.comment, this.app) + el.on('pressed', () => this.emit('timestamp', item, ts.timestamp)) + return el + }) + + // Stick 'em in. Form doesn't implement an "insert input" function because + // why would life be easy, so we'll mangle the inputs array ourselves. + + form.inputs.splice(mainElementIndex + 1, 0, ...tsElements) + + let previousIndex = mainElementIndex + for (const el of tsElements) { + // We do addChild rather than a simple splice because addChild does more + // stuff than just sticking it in the array (e.g. setting the child's + // .parent property). What if addInput gets updated to do more stuff in + // a similar fashion? Well, then we're scr*wed! :) + form.addChild(el, previousIndex + 1) + previousIndex++ + } + } + + const index = form.inputs.indexOf(selectedInput) + if (index >= 0) { + form.selectInput(form.inputs.indexOf(selectedInput)) + } + + this.fixAllLayout() + } + buildItems(resetIndex = false) { if (!this.grouplike) { throw new Error('Attempted to call buildItems before a grouplike was loaded') @@ -2077,6 +2308,7 @@ class GrouplikeListingElement extends Form { // already filled by a previous this.curIndex set). form.curIndex = form.curIndex + this.buildTimestampItems() this.fixAllLayout() } @@ -2096,6 +2328,8 @@ class GrouplikeListingElement extends Form { itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data)) } + itemElement.on('toggle-timestamps', () => this.toggleTimestamps(itemElement.item)) + /* itemElement.on('unselected labels', () => { if (!this.expandLabels) { @@ -3019,6 +3253,8 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } else if (telc.isEnter(keyBuf)) { if (isGroup(this.item)) { this.emit('browse') + } else if (this.app.hasTimestampsFile(this.item)) { + this.emit('toggle-timestamps') } else if (isTrack(this.item)) { this.emit('queue', {where: 'next', play: true}) } else if (!this.isPlayable) { @@ -3130,6 +3366,8 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writable.write('G') } else if (!this.isPlayable) { writable.write('F') + } else if (this.app.hasTimestampsFile(this.item)) { + writable.write(':') } else if (record.downloading) { writable.write(braille[Math.floor(Date.now() / 250) % 6]) } else if (this.app.SQP.playingTrack === this.item) { @@ -3154,6 +3392,48 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } } +class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { + constructor(item, timestamp, comment, app) { + super('') + + this.app = app + this.timestamp = timestamp + this.comment = comment + this.item = item + } + + drawTo(writable) { + const metadata = this.app.backend.getMetadataFor(this.item) + const duration = (metadata && metadata.duration) || 0 + const strings = getTimeStringsFromSec(this.timestamp, duration) + + this.text = ( + (duration + ? `(${strings.timeDone} - ${strings.percentDone})` + : `(${strings.timeDone})`) + + (this.comment + ? ` ${this.comment}` + : '') + ) + + super.drawTo(writable) + } + + writeStatus(writable) { + writable.write(ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN])) + writable.write(' ') + writable.write(ansi.setAttributes([ansi.C_RESET])) + writable.write(':') + writable.write(ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN])) + writable.write(' ') + this.drawX += 4 + } + + getLeftPadding() { + return 4 + } +} + class ListingJumpElement extends Form { constructor() { super() -- cgit 1.3.0-6-gf8a5 From 7af31d6ccb2d1b0c47c0bbb60a7e51c64bb01bf1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 17 Jul 2021 20:09:09 -0300 Subject: past 3 second threshold, (P) to seek to start --- ui.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 75dfe41..3601071 100644 --- a/ui.js +++ b/ui.js @@ -199,6 +199,7 @@ class AppElement extends FocusElement { canProcessMetadata: true, canSuspend: true, menubarColor: 4, // blue + seekToStartThreshold: 3, showTabberPane: true, stopPlayingUponQuit: true }, config) @@ -1190,15 +1191,39 @@ class AppElement extends FocusElement { } } + skipBackOrSeekToStart() { + // Perform the same action - skipping to the previous track or seeking to + // the start of the current track - for all target queue players. If any is + // past an arbitrary time position (default 3 seconds), seek to start; if + // all are before this position, skip to previous. + + let maxCurSec = 0 + this.forEachQueuePlayerToActOn(({ timeData }) => { + if (timeData) { + maxCurSec = Math.max(maxCurSec, timeData.curSecTotal) + } + }) + + if (Math.floor(maxCurSec) < this.config.seekToStartThreshold) { + this.actOnQueuePlayers(qp => qp.playPrevious(qp.playingTrack, true)) + } else { + this.actOnQueuePlayers(qp => qp.seekToStart()) + } + } + actOnQueuePlayers(fn) { - const actOn = this.queuePlayersToActOn.length ? this.queuePlayersToActOn : [this.SQP] - for (const queuePlayer of actOn) { + this.forEachQueuePlayerToActOn(queuePlayer => { fn(queuePlayer) const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateProgress() } - } + }) + } + + forEachQueuePlayerToActOn(fn) { + const actOn = this.queuePlayersToActOn.length ? this.queuePlayersToActOn : [this.SQP] + actOn.forEach(fn) } showMenuForItemElement(el, listing) { @@ -1564,7 +1589,7 @@ class AppElement extends FocusElement { } else if (input.isStop(keyBuf)) { this.actOnQueuePlayers(qp => qp.stopPlaying()) } else if (input.isSkipBack(keyBuf)) { - this.actOnQueuePlayers(qp => qp.playPrevious(qp.playingTrack, true)) + this.skipBackOrSeekToStart() } else if (input.isSkipAhead(keyBuf)) { this.actOnQueuePlayers(qp => qp.playNext(qp.playingTrack, true)) } -- cgit 1.3.0-6-gf8a5 From ee9ba81c076b7986e8fe752b3badbc6a8b7aec50 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 17 Jul 2021 20:32:57 -0300 Subject: show playback icon next to current timestamp --- ui.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 11 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 3601071..0383807 100644 --- a/ui.js +++ b/ui.js @@ -1138,11 +1138,18 @@ class AppElement extends FocusElement { .filter(line => !line.startsWith('#')) .filter(line => line) + const metadata = this.backend.getMetadataFor(item) + const duration = (metadata ? metadata.duration : Infinity) + const data = lines .map(line => line.match(/^\s*([0-9:]+)\s*(\S.*)\s*$/)) .filter(match => match) .map(match => ({timestamp: getSecFromTimestamp(match[1]), comment: match[2]})) .filter(({ timestamp: sec }) => !isNaN(sec)) + .map((cur, i, arr) => + (i + 1 === arr.length + ? {...cur, timestampEnd: duration} + : {...cur, timestampEnd: arr[i + 1].timestamp})) return data } @@ -2233,7 +2240,7 @@ class GrouplikeListingElement extends Form { // Generate some items! Just go over the data list and generate one for // each timestamp. const tsElements = timestampData.map(ts => { - const el = new TimestampGrouplikeItemElement(item, ts.timestamp, ts.comment, this.app) + const el = new TimestampGrouplikeItemElement(item, ts.timestamp, ts.timestampEnd, ts.comment, this.app) el.on('pressed', () => this.emit('timestamp', item, ts.timestamp)) return el }) @@ -2259,6 +2266,7 @@ class GrouplikeListingElement extends Form { form.selectInput(form.inputs.indexOf(selectedInput)) } + this.scheduleDrawWithoutPropertyChange() this.fixAllLayout() } @@ -3391,12 +3399,12 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writable.write('G') } else if (!this.isPlayable) { writable.write('F') - } else if (this.app.hasTimestampsFile(this.item)) { - writable.write(':') } else if (record.downloading) { writable.write(braille[Math.floor(Date.now() / 250) % 6]) } else if (this.app.SQP.playingTrack === this.item) { writable.write('\u25B6') + } else if (this.app.hasTimestampsFile(this.item)) { + writable.write(':') } else { writable.write(' ') } @@ -3418,11 +3426,12 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { - constructor(item, timestamp, comment, app) { + constructor(item, timestamp, timestampEnd, comment, app) { super('') this.app = app this.timestamp = timestamp + this.timestampEnd = timestampEnd this.comment = comment this.item = item } @@ -3431,11 +3440,15 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { const metadata = this.app.backend.getMetadataFor(this.item) const duration = (metadata && metadata.duration) || 0 const strings = getTimeStringsFromSec(this.timestamp, duration) + const stringsEnd = getTimeStringsFromSec(this.timestampEnd, duration) this.text = ( - (duration + /* + (trackDuration ? `(${strings.timeDone} - ${strings.percentDone})` : `(${strings.timeDone})`) + + */ + `(${strings.timeDone})` + (this.comment ? ` ${this.comment}` : '') @@ -3445,12 +3458,41 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { } writeStatus(writable) { - writable.write(ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN])) - writable.write(' ') - writable.write(ansi.setAttributes([ansi.C_RESET])) - writable.write(':') - writable.write(ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN])) - writable.write(' ') + let parts = [] + + const color = ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN]) + const reset = ansi.setAttributes([ansi.C_RESET]) + + const { SQP } = this.app + if ( + SQP.playingTrack === this.item && + SQP.timeData && + SQP.timeData.curSecTotal >= this.timestamp && + SQP.timeData.curSecTotal < this.timestampEnd + ) { + parts = [ + color, + ' ', + // reset, + '\u25B6 ', + // color, + ' ' + ] + } else { + parts = [ + color, + ' ', + reset, + ':', + color, + ' ' + ] + } + + for (const part of parts) { + writable.write(part) + } + this.drawX += 4 } -- cgit 1.3.0-6-gf8a5 From 8f0ccabc8b12465771f770508fd0786baf49a518 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 17 Jul 2021 21:18:59 -0300 Subject: show relative timestamp info in queue sidebar! --- ui.js | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 33 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 0383807..c62f93a 100644 --- a/ui.js +++ b/ui.js @@ -1819,7 +1819,12 @@ class AppElement extends FocusElement { const { playingTrack, timeData } = this.SQP const { items } = this.SQP.queueGrouplike - const { currentItem: selectedTrack } = this.queueListingElement + const { + currentInput: selectedInput, + currentItem: selectedTrack + } = this.queueListingElement + + const isTimestamp = (selectedInput instanceof TimestampGrouplikeItemElement) let trackRemainSec = 0 let trackPassedSec = 0 @@ -1832,6 +1837,7 @@ class AppElement extends FocusElement { const playingIndex = items.indexOf(playingTrack) const selectedIndex = items.indexOf(selectedTrack) + const timestampData = playingTrack && this.getTimestampData(playingTrack) // This will be set to a list of tracks, which will later be used to // calculate a particular duration (as described below) to be shown in @@ -1854,7 +1860,10 @@ class AppElement extends FocusElement { durationRange = items durationAdd = 0 durationSymbol = '' - } else if (selectedIndex === playingIndex) { + } else if ( + selectedIndex === playingIndex && + (!isTimestamp || selectedInput.isCurrentTimestamp) + ) { // Remaining length of the queue. if (timeData) { durationRange = items.slice(playingIndex + 1) @@ -1864,20 +1873,40 @@ class AppElement extends FocusElement { durationAdd = 0 } durationSymbol = '' - } else if (selectedIndex < playingIndex) { + } else if ( + selectedIndex < playingIndex || + (isTimestamp && selectedInput.data.timestamp <= trackPassedSec) + ) { // Time since the selected track ended. durationRange = items.slice(selectedIndex + 1, playingIndex) durationAdd = trackPassedSec // defaults to 0: no need to check timeData durationSymbol = '-' - } else if (selectedIndex > playingIndex) { + if (isTimestamp) { + if (selectedIndex < playingIndex) { + durationRange.unshift(items[selectedIndex]) + } + durationAdd -= selectedInput.data.timestampEnd + } + } else if ( + selectedIndex > playingIndex || + (isTimestamp && selectedInput.data.timestamp > trackPassedSec) + ) { // Time until the selected track begins. if (timeData) { - durationRange = items.slice(playingIndex + 1, selectedIndex) - durationAdd = trackRemainSec + if (selectedIndex === playingIndex) { + durationRange = [] + durationAdd = -trackPassedSec + } else { + durationRange = items.slice(playingIndex + 1, selectedIndex) + durationAdd = trackRemainSec + } } else { durationRange = items.slice(playingIndex, selectedIndex) durationAdd = 0 } + if (isTimestamp) { + durationAdd += selectedInput.data.timestamp + } durationSymbol = '+' } @@ -1890,18 +1919,53 @@ class AppElement extends FocusElement { let collapseExtraInfo = false if (playingTrack) { - let insertString - const distance = Math.abs(selectedIndex - playingIndex) - if (selectedIndex < playingIndex) { - insertString = ` (-${distance})` - collapseExtraInfo = true - } else if (selectedIndex > playingIndex) { - insertString = ` (+${distance})` - collapseExtraInfo = true + let trackPart + + { + const distance = Math.abs(selectedIndex - playingIndex) + + let insertString + if (selectedIndex < playingIndex) { + insertString = ` (-${distance})` + collapseExtraInfo = true + } else if (selectedIndex > playingIndex) { + insertString = ` (+${distance})` + collapseExtraInfo = true + } else { + insertString = '' + } + + trackPart = `${playingIndex + 1 + insertString} / ${items.length}` + } + + let timestampPart + + if (isTimestamp && selectedIndex === playingIndex) { + const selectedTimestampIndex = timestampData.indexOf(selectedInput.data) + + const found = timestampData.findIndex(ts => ts.timestamp > timeData.curSecTotal) + const playingTimestampIndex = (found >= 0 ? found - 1 : 0) + const distance = Math.abs(selectedTimestampIndex - playingTimestampIndex) + + let insertString + if (selectedTimestampIndex < playingTimestampIndex) { + insertString = ` (-${distance})` + collapseExtraInfo = true + } else if (selectedTimestampIndex > playingTimestampIndex) { + insertString = ` (+${distance})` + collapseExtraInfo = true + } else { + insertString = '' + } + + timestampPart = `${playingTimestampIndex + 1 + insertString} / ${timestampData.length}` + } + + if (timestampPart) { + this.queueLengthLabel.text = `(${this.SQP.playSymbol} ${trackPart} : ${timestampPart})` } else { - insertString = '' + this.queueLengthLabel.text = `(${this.SQP.playSymbol} ${trackPart})` } - this.queueLengthLabel.text = `(${this.SQP.playSymbol} ${playingIndex + 1 + insertString} / ${items.length})` } else { this.queueLengthLabel.text = `(${items.length})` } @@ -2240,7 +2304,7 @@ class GrouplikeListingElement extends Form { // Generate some items! Just go over the data list and generate one for // each timestamp. const tsElements = timestampData.map(ts => { - const el = new TimestampGrouplikeItemElement(item, ts.timestamp, ts.timestampEnd, ts.comment, this.app) + const el = new TimestampGrouplikeItemElement(item, ts, this.app) el.on('pressed', () => this.emit('timestamp', item, ts.timestamp)) return el }) @@ -2471,9 +2535,13 @@ class GrouplikeListingElement extends Form { } get currentItem() { - const element = this.form.inputs[this.form.curIndex] || null + const element = this.currentInput return element && element.item } + + get currentInput() { + return this.form.inputs[this.form.curIndex] || null + } } class GrouplikeListingForm extends ListScrollForm { @@ -3426,21 +3494,21 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { - constructor(item, timestamp, timestampEnd, comment, app) { + constructor(item, timestampData, app) { super('') this.app = app - this.timestamp = timestamp - this.timestampEnd = timestampEnd - this.comment = comment + this.data = timestampData this.item = item } drawTo(writable) { + const { data } = this + const metadata = this.app.backend.getMetadataFor(this.item) const duration = (metadata && metadata.duration) || 0 - const strings = getTimeStringsFromSec(this.timestamp, duration) - const stringsEnd = getTimeStringsFromSec(this.timestampEnd, duration) + const strings = getTimeStringsFromSec(data.timestamp, duration) + const stringsEnd = getTimeStringsFromSec(data.timestampEnd, duration) this.text = ( /* @@ -3449,8 +3517,8 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { : `(${strings.timeDone})`) + */ `(${strings.timeDone})` + - (this.comment - ? ` ${this.comment}` + (data.comment + ? ` ${data.comment}` : '') ) @@ -3463,13 +3531,7 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { const color = ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN]) const reset = ansi.setAttributes([ansi.C_RESET]) - const { SQP } = this.app - if ( - SQP.playingTrack === this.item && - SQP.timeData && - SQP.timeData.curSecTotal >= this.timestamp && - SQP.timeData.curSecTotal < this.timestampEnd - ) { + if (this.isCurrentTimestamp) { parts = [ color, ' ', @@ -3496,6 +3558,16 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { this.drawX += 4 } + get isCurrentTimestamp() { + const { SQP } = this.app + return ( + SQP.playingTrack === this.item && + SQP.timeData && + SQP.timeData.curSecTotal >= this.data.timestamp && + SQP.timeData.curSecTotal < this.data.timestampEnd + ) + } + getLeftPadding() { return 4 } -- cgit 1.3.0-6-gf8a5 From c30367f90211e6cfa61482bb68f829ee210e5cb6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 17 Jul 2021 21:43:22 -0300 Subject: auto expand/collapse timestamps (for SQP) --- ui.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index c62f93a..b67fc8d 100644 --- a/ui.js +++ b/ui.js @@ -549,6 +549,18 @@ class AppElement extends FocusElement { } } + // Unfortunately, there isn't really any reliable way to make these work if + // the containing queue isn't of the selected queue player. + const timestampData = track && this.getTimestampData(track) + if (timestampData && queuePlayer === this.SQP) { + this.queueListingElement.expandTimestamps(track) + } + + const oldTimestampData = oldTrack && this.getTimestampData(oldTrack) + if (oldTimestampData && queuePlayer === this.SQP) { + this.queueListingElement.collapseTimestamps(oldTrack) + } + if (track && this.enableAutoDJ) { queuePlayer.setVolumeMultiplier(0.5); const message = 'now playing: ' + getNameWithoutTrackNumber(track); @@ -1943,7 +1955,7 @@ class AppElement extends FocusElement { if (isTimestamp && selectedIndex === playingIndex) { const selectedTimestampIndex = timestampData.indexOf(selectedInput.data) - const found = timestampData.findIndex(ts => ts.timestamp > timeData.curSecTotal) + const found = timestampData.findIndex(ts => ts.timestamp > trackPassedSec) const playingTimestampIndex = (found >= 0 ? found - 1 : 0) const distance = Math.abs(selectedTimestampIndex - playingTimestampIndex) @@ -2232,8 +2244,11 @@ class GrouplikeListingElement extends Form { expandTimestamps(item) { if (this.grouplike && this.grouplike.items.includes(item)) { - this.expandedTimestamps.push(item) - this.buildTimestampItems() + const ET = this.expandedTimestamps + if (!ET.includes(item)) { + this.expandedTimestamps.push(item) + this.buildTimestampItems() + } } } -- cgit 1.3.0-6-gf8a5 From be3c0c9d03c8257237121bfd89acf25dec36ff48 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 17 Jul 2021 22:04:46 -0300 Subject: make next/previous buttons pay heed to timestamps! --- ui.js | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 6 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index b67fc8d..bc7f68d 100644 --- a/ui.js +++ b/ui.js @@ -1117,6 +1117,27 @@ class AppElement extends FocusElement { return this.timestampDictionary.get(item) || null } + getTimestampAtSec(item, sec) { + const timestampData = this.getTimestampData(item) + if (!timestampData) { + return null + } + + // Just like, start from the end, man. + // Why doesn't JavaScript have a findIndexFromEnd function??? + for (let i = timestampData.length - 1; i >= 0; i--) { + const ts = timestampData[i]; + if ( + ts.timestamp <= sec && + ts.timestampEnd >= sec + ) { + return ts + } + } + + return null + } + async readTimestampData(item) { const file = this.getTimestampsFile(item) @@ -1217,19 +1238,86 @@ class AppElement extends FocusElement { // all are before this position, skip to previous. let maxCurSec = 0 - this.forEachQueuePlayerToActOn(({ timeData }) => { - if (timeData) { - maxCurSec = Math.max(maxCurSec, timeData.curSecTotal) + this.forEachQueuePlayerToActOn(qp => { + if (qp.timeData) { + let effectiveCurSec = qp.timeData.curSecTotal + + const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + if (ts) { + effectiveCurSec -= ts.timestamp + } + + maxCurSec = Math.max(maxCurSec, effectiveCurSec) } }) if (Math.floor(maxCurSec) < this.config.seekToStartThreshold) { - this.actOnQueuePlayers(qp => qp.playPrevious(qp.playingTrack, true)) + this.skipBack() } else { - this.actOnQueuePlayers(qp => qp.seekToStart()) + this.seekToStart() } } + seekToStart() { + this.actOnQueuePlayers(qp => qp.seekToStart()) + this.actOnQueuePlayers(qp => { + if (!qp.playingTrack) { + return + } + + const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + if (ts) { + qp.seekTo(ts.timestamp) + return + } + + qp.seekToStart() + }) + } + + skipBack() { + this.actOnQueuePlayers(qp => { + if (!qp.playingTrack) { + return + } + + const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + if (ts) { + const timestampData = this.getTimestampData(qp.playingTrack) + const playingTimestampIndex = timestampData.indexOf(ts) + const previous = timestampData[playingTimestampIndex - 1] + if (previous) { + qp.seekTo(previous.timestamp) + return + } + } + + qp.playPrevious(qp.playingTrack, true) + }) + } + + skipAhead() { + this.actOnQueuePlayers(qp => { + if (!qp.playingTrack) { + return + } + + const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + + if (ts) { + const timestampData = this.getTimestampData(qp.playingTrack) + const playingTimestampIndex = timestampData.indexOf(ts) + const next = timestampData[playingTimestampIndex + 1] + if (next) { + qp.seekTo(next.timestamp) + return + } + } + + qp.playNext(qp.playingTrack, true) + }) + } + actOnQueuePlayers(fn) { this.forEachQueuePlayerToActOn(queuePlayer => { fn(queuePlayer) @@ -1610,7 +1698,7 @@ class AppElement extends FocusElement { } else if (input.isSkipBack(keyBuf)) { this.skipBackOrSeekToStart() } else if (input.isSkipAhead(keyBuf)) { - this.actOnQueuePlayers(qp => qp.playNext(qp.playingTrack, true)) + this.skipAhead() } } -- cgit 1.3.0-6-gf8a5 From df99d463242a30e863a3c84253549a18e2170d45 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 18 Jul 2021 20:00:24 -0300 Subject: miscellaneous improvements to selection restoring --- ui.js | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 151 insertions(+), 29 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index bc7f68d..a5481c8 100644 --- a/ui.js +++ b/ui.js @@ -536,7 +536,7 @@ class AppElement extends FocusElement { this.updateQueueLengthLabel() } - async handlePlaying(track, oldTrack, queuePlayer) { + async handlePlaying(track, oldTrack, startTime, queuePlayer) { const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateTrack() @@ -544,6 +544,7 @@ class AppElement extends FocusElement { if (queuePlayer === this.SQP) { this.updateQueueLengthLabel() + this.queueListingElement.collapseTimestamps(oldTrack) if (track && this.queueListingElement.currentItem === oldTrack) { this.queueListingElement.selectAndShow(track) } @@ -553,12 +554,9 @@ class AppElement extends FocusElement { // the containing queue isn't of the selected queue player. const timestampData = track && this.getTimestampData(track) if (timestampData && queuePlayer === this.SQP) { - this.queueListingElement.expandTimestamps(track) - } - - const oldTimestampData = oldTrack && this.getTimestampData(oldTrack) - if (oldTimestampData && queuePlayer === this.SQP) { - this.queueListingElement.collapseTimestamps(oldTrack) + if (this.queueListingElement.currentItem === track) { + this.queueListingElement.selectTimestampAtSec(track, startTime) + } } if (track && this.enableAutoDJ) { @@ -573,7 +571,7 @@ class AppElement extends FocusElement { } } - handleReceivedTimeData(data, queuePlayer) { + handleReceivedTimeData(timeData, oldTimeData, queuePlayer) { const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateProgress() @@ -581,6 +579,7 @@ class AppElement extends FocusElement { if (queuePlayer === this.SQP) { this.updateQueueLengthLabel() + this.updateQueueSelection(timeData, oldTimeData) } } @@ -1920,11 +1919,11 @@ class AppElement extends FocusElement { const { playingTrack, timeData } = this.SQP const { items } = this.SQP.queueGrouplike const { - currentInput: selectedInput, + currentInput: currentInput, currentItem: selectedTrack } = this.queueListingElement - const isTimestamp = (selectedInput instanceof TimestampGrouplikeItemElement) + const isTimestamp = (currentInput instanceof TimestampGrouplikeItemElement) let trackRemainSec = 0 let trackPassedSec = 0 @@ -1962,7 +1961,7 @@ class AppElement extends FocusElement { durationSymbol = '' } else if ( selectedIndex === playingIndex && - (!isTimestamp || selectedInput.isCurrentTimestamp) + (!isTimestamp || currentInput.isCurrentTimestamp) ) { // Remaining length of the queue. if (timeData) { @@ -1975,7 +1974,7 @@ class AppElement extends FocusElement { durationSymbol = '' } else if ( selectedIndex < playingIndex || - (isTimestamp && selectedInput.data.timestamp <= trackPassedSec) + (isTimestamp && currentInput.data.timestamp <= trackPassedSec) ) { // Time since the selected track ended. durationRange = items.slice(selectedIndex + 1, playingIndex) @@ -1985,11 +1984,11 @@ class AppElement extends FocusElement { if (selectedIndex < playingIndex) { durationRange.unshift(items[selectedIndex]) } - durationAdd -= selectedInput.data.timestampEnd + durationAdd -= currentInput.data.timestampEnd } } else if ( selectedIndex > playingIndex || - (isTimestamp && selectedInput.data.timestamp > trackPassedSec) + (isTimestamp && currentInput.data.timestamp > trackPassedSec) ) { // Time until the selected track begins. if (timeData) { @@ -2005,7 +2004,7 @@ class AppElement extends FocusElement { durationAdd = 0 } if (isTimestamp) { - durationAdd += selectedInput.data.timestamp + durationAdd += currentInput.data.timestamp } durationSymbol = '+' } @@ -2041,7 +2040,7 @@ class AppElement extends FocusElement { let timestampPart if (isTimestamp && selectedIndex === playingIndex) { - const selectedTimestampIndex = timestampData.indexOf(selectedInput.data) + const selectedTimestampIndex = timestampData.indexOf(currentInput.data) const found = timestampData.findIndex(ts => ts.timestamp > trackPassedSec) const playingTimestampIndex = (found >= 0 ? found - 1 : 0) @@ -2081,6 +2080,53 @@ class AppElement extends FocusElement { this.queueTimeLabel.y = this.queuePane.contentH - 1 } + updateQueueSelection(timeData, oldTimeData) { + if (!timeData) { + return + } + + const { playingTrack } = this.SQP + const { form } = this.queueListingElement + const { currentInput } = form + + if (!currentInput || currentInput.item !== playingTrack) { + return + } + + const timestamps = this.getTimestampData(playingTrack) + + if (!timestamps) { + return + } + + const tsOld = oldTimeData && + this.getTimestampAtSec(playingTrack, oldTimeData.curSecTotal) + const tsNew = + this.getTimestampAtSec(playingTrack, timeData.curSecTotal) + + if ( + tsNew !== tsOld && + currentInput instanceof TimestampGrouplikeItemElement && + currentInput.data === tsOld + ) { + const index = form.inputs.findIndex(el => ( + el.item === playingTrack && + el instanceof TimestampGrouplikeItemElement && + el.data === tsNew + )) + + if (index === -1) { + return + } + + form.curIndex = index + if (form.isSelected) { + form.updateSelectedElement() + } + form.scrollSelectedElementIntoView() + } + } + get SQP() { // Just a convenient shorthand. return this.selectedQueuePlayer @@ -2336,6 +2382,14 @@ class GrouplikeListingElement extends Form { if (!ET.includes(item)) { this.expandedTimestamps.push(item) this.buildTimestampItems() + + if (this.currentItem === item) { + if (this.isSelected) { + this.form.selectInput(this.form.inputs[this.form.curIndex + 1]) + } else { + this.form.curIndex += 1 + } + } } } } @@ -2343,8 +2397,20 @@ class GrouplikeListingElement extends Form { collapseTimestamps(item) { const ET = this.expandedTimestamps // :alien: if (ET.includes(item)) { + const restore = (this.currentItem === item) + ET.splice(ET.indexOf(item), 1) this.buildTimestampItems() + + if (restore) { + const { form } = this + const index = form.inputs.findIndex(inp => inp.item === item) + form.curIndex = index + if (form.isSelected) { + form.updateSelectedElement() + } + form.scrollSelectedElementIntoView() + } } } @@ -2361,6 +2427,30 @@ class GrouplikeListingElement extends Form { return this.expandedTimestamps.includes(item) } + selectTimestampAtSec(item, sec) { + this.expandTimestamps(item) + + const { form } = this + let index = form.inputs.findIndex(el => ( + el.item === item && + el instanceof TimestampGrouplikeItemElement && + el.data.timestamp >= sec + )) + + if (index === -1) { + index = form.inputs.findIndex(el => el.item === item) + if (index === -1) { + return + } + } + + form.curIndex = index + if (form.isSelected) { + form.updateSelectedElement() + } + form.scrollSelectedElementIntoView() + } + updateTimestamps() { const ET = this.expandedTimestamps if (ET) { @@ -2368,13 +2458,43 @@ class GrouplikeListingElement extends Form { } } - buildTimestampItems(item) { - const form = this.form + restoreSelectedInput(restoreInput) { + const { form } = this + const { inputs, currentInput } = form + + if (currentInput === restoreInput) { + return + } + + let inputToSelect + + if (inputs.includes(restoreInput)) { + inputToSelect = restoreInput + } else if (restoreInput instanceof InteractiveGrouplikeItemElement) { + inputToSelect = inputs.find(input => + input.item === restoreInput.item && + input instanceof InteractiveGrouplikeItemElement + ) + } else if (restoreInput instanceof TimestampGrouplikeItemElement) { + inputToSelect = inputs.find(input => + input.data === restoreInput.data && + input instanceof TimestampGrouplikeItemElement + ) + } + + if (!inputToSelect) { + return + } + + form.curIndex = inputs.indexOf(inputToSelect) + if (form.isSelected) { + form.updateSelectedElement() + } + form.scrollSelectedElementIntoView() + } - // We're going to restore this selection later. It's kinda hacky and won't - // work if the selected input was itself a timestamp item, but that - // [extremely RFC voice] hopefully won't happen! - const selectedInput = this.form.inputs[this.form.curIndex] + buildTimestampItems(restoreInput = this.currentInput) { + const form = this.form // Clear up any existing timestamp items, since we're about to generate new // ones! @@ -2428,11 +2548,7 @@ class GrouplikeListingElement extends Form { } } - const index = form.inputs.indexOf(selectedInput) - if (index >= 0) { - form.selectInput(form.inputs.indexOf(selectedInput)) - } - + this.restoreSelectedInput(restoreInput) this.scheduleDrawWithoutPropertyChange() this.fixAllLayout() } @@ -2444,6 +2560,7 @@ class GrouplikeListingElement extends Form { this.commentLabel.text = this.grouplike.comment || '' + const restoreInput = this.form.currentInput const wasSelected = this.isSelected const form = this.form @@ -2504,11 +2621,12 @@ class GrouplikeListingElement extends Form { } } + this.buildTimestampItems(restoreInput) + // Just to make the selected-track-info bar fill right away (if it wasn't // already filled by a previous this.curIndex set). form.curIndex = form.curIndex - this.buildTimestampItems() this.fixAllLayout() } @@ -2643,7 +2761,7 @@ class GrouplikeListingElement extends Form { } get currentInput() { - return this.form.inputs[this.form.curIndex] || null + return this.form.currentInput } } @@ -2685,6 +2803,10 @@ class GrouplikeListingForm extends ListScrollForm { return Math.max(0, this.inputs.findIndex(el => el instanceof InteractiveGrouplikeItemElement)) } + get currentInput() { + return this.inputs[this.curIndex] + } + selectAndShow(item) { const index = this.inputs.findIndex(inp => inp.item === item) if (index >= 0) { -- cgit 1.3.0-6-gf8a5 From 4e0cb3a38deb8600cbcacb3f87b9e6dba8930aa1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 6 Aug 2021 15:42:38 -0300 Subject: order: alphabetize order of groups --- ui.js | 65 +++++++++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 24 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index a5481c8..b586335 100644 --- a/ui.js +++ b/ui.js @@ -16,6 +16,7 @@ const { const { cloneGrouplike, + collapseGrouplike, countTotalTracks, flattenGrouplike, getCorrespondingFileForItem, @@ -326,6 +327,7 @@ class AppElement extends FocusElement { {value: 'reverse', label: 'Reverse all'}, {value: 'reverse-groups', label: 'Reverse order of groups'}, {value: 'alphabetic', label: 'Alphabetically'}, + {value: 'alphabetic-groups', label: 'Alphabetize order of groups'}, {value: 'normal', label: 'In order'} ], this.showContextMenu) @@ -1815,30 +1817,45 @@ class AppElement extends FocusElement { const oldName = item.name if (isGroup(item)) { - if (order === 'shuffle') { - 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 = { - 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)`, - items: orderBy( - flattenGrouplike(item).items, - t => getNameWithoutTrackNumber(t).replace(/[^a-zA-Z0-9]/g, '') - ) - } + switch (order) { + case 'shuffle': + item = { + name: `${oldName} (shuffled)`, + items: shuffleArray(flattenGrouplike(item).items) + } + break + case 'shuffle-groups': + item = shuffleOrderOfGroups(item) + item.name = `${oldName} (group order shuffled)` + break + case 'reverse': + item = { + name: `${oldName} (reversed)`, + items: flattenGrouplike(item).items.reverse() + } + break + case 'reverse-groups': + item = reverseOrderOfGroups(item) + item.name = `${oldName} (group order reversed)` + break + case 'alphabetic': + item = { + name: `${oldName} (alphabetic)`, + items: orderBy( + flattenGrouplike(item).items, + t => getNameWithoutTrackNumber(t).replace(/[^a-zA-Z0-9]/g, '') + ) + } + break + case 'alphabetic-groups': + item = { + name: `${oldName} (group order alphabetic)`, + items: orderBy( + collapseGrouplike(item).items, + t => t.name.replace(/[^a-zA-Z0-9]/g, '') + ) + } + break } } else { // Make it into a grouplike that just contains itself. -- cgit 1.3.0-6-gf8a5 From 341a34320e89ed1ebc29c5a9c8675d18dd02e185 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 6 Aug 2021 15:57:42 -0300 Subject: reveal in queue --- ui.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index b586335..49308fe 100644 --- a/ui.js +++ b/ui.js @@ -772,12 +772,12 @@ class AppElement extends FocusElement { // (made by newGrouplikeListing) as well as the queue grouplike listing. grouplikeListing.on('timestamp', (item, time) => this.playOrSeek(item, time)) - grouplikeListing.pathElement.on('select', (item, child) => this.reveal(item, child)) + grouplikeListing.pathElement.on('select', (item, child) => this.revealInLibrary(item, child)) grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing)) /* grouplikeListing.on('select', item => this.editNotesFile(item, false)) grouplikeListing.on('edit-notes', item => { - this.reveal(item) + this.revealInLibrary(item) this.editNotesFile(item, true) }) */ @@ -798,7 +798,7 @@ class AppElement extends FocusElement { grouplikeListing.loadGrouplike(grouplike, ...args) } - reveal(item, child) { + revealInLibrary(item, child) { if (!this.tabberPane.visible) { return } @@ -820,6 +820,13 @@ class AppElement extends FocusElement { } } + revealInQueue(item) { + const queueListing = this.queueListingElement + if (queueListing.selectAndShow(item)) { + this.root.select(queueListing) + } + } + play(item) { if (!this.config.canControlQueue) { return @@ -1353,10 +1360,11 @@ class AppElement extends FocusElement { ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)} : {label: 'Expand saved timestamps', action: () => this.expandTimestamps(item, listing)} ) + const isQueued = this.SQP.queueGrouplike.items.includes(item) if (listing.grouplike.isTheQueue && isTrack(item)) { return [ - item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)}, + item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal in library', action: () => this.revealInLibrary(item)}, timestampsItem, {divider: true}, canControlQueue && {label: 'Play later', action: () => this.playLater(item)}, @@ -1415,6 +1423,7 @@ class AppElement extends FocusElement { 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, -- cgit 1.3.0-6-gf8a5 From 450d751b76574584b1bce70c2108e7fd86510c8f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 6 Aug 2021 17:07:18 -0300 Subject: fix sub-groups not loading timestamps --- ui.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 49308fe..f03c24b 100644 --- a/ui.js +++ b/ui.js @@ -1106,6 +1106,10 @@ class AppElement extends FocusElement { // There's no parallelization here, but like, whateeeever. for (const item of grouplike.items) { + if (!isTrack(item)) { + continue + } + if (this.timestampDictionary.has(item)) { continue } -- cgit 1.3.0-6-gf8a5 From b29f82bc0afca425bb42fecc0583e804701110dd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 6 Aug 2021 18:30:05 -0300 Subject: read timestamps as JSON when they start with [ ...instead of {. lol --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index f03c24b..594c6f8 100644 --- a/ui.js +++ b/ui.js @@ -1171,7 +1171,7 @@ class AppElement extends FocusElement { return null } - if (contents.startsWith('{')) { + if (contents.startsWith('[')) { try { return JSON.parse(contents) } catch (error) { -- cgit 1.3.0-6-gf8a5 From 9c7e30f90f0e30535f87fbb28222c9a40940d9ec Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 00:11:15 -0300 Subject: show timestamp durations in main listing --- ui.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 594c6f8..2599427 100644 --- a/ui.js +++ b/ui.js @@ -2559,6 +2559,9 @@ class GrouplikeListingElement extends Form { const tsElements = timestampData.map(ts => { const el = new TimestampGrouplikeItemElement(item, ts, this.app) el.on('pressed', () => this.emit('timestamp', item, ts.timestamp)) + if (this.grouplike.isTheQueue) { + el.hideMetadata = true + } return el }) @@ -3755,6 +3758,7 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { this.app = app this.data = timestampData this.item = item + this.hideMetadata = false } drawTo(writable) { @@ -3777,6 +3781,18 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { : '') ) + if (!this.hideMetadata) { + const durationString = (data.timestampEnd === Infinity + ? 'to end' + : getTimeStringsFromSec(0, data.timestampEnd - data.timestamp).duration) + + // Try to line up so there's one column of negative padding - the duration + // of the timestamp(s) should start one column before the duration of the + // actual track. This makes for a nice nested look! + const rightPadding = ' '.repeat(duration > 3600 ? 4 : 2) + this.rightText = ` (${durationString})` + rightPadding + } + super.drawTo(writable) } -- cgit 1.3.0-6-gf8a5 From c047b8c57d4e5012578c072420d2b73dd5b59c4c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 00:11:39 -0300 Subject: handy combine-album.js utility this isn't exposed via the mtui command so like, just run it directly with node right now lol (this commit also makes "." parse in timestamp positions) --- ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 2599427..2da7e02 100644 --- a/ui.js +++ b/ui.js @@ -1187,7 +1187,7 @@ class AppElement extends FocusElement { const duration = (metadata ? metadata.duration : Infinity) const data = lines - .map(line => line.match(/^\s*([0-9:]+)\s*(\S.*)\s*$/)) + .map(line => line.match(/^\s*([0-9:.]+)\s*(\S.*)\s*$/)) .filter(match => match) .map(match => ({timestamp: getSecFromTimestamp(match[1]), comment: match[2]})) .filter(({ timestamp: sec }) => !isNaN(sec)) -- cgit 1.3.0-6-gf8a5 From 39732bd31e13c9a785eac1fc724bc9fc768be2ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 00:44:47 -0300 Subject: show timestamp hours column whenever appropriate --- ui.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 2da7e02..d6452d9 100644 --- a/ui.js +++ b/ui.js @@ -2557,7 +2557,7 @@ class GrouplikeListingElement extends Form { // Generate some items! Just go over the data list and generate one for // each timestamp. const tsElements = timestampData.map(ts => { - const el = new TimestampGrouplikeItemElement(item, ts, this.app) + const el = new TimestampGrouplikeItemElement(item, ts, timestampData, this.app) el.on('pressed', () => this.emit('timestamp', item, ts.timestamp)) if (this.grouplike.isTheQueue) { el.hideMetadata = true @@ -3752,20 +3752,24 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { - constructor(item, timestampData, app) { + constructor(item, timestampData, tsDataArray, app) { super('') this.app = app this.data = timestampData + this.tsData = tsDataArray this.item = item this.hideMetadata = false } drawTo(writable) { - const { data } = this + const { data, tsData } = this const metadata = this.app.backend.getMetadataFor(this.item) - const duration = (metadata && metadata.duration) || 0 + const last = tsData[tsData.length - 1] + const duration = ((metadata && metadata.duration) + || last.timestampEnd !== Infinity && last.timestampEnd + || last.timestamp) const strings = getTimeStringsFromSec(data.timestamp, duration) const stringsEnd = getTimeStringsFromSec(data.timestampEnd, duration) -- cgit 1.3.0-6-gf8a5 From b709c931dc01e411e06b7a596b26d36921c13be4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 19:51:47 -0300 Subject: make secret (c) key change entire ui theme color also add (C) (case-sensitive) to go to previous theme color --- ui.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index d6452d9..df8d914 100644 --- a/ui.js +++ b/ui.js @@ -109,6 +109,8 @@ const keyBindings = [ // ['isFocusLabels', 'L', {caseless: false}], // todo: better key? to let isToggleLoop be caseless again ['isSelectUp', telc.isShiftUp], ['isSelectDown', telc.isShiftDown], + ['isNextThemeColor', 'c', {caseless: false}], + ['isPreviousThemeColor', 'C', {caseless: false}], ['isPreviousPlayer', telc.isMetaUp], ['isPreviousPlayer', [0x1b, 'p']], @@ -199,7 +201,7 @@ class AppElement extends FocusElement { canControlQueuePlayers: true, canProcessMetadata: true, canSuspend: true, - menubarColor: 4, // blue + themeColor: 4, // blue seekToStartThreshold: 3, showTabberPane: true, stopPlayingUponQuit: true @@ -221,7 +223,8 @@ class AppElement extends FocusElement { this.menubar = new Menubar(this.showContextMenu) this.addChild(this.menubar) - this.menubar.color = this.config.menubarColor + this.setThemeColor(this.config.themeColor) + this.menubar.on('color', color => this.setThemeColor(color)) this.tabberPane = new Pane() this.addChild(this.tabberPane) @@ -2157,6 +2160,11 @@ class AppElement extends FocusElement { } } + setThemeColor(color) { + this.themeColor = color + this.menubar.color = color + } + get SQP() { // Just a convenient shorthand. return this.selectedQueuePlayer @@ -3684,15 +3692,16 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writeStatus(writable) { const markStatus = this.app.getMarkStatus(this.item) + const color = this.app.themeColor + 30 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 (markStatus === 'marked' || markStatus === 'partial') { - writable.write(ansi.setAttributes([ansi.C_BLUE + 10])) + writable.write(ansi.setAttributes([color + 10])) } else { - writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) + writable.write(ansi.setAttributes([color, ansi.A_BRIGHT])) } } else if (this.isTrack) { if (markStatus === 'marked') { @@ -3803,7 +3812,7 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { writeStatus(writable) { let parts = [] - const color = ansi.setAttributes([ansi.A_BRIGHT, ansi.C_CYAN]) + const color = ansi.setAttributes([ansi.A_BRIGHT, 30 + this.app.themeColor]) const reset = ansi.setAttributes([ansi.C_RESET]) if (this.isCurrentTimestamp) { @@ -5051,9 +5060,14 @@ class Menubar extends ListScrollForm { if (this.keyboardSelector.keyPressed(keyBuf)) { return false - } else if (telc.isCaselessLetter(keyBuf, 'c')) { + } else if (input.isNextThemeColor(keyBuf)) { // For fun :) - this.color = (this.color % 8) + 1 + this.color = (this.color === 8 ? 1 : this.color + 1) + this.emit('color', this.color) + return false + } else if (input.isPreviousThemeColor(keyBuf)) { + this.color = (this.color === 1 ? 8 : this.color - 1) + this.emit('color', this.color) return false } else if (telc.isCaselessLetter(keyBuf, 'a')) { this.attribute = (this.attribute % 3) + 1 -- cgit 1.3.0-6-gf8a5 From 24ed3e0bf3542f8cf32ed03399dd455be4d5435f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 10 Oct 2021 10:34:11 -0300 Subject: fix a bunch of crashes when acting before timeData ...is provided by the player --- ui.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index df8d914..1331344 100644 --- a/ui.js +++ b/ui.js @@ -1257,7 +1257,9 @@ class AppElement extends FocusElement { if (qp.timeData) { let effectiveCurSec = qp.timeData.curSecTotal - const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + const ts = (qp.timeData && + this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal)) + if (ts) { effectiveCurSec -= ts.timestamp } @@ -1280,7 +1282,9 @@ class AppElement extends FocusElement { return } - const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + const ts = (qp.timeData && + this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal)) + if (ts) { qp.seekTo(ts.timestamp) return @@ -1296,7 +1300,9 @@ class AppElement extends FocusElement { return } - const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + const ts = (qp.timeData && + this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal)) + if (ts) { const timestampData = this.getTimestampData(qp.playingTrack) const playingTimestampIndex = timestampData.indexOf(ts) @@ -1317,7 +1323,8 @@ class AppElement extends FocusElement { return } - const ts = this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal) + const ts = (qp.timeData && + this.getTimestampAtSec(qp.playingTrack, qp.timeData.curSecTotal)) if (ts) { const timestampData = this.getTimestampData(qp.playingTrack) -- cgit 1.3.0-6-gf8a5 From ec0b00d62f5fe02248de71552f5cd3d76589295c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 10 Oct 2021 10:46:23 -0300 Subject: "Loop mode" option: no loop, loop, shuffle This also reorganizes the menubar options a little. --- ui.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 7 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 1331344..bf67b8e 100644 --- a/ui.js +++ b/ui.js @@ -351,12 +351,13 @@ class AppElement extends FocusElement { return [ {label: playingTrack ? `("${playingTrack.name}")` : '(No track playing.)'}, {divider: true}, + {element: this.loopModeControl}, + {element: this.volumeSlider}, + {divider: true}, playingTrack && {element: this.playingControl}, - {element: this.loopingControl}, - {element: this.loopQueueControl}, - {element: this.pauseNextControl}, + playingTrack && {element: this.loopingControl}, + playingTrack && {element: this.pauseNextControl}, {element: this.autoDJControl}, - {element: this.volumeSlider}, {divider: true}, previous && {label: `Previous (${previous.name})`, action: () => this.SQP.playPrevious(playingTrack)}, next && {label: `Next (${next.name})`, action: () => this.SQP.playNext(playingTrack)}, @@ -403,6 +404,20 @@ class AppElement extends FocusElement { getEnabled: () => this.config.canControlPlayback }) + this.loopModeControl = new InlineListPickerElement('Loop mode', [ + {value: 'end', label: 'Don\'t loop'}, + {value: 'loop', label: 'Loop (same order)'}, + {value: 'shuffle', label: 'Loop (shuffle)'} + ], { + setValue: val => { + if (this.SQP) { + this.SQP.queueEndMode = val + } + }, + getValue: () => this.SQP && this.SQP.queueEndMode, + showContextMenu: this.showContextMenu + }) + this.pauseNextControl = new ToggleControl('Pause when this track ends?', { setValue: val => this.SQP.setPauseNextTrack(val), getValue: () => this.SQP.pauseNextTrack, @@ -3153,13 +3168,27 @@ class InlineListPickerElement extends FocusElement { // next or previous. (That's the point, it's inline.) This element is mainly // useful in forms or ContextMenus. - constructor(labelText, options, showContextMenu = null) { + constructor(labelText, options, optsOrShowContextMenu = null) { super() + this.labelText = labelText this.options = options - this.showContextMenu = showContextMenu - this.curIndex = 0 + + if (typeof optsOrShowContextMenu === 'function') { + this.showContextMenu = optsOrShowContextMenu + } + + if (typeof optsOrShowContextMenu === 'object') { + const opts = optsOrShowContextMenu + this.showContextMenu = opts.showContextMenu + this.getValue = opts.getValue + this.setValue = opts.setValue + } + this.keyboardIdentifier = this.labelText + + this.curIndex = 0 + this.refreshValue() } fixLayout() { @@ -3244,11 +3273,26 @@ class InlineListPickerElement extends FocusElement { return false } + + refreshValue() { + if (this.getValue) { + const value = this.getValue() + const index = this.options.findIndex(opt => opt.value === value) + if (index >= 0) { + this.curIndex = index + } + } + } + nextOption() { this.curIndex++ if (this.curIndex === this.options.length) { this.curIndex = 0 } + + if (this.setValue) { + this.setValue(this.curValue) + } } previousOption() { @@ -3256,6 +3300,10 @@ class InlineListPickerElement extends FocusElement { if (this.curIndex < 0) { this.curIndex = this.options.length - 1 } + + if (this.setValue) { + this.setValue(this.curValue) + } } get curValue() { @@ -3453,6 +3501,9 @@ class ToggleControl extends FocusElement { } + // Note: ToggleControl doesn't specify refreshValue because it doesn't have an + // internal state for the current value. It sets and draws based on the value + // getter provided externally. toggle() { this.setValue(!this.getValue()) } @@ -4716,6 +4767,16 @@ class ContextMenu extends FocusElement { return } + // Call refreshValue() on any items before they're shown, for items that + // provide it. (This is handy when reusing the same input across a menu that + // might be shown under different contexts.) + for (const item of items) { + const el = item.element + if (!el) continue + if (!el.refreshValue) continue + el.refreshValue() + } + if (!this.root.selectedElement.directAncestors.includes(this)) { this.selectedBefore = this.root.selectedElement } -- cgit 1.3.0-6-gf8a5 From fc1da6ee8ea604f1f6fcf2d0c82775fc7eeb8e32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 10 Oct 2021 10:58:36 -0300 Subject: update controls to loop queue on last track --- ui.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index bf67b8e..b8dd63c 100644 --- a/ui.js +++ b/ui.js @@ -361,6 +361,8 @@ class AppElement extends FocusElement { {divider: true}, previous && {label: `Previous (${previous.name})`, action: () => this.SQP.playPrevious(playingTrack)}, next && {label: `Next (${next.name})`, action: () => this.SQP.playNext(playingTrack)}, + !next && this.SQP.queueEndMode === 'loop' && + {label: `Next (loop queue)`, action: () => this.SQP.playNext(playingTrack)}, next && {label: '- Play later', action: () => this.playLater(next)} ] }}, -- cgit 1.3.0-6-gf8a5 From 8fdf8a581d9362f1a09b2f091aa48748875797f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 10 Oct 2021 14:02:59 -0300 Subject: don't explode if grouplike form is empty! --- ui.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ui.js') diff --git a/ui.js b/ui.js index b8dd63c..6d07f2b 100644 --- a/ui.js +++ b/ui.js @@ -2842,6 +2842,10 @@ class GrouplikeListingForm extends ListScrollForm { } keyPressed(keyBuf) { + if (this.inputs.length === 0) { + return + } + if (input.isSelectUp(keyBuf)) { this.selectUp() } else if (input.isSelectDown(keyBuf)) { -- cgit 1.3.0-6-gf8a5 From 39d4ec4d0de41c7f99a3b91a3128b6f3b5e8d3fc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Dec 2021 08:34:11 -0400 Subject: right click InlineListPickerElement to show menu This menu was already implemented, but previously, it only showed when pressing F (i.e. isMenu). --- ui.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 6d07f2b..18652f5 100644 --- a/ui.js +++ b/ui.js @@ -3245,17 +3245,7 @@ class InlineListPickerElement extends FocusElement { } else if (telc.isLeft(keyBuf)) { this.previousOption() } else if (input.isMenu(keyBuf) && this.showContextMenu) { - this.showContextMenu({ - x: this.absLeft + ansi.measureColumns(this.labelText) + 1, - y: this.absTop + 1, - items: this.options.map(({ value, label }, index) => ({ - label: label, - action: () => { - this.curIndex = index - }, - isDefault: index === this.curIndex - })) - }) + this.showMenu() } else { return true } @@ -3269,6 +3259,8 @@ class InlineListPickerElement extends FocusElement { } else { this.root.select(this) } + } else if (button === 'right') { + this.showMenu() } else if (button === 'scroll-up') { this.previousOption() } else if (button === 'scroll-down') { @@ -3280,6 +3272,20 @@ class InlineListPickerElement extends FocusElement { } + showMenu() { + this.showContextMenu({ + x: this.absLeft + ansi.measureColumns(this.labelText) + 1, + y: this.absTop + 1, + items: this.options.map(({ value, label }, index) => ({ + label: label, + action: () => { + this.curIndex = index + }, + isDefault: index === this.curIndex + })) + }) + } + refreshValue() { if (this.getValue) { const value = this.getValue() -- cgit 1.3.0-6-gf8a5 From e8a55f10dd9749ad240b165e318db0a1d2f00a9a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 1 Jun 2022 23:35:03 -0300 Subject: miscellaneous improvements to queue looping --- ui.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 16 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 18652f5..dc9dab9 100644 --- a/ui.js +++ b/ui.js @@ -351,7 +351,6 @@ class AppElement extends FocusElement { return [ {label: playingTrack ? `("${playingTrack.name}")` : '(No track playing.)'}, {divider: true}, - {element: this.loopModeControl}, {element: this.volumeSlider}, {divider: true}, playingTrack && {element: this.playingControl}, @@ -373,6 +372,8 @@ class AppElement extends FocusElement { return [ {label: `(Queue - ${curIndex >= 0 ? `${curIndex + 1}/` : ''}${items.length} items.)`}, {divider: true}, + {element: this.loopModeControl}, + {divider: true}, items.length && {label: 'Shuffle', action: () => this.shuffleQueue()}, items.length && {label: 'Clear', action: () => this.clearQueue()} ] @@ -406,7 +407,7 @@ class AppElement extends FocusElement { getEnabled: () => this.config.canControlPlayback }) - this.loopModeControl = new InlineListPickerElement('Loop mode', [ + this.loopModeControl = new InlineListPickerElement('Loop queue?', [ {value: 'end', label: 'Don\'t loop'}, {value: 'loop', label: 'Loop (same order)'}, {value: 'shuffle', label: 'Loop (shuffle)'} @@ -1973,7 +1974,7 @@ class AppElement extends FocusElement { return } - const { playingTrack, timeData } = this.SQP + const { playingTrack, timeData, queueEndMode } = this.SQP const { items } = this.SQP.queueGrouplike const { currentInput: currentInput, @@ -2073,25 +2074,34 @@ class AppElement extends FocusElement { const { duration: durationString } = getTimeStringsFromSec(0, durationTotal) this.queueTimeLabel.text = `(${durationSymbol + durationString + approxSymbol})` - let collapseExtraInfo = false if (playingTrack) { let trackPart + let trackPartShort + let trackPartReallyShort { const distance = Math.abs(selectedIndex - playingIndex) let insertString + let insertStringShort if (selectedIndex < playingIndex) { insertString = ` (-${distance})` - collapseExtraInfo = true + insertStringShort = `-${distance}` } else if (selectedIndex > playingIndex) { insertString = ` (+${distance})` - collapseExtraInfo = true + insertStringShort = `+${distance}` } else { insertString = '' + insertStringShort = '' } trackPart = `${playingIndex + 1 + insertString} / ${items.length}` + trackPartShort = (insertString + ? `${playingIndex + 1 + insertStringShort}/${items.length}` + : `${playingIndex + 1}/${items.length}`) + trackPartReallyShort = (insertString + ? insertStringShort + : `#${playingIndex + 1}`) } let timestampPart @@ -2106,10 +2116,8 @@ class AppElement extends FocusElement { let insertString if (selectedTimestampIndex < playingTimestampIndex) { insertString = ` (-${distance})` - collapseExtraInfo = true } else if (selectedTimestampIndex > playingTimestampIndex) { insertString = ` (+${distance})` - collapseExtraInfo = true } else { insertString = '' } @@ -2117,19 +2125,66 @@ class AppElement extends FocusElement { timestampPart = `${playingTimestampIndex + 1 + insertString} / ${timestampData.length}` } - if (timestampPart) { - this.queueLengthLabel.text = `(${this.SQP.playSymbol} ${trackPart} : ${timestampPart})` - } else { - this.queueLengthLabel.text = `(${this.SQP.playSymbol} ${trackPart})` + let queueLoopPart + let queueLoopPartShort + + if (selectedIndex === playingIndex) { + switch (queueEndMode) { + case 'loop': + queueLoopPart = 'Repeat' + queueLoopPartShort = 'R' + break + case 'shuffle': + queueLoopPart = 'Shuffle' + queueLoopPartShort = 'S' + break + case 'end': + default: + break + } + } + + let partsTogether + + const all = () => `(${this.SQP.playSymbol} ${partsTogether})` + const tooWide = () => all().length > this.queuePane.contentW + + // goto irl + determineParts: { + if (timestampPart) { + if (queueLoopPart) { + partsTogether = `${trackPart} : ${timestampPart} »${queueLoopPartShort}` + } else { + partsTogether = `(${this.SQP.playSymbol} ${trackPart} : ${timestampPart})` + } + break determineParts + } + + if (queueLoopPart) includeQueueLoop: { + partsTogether = `${trackPart} » ${queueLoopPart}` + if (tooWide()) { + partsTogether = `${trackPart} »${queueLoopPartShort}` + if (tooWide()) { + break includeQueueLoop + } + } + break determineParts + } + + partsTogether = trackPart + if (tooWide()) { + partsTogether = trackPartShort + if (tooWide()) { + partsTogether = trackPartReallyShort + } + } } + + this.queueLengthLabel.text = all() } else { this.queueLengthLabel.text = `(${items.length})` } - if (this.SQP.loopQueueAtEnd) { - this.queueLengthLabel.text += (collapseExtraInfo ? ` [L${unic.ELLIPSIS}]` : ` [Looping]`) - } - // Layout stuff to position the length and time labels correctly. this.queueLengthLabel.centerInParent() this.queueTimeLabel.centerInParent() -- cgit 1.3.0-6-gf8a5 From 43f1a1dd1b44065663a797603012394c52a9baea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 13 May 2023 13:31:58 -0300 Subject: use ESM module syntax & update tui-lib Exciting update! This doesn't make any substantial changes exactly but does update the most quickly-archaic parts of older Node code. --- ui.js | 131 +++++++++++++++++++++++++++--------------------------------------- 1 file changed, 53 insertions(+), 78 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index dc9dab9..f006a70 100644 --- a/ui.js +++ b/ui.js @@ -1,20 +1,35 @@ // The UI in MTUI! Interfaces with the backend to form the complete mtui app. -'use strict' +import {spawn} from 'node:child_process' +import {readFile, writeFile} from 'node:fs/promises' +import path from 'node:path' +import url from 'node:url' -const { getAllCrawlersForArg } = require('./crawlers') -const processSmartPlaylist = require('./smart-playlist') -const UndoManager = require('./undo-manager') +import {orderBy} from 'natural-orderby' +import open from 'open' -const { +import {Button, Form, ListScrollForm, TextInput} from 'tui-lib/ui/controls' +import {Dialog} from 'tui-lib/ui/dialogs' +import {Label, Pane, WrapLabel} from 'tui-lib/ui/presentation' +import {DisplayElement, FocusElement} from 'tui-lib/ui/primitives' + +import * as ansi from 'tui-lib/util/ansi' +import telc from 'tui-lib/util/telchars' +import unic from 'tui-lib/util/unichars' + +import {getAllCrawlersForArg} from './crawlers.js' +import processSmartPlaylist from './smart-playlist.js' +import UndoManager from './undo-manager.js' + +import { commandExists, getSecFromTimestamp, getTimeStringsFromSec, promisifyProcess, - shuffleArray -} = require('./general-util') + shuffleArray, +} from './general-util.js' -const { +import { cloneGrouplike, collapseGrouplike, countTotalTracks, @@ -30,46 +45,14 @@ const { parentSymbol, reverseOrderOfGroups, searchForItem, - shuffleOrderOfGroups -} = require('./playlist-utils') - -const { - ui: { - Dialog, - DisplayElement, - Label, - Pane, - WrapLabel, - form: { - Button, - FocusElement, - Form, - ListScrollForm, - TextInput, - } - }, - util: { - ansi, - telchars: telc, - unichars: unic, - } -} = require('tui-lib') + shuffleOrderOfGroups, +} from './playlist-utils.js' /* text editor features disabled because theyre very much incomplete and havent * gotten much use from me or anyonea afaik! 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') -const url = require('url') -const readFile = promisify(fs.readFile) -const writeFile = promisify(fs.writeFile) - const input = {} const keyBindings = [ @@ -186,7 +169,7 @@ telc.isRight = input.isRight telc.isSelect = input.isSelect telc.isBackspace = input.isBackspace -class AppElement extends FocusElement { +export default class AppElement extends FocusElement { constructor(backend, config = {}) { super() @@ -269,7 +252,7 @@ class AppElement extends FocusElement { this.queueTimeLabel = new Label('') this.queuePane.addChild(this.queueTimeLabel) - this.queueListingElement.on('select', item => this.updateQueueLengthLabel()) + this.queueListingElement.on('select', _item => this.updateQueueLengthLabel()) this.queueListingElement.on('open', item => this.openSpecialOrThroughSystem(item)) this.queueListingElement.on('queue', item => this.play(item)) this.queueListingElement.on('remove', item => this.unqueue(item)) @@ -441,7 +424,7 @@ class AppElement extends FocusElement { this.autoDJControl = new ToggleControl('Enable Auto-DJ?', { setValue: val => (this.enableAutoDJ = val), - getValue: val => this.enableAutoDJ, + getValue: () => this.enableAutoDJ, getEnabled: () => this.config.canControlPlayback }) @@ -1374,9 +1357,9 @@ class AppElement extends FocusElement { } showMenuForItemElement(el, listing) { - const { editMode } = this + // const { editMode } = this const { canControlQueue, canProcessMetadata } = this.config - const anyMarked = editMode && this.markGrouplike.items.length > 0 + // const anyMarked = editMode && this.markGrouplike.items.length > 0 const generatePageForItem = item => { const emitControls = play => () => { @@ -1387,7 +1370,7 @@ class AppElement extends FocusElement { }) } - const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') + // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing) ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)} : {label: 'Expand saved timestamps', action: () => this.expandTimestamps(item, listing)} @@ -1431,13 +1414,12 @@ class AppElement extends FocusElement { // 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'})}, + + // 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}, - */ + // {divider: true}, canControlQueue && isPlayable(item) && {element: this.whereControl}, canControlQueue && isGroup(item) && {element: this.orderControl}, @@ -1450,10 +1432,8 @@ class AppElement extends FocusElement { 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)}, - */ + // !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}, @@ -1475,7 +1455,7 @@ class AppElement extends FocusElement { ].filter(Boolean) // TODO: Implement this! :P - const isMarked = false + // const isMarked = false this.showContextMenu({ x: el.absLeft, @@ -2250,7 +2230,7 @@ class AppElement extends FocusElement { } get selectedQueuePlayer() { return this.getDep('selectedQueuePlayer') } - set selectedQueuePlayer(v) { return this.setDep('selectedQueuePlayer', v) } + set selectedQueuePlayer(v) { this.setDep('selectedQueuePlayer', v) } } class GrouplikeListingElement extends Form { @@ -2745,6 +2725,7 @@ class GrouplikeListingElement extends Form { // Just to make the selected-track-info bar fill right away (if it wasn't // already filled by a previous this.curIndex set). + /* eslint-disable-next-line no-self-assign */ form.curIndex = form.curIndex this.fixAllLayout() @@ -2916,7 +2897,6 @@ class GrouplikeListingForm extends ListScrollForm { set curIndex(newIndex) { this.setDep('curIndex', newIndex) this.emit('select', this.inputs[this.curIndex]) - return newIndex } get curIndex() { @@ -3007,7 +2987,6 @@ class GrouplikeListingForm extends ListScrollForm { } dragLeftRange(item) { - const { items } = this.app.markGrouplike if (this.selectMode === 'select') { if (!this.oldMarkedItems.includes(item)) { this.app.unmarkItem(item) @@ -3331,8 +3310,8 @@ class InlineListPickerElement extends FocusElement { this.showContextMenu({ x: this.absLeft + ansi.measureColumns(this.labelText) + 1, y: this.absTop + 1, - items: this.options.map(({ value, label }, index) => ({ - label: label, + items: this.options.map(({ label }, index) => ({ + label, action: () => { this.curIndex = index }, @@ -3378,7 +3357,7 @@ class InlineListPickerElement extends FocusElement { } get curIndex() { return this.getDep('curIndex') } - set curIndex(v) { return this.setDep('curIndex', v) } + set curIndex(v) { this.setDep('curIndex', v) } } // Quite hacky, but ATM I can't think of any way to neatly tie getDep/setDep @@ -3860,7 +3839,7 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } else if (!this.isPlayable) { writable.write('F') } else if (record.downloading) { - writable.write(braille[Math.floor(Date.now() / 250) % 6]) + writable.write(brailleChar) } else if (this.app.SQP.playingTrack === this.item) { writable.write('\u25B6') } else if (this.app.hasTimestampsFile(this.item)) { @@ -3905,7 +3884,6 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement { || last.timestampEnd !== Infinity && last.timestampEnd || last.timestamp) const strings = getTimeStringsFromSec(data.timestamp, duration) - const stringsEnd = getTimeStringsFromSec(data.timestampEnd, duration) this.text = ( /* @@ -4319,7 +4297,6 @@ class PlaybackInfoElement extends FocusElement { this.app.backend.queuePlayers.length > 1 && { label: 'Delete', action: () => { - const { parent } = this this.app.removeQueuePlayer(this.queuePlayer) } } @@ -4414,17 +4391,17 @@ class PlaybackInfoElement extends FocusElement { } get curSecTotal() { return this.getDep('curSecTotal') } - set curSecTotal(v) { return this.setDep('curSecTotal', v) } + set curSecTotal(v) { this.setDep('curSecTotal', v) } get lenSecTotal() { return this.getDep('lenSecTotal') } - set lenSecTotal(v) { return this.setDep('lenSecTotal', v) } + set lenSecTotal(v) { this.setDep('lenSecTotal', v) } get volume() { return this.getDep('volume') } - set volume(v) { return this.setDep('volume', v) } + set volume(v) { this.setDep('volume', v) } get isLooping() { return this.getDep('isLooping') } - set isLooping(v) { return this.setDep('isLooping', v) } + set isLooping(v) { this.setDep('isLooping', v) } get isPaused() { return this.getDep('isPaused') } - set isPaused(v) { return this.setDep('isPaused', v) } + set isPaused(v) { this.setDep('isPaused', v) } get currentTrack() { return this.getDep('currentTrack') } - set currentTrack(v) { return this.setDep('currentTrack', v) } + set currentTrack(v) { this.setDep('currentTrack', v) } } class OpenPlaylistDialog extends Dialog { @@ -5259,9 +5236,9 @@ class Menubar extends ListScrollForm { } get color() { return this.getDep('color') } - set color(v) { return this.setDep('color', v) } + set color(v) { this.setDep('color', v) } get attribute() { return this.getDep('attribute') } - set attribute(v) { return this.setDep('attribute', v) } + set attribute(v) { this.setDep('attribute', v) } } class PartyBanner extends DisplayElement { @@ -5396,5 +5373,3 @@ class NotesTextEditor extends TuiTextEditor { } } */ - -module.exports = AppElement -- cgit 1.3.0-6-gf8a5