diff options
Diffstat (limited to 'ui.js')
-rw-r--r-- | ui.js | 304 |
1 files changed, 263 insertions, 41 deletions
diff --git a/ui.js b/ui.js index b784688..cb38990 100644 --- a/ui.js +++ b/ui.js @@ -18,6 +18,7 @@ import telc from 'tui-lib/util/telchars' import unic from 'tui-lib/util/unichars' import {getAllCrawlersForArg} from './crawlers.js' +import {originalSymbol} from './socket.js' import processSmartPlaylist from './smart-playlist.js' import UndoManager from './undo-manager.js' @@ -33,9 +34,12 @@ import { cloneGrouplike, collapseGrouplike, countTotalTracks, + findItemObject, flattenGrouplike, getCorrespondingFileForItem, getCorrespondingPlayableForFile, + getFlatTrackList, + getFlatGroupList, getItemPath, getNameWithoutTrackNumber, isGroup, @@ -49,8 +53,13 @@ import { shuffleOrderOfGroups, } from './playlist-utils.js' +import { + updateRestoredTracksUsingPlaylists, + getWaitingTrackData +} from './serialized-backend.js' + /* text editor features disabled because theyre very much incomplete and havent - * gotten much use from me or anyonea afaik! + * gotten much use from me or anyone afaik! const TuiTextEditor = require('tui-text-editor') */ @@ -179,6 +188,8 @@ export default class AppElement extends FocusElement { this.isPartyHost = false this.enableAutoDJ = false + // this.playlistSources = [] + this.config = Object.assign({ canControlPlayback: true, canControlQueue: true, @@ -188,7 +199,8 @@ export default class AppElement extends FocusElement { themeColor: 4, // blue seekToStartThreshold: 3, showTabberPane: true, - stopPlayingUponQuit: true + stopPlayingUponQuit: true, + showPartyControls: false }, config) // TODO: Move edit mode stuff to the backend! @@ -232,6 +244,18 @@ export default class AppElement extends FocusElement { }) */ + this.logPane = new Pane() + this.addChild(this.logPane) + + this.log = new Log() + this.logPane.addChild(this.log) + this.logPane.visible = false + + this.log.on('log message', () => { + this.logPane.visible = true + this.fixLayout() + }) + if (!this.config.showTabberPane) { this.tabberPane.visible = false } @@ -243,8 +267,6 @@ export default class AppElement extends FocusElement { this.metadataStatusLabel.visible = false this.tabberPane.addChild(this.metadataStatusLabel) - this.newGrouplikeListing() - this.queueListingElement = new QueueListingElement(this) this.setupCommonGrouplikeListingEvents(this.queueListingElement) this.queuePane.addChild(this.queueListingElement) @@ -264,6 +286,11 @@ export default class AppElement extends FocusElement { this.queueListingElement.on('select main listing', () => this.selected()) + if (this.config.showPartyControls) { + const sharedSourcesListing = this.newGrouplikeListing() + sharedSourcesListing.loadGrouplike(this.backend.sharedSourcesGrouplike) + } + this.playbackPane = new Pane() this.addChild(this.playbackPane) @@ -452,12 +479,15 @@ export default class AppElement extends FocusElement { bindListeners() { for (const key of [ - 'handlePlaying', + 'handlePlayingDetails', 'handleReceivedTimeData', 'handleProcessMetadataProgress', 'handleQueueUpdated', 'handleAddedQueuePlayer', 'handleRemovedQueuePlayer', + 'handleLogMessage', + 'handleGotSharedSources', + 'handleSharedSourcesUpdated', 'handleSetLoopQueueAtEnd' ]) { this[key] = this[key].bind(this) @@ -489,10 +519,6 @@ export default class AppElement extends FocusElement { PIE.on('seek back', () => PIE.queuePlayer.seekBack(5)) PIE.on('seek ahead', () => PIE.queuePlayer.seekAhead(5)) PIE.on('toggle pause', () => PIE.queuePlayer.togglePause()) - - queuePlayer.on('received time data', this.handleReceivedTimeData) - queuePlayer.on('playing', this.handlePlaying) - queuePlayer.on('queue updated', this.handleQueueUpdated) } removeQueuePlayerListenersAndUI(queuePlayer, updateUI = true) { @@ -519,24 +545,39 @@ export default class AppElement extends FocusElement { this.queuePlayersToActOn.splice(index, 1) } - queuePlayer.removeListener('receivedTimeData', this.handleReceivedTimeData) - queuePlayer.removeListener('playing', this.handlePlaying) - queuePlayer.removeListener('queue updated', this.handleQueueUpdated) queuePlayer.stopPlaying() } attachBackendListeners() { + // Backend-specialized listeners this.backend.on('processMetadata progress', this.handleProcessMetadataProgress) this.backend.on('added queue player', this.handleAddedQueuePlayer) this.backend.on('removed queue player', this.handleRemovedQueuePlayer) - this.backend.on('set-loop-queue-at-end', this.handleSetLoopQueueAtEnd) + this.backend.on('log message', this.handleLogMessage) + this.backend.on('got shared sources', this.handleGotSharedSources) + this.backend.on('shared sources updated', this.handleSharedSourcesUpdated) + this.backend.on('set loop queue at end', this.handleSetLoopQueueAtEnd) + + // Backend as queue player proxy listeners + this.backend.on('QP: playing details', this.handlePlayingDetails) + this.backend.on('QP: received time data', this.handleReceivedTimeData) + this.backend.on('QP: queue updated', this.handleQueueUpdated) } removeBackendListeners() { + // Backend-specialized listeners this.backend.removeListener('processMetadata progress', this.handleProcessMetadataProgress) this.backend.removeListener('added queue player', this.handleAddedQueuePlayer) this.backend.removeListener('removed queue player', this.handleRemovedQueuePlayer) - this.backend.removeListener('set-loop-queue-at-end', this.handleSetLoopQueueAtEnd) + this.backend.removeListener('log message', this.handleLogMessage) + this.backend.removeListener('got shared sources', this.handleGotSharedSources) + this.backend.removeListener('shared sources updated', this.handleSharedSourcesUpdated) + this.backend.removeListener('set loop queue at end', this.handleSetLoopQueueAtEnd) + + // Backend as queue player proxy listeners + this.backend.removeListener('QP: playing details', this.handlePlayingDetails) + this.backend.removeListener('QP: received time data', this.handleReceivedTimeData) + this.backend.removeListener('QP: queue updated', this.handleQueueUpdated) } handleAddedQueuePlayer(queuePlayer) { @@ -550,11 +591,32 @@ export default class AppElement extends FocusElement { } } + handleLogMessage(messageInfo) { + this.log.newLogMessage(messageInfo) + } + + handleGotSharedSources(socketId, sharedSources) { + for (const grouplikeListing of this.tabber.tabberElements) { + if (grouplikeListing.grouplike === this.backend.sharedSourcesGrouplike) { + grouplikeListing.loadGrouplike(this.backend.sharedSourcesGrouplike, false) + } + } + } + + handleSharedSourcesUpdated(socketId, partyGrouplike) { + for (const grouplikeListing of this.tabber.tabberElements) { + if (grouplikeListing.grouplike === partyGrouplike) { + grouplikeListing.loadGrouplike(partyGrouplike, false) + } + } + this.clearCachedMarkStatuses() + } + handleSetLoopQueueAtEnd() { this.updateQueueLengthLabel() } - async handlePlaying(track, oldTrack, startTime, queuePlayer) { + async handlePlayingDetails(queuePlayer, track, oldTrack, startTime) { const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateTrack() @@ -589,7 +651,7 @@ export default class AppElement extends FocusElement { } } - handleReceivedTimeData(timeData, oldTimeData, queuePlayer) { + handleReceivedTimeData(queuePlayer, timeData, oldTimeData) { const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateProgress() @@ -916,6 +978,10 @@ export default class AppElement extends FocusElement { this.queueListingElement.selectAndShow(item) } + shareWithParty(item) { + this.backend.shareWithParty(item) + } + replaceMark(items) { this.markGrouplike.items = items.slice(0) // Don't share the array! :) this.emitMarkChanged() @@ -1043,7 +1109,11 @@ export default class AppElement extends FocusElement { } emitMarkChanged() { + this.clearCachedMarkStatuses() this.emit('mark changed') + } + + clearCachedMarkStatuses() { this.cachedMarkStatuses = new Map() this.scheduleDrawWithoutPropertyChange() } @@ -1445,6 +1515,32 @@ export default class AppElement extends FocusElement { }) } + const itemPath = getItemPath(item) + const [rootGroup, _partySources, sharedGroup] = itemPath + + // This is the hack mentioned in the todo!!!! + if (this.config.showPartyControls && rootGroup.isPartySources) { + const playlists = this.tabber.tabberElements + .map(grouplikeListing => getItemPath(grouplikeListing.grouplike)[0]) + .filter(root => !root.isPartySources) + + let possibleChoices = [] + if (item.downloaderArg) { + possibleChoices = getFlatTrackList({items: playlists}) + } else if (item.items) { + possibleChoices = getFlatGroupList({items: playlists}) + } + if (possibleChoices) { + item = findItemObject(item, possibleChoices) + } + + if (!item) { + return [ + {label: `(Couldn't find this in your music)`} + ] + } + } + // TODO: Implement this! :P // const isMarked = false @@ -1499,30 +1595,36 @@ export default class AppElement extends FocusElement { // 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}, - 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.config.showPartyControls && !rootGroup.isPartySources) + ? [ + {label: 'Share with party', action: () => this.shareWithParty(item)}, + {divider: true} + ] : [ - isGroup(item) && {element: this.selectGrouplikeItemsControl}, - this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item, true)}, - this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item, 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()}] + : [ + 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)}, + ]) ]) ] } @@ -1588,6 +1690,9 @@ export default class AppElement extends FocusElement { grouplike = await processSmartPlaylist(grouplike) + // this.playlistSources.push(grouplike) + // updateRestoredTracksUsingPlaylists(this.backend, this.playlistSources) + if (!this.tabber.currentElement || newTab && this.tabber.currentElement.grouplike) { const grouplikeListing = this.newGrouplikeListing() grouplikeListing.loadGrouplike(grouplike) @@ -1701,10 +1806,21 @@ export default class AppElement extends FocusElement { } */ + if (this.logPane.visible) { + this.logPane.w = leftWidth + this.logPane.h = 6 + this.log.fillParent() + this.log.fixAllLayout() + } + if (this.tabberPane.visible) { this.tabberPane.w = leftWidth this.tabberPane.y = bottomY this.tabberPane.h = topY - this.tabberPane.y + if (this.logPane.visible) { + this.tabberPane.h -= this.logPane.h + this.logPane.y = this.tabberPane.bottom + } /* if (this.textInfoPane.visible) { this.tabberPane.h -= this.textInfoPane.h @@ -4417,9 +4533,14 @@ class PlaybackInfoElement extends FocusElement { this.isLooping = player.isLooping this.isPaused = player.isPaused - this.progressBarLabel.text = '-'.repeat(Math.floor(this.w / lenSecTotal * curSecTotal)) + if (duration) { + this.progressBarLabel.text = '-'.repeat(Math.floor(this.w / lenSecTotal * curSecTotal)) + this.progressTextLabel.text = timeDone + ' / ' + duration + } else { + this.progressBarLabel.text = '' + this.progressTextLabel.text = timeDone + } - this.progressTextLabel.text = timeDone + ' / ' + duration if (player.isLooping) { this.progressTextLabel.text += ' [Looping]' } @@ -4430,6 +4551,7 @@ class PlaybackInfoElement extends FocusElement { refreshTrackText(maxNameWidth = Infinity) { const { playingTrack } = this.queuePlayer + const waitingTrackData = getWaitingTrackData(this.queuePlayer) if (playingTrack) { this.currentTrack = playingTrack const { name } = playingTrack @@ -4441,6 +4563,11 @@ class PlaybackInfoElement extends FocusElement { this.progressBarLabel.text = '' this.progressTextLabel.text = '(Starting..)' this.timeData = {} + } else if (waitingTrackData) { + const { name } = waitingTrackData + this.clearInfoText() + this.trackNameLabel.text = name + this.progressTextLabel.text = '(Waiting to play, once found in playlist source.)' } else { this.clearInfoText() } @@ -5483,3 +5610,98 @@ class NotesTextEditor extends TuiTextEditor { } } */ + +class Log extends ListScrollForm { + constructor() { + super('vertical') + } + + newLogMessage(messageInfo) { + if (this.inputs.length === 10) { + this.removeInput(this.inputs[0]) + } + + if (messageInfo.mayCombine) { + // If a message is specified to "combine", it'll replace an immediately + // previous message of the same code and sender. + const previous = this.inputs[this.inputs.length - 1] + if ( + previous && + previous.info.code === messageInfo.code && + previous.info.sender === messageInfo.sender + ) { + // If the code and sender match, just remove the previous message. + // It'll be replaced by the one we're about to add! + this.removeInput(previous) + } + } + + const logMessage = new LogMessage(messageInfo) + this.addInput(logMessage) + this.fixLayout() + this.scrollToEnd() + this.emit('log message', logMessage) + return logMessage + } +} + +class LogMessage extends FocusElement { + constructor(info) { + super() + + this.info = info + + const { + text, + isVerbose = false + } = info + + this.label = new LogMessageLabel(text, isVerbose) + this.addChild(this.label) + } + + fixLayout() { + this.w = this.parent.contentW + this.label.w = this.contentW + this.h = this.label.h + } + + clicked(button) { + if (button === 'left') { + this.root.select(this) + return false + } + } +} + +class LogMessageLabel extends WrapLabel { + constructor(text, isVerbose = false) { + super(text) + + this.isVerbose = isVerbose + } + + writeTextTo(writable) { + const w = this.w + const lines = this.getWrappedLines() + for (let i = 0; i < lines.length; i++) { + const text = this.processFormatting(lines[i]) + writable.write(ansi.moveCursor(this.absTop + i, this.absLeft)) + writable.write(text) + const width = ansi.measureColumns(text) + if (width < w && this.textAttributes.length) { + writable.write(ansi.setAttributes([ansi.A_RESET, ...this.textAttributes])) + writable.write(' '.repeat(w - width)) + } + } + } + + set textAttributes(val) {} + + get textAttributes() { + return [ + this.parent.isSelected ? 40 : null, + this.isVerbose ? 2 : null + ].filter(x => x !== null) + } +} |