diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index fa7f081..73c400b 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -35,6 +35,7 @@ module.exports = class{ socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)}); socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)}); socket.on("deleteChannelPlaylist", (data) => {this.deleteChannelPlaylist(socket, data)}); + socket.on("deleteChannelPlaylistMedia", (data) => {this.deleteChannelPlaylistMedia(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)}); @@ -290,4 +291,30 @@ module.exports = class{ return loggerUtils.socketExceptionHandler(socket, err); } } + + async deleteChannelPlaylistMedia(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}); + } + + //If we don't have a valid UUID + if(!validator.isUUID(data.uuid)){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, `'${data.uuid}' is not a valid UUID!`, "validation"); + //and ignore it! + return; + } + + //Delete media from channel playlist + chanDB.deletePlaylistMediaByUUID(data.playlist, data.uuid); + + //Return playlists from channel doc + socket.emit('chanPlaylists', chanDB.getPlaylists()); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } + } } \ No newline at end of file diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 90adb7f..6c346f7 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -606,6 +606,17 @@ channelSchema.methods.deletePlaylistByName = async function(name){ await this.save(); } +channelSchema.methods.deletePlaylistMediaByUUID = async function(name, uuid){ + //Find the playlist + let playlist = this.getPlaylistByName(name); + + //splice out the given playlist + this.media.playlists[playlist.listIndex].deleteMedia(uuid); + + //save the channel document + await this.save(); +} + channelSchema.methods.addToPlaylist = async function(name, media){ //Find the playlist let playlist = this.getPlaylistByName(name); diff --git a/src/schemas/channel/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index 488c89d..22ea1db 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -52,4 +52,21 @@ playlistSchema.methods.dehydrate = function(){ } } +playlistSchema.methods.deleteMedia = function(uuid){ + //Create new array to hold list of media to be kept + const keptMedia = []; + + //For every piece of media in the current playlist + for(let media of this.media){ + //It isn't the media to be deleted + if(media.uuid.toString() != uuid){ + //Add it to the list to be kept + keptMedia.push(media); + } + } + + //Set playlist media from keptMedia + this.media = keptMedia; +} + module.exports = playlistSchema; \ No newline at end of file diff --git a/www/css/channel.css b/www/css/channel.css index d502e40..138302b 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -297,13 +297,17 @@ span.user-entry{ margin: 0; left: 1em; top: 0.6em; + pointer-events: none; } #chat-panel-prompt-autocomplete-filler{ visibility: hidden; user-select: none; cursor: auto; - pointer-events: none; +} + +#chat-panel-prompt-autocomplete-display{ + pointer-events: all; } .toke{ diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index 37c861d..dbfd851 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -185,6 +185,11 @@ div.dragging-queue-entry{ flex-direction: row; } +/* Pass these up to the span to prevent moar dot-drawling */ +.queue-playlist-title-span p, .queue-playlist-title-span i{ + pointer-events: none; +} + .queue-playlist-count{ font-size: 0.8em; } @@ -242,7 +247,15 @@ div.dragging-queue-entry{ width: 0.5em; } +.queue-playlist-media-div{ + display: flex; + flex-direction: row; + justify-content: space-between; +} +.queue-playlist-media-delete-icon{ + height: 0.8em; +} /* date */ #queue-control-date{ diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 07c61cf..2b863ca 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -560,17 +560,16 @@ div.archived p{ color: var(--accent1-alt0); } -.queue-playlist-control:not(.danger-button):not(:hover).queue-playlist-control:not(.positive-button):not(:hover){ +.queue-playlist-control:not(.danger-text, .positive-button, .danger-button, :hover){ background-color: var(--bg1-alt0); color: var(--accent1); } - .queue-playlist-media-div.not-first{ border-top: var(--bg1) solid 1px; } -.queue-playlist-control:not(.queue-playlist-queue-random-button){ +.queue-playlist-control.not-first{ border-left: var(--accent1-alt0) solid 1px; } diff --git a/www/js/channel/panels/queuePanel/playlistManager.js b/www/js/channel/panels/queuePanel/playlistManager.js index 9480f9b..e6fa094 100644 --- a/www/js/channel/panels/queuePanel/playlistManager.js +++ b/www/js/channel/panels/queuePanel/playlistManager.js @@ -71,9 +71,28 @@ class playlistManager{ } } - //Keeping everything in one function was super lazy when 'this.' exists, ESPECIALLY when writing a class! - //Really not sure what I was thinking here, this functions a bit of a stinker, and I'll probably re-write it sooner than later... + checkOpenPlaylists(){ + //If open map is a string, indicating we just renamed a playlist with it's media open + if(typeof this.openMap == 'string'){ + //Create new map to hold status with the new name of the renamed playlist already added + this.openMap = new Map([[this.openMap, true]]); + }else{ + //Create new map to hold status + this.openMap = new Map(); + } + + + //For each container Div rendered + for(let containerDiv of this.channelPlaylistDiv.querySelectorAll('.queue-playlist-media-container-div')){ + //Set whether or not it's visible in the map + this.openMap.set(containerDiv.dataset['playlist'], (containerDiv.style.display != 'none')); + } + } + renderChannelPlaylists(data){ + //Check for open playlists + this.checkOpenPlaylists(); + //Clear channel playlist div this.channelPlaylistDiv.innerHTML = ''; @@ -87,9 +106,6 @@ class playlistManager{ //Set it's class this.playlistDiv.classList.add('queue-playlist-div'); - //Set playlist div dataset - this.playlistDiv.dataset.name = this.playlist.name; - //Create span to hold playlist entry line contents this.playlistSpan = document.createElement('span'); //Set classes @@ -160,27 +176,7 @@ class playlistManager{ this.playlistLabels.appendChild(this.playlistCount); //Define input listeners - this.playlistTitleSpan.addEventListener('click', toggleMedia.bind(this)); - - //I'd rather make this a class function but it's probably cleaner to not have to parent crawl - function toggleMedia(){ - //If the div is hidden - if(this.mediaContainer.style.display == 'none'){ - //Light up the button - this.playlistTitleSpan.classList.add('positive'); - //Flip the caret - this.playlistTitleCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill'); - //Show the div - this.mediaContainer.style.display = ''; - }else{ - //Unlight the button - this.playlistTitleSpan.classList.remove('positive'); - //Flip the caret - this.playlistTitleCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill'); - //Hide the div - this.mediaContainer.style.display = 'none'; - } - } + this.playlistTitleSpan.addEventListener('click', this.toggleMedia.bind(this)); } renderControls(){ @@ -203,7 +199,7 @@ class playlistManager{ //Create queue all button this.playlistQueueAllButton = document.createElement('button'); //Set it's classes - this.playlistQueueAllButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control'); + this.playlistQueueAllButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control', 'not-first'); //Inject text content this.playlistQueueAllButton.textContent = 'All'; //Set title @@ -212,7 +208,7 @@ class playlistManager{ //Create add from URL button this.playlistAddURLButton = document.createElement('button'); //Set it's classes - this.playlistAddURLButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'positive-button'); + this.playlistAddURLButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'positive-button', 'not-first'); //Set Tile this.playlistAddURLButton.title = 'Add To Playlist From URL' @@ -223,13 +219,14 @@ class playlistManager{ this.playlistAddIcon.classList.add('bi-plus-lg'); this.playlistLinkIcon.classList.add('bi-link-45deg'); + //Append icons to URL button this.playlistAddURLButton.appendChild(this.playlistAddIcon); this.playlistAddURLButton.appendChild(this.playlistLinkIcon); //Create default titles button this.playlistDefaultTitlesButton = document.createElement('button'); //Set classes - this.playlistDefaultTitlesButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-tags-fill', 'positive-button'); + this.playlistDefaultTitlesButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-tags-fill', 'positive-button', 'not-first'); //Set title this.playlistDefaultTitlesButton.title = 'Change Default Titles' //Set dataset @@ -238,15 +235,14 @@ class playlistManager{ //Create rename button this.playlistRenameButton = document.createElement('button'); //Set it's classes - this.playlistRenameButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-input-cursor-text', 'positive-button'); + this.playlistRenameButton.classList.add('queue-playlist-add-url-button', 'queue-playlist-control', 'bi-input-cursor-text', 'positive-button', 'not-first'); //Set title this.playlistRenameButton.title = 'Rename Playlist' - //Create delete button this.playlistDeleteButton = document.createElement('button'); //Set it's classes - this.playlistDeleteButton.classList.add('queue-playlist-delete-button', 'queue-playlist-control', 'danger-button', 'bi-trash-fill'); + this.playlistDeleteButton.classList.add('queue-playlist-delete-button', 'queue-playlist-control', 'danger-button', 'bi-trash-fill', 'not-first'); //Set title this.playlistDeleteButton.title = 'Delete Playlist' @@ -271,8 +267,15 @@ class playlistManager{ this.mediaContainer = document.createElement('div'); //Set classes this.mediaContainer.classList.add('queue-playlist-media-container-div'); - //Auto-hide media container - this.mediaContainer.style.display = 'none'; + + //If the playlist wasn't set to open in the open map + if(!this.openMap.get(this.playlist.name)){ + //Auto-hide media container + this.mediaContainer.style.display = 'none'; + } + + //Set dataset + this.mediaContainer.dataset['playlist'] = this.playlist.name; for(let mediaIndex in this.playlist.media){ //Grab media object from playlist @@ -295,8 +298,23 @@ class playlistManager{ //Inject text content mediaTitle.innerText = utils.unescapeEntities(media.title); + 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 from playlist'; + //Set dataset + //It's probably more effecient to set this at mediaContainer level, but I don't want to crawl multiple parents later on + deleteMediaIcon.dataset['playlist'] = this.playlist.name; + deleteMediaIcon.dataset['uuid'] = media.uuid; + //Append items to media div mediaDiv.appendChild(mediaTitle); + mediaDiv.appendChild(deleteMediaIcon); + + + //Handle input event listeners + deleteMediaIcon.addEventListener('click', this.deleteMedia.bind(this)); //Append media div to media container this.mediaContainer.appendChild(mediaDiv); @@ -306,6 +324,31 @@ class playlistManager{ this.mediaContainer; } + //I'd rather make this a class function but it's probably cleaner to not have to parent crawl + 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'; + } + } + addURL(event){ new addURLPopup( event, @@ -331,16 +374,32 @@ class playlistManager{ event, event.target.parentNode.dataset['playlist'], this.client, - this.queuePanel.ownerDoc + 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 = newName; + } + } } queueAll(event){ - client.socket.emit('queueChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); + this.client.socket.emit('queueChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); } deletePlaylist(event){ - client.socket.emit('deleteChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); + this.client.socket.emit('deleteChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); + } + + deleteMedia(event){ + this.client.socket.emit('deleteChannelPlaylistMedia', {playlist: event.target.dataset['playlist'], uuid: event.target.dataset['uuid']}); } } @@ -478,13 +537,15 @@ class defaultTitlesPopup{ } class renamePopup{ - constructor(event, playlist, client, doc){ + constructor(event, playlist, client, doc, cb){ //Set Client this.client = client; //Set playlist this.playlist = playlist + 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 :( this.popup = new canopyUXUtils.popup('/renamePlaylist', true, this.asyncConstructor.bind(this), doc); @@ -513,6 +574,12 @@ class renamePopup{ 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(); }