Continued work on playlist management UI

This commit is contained in:
rainbow napkin 2025-04-03 01:43:19 -04:00
parent 04dec153ac
commit c3781d6259
7 changed files with 201 additions and 32 deletions

View file

@ -38,6 +38,7 @@ module.exports = class{
socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)});
socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)});
socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)}); socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)});
socket.on("changeDefaultTitlesChannelPlaylist", (data) => {this.changeDefaultTitlesChannelPlaylist(socket, data)});
} }
//--- USER-FACING PLAYLIST FUNCTIONS --- //--- USER-FACING PLAYLIST FUNCTIONS ---
@ -64,6 +65,7 @@ module.exports = class{
chanDB = await channelModel.findOne({name: this.channel.name}); chanDB = await channelModel.findOne({name: this.channel.name});
} }
//If the title is too long //If the title is too long
if(!validator.isLength(data.playlist, {max:30})){ if(!validator.isLength(data.playlist, {max:30})){
//Bitch, moan, complain... //Bitch, moan, complain...
@ -75,10 +77,30 @@ module.exports = class{
//Escape/trim the playlist name //Escape/trim the playlist name
const name = validator.escape(validator.trim(data.playlist)); 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 //Add playlist to the channel doc
chanDB.media.playlists.push({ chanDB.media.playlists.push({
name, name,
defaultTitles: data.defaultTitles defaultTitles: safeTitles
}); });
//Save the channel doc //Save the channel doc
@ -209,6 +231,14 @@ module.exports = class{
//Escape/trim the playlist name //Escape/trim the playlist name
const name = validator.escape(validator.trim(data.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 //Find playlist
let playlist = chanDB.getPlaylistByName(data.playlist); let playlist = chanDB.getPlaylistByName(data.playlist);
@ -224,4 +254,40 @@ module.exports = class{
return loggerUtils.socketExceptionHandler(socket, err); 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);
}
}
} }

View file

@ -29,7 +29,7 @@ const playlistSchema = new mongoose.Schema({
media: [playlistMediaSchema], media: [playlistMediaSchema],
defaultTitles:[{ defaultTitles:[{
type: mongoose.SchemaTypes.String, type: mongoose.SchemaTypes.String,
required: true, required: false,
default: [] default: []
}] }]
}); });

View file

@ -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 <https://www.gnu.org/licenses/>. %>
<link rel="stylesheet" type="text/css" href="/css/popup/playlistDefaultTitles.css">
<h3 class="popup-title">Edit Playlist Default Titles</h3>
<div id="playlist-default-titles-popup-div">
<textarea id="playlist-default-titles-popup-prompt"></textarea>
<button class="positive-button" id="playlist-default-media-popup-button">Change Titles</button>
</div>

View file

@ -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 <https://www.gnu.org/licenses/>. %>
<%# <link rel="stylesheet" type="text/css" href="/css/popup/schedule.css"> %>
<h3 class="popup-title">Rename Playlist</h3>
<div>
<input id="playlist-rename-popup-prompt" placeholder="New Name">
<button class="positive-button" id="playlist-rename-popup-button">Rename Playlist</button>
</div>

View file

@ -0,0 +1,9 @@
#playlist-default-titles-popup-div{
display: flex;
flex-direction: column;
}
#playlist-default-titles-popup-prompt{
height: 5em;
resize: vertical;
}

View file

