Continued work on playlist management UI
This commit is contained in:
parent
04dec153ac
commit
c3781d6259
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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: []
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
21
src/views/partial/popup/playlistDefaultTitles.ejs
Normal file
21
src/views/partial/popup/playlistDefaultTitles.ejs
Normal 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>
|
||||||
21
src/views/partial/popup/renamePlaylist.ejs
Normal file
21
src/views/partial/popup/renamePlaylist.ejs
Normal 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>
|
||||||
9
www/css/popup/playlistDefaultTitles.css
Normal file
9
www/css/popup/playlistDefaultTitles.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#playlist-default-titles-popup-div{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-default-titles-popup-prompt{
|
||||||
|
height: 5em;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,7 +395,8 @@ 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
|
//Bit hacky but the only way to remove an event listener while keeping the function bound to this
|
||||||
//Isn't javascript precious?
|
//Isn't javascript precious?
|
||||||
this.keyClose = ((event)=>{
|
this.keyClose = ((event)=>{
|
||||||
|
|
@ -409,6 +412,7 @@ class canopyUXUtils{
|
||||||
//Add event listener to close popup when enter is hit
|
//Add event listener to close popup when enter is hit
|
||||||
this.doc.addEventListener('keydown', this.keyClose);
|
this.doc.addEventListener('keydown', this.keyClose);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fillPopupContent(){
|
async fillPopupContent(){
|
||||||
if(this.ajaxPopup){
|
if(this.ajaxPopup){
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue