+ /*Canopy - The next generation of stoner streaming software
+Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.*/
+/**
+ * Class representing Playlist Manager UX within the Queue Panel
+ */
+class playlistManager{
+
+ /**
+ *
+ * @param {channel} client - Parent Client Management Object
+ * @param {Document} panelDocument - Panel Document
+ * @param {queuePanel} queuePanel - Parent Queue Panel Object
+ */
+ constructor(client, panelDocument, queuePanel){
+ /**
+ * Parent Client Management Object
+ */
+ this.client = client;
+
+ /**
+ * Panel Document
+ */
+ this.panelDocument = panelDocument;
+
+ /**
+ * Parent Queue Panel Object
+ */
+ this.queuePanel = queuePanel;
+
+ /**
+ * Map of which playlists are open and which are not, for better refresh handling
+ */
+ this.openMap = {
+ Channel: new Map(),
+ User: new Map()
+ };
+
+ //Define Listeners
+ this.defineListeners();
+ }
+
+ /**
+ * Handles Network-Related Event Listeners
+ */
+ defineListeners(){
+ this.client.socket.on("chanPlaylists", this.renderChannelPlaylists.bind(this));
+ this.client.socket.on("userPlaylists", this.renderUserPlaylists.bind(this));
+ }
+
+ docSwitch(){
+ //Grab menus
+ this.channelPlaylistDiv = this.panelDocument.querySelector("#queue-channel-playlist-div");
+ this.userPlaylistDiv = this.panelDocument.querySelector("#queue-user-playlist-div");
+
+ //Grab controls
+ this.createPlaylistSpan = this.panelDocument.querySelector('#queue-add-playlist-span');
+ this.channelPlaylistLabel = this.panelDocument.querySelector('#queue-channel-playlist-span');
+ this.channelPlaylistCaret = this.panelDocument.querySelector('#queue-channel-playlist-toggle');
+ this.userPlaylistLabel = this.panelDocument.querySelector('#queue-user-playlist-span');
+ this.userPlaylistCaret = this.panelDocument.querySelector('#queue-user-playlist-toggle');
+
+ //Force playlist re-render to fix controls
+ this.client.socket.emit('getChannelPlaylists');
+ this.client.socket.emit('getUserPlaylists');
+
+ //Setup Input
+ this.setupInput();
+ }
+
+ /**
+ * Handles Input-Related Event Listeners
+ */
+ setupInput(){
+ this.createPlaylistSpan.addEventListener('click', (event)=>{new newPlaylistPopup(event, this.client, this.queuePanel.ownerDoc)})
+ this.channelPlaylistLabel.addEventListener('click', this.toggleChannelPlaylists.bind(this));
+ this.userPlaylistLabel.addEventListener('click', this.toggleUserPlaylists.bind(this));
+ }
+
+ /* queue control button functions */
+
+ /**
+ * Toggle Channel Playlists
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ toggleChannelPlaylists(event){
+ //If the div is hidden
+ if(this.channelPlaylistDiv.style.display == 'none'){
+ //Light up the button
+ this.channelPlaylistLabel.classList.add('positive');
+ //Flip the caret
+ this.channelPlaylistCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill');
+ //Show the div
+ this.channelPlaylistDiv.style.display = '';
+ }else{
+ //Unlight the button
+ this.channelPlaylistLabel.classList.remove('positive');
+ //Flip the caret
+ this.channelPlaylistCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill');
+ //Hide the div
+ this.channelPlaylistDiv.style.display = 'none';
+ }
+ }
+
+ /**
+ * Toggle User Playlists
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ toggleUserPlaylists(event){
+ //If the div is hidden
+ if(this.userPlaylistDiv.style.display == 'none'){
+ //Light up the button
+ this.userPlaylistLabel.classList.add('positive');
+ //Flip the caret
+ this.userPlaylistCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill');
+ //Show the div
+ this.userPlaylistDiv.style.display = '';
+ }else{
+ //Unlight the button
+ this.userPlaylistLabel.classList.remove('positive');
+ //Flip the caret
+ this.userPlaylistCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill');
+ //Hide the div
+ this.userPlaylistDiv.style.display = 'none';
+ }
+ }
+
+ /**
+ * Checks which playlists where open before a refresh and re-opens them
+ * @param {String} location - Whether or not we're dealing with user or channel playlists
+ */
+ checkOpenPlaylists(location){
+ //If open map is a string, indicating we just renamed a playlist with it's media open
+ if(typeof this.openMap[location] == 'string'){
+ //Create new map to hold status with the new name of the renamed playlist already added
+ this.openMap[location] = new Map([[this.openMap[location], true]]);
+ }else{
+ //Create new map to hold status
+ this.openMap[location] = new Map();
+ }
+
+ let mediaContainerDivs = [];
+
+ if(location == 'Channel'){
+ mediaContainerDivs = this.channelPlaylistDiv.querySelectorAll('.queue-playlist-media-container-div')
+ }else{
+ mediaContainerDivs = this.userPlaylistDiv.querySelectorAll('.queue-playlist-media-container-div')
+ }
+
+ //For each container Div rendered
+ for(let containerDiv of mediaContainerDivs){
+ //Set whether or not it's visible in the map
+ this.openMap[location].set(containerDiv.dataset['playlist'], (containerDiv.style.display != 'none'));
+ }
+ }
+
+ //Main playlist rendering functions
+
+ /**
+ * Renders Channel Playlist list
+ * @param {Object} data - Data from server
+ */
+ renderChannelPlaylists(data){
+ //Check for open playlists
+ this.checkOpenPlaylists('Channel');
+
+ //Clear channel playlist div
+ this.channelPlaylistDiv.innerHTML = '';
+
+ //Append rendered playlists
+ this.channelPlaylistDiv.append(...this.renderPlaylists(data, 'Channel'));
+ }
+
+ /**
+ * Renders User Playlist list
+ * @param {Object} data - Data from server
+ */
+ renderUserPlaylists(data){
+ //Check for open playlists
+ this.checkOpenPlaylists('User');
+
+ //Clear channel playlist div
+ this.userPlaylistDiv.innerHTML = '';
+
+ //Append rendered playlists
+ this.userPlaylistDiv.append(...this.renderPlaylists(data, 'User'));
+ }
+
+ /**
+ * Render set of playlists out to Playlist Management Menu
+ * @param {Object} data - Data from server
+ * @param {String} location - Location to load from, either Channel or User
+ * @returns {Node} Rendered out playlist list
+ */
+ renderPlaylists(data, location){
+ const playlists = [];
+
+ //For every playlist sent down from the server
+ for(let playlistIndex in data){
+ //Get playlist from data
+ const playlist = data[playlistIndex];
+
+ //Create a new playlist div
+ const playlistDiv = document.createElement('div');
+ //Set it's class
+ playlistDiv.classList.add('queue-playlist-div');
+
+ //Create span to hold playlist entry line contents
+ const playlistSpan = document.createElement('span');
+ //Set classes
+ playlistSpan.classList.add('queue-playlist-span');
+
+ //If this isn't our first rodeo
+ if(playlistIndex != 0){
+ //make note
+ playlistSpan.classList.add('not-first');
+ }
+
+ //assemble playlist entry line
+ playlistSpan.append(
+ this.renderLabels(playlist, location),
+ this.renderControls(playlist, location)
+ );
+
+ //assemble playlist div
+ playlistDiv.append(
+ playlistSpan,
+ this.renderMedia(playlist, location),
+ );
+
+ //add playlist div to playlists array
+ playlists.push(playlistDiv);
+ }
+
+ return playlists;
+ }
+
+ //aux rendering functions
+ /**
+ * Renders Playlist labels
+ * @param {Object} playlist - Playlist from server to render label for
+ * @param {String} location - Location of playlist (Channel or User)
+ * @returns {Node} Rendered out playlist label
+ */
+ renderLabels(playlist, location){
+ //Create playlist label span
+ const playlistLabels = document.createElement('span');
+ //Set it's class
+ playlistLabels.classList.add('queue-playlist-labels-span');
+
+ //create playlist title span
+ const playlistTitleSpan = document.createElement('span');
+ //Set class
+ playlistTitleSpan.classList.add('queue-playlist-title-span', 'interactive');
+
+ //Create playlist title caret
+ const playlistTitleCaret = document.createElement('i');
+
+ //If this is supposed to be open
+ if(this.openMap[location].get(playlist.name)){
+ //Set class accordingly
+ playlistTitleSpan.classList.add('positive');
+ playlistTitleCaret.classList.add('bi-caret-down-fill');
+ //otherwise
+ }else{
+ //Set class accordingly
+ playlistTitleCaret.classList.add('bi-caret-right-fill');
+ }
+
+ //Create playlist title label
+ const playlistTitle = document.createElement('p');
+ //Set it's class
+ playlistTitle.classList.add('queue-playlist-title');
+ //Unescape Sanatized Enteties and safely inject as plaintext
+ playlistTitle.innerText = utils.unescapeEntities(playlist.name);
+
+ //Construct playlist title span
+ playlistTitleSpan.appendChild(playlistTitleCaret);
+ playlistTitleSpan.appendChild(playlistTitle);
+
+ //Create playlist count label
+ const playlistCount = document.createElement('p');
+ //Set it's class
+ playlistCount.classList.add('queue-playlist-count');
+ //List video count
+ playlistCount.innerText = `Count: ${playlist.media.length}`;
+
+ //Append items to playlist labels span
+ playlistLabels.appendChild(playlistTitleSpan);
+ playlistLabels.appendChild(playlistCount);
+
+ //Define input listeners
+ playlistTitleSpan.addEventListener('click', this.toggleMedia.bind(this));
+
+ return playlistLabels;
+ }
+
+ /**
+ * Renders out Playlist Controls
+ * @param {Object} playlist - Playlist from server to render label for
+ * @param {String} location - Location of playlist (Channel or User)
+ * @returns {Node} Rendered out playlist controls
+ */
+ renderControls(playlist, location){
+ //Create playlist control span
+ const playlistControls = document.createElement('span');
+ //Set it's class
+ playlistControls.classList.add('queue-playlist-control-span');
+ //Set dataset
+ playlistControls.dataset['playlist'] = playlist.name;
+ playlistControls.dataset['location'] = location;
+
+ //Create queue all button
+ const playlistQueueRandomButton = document.createElement('button');
+ //Set it's classes
+ playlistQueueRandomButton.classList.add('queue-playlist-queue-random-button', 'queue-playlist-control');
+ //Inject text content
+ playlistQueueRandomButton.textContent = 'Random';
+ //Set title
+ playlistQueueRandomButton.title = 'Queue Random Item from Playlist';
+
+ //Create queue all button
+ const playlistQueueAllButton = document.createElement('button');
+ //Set it's classes
+ playlistQueueAllButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control', 'not-first');
+ //Inject text content
+ playlistQueueAllButton.textContent = 'All';
+ //Set title
+ playlistQueueAllButton.title = 'Queue Entire Playlist';
+
+ //Create add from URL button
+ const playlistAddURLButton = document.createElement('button');
+ //Set it's classes
+ playlistAddURLButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'positive-button', 'not-first');
+ //Set Tile
+ playlistAddURLButton.title = 'Add To Playlist From URL'
+
+ //Create playlist icons (we're using two so we're putting them inside the button :P)
+ const playlistAddIcon = document.createElement('i');
+ const playlistLinkIcon = document.createElement('i');
+ //set classes
+ playlistAddIcon.classList.add('bi-plus-lg');
+ playlistLinkIcon.classList.add('bi-link-45deg');
+
+ //Append icons to URL button
+ playlistAddURLButton.appendChild(playlistAddIcon);
+ playlistAddURLButton.appendChild(playlistLinkIcon);
+
+ //Create default titles button
+ const playlistDefaultTitlesButton = document.createElement('button');
+ //Set classes
+ playlistDefaultTitlesButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-tags-fill', 'positive-button', 'not-first');
+ //Set title
+ playlistDefaultTitlesButton.title = 'Change Default Titles'
+ //Set dataset
+ playlistDefaultTitlesButton.dataset['titles'] = JSON.stringify(playlist.defaultTitles);
+
+ //Create rename button
+ const playlistRenameButton = document.createElement('button');
+ //Set it's classes
+ playlistRenameButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-input-cursor-text', 'positive-button', 'not-first');
+ //Set title
+ playlistRenameButton.title = 'Rename Playlist'
+
+ //Create delete button
+ const playlistDeleteButton = document.createElement('button');
+ //Set it's classes
+ playlistDeleteButton.classList.add('queue-playlist-delete-button', 'queue-playlist-control', 'danger-button', 'bi-trash-fill', 'not-first');
+ //Set title
+ playlistDeleteButton.title = 'Delete Playlist'
+
+ //Append items to playlist control span
+ playlistControls.append(
+ playlistQueueRandomButton,
+ playlistQueueAllButton,
+ playlistAddURLButton,
+ playlistDefaultTitlesButton,
+ playlistRenameButton,
+ playlistDeleteButton
+ );
+
+ //Define input event listeners
+ playlistAddURLButton.addEventListener('click', this.addURL.bind(this));
+ playlistDefaultTitlesButton.addEventListener('click', this.editDefaultTitles.bind(this));
+ playlistRenameButton.addEventListener('click', this.renamePlaylist.bind(this));
+ playlistQueueRandomButton.addEventListener('click', this.queueRandom.bind(this));
+ playlistQueueAllButton.addEventListener('click', this.queueAll.bind(this));
+ playlistDeleteButton.addEventListener('click', this.deletePlaylist.bind(this));
+
+ return playlistControls;
+ }
+
+ /**
+ * Renders media object out for an entire playlist
+ * @param {Object} playlist - Playlist from server to render label for
+ * @param {String} location - Location of playlist (Channel or User)
+ * @returns {Node} Rendered out playlist
+ */
+ renderMedia(playlist, location){
+ //Create media container div
+ const mediaContainer = document.createElement('div');
+ //Set classes
+ mediaContainer.classList.add('queue-playlist-media-container-div');
+
+ //If the playlist wasn't set to open in the open map
+ if(!this.openMap[location].get(playlist.name)){
+ //Auto-hide media container
+ mediaContainer.style.display = 'none';
+ }
+
+ //Set dataset
+ mediaContainer.dataset['playlist'] = playlist.name;
+
+ for(let mediaIndex in playlist.media){
+ //Grab media object from playlist
+ const media = playlist.media[mediaIndex];
+
+ //Sanatize title text
+ const title = utils.unescapeEntities(media.title);
+
+ //Create media div
+ const mediaDiv = document.createElement('div');
+ //Set class
+ mediaDiv.classList.add('queue-playlist-media-div');
+ //Inject title
+ mediaDiv.title = title;
+
+ //If this isn't our first rodeo
+ if(mediaIndex != 0){
+ mediaDiv.classList.add('not-first');
+ }
+
+
+ //Create media title
+ const mediaTitle = document.createElement('p');
+ //Set class
+ mediaTitle.classList.add('queue-playlist-media-title');
+ //Inject text content
+ mediaTitle.innerText = title;
+
+ //Append items to media div
+ mediaDiv.append(
+ mediaTitle,
+ this.renderMediaControls(media, playlist, location)
+ );
+
+
+ //Append media div to media container
+ mediaContainer.appendChild(mediaDiv);
+ }
+
+ //return media container
+ return mediaContainer;
+ }
+
+ /**
+ * Renders controls out for a single media entry within a playlist
+ * @param {Object} media - Media object from playlist to render controls for
+ * @param {Object} playlist - Playlist from server to render label for
+ * @param {String} location - Location of playlist (Channel or User)
+ * @returns {Node} Rendered out playlist
+ */
+ renderMediaControls(media, playlist, location){
+ //Create media control span
+ const mediaControlSpan = document.createElement('span');
+ //Set it's class
+ mediaControlSpan.classList.add('queue-playlist-media-control-span');
+ //Set dataset
+ mediaControlSpan.dataset['playlist'] = playlist.name;
+ mediaControlSpan.dataset['uuid'] = media.uuid;
+ mediaControlSpan.dataset['location'] = location;
+
+ //Create Queue Media icon
+ const queueMediaIcon = document.createElement('i');
+ //set class
+ queueMediaIcon.classList.add('queue-playlist-control', 'queue-playlist-media-queue-icon', 'bi-play-circle');
+ //Set title
+ queueMediaIcon.title = (`Queue '${media.title}'`);
+
+ //Create delete media icon
+ const deleteMediaIcon = document.createElement('i');
+ //set class
+ deleteMediaIcon.classList.add('queue-playlist-control', 'queue-playlist-media-delete-icon', 'danger-text', 'bi-trash-fill');
+ //Set title
+ deleteMediaIcon.title = `Delete '${media.title}' from playlist '${playlist.name}'`;
+
+ //Append items to media control span
+ mediaControlSpan.appendChild(queueMediaIcon);
+ mediaControlSpan.appendChild(deleteMediaIcon);
+
+ //Handle input event listeners
+ queueMediaIcon.addEventListener('click', this.queueMedia.bind(this));
+ deleteMediaIcon.addEventListener('click', this.deleteMedia.bind(this));
+
+ //Return media control span
+ return mediaControlSpan;
+ }
+
+ /**
+ * Toggle Media List
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ toggleMedia(event){
+ //Grab playlist title caret
+ const playlistTitleCaret = event.target.querySelector('i');
+ //I hope my mother doesn't see this next line, god I hate dot crawling...
+ const mediaContainer = event.target.parentNode.parentNode.nextElementSibling;
+
+ //If the div is hidden
+ if(mediaContainer.style.display == 'none'){
+ //Light up the button
+ event.target.classList.add('positive');
+ //Flip the caret
+ playlistTitleCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill');
+ //Show the div
+ mediaContainer.style.display = '';
+ }else{
+ //Unlight the button
+ event.target.classList.remove('positive');
+ //Flip the caret
+ playlistTitleCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill');
+ //Hide the div
+ mediaContainer.style.display = 'none';
+ }
+ }
+
+ /**
+ * Add URL to playlist
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ addURL(event){
+ new addURLPopup(
+ event,
+ event.target.parentNode.dataset['playlist'],
+ event.target.parentNode.dataset['location'],
+ this.client,
+ this.queuePanel.ownerDoc
+ );
+ }
+
+ //playlist control functions
+
+ /**
+ * Sends request to server to edit default titles
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ editDefaultTitles(event){
+ new defaultTitlesPopup(
+ event,
+ event.target.parentNode.dataset['playlist'],
+ JSON.parse(event.target.dataset['titles']),
+ event.target.parentNode.dataset['location'],
+ this.client,
+ this.queuePanel.ownerDoc
+ );
+ }
+
+ /**
+ * Sends request to server to rename playlists
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ renamePlaylist(event){
+ new renamePopup(
+ event,
+ event.target.parentNode.dataset['playlist'],
+ this.client,
+ this.queuePanel.ownerDoc,
+ handleOpenedMedia.bind(this)
+ );
+
+ function handleOpenedMedia(newName){
+ //do an ugly dot crawl to get the media container div
+ const mediaContainer = event.target.parentNode.parentNode.nextElementSibling;
+
+ //If the media container is visible
+ if(mediaContainer.style.display != 'none'){
+ //Set openMap to new name indicating the new playlist has it's media opened
+ this.openMap[event.target.parentNode.dataset['location']] = newName;
+ }
+ }
+ }
+
+ /**
+ * Sends request to server to queue all playlist items
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ queueAll(event){
+ this.client.socket.emit(`queue${event.target.parentNode.dataset['location']}Playlist`, {playlist: event.target.parentNode.dataset['playlist']});
+ }
+
+ /**
+ * Sends request to server to queue a playlist item
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ queueMedia(event){
+ this.client.socket.emit(`queueFrom${event.target.parentNode.dataset['location']}Playlist`,{playlist: event.target.parentNode.dataset['playlist'], uuid: event.target.parentNode.dataset['uuid']});
+ }
+
+ /**
+ * Sends request to server to queue a random playlist item
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ queueRandom(event){
+ this.client.socket.emit(`queueRandomFrom${event.target.parentNode.dataset['location']}Playlist`,{playlist: event.target.parentNode.dataset['playlist']});
+ }
+
+ /**
+ * Sends request to server to delete a playlist
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ deletePlaylist(event){
+ this.client.socket.emit(`delete${event.target.parentNode.dataset['location']}Playlist`, {playlist: event.target.parentNode.dataset['playlist']});
+ }
+
+ /**
+ * Sends request to server to delete a playlist item
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ deleteMedia(event ){
+ this.client.socket.emit(`delete${event.target.parentNode.dataset['location']}PlaylistMedia`, {playlist: event.target.parentNode.dataset['playlist'], uuid: event.target.parentNode.dataset['uuid']});
+ }
+
+}
+
+/**
+ * Class representing pop-up dialogue for creating a new playlist
+ */
+class newPlaylistPopup{
+ /**
+ * Instantiates a New Playlist Popup
+ * @param {Event} event - Event passed down from Event Listener
+ * @param {channel} client - Parent Client Management Object
+ * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
+ */
+ constructor(event, client, doc){
+ /**
+ * Parent Client Management Object
+ */
+ this.client = client;
+
+ //Create media popup and call async constructor when done
+ //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :(
+ /**
+ * canopyUXUtils.popup() object
+ */
+ this.popup = new canopyUXUtils.popup('/newPlaylist', true, this.asyncConstructor.bind(this), doc, false);
+ }
+
+ /**
+ * Continuation of object construction, called after child popup object construction
+ */
+ asyncConstructor(){
+ this.name = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-name');
+ this.defaultTitles = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-default-titles');
+ this.location = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-location');
+ this.saveButton = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-save');
+
+ this.setupInput();
+ }
+
+ /**
+ * Defines input-related Event Handlers
+ */
+ setupInput(){
+ //Setup input
+ this.saveButton.addEventListener('click', this.createPlaylist.bind(this));
+ this.popup.popupDiv.addEventListener('keydown', this.createPlaylist.bind(this));
+ }
+
+ /**
+ * Sends request to create a playlist off to the server
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ createPlaylist(event){
+ //If we clicked or hit enter
+ if(event.key == null || (event.key == "Enter" && this.defaultTitles !== this.popup.doc.activeElement)){
+
+ //Tell the server to create a new playlist
+ this.client.socket.emit(`create${this.location.value}Playlist`, {
+ playlist: this.name.value,
+ defaultTitles: this.defaultTitles.value.split('\n')
+ });
+
+ //Close the popup
+ this.popup.closePopup();
+ }
+ }
+}
+
+/**
+ * Class representing pop-up dialogue which adds media to a given playlist
+ */
+class addURLPopup{
+ /**
+ * Instantiates a new Add URL Pop-up
+ * @param {Event} event - Event passed down from Event Listener
+ * @param {String} playlist - Playlist name
+ * @param {String} location - Location of playlist, either Channel or User
+ * @param {channel} client - Parent Client Management Object
+ * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
+ */
+ constructor(event, playlist, location, client, doc){
+ /**
+ * Parent Client Management Object
+ */
+ this.client = client;
+
+ /**
+ * Playlist Name
+ */
+ this.playlist = playlist
+
+ /**
+ * Location of playlist, either Channel or User
+ */
+ this.location = location;
+
+ //Create media popup and call async constructor when done
+ //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :(
+ /**
+ * canopyUXUtils.popup() object
+ */
+ this.popup = new canopyUXUtils.popup('/addToPlaylist', true, this.asyncConstructor.bind(this), doc);
+ }
+
+ /**
+ * Continuation of object construction, called after child popup object construction
+ */
+ asyncConstructor(){
+ this.urlPrompt = this.popup.contentDiv.querySelector('#playlist-add-media-popup-prompt');
+ this.addButton = this.popup.contentDiv.querySelector('#playlist-add-media-popup-button');
+
+ this.setupInput();
+ }
+
+ /**
+ * Defines input-related Event Handlers
+ */
+ setupInput(){
+ //Setup input
+ this.addButton.addEventListener('click', this.addToPlaylist.bind(this));
+ this.popup.popupDiv.addEventListener('keydown', this.addToPlaylist.bind(this));
+ }
+
+ /**
+ * Handles sending request to add to a playlist to the server
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ addToPlaylist(event){
+ //If we clicked or hit enter
+ if(event.key == null || event.key == "Enter"){
+
+ //Tell the server to add url to the playlist
+ this.client.socket.emit(`addTo${this.location}Playlist`, {
+ //this.client.socket.emit(`addToChannelPlaylist`, {
+ playlist: this.playlist,
+ url: this.urlPrompt.value
+ });
+
+ //Close the popup
+ this.popup.closePopup();
+ }
+ }
+}
+
+/**
+ * Class Representing popup dialogue for changing playlists defualt titles
+ */
+class defaultTitlesPopup{
+ /**
+ * Instantiates a new Default Titles Popup
+ * @param {Event} event - Event passed down from Event Listener
+ * @param {String} playlist - Playlist name
+ * @param {String} titles - List of titles, denoted by newlines
+ * @param {String} location - Location of playlist, either Channel or User
+ * @param {channel} client - Parent Client Management Object
+ * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
+ */
+ constructor(event, playlist, titles, location, client, doc){
+ /**
+ * Parent Client Management Object
+ */
+ this.client = client;
+
+ /**
+ * Playlist Name
+ */
+ this.playlist = playlist
+
+ /**
+ * Location of playlist, either Channel or User
+ */
+ this.location = location;
+
+ /**
+ * Array of titles to set
+ */
+ this.titles = titles.join('\n');
+
+ //Create media popup and call async constructor when done
+ //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :(
+ /**
+ * canopyUXUtils.popup() object
+ */
+ this.popup = new canopyUXUtils.popup('/playlistDefaultTitles', true, this.asyncConstructor.bind(this), doc, false);
+ }
+
+ /**
+ * Continuation of object construction, called after child popup object construction
+ */
+ asyncConstructor(){
+ this.titlePrompt = this.popup.contentDiv.querySelector('#playlist-default-titles-popup-prompt');
+ this.titleButton = this.popup.contentDiv.querySelector('#playlist-default-media-popup-button');
+
+ this.titlePrompt.textContent = utils.unescapeEntities(this.titles);
+ this.setupInput();
+ }
+
+ /**
+ * Defines input-related Event Handlers
+ */
+ setupInput(){
+ //Setup input
+ this.titleButton.addEventListener('click', this.changeDefaultTitles.bind(this));
+ this.popup.popupDiv.addEventListener('keydown', this.changeDefaultTitles.bind(this));
+ }
+
+ /**
+ * Handles sending request to change default titles of playlist to the server
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ changeDefaultTitles(event){
+ //If we clicked or hit enter while the prompt wasn't active
+ if(event.key == null || (event.key == "Enter" && this.titlePrompt !== this.popup.doc.activeElement)){
+
+ //Tell the server to change the titles
+ this.client.socket.emit(`changeDefaultTitles${this.location}Playlist`, {
+ playlist: this.playlist,
+ defaultTitles: this.titlePrompt.value.split('\n')
+ });
+
+ //Close the popup
+ this.popup.closePopup();
+ }
+ }
+}
+
+/**
+ * Class representing pop-up dialogue to rename a playlist
+ */
+class renamePopup{
+ /**
+ * Instantiates a new Rename Pop-up
+ * @param {Event} event - Event passed down from Event Listener
+ * @param {String} playlist - Playlist name
+ * @param {channel} client - Parent Client Management Object
+ * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
+ * @param {Function} cb - Callback function, passed new name upon rename
+ */
+ constructor(event, playlist, client, doc, cb){
+ /**
+ * Parent Client Management Object
+ */
+ this.client = client;
+
+ /**
+ * Playlist Name
+ */
+ this.playlist = playlist
+
+ /**
+ * Callback Function, passed new name upon rename
+ */
+ this.cb = cb;
+
+ //Create media popup and call async constructor when done
+ //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :(
+ /**
+ * canopyUXUtils.popup() object
+ */
+ this.popup = new canopyUXUtils.popup('/renamePlaylist', true, this.asyncConstructor.bind(this), doc);
+ }
+
+ /**
+ * Continuation of object construction, called after child popup object construction
+ */
+ asyncConstructor(){
+ this.renamePrompt = this.popup.contentDiv.querySelector('#playlist-rename-popup-prompt');
+ this.renameButton = this.popup.contentDiv.querySelector('#playlist-rename-popup-button');
+
+ this.setupInput();
+ }
+
+ /**
+ * Defines input-related Event Handlers
+ */
+ setupInput(){
+ //Setup input
+ this.renameButton.addEventListener('click', this.renamePlaylist.bind(this));
+ this.popup.popupDiv.addEventListener('keydown', this.renamePlaylist.bind(this));
+ }
+
+ /**
+ * Handles sending request to rename playlist to the server
+ * @param {Event} event - Event passed down from Event Listener
+ */
+ renamePlaylist(event){
+ //If we clicked or hit enter while the prompt wasn't active
+ if(event.key == null || event.key == "Enter"){
+
+ //Tell the server to change the titles
+ this.client.socket.emit('renameChannelPlaylist', {
+ playlist: this.playlist,
+ name: this.renamePrompt.value
+ });
+
+ //if CB is a function
+ if(typeof this.cb == 'function'){
+ //Hand it back the new name
+ this.cb(this.renamePrompt.value);
+ }
+
+ //Close the popup
+ this.popup.closePopup();
+ }
+ }
+}
+
+