Source: panels/queuePanel/playlistManager.js

/*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{

    /**
     * Instantiates a new playlist manager
     * @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));
    }

    /**
     * Handles Up-stream Document/Panel Changes from the parent Queue Panel object
     */
    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();
        }
    }
}