Finished up with Playlist Managment UI.

This commit is contained in:
rainbow napkin 2025-04-04 08:18:20 -04:00
parent c8c59feb7f
commit f21b66fae0
7 changed files with 180 additions and 42 deletions

View file

@ -35,6 +35,7 @@ module.exports = class{
socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)}); socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)});
socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)}); socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)});
socket.on("deleteChannelPlaylist", (data) => {this.deleteChannelPlaylist(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("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)});
@ -290,4 +291,30 @@ module.exports = class{
return loggerUtils.socketExceptionHandler(socket, err); 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);
}
}
} }

View file

@ -606,6 +606,17 @@ channelSchema.methods.deletePlaylistByName = async function(name){
await this.save(); 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){ channelSchema.methods.addToPlaylist = async function(name, media){
//Find the playlist //Find the playlist
let playlist = this.getPlaylistByName(name); let playlist = this.getPlaylistByName(name);

View file

@ -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; module.exports = playlistSchema;

View file

@ -297,13 +297,17 @@ span.user-entry{
margin: 0; margin: 0;
left: 1em; left: 1em;
top: 0.6em; top: 0.6em;
pointer-events: none;
} }
#chat-panel-prompt-autocomplete-filler{ #chat-panel-prompt-autocomplete-filler{
visibility: hidden; visibility: hidden;
user-select: none; user-select: none;
cursor: auto; cursor: auto;
pointer-events: none; }
#chat-panel-prompt-autocomplete-display{
pointer-events: all;
} }
.toke{ .toke{

View file

@ -185,6 +185,11 @@ div.dragging-queue-entry{
flex-direction: row; 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{ .queue-playlist-count{
font-size: 0.8em; font-size: 0.8em;
} }
@ -242,7 +247,15 @@ div.dragging-queue-entry{
width: 0.5em; 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 */ /* date */
#queue-control-date{ #queue-control-date{

View file

@ -560,17 +560,16 @@ div.archived p{
color: var(--accent1-alt0); 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); background-color: var(--bg1-alt0);
color: var(--accent1); color: var(--accent1);
} }
.queue-playlist-media-div.not-first{ .queue-playlist-media-div.not-first{
border-top: var(--bg1) solid 1px; 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; border-left: var(--accent1-alt0) solid 1px;
} }

View file

@ -71,9 +71,28 @@ class playlistManager{
} }
} }
//Keeping everything in one function was super lazy when 'this.' exists, ESPECIALLY when writing a class! checkOpenPlaylists(){
//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... //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){ renderChannelPlaylists(data){
//Check for open playlists
this.checkOpenPlaylists();
//Clear channel playlist div //Clear channel playlist div
this.channelPlaylistDiv.innerHTML = ''; this.channelPlaylistDiv.innerHTML = '';
@ -87,9 +106,6 @@ class playlistManager{
//Set it's class //Set it's class
this.playlistDiv.classList.add('queue-playlist-div'); 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 //Create span to hold playlist entry line contents
this.playlistSpan = document.createElement('span'); this.playlistSpan = document.createElement('span');
//Set classes //Set classes
@ -160,27 +176,7 @@ class playlistManager{
this.playlistLabels.appendChild(this.playlistCount); this.playlistLabels.appendChild(this.playlistCount);
//Define input listeners //Define input listeners
this.playlistTitleSpan.addEventListener('click', toggleMedia.bind(this)); this.playlistTitleSpan.addEventListener('click', this.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';
}
}
} }
renderControls(){ renderControls(){
@ -203,7 +199,7 @@ class playlistManager{
//Create queue all button //Create queue all button
this.playlistQueueAllButton = document.createElement('button'); this.playlistQueueAllButton = document.createElement('button');
//Set it's classes //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 //Inject text content
this.playlistQueueAllButton.textContent = 'All'; this.playlistQueueAllButton.textContent = 'All';
//Set title //Set title
@ -212,7 +208,7 @@ class playlistManager{
//Create add from URL button //Create add from URL button
this.playlistAddURLButton = document.createElement('button'); this.playlistAddURLButton = document.createElement('button');
//Set it's classes //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 //Set Tile
this.playlistAddURLButton.title = 'Add To Playlist From URL' this.playlistAddURLButton.title = 'Add To Playlist From URL'
@ -223,13 +219,14 @@ class playlistManager{
this.playlistAddIcon.classList.add('bi-plus-lg'); this.playlistAddIcon.classList.add('bi-plus-lg');
this.playlistLinkIcon.classList.add('bi-link-45deg'); this.playlistLinkIcon.classList.add('bi-link-45deg');
//Append icons to URL button
this.playlistAddURLButton.appendChild(this.playlistAddIcon); this.playlistAddURLButton.appendChild(this.playlistAddIcon);
this.playlistAddURLButton.appendChild(this.playlistLinkIcon); this.playlistAddURLButton.appendChild(this.playlistLinkIcon);
//Create default titles button //Create default titles button
this.playlistDefaultTitlesButton = document.createElement('button'); this.playlistDefaultTitlesButton = document.createElement('button');
//Set classes //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 //Set title
this.playlistDefaultTitlesButton.title = 'Change Default Titles' this.playlistDefaultTitlesButton.title = 'Change Default Titles'
//Set dataset //Set dataset
@ -238,15 +235,14 @@ class playlistManager{
//Create rename button //Create rename button
this.playlistRenameButton = document.createElement('button'); this.playlistRenameButton = document.createElement('button');
//Set it's classes //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 //Set title
this.playlistRenameButton.title = 'Rename Playlist' this.playlistRenameButton.title = 'Rename Playlist'
//Create delete button //Create delete button
this.playlistDeleteButton = document.createElement('button'); this.playlistDeleteButton = document.createElement('button');
//Set it's classes //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 //Set title
this.playlistDeleteButton.title = 'Delete Playlist' this.playlistDeleteButton.title = 'Delete Playlist'
@ -271,8 +267,15 @@ class playlistManager{
this.mediaContainer = document.createElement('div'); this.mediaContainer = document.createElement('div');
//Set classes //Set classes
this.mediaContainer.classList.add('queue-playlist-media-container-div'); this.mediaContainer.classList.add('queue-playlist-media-container-div');
//If the playlist wasn't set to open in the open map
if(!this.openMap.get(this.playlist.name)){
//Auto-hide media container //Auto-hide media container
this.mediaContainer.style.display = 'none'; this.mediaContainer.style.display = 'none';
}
//Set dataset
this.mediaContainer.dataset['playlist'] = this.playlist.name;
for(let mediaIndex in this.playlist.media){ for(let mediaIndex in this.playlist.media){
//Grab media object from playlist //Grab media object from playlist
@ -295,8 +298,23 @@ class playlistManager{
//Inject text content //Inject text content
mediaTitle.innerText = utils.unescapeEntities(media.title); 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 //Append items to media div
mediaDiv.appendChild(mediaTitle); mediaDiv.appendChild(mediaTitle);
mediaDiv.appendChild(deleteMediaIcon);
//Handle input event listeners
deleteMediaIcon.addEventListener('click', this.deleteMedia.bind(this));
//Append media div to media container //Append media div to media container
this.mediaContainer.appendChild(mediaDiv); this.mediaContainer.appendChild(mediaDiv);
@ -306,6 +324,31 @@ class playlistManager{
this.mediaContainer; 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){ addURL(event){
new addURLPopup( new addURLPopup(
event, event,
@ -331,16 +374,32 @@ class playlistManager{
event, event,
event.target.parentNode.dataset['playlist'], event.target.parentNode.dataset['playlist'],
this.client, 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){ 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){ 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{ class renamePopup{
constructor(event, playlist, client, doc){ constructor(event, playlist, client, doc, cb){
//Set Client //Set Client
this.client = client; this.client = client;
//Set playlist //Set playlist
this.playlist = playlist this.playlist = playlist
this.cb = cb;
//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('/renamePlaylist', true, this.asyncConstructor.bind(this), doc); this.popup = new canopyUXUtils.popup('/renamePlaylist', true, this.asyncConstructor.bind(this), doc);
@ -513,6 +574,12 @@ class renamePopup{
name: this.renamePrompt.value 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 //Close the popup
this.popup.closePopup(); this.popup.closePopup();
} }