@ -39,6 +39,9 @@ class playlistManager{
this.channelPlaylistLabel = this.panelDocument.querySelector('#queue-channel-playlist-span'); this.channelPlaylistLabel = this.panelDocument.querySelector('#queue-channel-playlist-span');
this.channelPlaylistCaret = this.panelDocument.querySelector('#queue-channel-playlist-toggle'); this.channelPlaylistCaret = this.panelDocument.querySelector('#queue-channel-playlist-toggle');
//Force playlist re-render to fix controls
this.client.socket.emit('getChannelPlaylists');
//Setup Input //Setup Input
this.setupInput(); this.setupInput();
} }
@ -249,7 +252,9 @@ class playlistManager{
playlistControls.appendChild(playlistDeleteButton); playlistControls.appendChild(playlistDeleteButton);
//Define input event listeners //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); playlistQueueAllButton.addEventListener('click', queueAll);
playlistDeleteButton.addEventListener('click', deletePlaylist); playlistDeleteButton.addEventListener('click', deletePlaylist);
@ -316,7 +321,7 @@ class newPlaylistPopup{
//Create media popup and call async constructor when done //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 :( //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(){ asyncConstructor(){
@ -336,7 +341,7 @@ class newPlaylistPopup{
createPlaylist(event){ createPlaylist(event){
//If we clicked or hit enter //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 we're saving to the channel
if(this.location.value == 'channel'){ if(this.location.value == 'channel'){
@ -383,7 +388,7 @@ class addURLPopup{
//If we clicked or hit enter //If we clicked or hit enter
if(event.key == null || event.key == "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', { this.client.socket.emit('addToChannelPlaylist', {
playlist: this.playlist, playlist: this.playlist,
url: this.urlPrompt.value url: this.urlPrompt.value
@ -408,30 +413,73 @@ class defaultTitlesPopup{
//Create media popup and call async constructor when done //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 :( //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(){ asyncConstructor(){
this.urlPrompt = this.popup.contentDiv.querySelector('#playlist-add-media-popup-prompt'); this.titlePrompt = this.popup.contentDiv.querySelector('#playlist-default-titles-popup-prompt');
this.addButton = this.popup.contentDiv.querySelector('#playlist-add-media-popup-button'); this.titleButton = this.popup.contentDiv.querySelector('#playlist-default-media-popup-button');
this.titlePrompt.textContent = utils.unescapeEntities(this.titles);
this.setupInput(); this.setupInput();
} }
setupInput(){ setupInput(){
//Setup input //Setup input
this.addButton.addEventListener('click', this.addToPlaylist.bind(this)); this.titleButton.addEventListener('click', this.changeDefaultTitles.bind(this));
this.popup.popupDiv.addEventListener('keydown', this.addToPlaylist.bind(this)); this.popup.popupDiv.addEventListener('keydown', this.changeDefaultTitles.bind(this));
} }
addToPlaylist(event){ changeDefaultTitles(event){
//If we clicked or hit enter //If we clicked or hit enter while the prompt wasn't active
if(event.key == null || event.key == "Enter"){ if(event.key == null || (event.key == "Enter" && this.titlePrompt !== this.popup.doc.activeElement)){
//Tell the server to create a new channel playlist //Tell the server to change the titles
this.client.socket.emit('addToChannelPlaylist', { this.client.socket.emit('changeDefaultTitlesChannelPlaylist', {
playlist: this.playlist, playlist: this.playlist,
url: this.urlPrompt.value defaultTitles: this.titlePrompt.value.split('\n')
});
//Close the popup
this.popup.closePopup();
}
}
}
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 //Close the popup

View file

@ -355,12 +355,14 @@ class canopyUXUtils{
} }
static popup = class{ static popup = class{
constructor(content, ajaxPopup = false, cb, doc = document){ constructor(content, ajaxPopup = false, cb, doc = document, keyClose = true){
//Define non-popup node values //Define non-popup node values
this.content = content; this.content = content;
this.ajaxPopup = ajaxPopup; this.ajaxPopup = ajaxPopup;
this.cb = cb; this.cb = cb;
this.doc = doc; this.doc = doc;
this.keyClose = keyClose
//define popup nodes //define popup nodes
this.createPopup(); this.createPopup();
@ -393,21 +395,23 @@ class canopyUXUtils{
this.popupDiv.appendChild(this.closeIcon); this.popupDiv.appendChild(this.closeIcon);
this.popupDiv.appendChild(this.contentDiv); 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 //Add event listener to close popup when enter is hit
//Isn't javascript precious? this.doc.addEventListener('keydown', this.keyClose);
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);
} }
async fillPopupContent(){ async fillPopupContent(){