Added schedule locking and day jumping on scroll lock.

This commit is contained in:
rainbow napkin 2025-02-09 00:45:36 -05:00
parent c83ca63f9a
commit 44dd613ea3
8 changed files with 221 additions and 125 deletions

View file

@ -139,8 +139,11 @@ module.exports = class{
//Get schedule as a temporary array //Get schedule as a temporary array
const queue = Array.from(this.channel.queue.schedule); const queue = Array.from(this.channel.queue.schedule);
//Get schedule lock status
const queueLock = this.channel.queue.locked;
//Send off the metadata to our user's clients //Send off the metadata to our user's clients
this.emit("clientMetadata", {user: userObj, flairList, queue}); this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock});
} }
async sendSiteEmotes(){ async sendSiteEmotes(){

View file

@ -21,6 +21,7 @@ const validator = require('validator');
const queuedMedia = require('./queuedMedia'); const queuedMedia = require('./queuedMedia');
const yanker = require('../../../utils/media/yanker'); const yanker = require('../../../utils/media/yanker');
const loggerUtils = require('../../../utils/loggerUtils'); const loggerUtils = require('../../../utils/loggerUtils');
const channelModel = require('../../../schemas/channel/channelSchema');
module.exports = class{ module.exports = class{
constructor(server, channel){ constructor(server, channel){
@ -43,145 +44,182 @@ module.exports = class{
this.nextTimer = null; this.nextTimer = null;
//Create variable to hold currently playing media object //Create variable to hold currently playing media object
this.nowPlaying = null; this.nowPlaying = null;
//create boolean to hold schedule lock
this.locked = false;
} }
defineListeners(socket){ defineListeners(socket){
socket.on("queue", (data) => {this.queueURL(socket, data)}); socket.on("queue", (data) => {this.queueURL(socket, data)});
socket.on("delete", (data => {this.deleteMedia(socket, data)})); socket.on("delete", (data) => {this.deleteMedia(socket, data)});
socket.on("move", (data => {this.moveMedia(socket, data)})); socket.on("move", (data) => {this.moveMedia(socket, data)});
socket.on("clear", (data => {this.deleteRange(socket, data)})); socket.on("clear", (data) => {this.deleteRange(socket, data)});
socket.on("lock", (data) => {this.toggleLock(socket)});
} }
async queueURL(socket, data){ async queueURL(socket, data){
try{ //Get the current channel from the database
//Set url const chanDB = await channelModel.findOne({name: socket.chan});
var url = data.url;
//If we where given a bad URL if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
if(!validator.isURL(url)){ try{
//Attempt to fix the situation by encoding it //Set url
url = encodeURI(url); var url = data.url;
//If it's still bad //If we where given a bad URL
if(!validator.isURL(url)){ if(!validator.isURL(url)){
//Attempt to fix the situation by encoding it
url = encodeURI(url);
//If it's still bad
if(!validator.isURL(url)){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation");
//and ignore it!
return;
}
}
//If the title is too long
if(!validator.isLength(data.title, {max:30})){
//Bitch, moan, complain... //Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation"); loggerUtils.socketErrorHandler(socket, "Title too long!", "validation");
//and ignore it! //and ignore it!
return; return;
} }
}
//If the title is too long //Set title
if(!validator.isLength(data.title, {max:30})){ const title = validator.escape(validator.trim(data.title));
//Bitch, moan, complain... //set start
loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); var start = data.start;
//and ignore it!
return;
}
//Set title //If start time isn't an integer after the current epoch
const title = validator.escape(validator.trim(data.title)); if(start != null &&!validator.isInt(String(start), (new Date().getTime()))){
//set start //Null out time to tell the later parts of the function to start it now
var start = data.start; start = null;
//If start time isn't an integer after the current epoch
if(start != null &&!validator.isInt(String(start), (new Date().getTime()))){
//Null out time to tell the later parts of the function to start it now
start = null;
}
//Pull media list
const mediaList = await yanker.yankMedia(url, title);
//If we didn't find any media
if(mediaList == null || mediaList.length <= 0){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "No media found!", "queue");
//and ignore it!
return;
}
//If we have an invalid time
if(start == null || start < (new Date).getTime()){
//Get last item from schedule
const lastItem = (Array.from(this.schedule)[this.schedule.size - 1]);
//if we have a last item
if(lastItem != null){
//Throw it on five ms after the last item
start = lastItem[1].startTime + (lastItem[1].duration * 1000) + 5;
} }
}
//Queue the first media object given //Pull media list
this.queueMedia(mediaList[0], start, socket); const mediaList = await yanker.yankMedia(url, title);
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err); //If we didn't find any media
if(mediaList == null || mediaList.length <= 0){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "No media found!", "queue");
//and ignore it!
return;
}
//If we have an invalid time
if(start == null || start < (new Date).getTime()){
//Get last item from schedule
const lastItem = (Array.from(this.schedule)[this.schedule.size - 1]);
//if we have a last item
if(lastItem != null){
//Throw it on five ms after the last item
start = lastItem[1].startTime + (lastItem[1].duration * 1000) + 5;
}
}
//Queue the first media object given
this.queueMedia(mediaList[0], start, socket);
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
} }
} }
deleteRange(socket, data){ async deleteRange(socket, data){
try{ //Get the current channel from the database
//If start time isn't an integer const chanDB = await channelModel.findOne({name: socket.chan});
if(data.start != null && !validator.isInt(String(data.start))){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad start date!", "queue");
//and ignore it!
return;
}
//If end time isn't an integer if((!this.locked && await chanDB.permCheck(socket.user, 'clearSchedule')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
if(data.end != null && !validator.isInt(String(data.end))){ try{
//Bitch, moan, complain... //If start time isn't an integer
loggerUtils.socketErrorHandler(socket, "Bad end date!", "queue"); if(data.start != null && !validator.isInt(String(data.start))){
//and ignore it! //Bitch, moan, complain...
return; loggerUtils.socketErrorHandler(socket, "Bad start date!", "queue");
} //and ignore it!
return;
}
this.removeRange(data.start, data.end, socket); //If end time isn't an integer
}catch(err){ if(data.end != null && !validator.isInt(String(data.end))){
return loggerUtils.socketExceptionHandler(socket, err); //Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad end date!", "queue");
//and ignore it!
return;
}
this.removeRange(data.start, data.end, socket);
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
} }
} }
deleteMedia(socket, data){ async deleteMedia(socket, data){
try{ //Get the current channel from the database
//If we don't have a valid UUID const chanDB = await channelModel.findOne({name: socket.chan});
if(!validator.isUUID(data.uuid)){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
//and ignore it!
return;
}
//Remove media by UUID if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
this.removeMedia(data.uuid, socket); try{
}catch(err){ //If we don't have a valid UUID
return loggerUtils.socketExceptionHandler(socket, err); if(!validator.isUUID(data.uuid)){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
//and ignore it!
return;
}
//Remove media by UUID
this.removeMedia(data.uuid, socket);
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
} }
} }
moveMedia(socket, data){ async moveMedia(socket, data){
try{ //Get the current channel from the database
//If we don't have a valid UUID const chanDB = await channelModel.findOne({name: socket.chan});
if(!validator.isUUID(data.uuid)){
//Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
//and ignore it!
return;
}
//If start time isn't an integer after the current epoch if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
if(data.start != null && !validator.isInt(String(data.start))){ try{
//Null out time to tell the later parts of the function to start it now //If we don't have a valid UUID
data.start = undefined; if(!validator.isUUID(data.uuid)){
} //Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
//and ignore it!
return;
}
//Move media by UUID //If start time isn't an integer after the current epoch
this.rescheduleMedia(data.uuid, data.start, socket); if(data.start != null && !validator.isInt(String(data.start))){
}catch(err){ //Null out time to tell the later parts of the function to start it now
return loggerUtils.socketExceptionHandler(socket, err); data.start = undefined;
}
//Move media by UUID
this.rescheduleMedia(data.uuid, data.start, socket);
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
}
}
async toggleLock(socket){
//Get the current channel from the database
const chanDB = await channelModel.findOne({name: socket.chan});
//If the user is a schedule admin
if(await chanDB.permCheck(socket.user, 'scheduleAdmin')){
//Toggle the schedule lock
this.locked = !this.locked;
//Update schedule lock status for everyone in the channel
this.server.io.in(this.channel.name).emit("lock", {locked: this.locked});
} }
} }

View file

@ -82,6 +82,24 @@ const channelPermissionSchema = new mongoose.Schema({
default: "admin", default: "admin",
required: true required: true
}, },
scheduleMedia: {
type: mongoose.SchemaTypes.String,
enum: rankEnum,
default: "admin",
required: true
},
clearSchedule:{
type: mongoose.SchemaTypes.String,
enum: rankEnum,
default: "admin",
required: true
},
scheduleAdmin:{
type: mongoose.SchemaTypes.String,
enum: rankEnum,
default: "admin",
required: true
},
deleteChannel: { deleteChannel: {
type: mongoose.SchemaTypes.String, type: mongoose.SchemaTypes.String,
enum: rankEnum, enum: rankEnum,

View file

@ -17,13 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<div id="queue-panel-layout-controller"> <div id="queue-panel-layout-controller">
<div id="queue-controls"> <div id="queue-controls">
<div id="queue-control-buttons"> <div id="queue-control-buttons">
<button id="queue-add-media"><i class="bi-plus-lg"></i></button> <button id="queue-add-media" class="bi-plus-lg"></button>
<button id="queue-search-media"><i class="bi-search"></i></button> <button id="queue-search-media" class="bi-search"></button>
<button id="queue-scroll-lock" class="positive-button"><i class="bi-clock-fill"></i><i class="bi-arrows-expand"></i></button> <button id="queue-scroll-lock" class="positive-button"><i class="bi-clock-fill"></i><i class="bi-arrows-expand"></i></button>
<button id="queue-date"><i class="bi-calendar-fill"></i></button> <button id="queue-date" class="bi-calendar-fill"></button>
<button id="queue-playlists"><i class="bi-list"></i></button> <button id="queue-playlists" class="bi-list"></button>
<button id="queue-clear" class="danger-button"><i class="bi-trash-fill"></i></button> <button id="queue-clear" class="danger-button bi-trash-fill"></button>
<button id="queue-lock" class="positive-button"><i class="bi-unlock-fill"></i></button> <button id="queue-lock" class="positive-button bi-unlock-fill"></button>
</div> </div>
<div id="queue-control-date" style="display: none"> <div id="queue-control-date" style="display: none">
<i class="bi-caret-left" id="queue-control-date-decrement"></i> <i class="bi-caret-left" id="queue-control-date-decrement"></i>

View file

@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
z-index: 3; z-index: 4;
padding: 0.15em 0.3em; padding: 0.15em 0.3em;
} }

View file

@ -54,11 +54,15 @@ class channel{
this.socket.on("clientMetadata", this.handleClientInfo.bind(this)); this.socket.on("clientMetadata", this.handleClientInfo.bind(this));
this.socket.on("error", console.log); this.socket.on("error", utils.ux.displayResponseError);
this.socket.on("queue", (data) => { this.socket.on("queue", (data) => {
this.queue = new Map(data.queue); this.queue = new Map(data.queue);
}) });
this.socket.on("lock", (data) => {
this.queueLock = data.locked;
});
} }
handleClientInfo(data){ handleClientInfo(data){
@ -75,6 +79,9 @@ class channel{
//Store queue for use by the queue panel //Store queue for use by the queue panel
this.queue = new Map(data.queue); this.queue = new Map(data.queue);
//Store queue lock status
this.queueLock = data.queueLock;
} }
} }

