From c3781d62593d1e9ec5f01c261babfe40bdb7dd3a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 3 Apr 2025 01:43:19 -0400 Subject: [PATCH] Continued work on playlist management UI --- src/app/channel/media/playlistHandler.js | 68 +++++++++++++++- src/schemas/channel/media/playlistSchema.js | 2 +- .../partial/popup/playlistDefaultTitles.ejs | 21 +++++ src/views/partial/popup/renamePlaylist.ejs | 21 +++++ www/css/popup/playlistDefaultTitles.css | 9 +++ .../panels/queuePanel/playlistManager.js | 78 +++++++++++++++---- www/js/utils.js | 34 ++++---- 7 files changed, 201 insertions(+), 32 deletions(-) create mode 100644 src/views/partial/popup/playlistDefaultTitles.ejs create mode 100644 src/views/partial/popup/renamePlaylist.ejs create mode 100644 www/css/popup/playlistDefaultTitles.css diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index 5b56c8b..fa7f081 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -38,6 +38,7 @@ module.exports = class{ socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)}); + socket.on("changeDefaultTitlesChannelPlaylist", (data) => {this.changeDefaultTitlesChannelPlaylist(socket, data)}); } //--- USER-FACING PLAYLIST FUNCTIONS --- @@ -64,6 +65,7 @@ module.exports = class{ chanDB = await channelModel.findOne({name: this.channel.name}); } + //If the title is too long if(!validator.isLength(data.playlist, {max:30})){ //Bitch, moan, complain... @@ -75,10 +77,30 @@ module.exports = class{ //Escape/trim the playlist name const name = validator.escape(validator.trim(data.playlist)); + //If the channel already exists + if(chanDB.getPlaylistByName(name) != null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, `Playlist named '${name}' already exists!`, "validation"); + //and ignore it! + return; + } + + //Create empty array to hold titles + const safeTitles = []; + + //For each default title passed by the data + for(let title of data.defaultTitles){ + //If the title isn't too long + if(validator.isLength(title, {min:1, max:30})){ + //Add it to the safe title list + safeTitles.push(validator.escape(validator.trim(title))) + } + } + //Add playlist to the channel doc chanDB.media.playlists.push({ name, - defaultTitles: data.defaultTitles + defaultTitles: safeTitles }); //Save the channel doc @@ -209,6 +231,14 @@ module.exports = class{ //Escape/trim the playlist name const name = validator.escape(validator.trim(data.name)); + //If the channel already exists + if(chanDB.getPlaylistByName(name) != null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, `Playlist named '${name}' already exists!`, "validation"); + //and ignore it! + return; + } + //Find playlist let playlist = chanDB.getPlaylistByName(data.playlist); @@ -224,4 +254,40 @@ module.exports = class{ return loggerUtils.socketExceptionHandler(socket, err); } } + + async changeDefaultTitlesChannelPlaylist(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}); + } + + //Find playlist + let playlist = chanDB.getPlaylistByName(data.playlist); + + //Create empty array to hold titles + const safeTitles = []; + + //For each default title passed by the data + for(let title of data.defaultTitles){ + //If the title isn't too long or too short + if(validator.isLength(title, {min: 1, max:30})){ + //Add it to the safe title list + safeTitles.push(validator.escape(validator.trim(title))) + } + } + + //Change playlist name + chanDB.media.playlists[playlist.listIndex].defaultTitles = safeTitles; + + //Save channel document + await chanDB.save(); + + //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/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index a2d2b9f..488c89d 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -29,7 +29,7 @@ const playlistSchema = new mongoose.Schema({ media: [playlistMediaSchema], defaultTitles:[{ type: mongoose.SchemaTypes.String, - required: true, + required: false, default: [] }] }); diff --git a/src/views/partial/popup/playlistDefaultTitles.ejs b/src/views/partial/popup/playlistDefaultTitles.ejs new file mode 100644 index 0000000..087df4e --- /dev/null +++ b/src/views/partial/popup/playlistDefaultTitles.ejs @@ -0,0 +1,21 @@ +<%# 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/src/views/partial/popup/renamePlaylist.ejs b/src/views/partial/popup/renamePlaylist.ejs new file mode 100644 index 0000000..de5e2f7 --- /dev/null +++ b/src/views/partial/popup/renamePlaylist.ejs @@ -0,0 +1,21 @@ +<%# 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/popup/playlistDefaultTitles.css b/www/css/popup/playlistDefaultTitles.css new file mode 100644 index 0000000..f56a783 --- /dev/null +++ b/www/css/popup/playlistDefaultTitles.css @@ -0,0 +1,9 @@ +#playlist-default-titles-popup-div{ + display: flex; + flex-direction: column; +} + +#playlist-default-titles-popup-prompt{ + height: 5em; + resize: vertical; +} \ No newline at end of file diff --git a/www/js/channel/panels/queuePanel/playlistManager.js b/www/js/channel/panels/queuePanel/playlistManager.js index 32b5af4..6349857 100644 --- a/www/js/channel/panels/queuePanel/playlistManager.js +++ b/www/js/channel/panels/queuePanel/playlistManager.js @@ -39,6 +39,9 @@ class playlistManager{ this.channelPlaylistLabel = this.panelDocument.querySelector('#queue-channel-playlist-span'); this.channelPlaylistCaret = this.panelDocument.querySelector('#queue-channel-playlist-toggle'); + //Force playlist re-render to fix controls + this.client.socket.emit('getChannelPlaylists'); + //Setup Input this.setupInput(); } @@ -249,7 +252,9 @@ class playlistManager{ playlistControls.appendChild(playlistDeleteButton); //Define input event listeners - playlistAddURLButton.addEventListener('click', (event)=>{new addURLPopup(event, playlist.name, this.client, this.queuePanel.ownerDoc)}) + playlistAddURLButton.addEventListener('click', (event)=>{new addURLPopup(event, playlist.name, this.client, this.queuePanel.ownerDoc)}); + playlistDefaultTitlesButton.addEventListener('click', (event)=>{new defaultTitlesPopup(event, playlist.name, playlist.defaultTitles, this.client, this.queuePanel.ownerDoc)}); + playlistRenameButton.addEventListener('click', (event)=>{new renamePopup(event, playlist.name, this.client, this.queuePanel.ownerDoc)}); playlistQueueAllButton.addEventListener('click', queueAll); playlistDeleteButton.addEventListener('click', deletePlaylist); @@ -316,7 +321,7 @@ class newPlaylistPopup{ //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); + this.popup = new canopyUXUtils.popup('/newPlaylist', true, this.asyncConstructor.bind(this), doc, false); } asyncConstructor(){ @@ -336,7 +341,7 @@ class newPlaylistPopup{ createPlaylist(event){ //If we clicked or hit enter - if(event.key == null || event.key == "Enter"){ + if(event.key == null || (event.key == "Enter" && this.defaultTitles !== this.popup.doc.activeElement)){ //If we're saving to the channel if(this.location.value == 'channel'){ @@ -383,7 +388,7 @@ class addURLPopup{ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ - //Tell the server to create a new channel playlist + //Tell the server to add url to the playlist this.client.socket.emit('addToChannelPlaylist', { playlist: this.playlist, url: this.urlPrompt.value @@ -408,30 +413,31 @@ class defaultTitlesPopup{ //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('/addToPlaylist', true, this.asyncConstructor.bind(this), doc); + this.popup = new canopyUXUtils.popup('/playlistDefaultTitles', true, this.asyncConstructor.bind(this), doc, false); } asyncConstructor(){ - this.urlPrompt = this.popup.contentDiv.querySelector('#playlist-add-media-popup-prompt'); - this.addButton = this.popup.contentDiv.querySelector('#playlist-add-media-popup-button'); + 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(); } setupInput(){ //Setup input - this.addButton.addEventListener('click', this.addToPlaylist.bind(this)); - this.popup.popupDiv.addEventListener('keydown', this.addToPlaylist.bind(this)); + this.titleButton.addEventListener('click', this.changeDefaultTitles.bind(this)); + this.popup.popupDiv.addEventListener('keydown', this.changeDefaultTitles.bind(this)); } - addToPlaylist(event){ - //If we clicked or hit enter - if(event.key == null || event.key == "Enter"){ + 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 create a new channel playlist - this.client.socket.emit('addToChannelPlaylist', { + //Tell the server to change the titles + this.client.socket.emit('changeDefaultTitlesChannelPlaylist', { playlist: this.playlist, - url: this.urlPrompt.value + defaultTitles: this.titlePrompt.value.split('\n') }); //Close the popup @@ -439,3 +445,45 @@ class defaultTitlesPopup{ } } } + +class renamePopup{ + constructor(event, playlist, client, doc){ + //Set Client + this.client = client; + + //Set playlist + this.playlist = playlist + + //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); + } + + asyncConstructor(){ + this.renamePrompt = this.popup.contentDiv.querySelector('#playlist-rename-popup-prompt'); + this.renameButton = this.popup.contentDiv.querySelector('#playlist-rename-popup-button'); + + this.setupInput(); + } + + setupInput(){ + //Setup input + this.renameButton.addEventListener('click', this.changeDefaultTitles.bind(this)); + this.popup.popupDiv.addEventListener('keydown', this.changeDefaultTitles.bind(this)); + } + + changeDefaultTitles(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 + }); + + //Close the popup + this.popup.closePopup(); + } + } +} \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index 63c1ac0..32ba304 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -355,12 +355,14 @@ class canopyUXUtils{ } static popup = class{ - constructor(content, ajaxPopup = false, cb, doc = document){ + constructor(content, ajaxPopup = false, cb, doc = document, keyClose = true){ //Define non-popup node values this.content = content; this.ajaxPopup = ajaxPopup; this.cb = cb; this.doc = doc; + this.keyClose = keyClose + //define popup nodes this.createPopup(); @@ -393,21 +395,23 @@ class canopyUXUtils{ this.popupDiv.appendChild(this.closeIcon); this.popupDiv.appendChild(this.contentDiv); + //If we're closing on keydown + if(this.keyClose){ + //Bit hacky but the only way to remove an event listener while keeping the function bound to this + //Isn't javascript precious? + this.keyClose = ((event)=>{ + //If we hit enter or escape + if(event.key == "Enter" || event.key == "Escape"){ + //Close the pop-up + this.closePopup(); + //Remove this event listener + this.doc.removeEventListener('keydown', this.keyClose); + } + }).bind(this); - //Bit hacky but the only way to remove an event listener while keeping the function bound to this - //Isn't javascript precious? - this.keyClose = ((event)=>{ - //If we hit enter or escape - if(event.key == "Enter" || event.key == "Escape"){ - //Close the pop-up - this.closePopup(); - //Remove this event listener - this.doc.removeEventListener('keydown', this.keyClose); - } - }).bind(this); - - //Add event listener to close popup when enter is hit - this.doc.addEventListener('keydown', this.keyClose); + //Add event listener to close popup when enter is hit + this.doc.addEventListener('keydown', this.keyClose); + } } async fillPopupContent(){