From 1723e8ebd0925415ad89cc9abeaaf12624f6891a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 30 Mar 2025 14:11:49 -0400 Subject: [PATCH] Started work on playlist management UI --- src/app/channel/media/playlistHandler.js | 226 +++++++++++------ src/app/channel/media/queue.js | 2 +- src/schemas/channel/channelSchema.js | 41 +--- src/schemas/channel/media/playlistSchema.js | 23 +- src/views/partial/panels/queue.ejs | 16 ++ src/views/partial/popup/newPlaylist.ejs | 28 +++ www/css/panel/queue.css | 78 +++++- www/css/theme/movie-night.css | 19 ++ www/js/channel/panels/queuePanel.js | 257 +++++++++++++++++++- 9 files changed, 586 insertions(+), 104 deletions(-) create mode 100644 src/views/partial/popup/newPlaylist.ejs diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index 8b73805..5b56c8b 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -32,106 +32,196 @@ module.exports = class{ } defineListeners(socket){ - //socket.on("queue", (data) => {this.queueURL(socket, data)}); socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)}); socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)}); socket.on("deleteChannelPlaylist", (data) => {this.deleteChannelPlaylist(socket, data)}); socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); + socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)}); } //--- USER-FACING PLAYLIST FUNCTIONS --- async getChannelPlaylists(socket, chanDB){ - //if we wherent handed a channel document - if(chanDB == null){ - //Pull it based on channel name - chanDB = await channelModel.findOne({name: this.channel.name}); - } + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } - //Return playlists - socket.emit('chanPlaylists', chanDB.getPlaylists()); + //Return playlists + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } } async createChannelPlaylist(socket, data, chanDB){ - //if we wherent handed a channel document - if(chanDB == null){ - //Pull it based on channel name - chanDB = await channelModel.findOne({name: this.channel.name}); + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //If the title is too long + if(!validator.isLength(data.playlist, {max:30})){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist name too long!", "validation"); + //and ignore it! + return; + } + + //Escape/trim the playlist name + const name = validator.escape(validator.trim(data.playlist)); + + //Add playlist to the channel doc + chanDB.media.playlists.push({ + name, + defaultTitles: data.defaultTitles + }); + + //Save the channel doc + await chanDB.save(); + + //Return playlists from channel doc + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //Add playlist to the channel doc - chanDB.media.playlists.push({ - name: data.playlist, - defaultTitles: data.defaultTitles - }); - - //Save the channel doc - await chanDB.save(); - - //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); } async deleteChannelPlaylist(socket, data, chanDB){ - //if we wherent handed a channel document - if(chanDB == null){ - //Pull it based on channel name - chanDB = await channelModel.findOne({name: this.channel.name}); + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //Delete playlist name + await chanDB.deletePlaylistByName(data.playlist); + + //Return playlists from channel doc + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //Delete playlist name - await chanDB.deletePlaylistByName(data.playlist); - - //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); } async addToChannelPlaylist(socket, data, chanDB){ - //if we wherent handed a channel document - if(chanDB == null){ - //Pull it based on channel name - chanDB = await channelModel.findOne({name: this.channel.name}); + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + let url = data.url + + //If we where given a bad URL + if(!validator.isURL(url)){ + //Attempt to fix the situation by encoding it + url = encodeURI(url); + + //If it's still bad + if(!validator.isURL(url)){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation"); + //and ignore it! + return; + } + } + + //Pull media metadata + let mediaList = await yanker.yankMedia(url); + + //If we didn't get any media + if(mediaList.length == 0 || mediaList == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "No media found!", "queue"); + //and ignore it! + return; + } + + //Add media object to the given playlist + await chanDB.addToPlaylist(data.playlist, mediaList[0]); + + //Return playlists from channel doc + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //Pull media metadata - let mediaList = await yanker.yankMedia(data.url); - - //Add media object to the given playlist - await chanDB.addToPlaylist(data.playlist, mediaList[0]); - - //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); } async queueChannelPlaylist(socket, data, chanDB){ - //if we wherent handed a channel document - if(chanDB == null){ - //Pull it based on channel name - chanDB = await channelModel.findOne({name: this.channel.name}); + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //Pull a valid start time from input, or make one up if we can't + let start = this.channel.queue.getStart(data.start); + + //Grab playlist from the DB + let playlist = chanDB.getPlaylistByName(data.playlist); + + //Create an empty array to hold our media list + const mediaList = []; + + //Iterate through playlist media + for(let item of playlist.media){ + //Rehydrate a full phat media object from the flat DB entry + let mediaObj = item.rehydrate(); + + //Set media title from default titles + mediaObj.title = playlist.defaultTitles[Math.floor(Math.random() * playlist.defaultTitles.length)]; + + //Push rehydrated item on to the mediaList + mediaList.push(mediaObj); + } + + //Convert array of standard media objects to queued media objects, and push to schedule + this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } + } - //Pull a valid start time from input, or make one up if we can't - let start = this.channel.queue.getStart(data.start); + async renameChannelPlaylist(socket, data, chanDB){ + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } - //Grab playlist from the DB - let playlist = chanDB.getPlaylistByName(data.playlist); + //If the title is too long + if(!validator.isLength(data.name, {max:30})){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist name too long!", "validation"); + //and ignore it! + return; + } - //Create an empty array to hold our media list - const mediaList = []; + //Escape/trim the playlist name + const name = validator.escape(validator.trim(data.name)); - //Iterate through playlist media - for(let item of playlist.media){ - //Rehydrate a full phat media object from the flat DB entry - let mediaObj = item.rehydrate(); + //Find playlist + let playlist = chanDB.getPlaylistByName(data.playlist); - //Set media title from default titles - mediaObj.title = playlist.defaultTitles[Math.floor(Math.random() * playlist.defaultTitles.length)]; + //Change playlist name + chanDB.media.playlists[playlist.listIndex].name = name; - //Push rehydrated item on to the mediaList - mediaList.push(mediaObj); + //Save channel document + await chanDB.save(); + + //Return playlists from channel doc + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //Convert array of standard media objects to queued media objects, and push to schedule - this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB); } } \ No newline at end of file diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index d6610f7..bc21bcb 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -626,7 +626,7 @@ module.exports = class{ } //If we fucked with the DB during the main loop - if(chanDB == null && !volatile){ + if(chanDB != null && !volatile){ try{ //Save the database chanDB.save(); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 20112bd..90adb7f 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -559,10 +559,7 @@ channelSchema.methods.getPlaylists = function(){ //this.media.playlists.forEach((playlist) => { for(let playlist of this.media.playlists){ //Push an object with select information from the emote to the emote list - playlists.push({ - name: playlist.name, - count: playlist.media.length - }); + playlists.push(playlist.dehydrate()); } //return the emote list @@ -584,11 +581,13 @@ channelSchema.methods.getPlaylistByName = function(name){ let foundPlaylist = null; //Crawl through active playlists - this.playlistCrawl((playlist) => { + this.playlistCrawl((playlist, listIndex) => { //If we found a match based on name if(playlist.name == name){ //Keep it foundPlaylist = playlist; + //Pass down the list index + foundPlaylist.listIndex = listIndex; } }); @@ -597,45 +596,25 @@ channelSchema.methods.getPlaylistByName = function(name){ } channelSchema.methods.deletePlaylistByName = async function(name){ - //Create null value to hold our found playlist - let foundIndex = null; - - //Crawl through active playlists - this.playlistCrawl((playlist, listIndex) => { - //If we found a match based on name - if(playlist.name == name){ - //Yoink it's index - foundIndex = listIndex; - } - }); + //Find the playlist + let playlist = this.getPlaylistByName(name); //splice out the given playlist - this.media.playlists.splice(foundIndex, 1); + this.media.playlists.splice(playlist.listIndex, 1); //save the channel document await this.save(); } channelSchema.methods.addToPlaylist = async function(name, media){ - //Create variable to hold found index - let foundIndex = null - - //Crawl through active playlists - this.playlistCrawl((playlist, listIndex) => { - //If the playlist name matches - if(playlist.name == name){ - //Push the given media into the found playlist - - //Make note of the found index - foundIndex = listIndex - } - }); + //Find the playlist + let playlist = this.getPlaylistByName(name); //Set media status schema discriminator media.status = 'saved'; //Add the media to the playlist - this.media.playlists[foundIndex].media.push(media); + this.media.playlists[playlist.listIndex].media.push(media); //Save the changes made to the chan doc await this.save(); diff --git a/src/schemas/channel/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index e5c9750..e4d7d10 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -24,6 +24,7 @@ const playlistSchema = new mongoose.Schema({ name: { type: mongoose.SchemaTypes.String, required: true, + unique: true }, media: [playlistMediaSchema], defaultTitles:[{ @@ -33,8 +34,26 @@ const playlistSchema = new mongoose.Schema({ }] }); -playlistSchema.methods.test = function(){ - console.log(this.name); +//methods +playlistSchema.methods.dehydrate = function(){ + //Create empty array to hold media + const mediaArray = []; + + //Fill media array + for(let media of this.media){ + mediaArray.push({ + title: media.title, + url: media.url, + duration: media.duration + }); + } + + //return dehydrated playlist + return { + name: this.name, + media: mediaArray, + defaultTitles: this.defaultTitles + } } module.exports = playlistSchema; \ No newline at end of file diff --git a/src/views/partial/panels/queue.ejs b/src/views/partial/panels/queue.ejs index 5b5b7d3..d069434 100644 --- a/src/views/partial/panels/queue.ejs +++ b/src/views/partial/panels/queue.ejs @@ -41,6 +41,22 @@ along with this program. If not, see . %> +
diff --git a/src/views/partial/popup/newPlaylist.ejs b/src/views/partial/popup/newPlaylist.ejs new file mode 100644 index 0000000..196ad3e --- /dev/null +++ b/src/views/partial/popup/newPlaylist.ejs @@ -0,0 +1,28 @@ +<%# 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 . %> + +
+ + + + + + + +
\ No newline at end of file diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index 75a7224..7e75190 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -25,7 +25,6 @@ along with this program. If not, see .*/ left: 0; right: 0; z-index: 4; - padding: 0.15em 0.3em; } #queue-control-offset{ @@ -38,6 +37,7 @@ along with this program. If not, see .*/ display: flex; flex-direction: row; justify-content: space-between; + margin: 0.15em 0.3em; } @@ -133,6 +133,82 @@ div.dragging-queue-entry{ } /* queue controls */ +/* media prompts */ +#queue-media-prompts{ + margin: 0.15em 0.3em; +} + +/* playlist manager */ +.queue-playlist-label-span{ + display: flex; + flex-direction: row; + width: fit-content; + margin-left: 0.15em; +} + +.queue-playlist-label-span p{ + margin: 0; +} + +.queue-playlists-div{ + margin: 0; +} + +.queue-playlist-span, .queue-playlist-labels-span{ + display: flex; + flex-direction: row; + align-items: baseline; + text-wrap: nowrap; +} + +.queue-playlist-span{ + justify-content: space-between; +} + +.queue-playlist-div{ + padding: 0 0.15em; + margin: 0 0.15em; +} + +.queue-playlist-control{ + border-radius: 0; + padding: 0.2em 0.1em; + font-size: 0.8em; +} + +.queue-playlist-span p{ + margin: 0 0.15em; + user-select: none; +} + +.queue-playlist-count{ + font-size: 0.8em; +} + +#queue-create-playlist-popup-div{ + display: flex; + flex-direction: column; +} + +#queue-create-playlist-popup-div span{ + display: flex; + justify-content: space-around; + text-wrap: nowrap; +} + +#queue-create-playlist-popup-div span select{ + width: 100%; +} + + +#queue-create-playlist-popup-div *{ + margin: 0.1em; +} + +#queue-create-playlist-popup-default-titles{ + resize: vertical; +} + /* date */ #queue-control-date{ display: flex; diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 4bec538..1c80189 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -539,6 +539,25 @@ div.archived p{ color: var(--bg2-alt1); } +.queue-playlists-div{ + background-color: var(--bg1); + color: var(--accent1); + border-block: var(--accent1) solid 1px; +} + +.not-first-queue-playlist-div{ + border-top: var(--bg1-alt0) solid 1px; +} + +.queue-playlist-count{ + color: var(--accent1-alt0); +} + +.queue-playlist-control:not(.danger-button):not(:hover){ + background-color: var(--bg1-alt0); + color: var(--accent1); +} + /* altcha theming*/ div.altcha{ box-shadow: 4px 4px 1px var(--bg1-alt0) inset; diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js index 8a5a5e1..d908944 100644 --- a/www/js/channel/panels/queuePanel.js +++ b/www/js/channel/panels/queuePanel.js @@ -17,6 +17,9 @@ class queuePanel extends panelObj{ //Autoscroll boolean this.autoscroll = true; + //Setup child classes + this.playlistManager = new playlistManager(client, panelDocument, this); + //Define non-input event listeners this.defineListeners(); } @@ -43,13 +46,15 @@ class queuePanel extends panelObj{ //Get main control buttons this.addMediaButton = this.panelDocument.querySelector('#queue-add-media'); this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock'); - this.queueDateButton = this.panelDocument.querySelector('#queue-date') + this.queueDateButton = this.panelDocument.querySelector('#queue-date'); + this.playlistMenuButton = this.panelDocument.querySelector("#queue-playlists"); this.clearMediaButton = this.panelDocument.querySelector('#queue-clear'); this.queueLockButton = this.panelDocument.querySelector('#queue-lock'); //Get control divs this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); this.queueDateDiv = this.panelDocument.querySelector('#queue-control-date'); + this.playlistDiv = this.panelDocument.querySelector('#queue-playlist-prompts'); //Get control div elements //Add Media @@ -70,6 +75,10 @@ class queuePanel extends panelObj{ //Setup panel input this.setupInput(); + + //Pass the new panelDoc and docSwitch() call down to child classes + this.playlistManager.panelDocument = this.panelDocument; + this.playlistManager.docSwitch(); } closer(){ @@ -99,6 +108,7 @@ class queuePanel extends panelObj{ this.addMediaButton.addEventListener('click', this.toggleAddMedia.bind(this)); this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this)); this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this)); + this.playlistMenuButton.addEventListener('click', this.togglePlaylistDiv.bind(this)); this.clearMediaButton.addEventListener('click', this.clearMedia.bind(this)); this.queueLockButton.addEventListener('click', this.lockSchedule.bind(this)); @@ -203,6 +213,24 @@ class queuePanel extends panelObj{ } } + togglePlaylistDiv(event){ + //If the div is hidden + if(this.playlistDiv.style.display == 'none'){ + //Query playlists + client.socket.emit('getChannelPlaylists'); + + //Light up the button + this.playlistMenuButton.classList.add('positive-button'); + //Show the div + this.playlistDiv.style.display = ''; + }else{ + //Unlight the button + this.playlistMenuButton.classList.remove('positive-button'); + //Hide the div + this.playlistDiv.style.display = 'none'; + } + } + clearMedia(event){ //Call up the popup new clearPopup(event, this.client, null, this.ownerDoc); @@ -1003,6 +1031,233 @@ class queuePanel extends panelObj{ } } +class playlistManager{ + constructor(client, panelDocument, queuePanel){ + //Set client + this.client = client; + //Set panel document + this.panelDocument = panelDocument; + //Set parent queue panel + this.queuePanel = queuePanel; + + //Define Listeners + this.defineListeners(); + } + + defineListeners(){ + this.client.socket.on("chanPlaylists", this.renderChannelPlaylists.bind(this)); + } + + docSwitch(){ + //Grab menus + this.channelPlaylistDiv = this.panelDocument.querySelector("#queue-channel-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'); + + //Setup Input + this.setupInput(); + } + + setupInput(){ + this.createPlaylistSpan.addEventListener('click', (event)=>{new newPlaylistPopup(event, this.client, this.queuePanel.ownerDoc)}) + this.channelPlaylistLabel.addEventListener('click', this.toggleChannelPlaylists.bind(this)); + } + + /* queue control button functions */ + 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'; + } + } + + renderChannelPlaylists(data){ + //Clear channel playlist div + this.channelPlaylistDiv.innerHTML = ''; + + //For every playlist sent down from the server + for(let playlistIndex in data){ + //Get playlist from data + const playlist = data[playlistIndex]; + + //--Create Base Structure--- + //Create a new playlist div + const playlistDiv = document.createElement('div'); + //Set it's class + playlistDiv.classList.add('queue-playlist-div'); + + //Set playlist div dataset + playlistDiv.dataset.name = playlist.name; + + //If this isn't our first rodeo + if(playlistIndex != 0){ + //make note + playlistDiv.classList.add('not-first-queue-playlist-div'); + } + + //Create span to hold playlist entry line contents + const playlistSpan = document.createElement('span'); + //Set classes + playlistSpan.classList.add('queue-playlist-span'); + + //--Create Labels--- + //Create playlist label span + const playlistLabels = document.createElement('span'); + //Set it's class + playlistLabels.classList.add('queue-playlist-labels-span'); + + //Create playlist title label + const playlistTitle = document.createElement('p'); + //Set it's class + playlistTitle.classList.add('queue-playlist-title'); + + //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}`; + + //--Create Controls-- + //Create playlist control span + const playlistControls = document.createElement('span'); + //Set it's class + playlistControls.classList.add('queue-playlist-control-span'); + //Unescape Sanatized Enteties and safely inject as plaintext + playlistTitle.innerText = utils.unescapeEntities(playlist.name); + + //Create queue all button + const playlistQueueRandomButton = document.createElement('button'); + //Set it's classes + playlistQueueRandomButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control'); + //Inject text content + playlistQueueRandomButton.textContent = 'Queue Random'; + + //Create queue all button + const playlistQueueAllButton = document.createElement('button'); + //Set it's classes + playlistQueueAllButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control'); + //Inject text content + playlistQueueAllButton.textContent = 'Queue All'; + + //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'); + + //--Create Media Elements-- + //Create Media Container Div + const mediaDiv = renderMedia(); + + //Append items to playlist labels span + playlistLabels.appendChild(playlistTitle); + playlistLabels.appendChild(playlistCount); + + //Append items to playlist control span + playlistControls.appendChild(playlistQueueRandomButton); + playlistControls.appendChild(playlistQueueAllButton); + playlistControls.appendChild(playlistDeleteButton); + + //Append items to playlist span + playlistSpan.appendChild(playlistLabels); + playlistSpan.appendChild(playlistControls); + + //Append items to playlist div + playlistDiv.appendChild(playlistSpan); + playlistDiv.appendChild(mediaDiv); + + //Append current playlist span to the channel playlist div + this.channelPlaylistDiv.appendChild(playlistDiv); + + //Define input event listeners + playlistQueueAllButton.addEventListener('click', queueAll); + playlistDeleteButton.addEventListener('click', deletePlaylist); + + //aux rendering functions + function renderMedia(){ + //Create media container div + const mediaContainer = document.createElement('div'); + //Set classes + mediaContainer.classList.add('queue-playlist-media-div'); + + //return media container + return mediaContainer; + } + + //playlist control functions + function queueAll(){ + client.socket.emit('queueChannelPlaylist', {playlist: playlist.name}); + } + + function deletePlaylist(){ + client.socket.emit('deleteChannelPlaylist', {playlist: playlist.name}); + } + } + } + +} + +class newPlaylistPopup{ + constructor(event, client, doc){ + //Set Client + 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 :( + this.popup = new canopyUXUtils.popup('/newPlaylist', true, this.asyncConstructor.bind(this), doc); + } + + 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(); + } + + setupInput(){ + //Setup input + this.saveButton.addEventListener('click', this.createPlaylist.bind(this)); + this.popup.popupDiv.addEventListener('keydown', this.createPlaylist.bind(this)); + } + + createPlaylist(event){ + //If we clicked or hit enter + if(event.key == null || event.key == "Enter"){ + + //If we're saving to the channel + if(this.location.value == 'channel'){ + //Tell the server to create a new channel playlist + this.client.socket.emit('createChannelPlaylist', { + playlist: this.name.value, + defaultTitles: this.defaultTitles.value.split('\n') + }) + } + + //Close the popup + this.popup.closePopup(); + } + } + + +} + class schedulePopup{ constructor(event, client, url, title, cb, doc){ //Set Client