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 . %>
+
+
Edit Playlist Default Titles
+
+
+
+
\ 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 . %>
+<%# %>
+
Rename Playlist
+
+
+
+
\ 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(){