diff options
-rw-r--r-- | backend.js | 5 | ||||
-rw-r--r-- | socket.js | 133 | ||||
-rw-r--r-- | ui.js | 49 |
3 files changed, 162 insertions, 25 deletions
diff --git a/backend.js b/backend.js index fe7014b..e75b1a8 100644 --- a/backend.js +++ b/backend.js @@ -283,6 +283,11 @@ class QueuePlayer extends EventEmitter { return focusItem } + replaceAllItems(newItems) { + this.queueGrouplike.items = newItems + this.emitQueueUpdated() + } + clearQueuePast(track) { const { items } = this.queueGrouplike const index = items.indexOf(track) + 1 diff --git a/socket.js b/socket.js index 12d08af..aea2ee8 100644 --- a/socket.js +++ b/socket.js @@ -22,9 +22,15 @@ import { } from './serialized-backend.js' import { - silenceEvents + getTimeStringsFromSec, + silenceEvents, } from './general-util.js' +// This is expected to be the same across both the client and the server. +// There will probably be inconsistencies between sender clients and receiving +// clients / the server otherwise. +const DEFAULT_NICKNAME = '(Unnamed)' + function serializeCommandToData(command) { // Turn a command into a string/buffer that can be sent over a socket. return JSON.stringify(command) @@ -96,7 +102,7 @@ function validateCommand(command) { case 'clear-queue-up-to': return ( typeof command.queuePlayer === 'string' && - isItemRef(command.topItem) + isItemRef(command.track) ) case 'distribute-queue': return ( @@ -138,7 +144,8 @@ function validateCommand(command) { return ( typeof command.queuePlayer === 'string' && Array.isArray(command.tracks) && - command.tracks.every(track => isItemRef(track)) + command.tracks.every(track => isItemRef(track)) && + ['shuffle'].includes(command.why) ) case 'seek-to': return ( @@ -148,6 +155,7 @@ function validateCommand(command) { case 'set-nickname': return ( typeof command.nickname === 'string' && + typeof command.oldNickname === 'string' && command.nickname.length >= 1 && command.nickname.length <= 12 ) @@ -225,7 +233,7 @@ export function makeSocketServer() { server.on('connection', socket => { sockets.push(socket) - let nickname = '(Unnamed)' + let nickname = DEFAULT_NICKNAME socket.on('close', () => { if (sockets.includes(socket)) { @@ -261,7 +269,7 @@ export function makeSocketServer() { readySockets.push(socket) if (readySockets.length === sockets.length) { for (const socket of sockets) { - socket.write(JSON.stringify({ + socket.write(serializeCommandToData({ sender: 'server', code: 'set-pause', queuePlayer: command.queuePlayer, @@ -277,13 +285,13 @@ export function makeSocketServer() { case 'sync-playback': for (const QP of server.canonicalBackend.queuePlayers) { if (QP.timeData) { - socket.write(JSON.stringify({ + socket.write(serializeCommandToData({ sender: 'server', code: 'seek-to', queuePlayer: QP.id, time: QP.timeData.curSecTotal }) + '\n') - socket.write(JSON.stringify({ + socket.write(serializeCommandToData({ sender: 'server', code: 'set-pause', queuePlayer: QP.id, @@ -304,8 +312,11 @@ export function makeSocketServer() { } // If it's a 'set-nickname' command, save the nickname. + // Also attach the old nickname for display in log messages. if (command.code === 'set-nickname') { + command.oldNickname = nickname + command.senderNickname = nickname nickname = command.nickname } @@ -314,7 +325,7 @@ export function makeSocketServer() { const otherSockets = sockets.filter(s => s !== socket) for (const socket of otherSockets) { - socket.write(JSON.stringify(command) + '\n') + socket.write(serializeCommandToData(command) + '\n') } })) @@ -326,7 +337,7 @@ export function makeSocketServer() { } } - socket.write(JSON.stringify({ + socket.write(serializeCommandToData({ sender: 'server', code: 'initialize-backend', backend: savedBackend @@ -343,6 +354,7 @@ export function makeSocketClient() { const client = new EventEmitter() client.socket = new net.Socket() + client.nickname = DEFAULT_NICKNAME client.sendCommand = function(command) { const data = serializeCommandToData(command) @@ -351,7 +363,9 @@ export function makeSocketClient() { } client.setNickname = function(nickname) { - client.sendCommand({code: 'set-nickname', nickname}) + let oldNickname = client.nickname + client.nickname = nickname + client.sendCommand({code: 'set-nickname', nickname, oldNickname}) } client.socket.on('data', perLine(line => { @@ -384,11 +398,78 @@ export function attachBackendToSocketClient(backend, client, { backend.setAlwaysStartPaused(true) function logCommand(command) { - const nickname = command.sender === 'server' ? 'the server' : command.nickname - backend.showLogMessage(`${nickname} sent ${command.code}!`) + const nickToMessage = nickname => `\x1b[32;1m${nickname}\x1b[0m` + const itemToMessage = item => `\x1b[32m"${item.name}"\x1b[0m` + let fullmsg = '' // may be overridden + let actionmsg = `sent ${command.code}` // fallback + switch (command.code) { + case 'clear-queue': + actionmsg = 'cleared the queue' + break + case 'clear-queue-past': + actionmsg = `cleared the queue past ${itemToMessage(command.track)}` + break + case 'clear-queue-up-to': + actionmsg = `cleared the queue up to ${itemToMessage(command.track)}` + break + case 'distribute-queue': + actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}` + break + case 'initialize-backend': + return + case 'play': + actionmsg = `started playing ${itemToMessage(command.track)}` + break + case 'queue': { + let afterMessage = '' + if (isItemRef(command.afterItem)) { + afterMessage = ` after ${itemToMessage(command.afterItem)}` + } else if (command.afterItem === 'FRONT') { + afterMessage = ` at the front of the queue` + } + actionmsg = `queued ${itemToMessage(command.topItem)}` + afterMessage + break + } + case 'restore-queue': + if (command.why === 'shuffle') { + actionmsg = 'shuffled the queue' + } + break + case 'seek-to': + // TODO: the second value here should be the duration of the track + // (this will make values like 0:0x:yy / 1:xx:yy appear correctly) + actionmsg = `seeked to ${getTimeStringsFromSec(command.time, command.time).timeDone}` + break + case 'set-nickname': + fullmsg = `${nickToMessage(command.nickname)} updated their nickname (from ${nickToMessage(command.oldNickname)})` + break + case 'set-pause': + if (command.paused) { + actionmsg = 'paused the player' + } else { + actionmsg = 'resumed the player' + } + break + case 'stop-playing': + actionmsg = 'stopped the player' + break + case 'unqueue': + actionmsg = `removed ${itemToMessage(command.topItem)} from the queue` + break + case 'status': + return + } + if (!fullmsg) { + const nickname = command.sender === 'server' ? 'the server' : command.senderNickname + fullmsg = `${nickToMessage(nickname)} ${actionmsg}` + } + backend.showLogMessage(fullmsg) } - client.on('sent-command', logCommand) + client.on('sent-command', command => { + command.senderNickname = client.nickname + logCommand(command) + }) client.on('command', async command => { logCommand(command) @@ -419,17 +500,17 @@ export function attachBackendToSocketClient(backend, client, { return case 'clear-queue-past': if (QP) silenceEvents(QP, ['clear-queue-past'], () => QP.clearQueuePast( - restoreNewItem(command.topItem, getPlaylistSources()) + restoreNewItem(command.track, getPlaylistSources()) )) return case 'clear-queue-up-to': if (QP) silenceEvents(QP, ['clear-queue-up-to'], () => QP.clearQueueUpTo( - restoreNewItem(command.topItem, getPlaylistSources()) + restoreNewItem(command.track, getPlaylistSources()) )) return case 'distribute-queue': if (QP) silenceEvents(QP, ['distribute-queue'], () => QP.distributeQueue( - restoreNewItem(command.topItem, getPlaylistSources()), + restoreNewItem(command.topItem), { how: command.opts.how, rangeEnd: command.opts.rangeEnd @@ -452,7 +533,7 @@ export function attachBackendToSocketClient(backend, client, { return case 'queue': if (QP) silenceEvents(QP, ['queue'], () => QP.queue( - restoreNewItem(command.topItem, getPlaylistSources()), + restoreNewItem(command.topItem), isItemRef(command.afterItem) ? restoreNewItem(command.afterItem, getPlaylistSources()) : command.afterItem, { movePlayingTrack: command.opts.movePlayingTrack @@ -461,14 +542,17 @@ export function attachBackendToSocketClient(backend, client, { return case 'restore-queue': if (QP) { - QP.queueGrouplike.items = command.tracks.map(refData => restoreNewItem(refData)) - // TODO: target just the one queue player. hacks = illegal - updateRestoredTracksUsingPlaylists(backend, getPlaylistSources()) + QP.replaceAllItems(command.tracks.map( + refData => restoreNewItem(refData, getPlaylistSources()) + )) } + return case 'seek-to': if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time)) return case 'set-pause': { + // TODO: there's an event leak here when toggling pause while + // nothing is playing let playingThisTrack = true QP.once('playing new track', () => { playingThisTrack = false @@ -500,19 +584,19 @@ export function attachBackendToSocketClient(backend, client, { }) }) - backend.on('clear-queue-past', (queuePlayer, topItem) => { + backend.on('clear-queue-past', (queuePlayer, track) => { client.sendCommand({ code: 'clear-queue-past', queuePlayer: queuePlayer.id, - topItem: saveItemReference(topItem) + track: saveItemReference(track) }) }) - backend.on('clear-queue-up-to', (queuePlayer, topItem) => { + backend.on('clear-queue-up-to', (queuePlayer, track) => { client.sendCommand({ code: 'clear-queue-up-to', queuePlayer: queuePlayer.id, - topItem: saveItemReference(topItem) + track: saveItemReference(track) }) }) @@ -572,6 +656,7 @@ export function attachBackendToSocketClient(backend, client, { backend.on('shuffle-queue', queuePlayer => { client.sendCommand({ code: 'restore-queue', + why: 'shuffle', queuePlayer: queuePlayer.id, tracks: queuePlayer.queueGrouplike.items.map(saveItemReference) }) diff --git a/ui.js b/ui.js index 05e359e..df02edf 100644 --- a/ui.js +++ b/ui.js @@ -5530,6 +5530,10 @@ class Log extends ListScrollForm { } newLogMessage(text) { + if (this.inputs.length === 10) { + this.removeInput(this.inputs[0]) + } + const logMessage = new LogMessage(text) this.addInput(logMessage) this.fixLayout() @@ -5538,4 +5542,47 @@ class Log extends ListScrollForm { } } -class LogMessage extends Button {} +class LogMessage extends FocusElement { + constructor(text) { + super() + + this.label = new LogMessageLabel(text) + 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 { + 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] : [] + } +} |