View file

@ -45,6 +45,7 @@ class queuePanel extends panelObj{
this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock'); this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock');
this.queueDateButton = this.panelDocument.querySelector('#queue-date') this.queueDateButton = this.panelDocument.querySelector('#queue-date')
this.clearMediaButton = this.panelDocument.querySelector('#queue-clear'); this.clearMediaButton = this.panelDocument.querySelector('#queue-clear');
this.queueLockButton = this.panelDocument.querySelector('#queue-lock');
//Get control divs //Get control divs
this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts');
@ -61,6 +62,9 @@ class queuePanel extends panelObj{
this.queueDateIncrement = this.panelDocument.querySelector('#queue-control-date-increment'); this.queueDateIncrement = this.panelDocument.querySelector('#queue-control-date-increment');
this.queueDatePrompt = this.panelDocument.querySelector('#queue-control-date-prompt'); this.queueDatePrompt = this.panelDocument.querySelector('#queue-control-date-prompt');
//Display lock status
this.handleScheduleLock();
//Render out the queue //Render out the queue
this.fullRender(); this.fullRender();
@ -76,8 +80,9 @@ class queuePanel extends panelObj{
defineListeners(){ defineListeners(){
//Render queue when we receive a new copy of the queue data from the server //Render queue when we receive a new copy of the queue data from the server
this.client.socket.on("clientMetadata", (data) => {this.renderQueue();}) this.client.socket.on("clientMetadata", (data) => {this.renderQueue();});
this.client.socket.on("queue", (data) => {this.renderQueue();}) this.client.socket.on("queue", (data) => {this.renderQueue();});
this.client.socket.on("lock", this.handleScheduleLock.bind(this));
} }
setupInput(){ setupInput(){
@ -92,6 +97,7 @@ class queuePanel extends panelObj{
this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this)); this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this));
this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this)); this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this));
this.clearMediaButton.addEventListener('click', this.clearMedia.bind(this)); this.clearMediaButton.addEventListener('click', this.clearMedia.bind(this));
this.queueLockButton.addEventListener('click', this.lockSchedule.bind(this));
//control bar divs //control bar divs
//Add Media //Add Media
@ -103,6 +109,20 @@ class queuePanel extends panelObj{
this.queueDatePrompt.addEventListener('change', this.setQueueDate.bind(this)); this.queueDatePrompt.addEventListener('change', this.setQueueDate.bind(this));
} }
/* socket.io listeners */
handleScheduleLock(){
//Get queue lock button icon
const icon = this.queueLockButton.querySelector('i');
if(this.client.queueLock){
this.queueLockButton.classList.remove('positive-button', 'bi-unlock-fill');
this.queueLockButton.classList.add('danger-button', 'bi-lock-fill');
}else{
this.queueLockButton.classList.remove('danger-button', 'bi-lock-fill');
this.queueLockButton.classList.add('positive-button', 'bi-unlock-fill');
}
}
/* queue control button functions */ /* queue control button functions */
toggleAddMedia(event){ toggleAddMedia(event){
//If the div is hidden //If the div is hidden
@ -119,7 +139,13 @@ class queuePanel extends panelObj{
} }
} }
lockScroll(event){ lockScroll(event, jumpToDay = true){
//If we're supposed to jump to the current day
if(jumpToDay){
//Set schedule to current day
this.setDay();
}
//Enable scroll lock //Enable scroll lock
this.autoscroll = true; this.autoscroll = true;
@ -159,6 +185,10 @@ class queuePanel extends panelObj{
new clearPopup(event, this.client, null, this.ownerDoc); new clearPopup(event, this.client, null, this.ownerDoc);
} }
lockSchedule(event){
client.socket.emit('lock');
}
/* add queue controls */ /* add queue controls */
queueLast(event){ queueLast(event){
//Send off the request //Send off the request
@ -203,7 +233,7 @@ class queuePanel extends panelObj{
} }
} }
setDay(date){ setDay(date = new Date()){
//Set day //Set day
this.day = date; this.day = date;
//Zero out to midnight //Zero out to midnight
@ -220,7 +250,7 @@ class queuePanel extends panelObj{
//If autoscroll is enabled //If autoscroll is enabled
if(this.autoscroll){ if(this.autoscroll){
//Simulate a button click to un/re-light the button and trigger a scroll when the date is set to today //Simulate a button click to un/re-light the button and trigger a scroll when the date is set to today
this.lockScroll(); this.lockScroll(null, false);
} }
} }
@ -610,7 +640,7 @@ class queuePanel extends panelObj{
let bottomInput = this.queueContainer.offsetHeight - ((target.offsetTop + target.offsetHeight) + (this.queueLayoutController.scrollTopMax - this.queueLayoutController.scrollTop)); let bottomInput = this.queueContainer.offsetHeight - ((target.offsetTop + target.offsetHeight) + (this.queueLayoutController.scrollTopMax - this.queueLayoutController.scrollTop));
//If the item we're dragging is fackin uge' //If the item we're dragging is fackin uge'
if(target.offsetHeight > (this.queueLayoutController.offsetHeight - ((detectionDistance * 2) + 10))){ if(target.offsetHeight > (this.queueLayoutController.offsetHeight - ((detectionDistance * 2) + 20))){
//AND THEY FUCKING SAID YOU COULDN'T GET MOUSE POS OUTSIDE OF AN EVENT WITHOUT :HOVER TRICKS EAT MY FUCKING ASS //AND THEY FUCKING SAID YOU COULDN'T GET MOUSE POS OUTSIDE OF AN EVENT WITHOUT :HOVER TRICKS EAT MY FUCKING ASS
topInput = Math.round(target.offsetTop - Number(target.dataset['dragoffset']) - (this.queueLayoutController.getBoundingClientRect().top + this.queueControlOffset.offsetHeight)); topInput = Math.round(target.offsetTop - Number(target.dataset['dragoffset']) - (this.queueLayoutController.getBoundingClientRect().top + this.queueControlOffset.offsetHeight));
bottomInput = this.queueLayoutController.offsetHeight - (topInput + this.queueControlOffset.offsetHeight); bottomInput = this.queueLayoutController.offsetHeight - (topInput + this.queueControlOffset.offsetHeight);

View file

@ -105,7 +105,7 @@ class canopyUXUtils{
try{ try{
const errors = body.errors; const errors = body.errors;
errors.forEach((err)=>{ errors.forEach((err)=>{
new canopyUXUtils.popup(`<h3>Server Error:</h3><p><br>${err.msg}</p>`); new canopyUXUtils.popup(`<h3>${err.type} Error:</h3><p><br>${err.msg}</p>`);
}); });
}catch(err){ }catch(err){
console.error("Display Error Body:"); console.error("Display Error Body:");