520 lines
20 KiB
JavaScript
520 lines
20 KiB
JavaScript
/*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/>.*/
|
|
class playlistManager{
|
|
constructor(client, panelDocument, queuePanel){
|
|
//Set client
|
|
this.client = client;
|
|
//Set panel document
|
|
this.panelDocument = panelDocument;
|
|
//Set parent queue panel
|
|
this.queuePanel = queuePanel;
|
|
|
|
//Define Listeners
|
|
this.defineListeners();
|
|
}
|
|
|
|
defineListeners(){
|
|
this.client.socket.on("chanPlaylists", this.renderChannelPlaylists.bind(this));
|
|
}
|
|
|
|
docSwitch(){
|
|
//Grab menus
|
|
this.channelPlaylistDiv = this.panelDocument.querySelector("#queue-channel-playlist-div");
|
|
|
|
//Grab controls
|
|
this.createPlaylistSpan = this.panelDocument.querySelector('#queue-add-playlist-span');
|
|
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();
|
|
}
|
|
|
|
setupInput(){
|
|
this.createPlaylistSpan.addEventListener('click', (event)=>{new newPlaylistPopup(event, this.client, this.queuePanel.ownerDoc)})
|
|
this.channelPlaylistLabel.addEventListener('click', this.toggleChannelPlaylists.bind(this));
|
|
}
|
|
|
|
/* queue control button functions */
|
|
toggleChannelPlaylists(event){
|
|
//If the div is hidden
|
|
if(this.channelPlaylistDiv.style.display == 'none'){
|
|
//Light up the button
|
|
this.channelPlaylistLabel.classList.add('positive');
|
|
//Flip the caret
|
|
this.channelPlaylistCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill');
|
|
//Show the div
|
|
this.channelPlaylistDiv.style.display = '';
|
|
}else{
|
|
//Unlight the button
|
|
this.channelPlaylistLabel.classList.remove('positive');
|
|
//Flip the caret
|
|
this.channelPlaylistCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill');
|
|
//Hide the div
|
|
this.channelPlaylistDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
//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...
|
|
renderChannelPlaylists(data){
|
|
//Clear channel playlist div
|
|
this.channelPlaylistDiv.innerHTML = '';
|
|
|
|
//For every playlist sent down from the server
|
|
for(let playlistIndex in data){
|
|
//Get playlist from data
|
|
this.playlist = data[playlistIndex];
|
|
|
|
//Create a new playlist div
|
|
this.playlistDiv = document.createElement('div');
|
|
//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
|
|
this.playlistSpan.classList.add('queue-playlist-span');
|
|
|
|
//If this isn't our first rodeo
|
|
if(this.playlistIndex != 0){
|
|
//make note
|
|
this.playlistSpan.classList.add('not-first');
|
|
}
|
|
|
|
//Render out playlist entry contents
|
|
this.renderLabels();
|
|
this.renderControls();
|
|
this.renderMedia();
|
|
|
|
//Append entry items to playlist entry line
|
|
this.playlistSpan.appendChild(this.playlistLabels);
|
|
this.playlistSpan.appendChild(this.playlistControls);
|
|
|
|
//Append entry line and contents to playlist div
|
|
this.playlistDiv.appendChild(this.playlistSpan);
|
|
this.playlistDiv.appendChild(this.mediaContainer);
|
|
|
|
//Append current playlist div to the channel playlists div
|
|
this.channelPlaylistDiv.appendChild(this.playlistDiv);
|
|
}
|
|
|
|
}
|
|
|
|
//aux rendering functions
|
|
renderLabels(){
|
|
//Create playlist label span
|
|
this.playlistLabels = document.createElement('span');
|
|
//Set it's class
|
|
this.playlistLabels.classList.add('queue-playlist-labels-span');
|
|
|
|
//create playlist title span
|
|
this.playlistTitleSpan = document.createElement('span');
|
|
//Set class
|
|
this.playlistTitleSpan.classList.add('queue-playlist-title-span', 'interactive');
|
|
|
|
//Create playlist title caret
|
|
this.playlistTitleCaret = document.createElement('i');
|
|
//Set class
|
|
this.playlistTitleCaret.classList.add('bi-caret-right-fill');
|
|
|
|
//Create playlist title label
|
|
this.playlistTitle = document.createElement('p');
|
|
//Set it's class
|
|
this.playlistTitle.classList.add('queue-playlist-title');
|
|
//Unescape Sanatized Enteties and safely inject as plaintext
|
|
this.playlistTitle.innerText = utils.unescapeEntities(this.playlist.name);
|
|
|
|
//Construct playlist title span
|
|
this.playlistTitleSpan.appendChild(this.playlistTitleCaret);
|
|
this.playlistTitleSpan.appendChild(this.playlistTitle);
|
|
|
|
//Create playlist count label
|
|
this.playlistCount = document.createElement('p');
|
|
//Set it's class
|
|
this.playlistCount.classList.add('queue-playlist-count');
|
|
//List video count
|
|
this.playlistCount.innerText = `Count: ${this.playlist.media.length}`;
|
|
|
|
//Append items to playlist labels span
|
|
this.playlistLabels.appendChild(this.playlistTitleSpan);
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
renderControls(){
|
|
//Create playlist control span
|
|
this.playlistControls = document.createElement('span');
|
|
//Set it's class
|
|
this.playlistControls.classList.add('queue-playlist-control-span');
|
|
//Set dataset
|
|
this.playlistControls.dataset['playlist'] = this.playlist.name;
|
|
|
|
//Create queue all button
|
|
this.playlistQueueRandomButton = document.createElement('button');
|
|
//Set it's classes
|
|
this.playlistQueueRandomButton.classList.add('queue-playlist-queue-random-button', 'queue-playlist-control');
|
|
//Inject text content
|
|
this.playlistQueueRandomButton.textContent = 'Random';
|
|
//Set title
|
|
this.playlistQueueRandomButton.title = 'Queue Random Item from Playlist';
|
|
|
|
//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');
|
|
//Inject text content
|
|
this.playlistQueueAllButton.textContent = 'All';
|
|
//Set title
|
|
this.playlistQueueAllButton.title = 'Queue Entire Playlist';
|
|
|
|
//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');
|
|
//Set Tile
|
|
this.playlistAddURLButton.title = 'Add To Playlist From URL'
|
|
|
|
//Create playlist icons (we're using two so we're putting them inside the button :P)
|
|
this.playlistAddIcon = document.createElement('i');
|
|
this.playlistLinkIcon = document.createElement('i');
|
|
//set classes
|
|
this.playlistAddIcon.classList.add('bi-plus-lg');
|
|
this.playlistLinkIcon.classList.add('bi-link-45deg');
|
|
|
|
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');
|
|
//Set title
|
|
this.playlistDefaultTitlesButton.title = 'Change Default Titles'
|
|
//Set dataset
|
|
this.playlistDefaultTitlesButton.dataset['titles'] = JSON.stringify(this.playlist.defaultTitles);
|
|
|
|
//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');
|
|
//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');
|
|
//Set title
|
|
this.playlistDeleteButton.title = 'Delete Playlist'
|
|
|
|
//Append items to playlist control span
|
|
this.playlistControls.appendChild(this.playlistQueueRandomButton);
|
|
this.playlistControls.appendChild(this.playlistQueueAllButton);
|
|
this.playlistControls.appendChild(this.playlistAddURLButton);
|
|
this.playlistControls.appendChild(this.playlistDefaultTitlesButton);
|
|
this.playlistControls.appendChild(this.playlistRenameButton);
|
|
this.playlistControls.appendChild(this.playlistDeleteButton);
|
|
|
|
//Define input event listeners
|
|
this.playlistAddURLButton.addEventListener('click', this.addURL.bind(this));
|
|
this.playlistDefaultTitlesButton.addEventListener('click', this.editDefaultTitles.bind(this));
|
|
this.playlistRenameButton.addEventListener('click', this.renamePlaylist.bind(this));
|
|
this.playlistQueueAllButton.addEventListener('click', this.queueAll.bind(this));
|
|
this.playlistDeleteButton.addEventListener('click', this.deletePlaylist.bind(this));
|
|
}
|
|
|
|
renderMedia(){
|
|
//Create media container div
|
|
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';
|
|
|
|
for(let mediaIndex in this.playlist.media){
|
|
//Grab media object from playlist
|
|
const media = this.playlist.media[mediaIndex];
|
|
|
|
//Create media div
|
|
const mediaDiv = document.createElement('div');
|
|
//Set class
|
|
mediaDiv.classList.add('queue-playlist-media-div');
|
|
|
|
//If this isn't our first rodeo
|
|
if(mediaIndex != 0){
|
|
mediaDiv.classList.add('not-first');
|
|
}
|
|
|
|
//Create media title
|
|
const mediaTitle = document.createElement('p');
|
|
//Set class
|
|
mediaTitle.classList.add('queue-playlist-media-title');
|
|
//Inject text content
|
|
mediaTitle.innerText = utils.unescapeEntities(media.title);
|
|
|
|
//Append items to media div
|
|
mediaDiv.appendChild(mediaTitle);
|
|
|
|
//Append media div to media container
|
|
this.mediaContainer.appendChild(mediaDiv);
|
|
}
|
|
|
|
//return media container
|
|
this.mediaContainer;
|
|
}
|
|
|
|
addURL(event){
|
|
new addURLPopup(
|
|
event,
|
|
event.target.parentNode.dataset['playlist'],
|
|
this.client,
|
|
this.queuePanel.ownerDoc
|
|
);
|
|
}
|
|
|
|
//playlist control functions
|
|
editDefaultTitles(event){
|
|
new defaultTitlesPopup(
|
|
event,
|
|
event.target.parentNode.dataset['playlist'],
|
|
JSON.parse(event.target.dataset['titles']),
|
|
this.client,
|
|
this.queuePanel.ownerDoc
|
|
);
|
|
}
|
|
|
|
renamePlaylist(event){
|
|
new renamePopup(
|
|
event,
|
|
event.target.parentNode.dataset['playlist'],
|
|
this.client,
|
|
this.queuePanel.ownerDoc
|
|
);
|
|
}
|
|
|
|
queueAll(event){
|
|
client.socket.emit('queueChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']});
|
|
}
|
|
|
|
deletePlaylist(event){
|
|
client.socket.emit('deleteChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']});
|
|
}
|
|
|
|
}
|
|
|
|
class newPlaylistPopup{
|
|
constructor(event, client, doc){
|
|
//Set Client
|
|
this.client = client;
|
|
|
|
//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, false);
|
|
}
|
|
|
|
asyncConstructor(){
|
|
this.name = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-name');
|
|
this.defaultTitles = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-default-titles');
|
|
this.location = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-location');
|
|
this.saveButton = this.popup.contentDiv.querySelector('#queue-create-playlist-popup-save');
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
//Setup input
|
|
this.saveButton.addEventListener('click', this.createPlaylist.bind(this));
|
|
this.popup.popupDiv.addEventListener('keydown', this.createPlaylist.bind(this));
|
|
}
|
|
|
|
createPlaylist(event){
|
|
//If we clicked or hit 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'){
|
|
//Tell the server to create a new channel playlist
|
|
this.client.socket.emit('createChannelPlaylist', {
|
|
playlist: this.name.value,
|
|
defaultTitles: this.defaultTitles.value.split('\n')
|
|
})
|
|
}
|
|
|
|
//Close the popup
|
|
this.popup.closePopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
class addURLPopup{
|
|
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('/addToPlaylist', true, this.asyncConstructor.bind(this), doc);
|
|
}
|
|
|
|
asyncConstructor(){
|
|
this.urlPrompt = this.popup.contentDiv.querySelector('#playlist-add-media-popup-prompt');
|
|
this.addButton = this.popup.contentDiv.querySelector('#playlist-add-media-popup-button');
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
//Setup input
|
|
this.addButton.addEventListener('click', this.addToPlaylist.bind(this));
|
|
this.popup.popupDiv.addEventListener('keydown', this.addToPlaylist.bind(this));
|
|
}
|
|
|
|
addToPlaylist(event){
|
|
//If we clicked or hit enter
|
|
if(event.key == null || event.key == "Enter"){
|
|
|
|
//Tell the server to add url to the playlist
|
|
this.client.socket.emit('addToChannelPlaylist', {
|
|
playlist: this.playlist,
|
|
url: this.urlPrompt.value
|
|
});
|
|
|
|
//Close the popup
|
|
this.popup.closePopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
class defaultTitlesPopup{
|
|
constructor(event, playlist, titles, client, doc){
|
|
//Set Client
|
|
this.client = client;
|
|
|
|
//Set playlist
|
|
this.playlist = playlist
|
|
|
|
//Set title string
|
|
this.titles = titles.join('\n');
|
|
|
|
//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('/playlistDefaultTitles', true, this.asyncConstructor.bind(this), doc, false);
|
|
}
|
|
|
|
asyncConstructor(){
|
|
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.titleButton.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" && this.titlePrompt !== this.popup.doc.activeElement)){
|
|
|
|
//Tell the server to change the titles
|
|
this.client.socket.emit('changeDefaultTitlesChannelPlaylist', {
|
|
playlist: this.playlist,
|
|
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
|
|
this.popup.closePopup();
|
|
}
|
|
}
|
|
} |