« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--backend.js16
-rwxr-xr-xindex.js5
-rw-r--r--socket.js26
-rw-r--r--todo.txt2
-rw-r--r--ui.js72
5 files changed, 100 insertions, 21 deletions
diff --git a/backend.js b/backend.js
index b629424..86da76e 100644
--- a/backend.js
+++ b/backend.js
@@ -690,8 +690,8 @@ export default class Backend extends EventEmitter {
   } = {}) {
     super()
 
-    this.playerName = playerName;
-    this.playerOptions = playerOptions;
+    this.playerName = playerName
+    this.playerOptions = playerOptions
 
     if (playerOptions.length && !playerName) {
       throw new Error(`Must specify playerName to specify playerOptions`);
@@ -888,4 +888,16 @@ export default class Backend extends EventEmitter {
   showLogMessage(messageInfo) {
     this.emit('log message', messageInfo)
   }
+
+  loadPartyGrouplike(partyGrouplike) {
+    this.emit('got party grouplike', partyGrouplike)
+  }
+
+  shareWithParty(item) {
+    this.emit('share with party', item)
+  }
+
+  partyGrouplikeUpdated(partyGrouplike) {
+    this.emit('party grouplike updated', partyGrouplike)
+  }
 }
diff --git a/index.js b/index.js
index efee187..21bd80a 100755
--- a/index.js
+++ b/index.js
@@ -127,7 +127,10 @@ async function main() {
   const { appElement, dirtyTerminal, flushable, root } = await setupClient({
     backend,
     screenInterface: new CommandLineInterface(),
-    writable: process.stdout
+    writable: process.stdout,
+    appConfig: {
+      showPartyControls: !!(options['socket-server'] || options['socket-client'])
+    }
   })
 
   appElement.on('quitRequested', () => {
diff --git a/socket.js b/socket.js
index c6c3d19..5f3ded0 100644
--- a/socket.js
+++ b/socket.js
@@ -40,6 +40,8 @@ import {
 // clients / the server otherwise.
 const DEFAULT_NICKNAME = '(Unnamed)'
 
+const originalSymbol = Symbol('Original item')
+
 function serializeCommandToData(command) {
   // Turn a command into a string/buffer that can be sent over a socket.
   return JSON.stringify(command)
@@ -422,8 +424,11 @@ export function attachBackendToSocketClient(backend, client, {
   // All actual logic for instances of the mtui backend interacting with each
   // other through commands lives here.
 
+  const partyGrouplike = {name: 'My Party Sources', isPartySources: true, items: []}
+
   backend.setAlwaysStartPaused(true)
   backend.setWaitWhenDonePlaying(true)
+  backend.loadPartyGrouplike(partyGrouplike)
 
   function logCommand(command) {
     const nickToMessage = nickname => `\x1b[32;1m${nickname}\x1b[0m`
@@ -744,6 +749,27 @@ export function attachBackendToSocketClient(backend, client, {
       topItem: saveItemReference(topItem)
     })
   })
+
+  backend.on('share with party', origItem => {
+    let newItem = {
+      ...origItem,
+      [parentSymbol]: partyGrouplike,
+      [originalSymbol]: origItem
+    }
+
+    if (isGroup(newItem)) {
+      newItem = updateGroupFormat(newItem)
+    } else if (isTrack(newItem)) {
+      newItem = updateTrackFormat(newItem)
+    } else {
+      return
+    }
+
+    if (partyGrouplike.items.every(x => x[originalSymbol] !== origItem)) {
+      partyGrouplike.items.push(newItem)
+      backend.partyGrouplikeUpdated(partyGrouplike)
+    }
+  })
 }
 
 export function attachSocketServerToBackend(server, backend) {
diff --git a/todo.txt b/todo.txt
index bfb6e98..f2a6562 100644
--- a/todo.txt
+++ b/todo.txt
@@ -691,6 +691,8 @@ TODO: Apparently pressing any key while the UI is booting up will make the
       screen is resized. I think we're interrupting a control sequence somehow,
       and that isn't being handled very well?
 
+TODO: Tabber tab list should be accessible via tab (key).
+
 TODO: Pressing escape while you've got items selected should deselect those
       items, rather than stop playback! ...Or SHOULD IT??? Well, yes. But it's
       still handy to not be locked out of stopping playback altogether.
diff --git a/ui.js b/ui.js
index 65ca2d1..b9c417a 100644
--- a/ui.js
+++ b/ui.js
@@ -195,7 +195,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!
@@ -478,7 +479,9 @@ export default class AppElement extends FocusElement {
       'handleAddedQueuePlayer',
       'handleRemovedQueuePlayer',
       'handleSetLoopQueueAtEnd',
-      'handleLogMessage'
+      'handleLogMessage',
+      'handleGotPartyGrouplike',
+      'handlePartyGrouplikeUpdated'
     ]) {
       this[key] = this[key].bind(this)
     }
@@ -551,6 +554,8 @@ export default class AppElement extends FocusElement {
     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 party grouplike', this.handleGotPartyGrouplike)
+    this.backend.on('party grouplike updated', this.handlePartyGrouplikeUpdated)
   }
 
   removeBackendListeners() {
@@ -559,6 +564,8 @@ export default class AppElement extends FocusElement {
     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 party grouplike', this.handleGotPartyGrouplike)
+    this.backend.removeListener('party grouplike updated', this.handlePartyGrouplikeUpdated)
   }
 
   handleAddedQueuePlayer(queuePlayer) {
@@ -580,6 +587,18 @@ export default class AppElement extends FocusElement {
     this.log.newLogMessage(messageInfo)
   }
 
+  handleGotPartyGrouplike(partyGrouplike) {
+    this.newPartyTab(partyGrouplike)
+  }
+
+  handlePartyGrouplikeUpdated(partyGrouplike) {
+    for (const grouplikeListing of this.tabber.tabberElements) {
+      if (grouplikeListing.grouplike === partyGrouplike) {
+        grouplikeListing.loadGrouplike(partyGrouplike, false)
+      }
+    }
+  }
+
   async handlePlayingDetails(track, oldTrack, startTime, queuePlayer) {
     const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer)
     if (PIE) {
@@ -942,6 +961,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()
@@ -1474,6 +1497,7 @@ export default class AppElement extends FocusElement {
       // TODO: Implement this! :P
       // const isMarked = false
 
+      const rootGroup = getItemPath(item)[0]
       // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
       const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing)
         ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)}
@@ -1525,22 +1549,29 @@ 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},
+          ...((this.config.showPartyControls && !rootGroup.isPartySources)
+            ? [
+              {label: 'Share with party', action: () => this.shareWithParty(item)},
+              {divider: true}
+            ]
+            : [
+              canControlQueue && isPlayable(item) && {element: this.whereControl},
+              canControlQueue && isGroup(item) && {element: this.orderControl},
+              canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)},
+              canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)},
+              {divider: true},
+
+              canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))},
+              canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))},
+              canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))},
+              isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)},
+              isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)},
+              // !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)},
+              // hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)},
+              canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
+              isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)},
+              {divider: true},
+            ]),
 
           timestampsItem,
           ...(item === this.markGrouplike
@@ -1919,6 +1950,11 @@ export default class AppElement extends FocusElement {
     })
   }
 
+  newPartyTab(partyGrouplike) {
+    const listing = this.newGrouplikeListing()
+    listing.loadGrouplike(partyGrouplike)
+  }
+
   cloneCurrentTab() {
     const grouplike = this.tabber.currentElement.grouplike
     const listing = this.newGrouplikeListing()