canopy/www/js/channel/panels/queuePanel.js

745 lines
28 KiB
JavaScript

class queuePanel extends panelObj{
constructor(client, panelDocument){
//Call derived constructor
super(client, "Media Schedule", "/panel/queue", panelDocument);
//Current day
this.day = new Date();
//Zero out day to midnight
this.day.setHours(0,0,0,0);
//Store releative scale of items in seconds, defaulting to 30 minute chunks
this.scale = 30 * 60;
//Create variable to hold rescale timer
this.rescaleTimer = null;
//Autoscroll boolean
this.autoscroll = true;
//Define non-input event listeners
this.defineListeners();
}
docSwitch(){
//Call derived doc switch function
super.docSwitch();
//Get queue div
this.queue = this.panelDocument.querySelector('#queue');
//Get queue container
this.queueContainer = this.queue.querySelector("#queue-container");
//Get queue marker contianer
this.queueMarkerContainer = this.queue.querySelector('#queue-marker-container');
//Get queue layout controller
this.queueLayoutController = this.panelDocument.querySelector('#queue-panel-layout-controller');
//Re-acquire time marker
this.timeMarker = this.panelDocument.querySelector('#time-marker');
//Get main control buttons
this.addMediaButton = this.panelDocument.querySelector('#queue-add-media');
this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock');
this.queueDateButton = this.panelDocument.querySelector('#queue-date')
//Get control divs
this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts');
this.queueDateDiv = this.panelDocument.querySelector('#queue-control-date');
//Get control div elements
//Add Media
this.addMediaLinkPrompt = this.panelDocument.querySelector('#media-link-input');
this.addMediaNamePrompt = this.panelDocument.querySelector('#media-name-input');
this.queueLastButton = this.panelDocument.querySelector('#queue-last-button');
this.queueAtButton = this.panelDocument.querySelector('#queue-at-button');
//Date Queue date
this.queueDateDecrement = this.panelDocument.querySelector('#queue-control-date-decrement');
this.queueDateIncrement = this.panelDocument.querySelector('#queue-control-date-increment');
this.queueDatePrompt = this.panelDocument.querySelector('#queue-control-date-prompt');
//Render out the queue
this.fullRender();
//Setup panel input
this.setupInput();
}
closer(){
//Clear any remaining timers
clearTimeout(this.timeMarkerTimer);
}
defineListeners(){
//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("queue", (data) => {this.renderQueue();})
}
setupInput(){
//Re-render queue and time-marker on window resize as time-relative absolute positioning will be absolutely thrown
this.ownerDoc.defaultView.addEventListener('resize', this.resizeRender.bind(this));
//queue
this.queue.addEventListener('wheel', this.scaleScroll.bind(this));
//control bar controls
this.addMediaButton.addEventListener('click', this.toggleAddMedia.bind(this));
this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this));
this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this));
//control bar divs
//Add Media
this.queueLastButton.addEventListener('click', this.queueLast.bind(this))
//Queue Date
this.queueDateDecrement.addEventListener('click', this.decrementDate.bind(this));
this.queueDateIncrement.addEventListener('click', this.incrementDate.bind(this));
this.queueDatePrompt.addEventListener('change', this.setQueueDate.bind(this));
}
/* queue control button functions */
toggleAddMedia(event){
//If the div is hidden
if(this.addMediaDiv.style.display == 'none'){
//Light up the button
this.addMediaButton.classList.add('positive-button');
//Show the div
this.addMediaDiv.style.display = '';
}else{
//Unlight the button
this.addMediaButton.classList.remove('positive-button');
//Hide the div
this.addMediaDiv.style.display = 'none';
}
}
lockScroll(event){
//Enable scroll lock
this.autoscroll = true;
//If we have a time marker
if(this.timeMarker != null){
//Light the indicator
this.scrollLockButton.classList.add('positive-button');
//Render the marker early to insta-jump
this.renderTimeMarker();
}else{
//Unlight the indicator
this.scrollLockButton.classList.remove('positive-button');
}
}
toggleDateControl(event){
//If the div is hidden
if(this.queueDateDiv.style.display == 'none'){
//Light up the button
this.queueDateButton.classList.add('positive-button');
//Set date text
this.queueDatePrompt.valueAsDate = utils.ux.localizeDate(this.day);
//Show the div
this.queueDateDiv.style.display = '';
}else{
//Unlight the button
this.queueDateButton.classList.remove('positive-button');
//Hide the div
this.queueDateDiv.style.display = 'none';
}
}
/* add queue controls */
queueLast(event){
//Send off the request
this.client.socket.emit("queue",{url:this.addMediaLinkPrompt.value, title:this.addMediaNamePrompt.value});
this.addMediaLinkPrompt.value = '';
this.addMediaNamePrompt.value = '';
}
/* set date controls */
incrementDate(event){
//Increment day
this.day.setDate(this.day.getDate() + 1);
//Set day
this.setDay(this.day);
}
decrementDate(event){
//Decrement day
this.day.setDate(this.day.getDate() - 1);
//Set day
this.setDay(this.day);
}
setQueueDate(event){
//If we have a valid date
if(this.queueDatePrompt.valueAsDate != null){
//Set the day
this.setDay(utils.ux.normalizeDate(this.queueDatePrompt.valueAsDate));
}
}
setDay(date){
//Set day
this.day = date;
//Zero out to midnight
this.day.setHours(0,0,0,0);
//Set prompt to current day
this.queueDatePrompt.valueAsDate = utils.ux.localizeDate(this.day);
//Re-render the queue
this.renderQueue();
//Re-render/hide the time marker
this.renderTimeMarker();
//If autoscroll is enabled
if(this.autoscroll){
//Simulate a button click to un/re-light the button and trigger a scroll when the date is set to today
this.lockScroll();
}
}
scaleScroll(event){
if(event.ctrlKey){
//Capture inverse scroll wheel direction
const scrollDirection = event.wheelDeltaY / Math.abs(event.wheelDeltaY) * -1;
//Default scale factor to 5 seconds
let scaleFactor = 5;
//Tried to do this with math but couldnt because im bad at math so heres the if statement of shame :(
if(this.scale >= 7200){
scaleFactor = 3600
}else if(this.scale >= 3600){
scaleFactor = scrollDirection > 0 ? 3600 : 1800;
}else if(this.scale >= 1800){
scaleFactor = scrollDirection > 0 ? 1800 : 900;
}else if(this.scale == 900){
scaleFactor = scrollDirection > 0 ? 900 : 300;
}else if(this.scale > 300){
//If we're above five minutes use five minutes
scaleFactor = 300;
}else if(this.scale == 300){
//If we're at five minutes scroll up by five minutes or scroll down to one minute
scaleFactor = scrollDirection > 0 ? 300 : 240;
}else if(this.scale == 60){
//If we're at one minutes scroll up by four minutes or scroll down by 10 seconds
scaleFactor = scrollDirection > 0 ? 240 : 10;
}else if(this.scale > 10){
scaleFactor = 10;
}else if(this.scale == 10){
scaleFactor = scrollDirection > 0 ? 10 : 5;
}
//Prevent page-wide zoom in/out
event.preventDefault();
//Clear out the queue UI
this.clearQueue();
//Calculate new scale
const newScale = this.scale + (scaleFactor * scrollDirection);
//Clamp scale between 10 seconds and half a day
this.scale = Math.max(5, Math.min(43200, newScale));
//If we have no scale label
if(this.scaleLabel == null){
//Make it
this.scaleLabel = document.createElement('p');
this.scaleLabel.id = 'queue-marker-scale-label';
this.queue.appendChild(this.scaleLabel);
}
//Set scale label text to humie readable time scale
this.scaleLabel.innerHTML = `Time Scale:<br>${this.humieFriendlyDuration(this.scale)}`
//Clear any previous rescale timer
clearTimeout(this.rescaleTimer);
//Set timeout to re-render after input stops
this.rescaleTimer = setTimeout(this.fullRender.bind(this), 500);
//Otherwise, if we're just scrolling
}else{
//If we're looking at today
if(utils.isSameDate(new Date(), this.day)){
//Disable scroll lock
this.autoscroll = false;
//Unlight the indicator
this.scrollLockButton.classList.remove('positive-button');
}
}
}
humieFriendlyDuration(seconds){
//If we have an invalid duration
if(seconds <= 0){
//bitch, moan, and complain!
return('Invalid input duration');
}
//Create an empty array to hold the time strings
const timeStrings = [];
//Pull hours from time
const hours = Math.floor(seconds / 3600);
//Remove recorded hours
seconds -= hours * 3600;
//Pull minutes from time
const minutes = Math.floor(seconds / 60);
//Remove recorded minutes
seconds -= minutes * 60;
//If we have an hour
if(hours == 1){
//Add the string
timeStrings.push('1 Hour');
//If we have hours
}else if(hours > 0){
//Add the string
timeStrings.push(`${hours} Hours`);
}
//If we have a minute
if(minutes == 1){
//Add the string
timeStrings.push('1 Minute');
//If we have minutes
}else if(minutes > 0){
//Add the string
timeStrings.push(`${minutes} Minutes`);
}
//Add the 'and ' if we need it
const secondsPrefix = timeStrings.length > 0 ? 'and ' : '';
//If we have a second
if(seconds == 1){
//Add the string
timeStrings.push(`${secondsPrefix}1 Second`);
//If we have more than a second
}else if(seconds > 1){
//Add the string
timeStrings.push(`${secondsPrefix}${seconds} Seconds`);
}
//Join the time strings together
return timeStrings.join(', ');
}
resizeRender(event){
const date = new Date();
this.renderQueue(date);
this.renderTimeMarker(date);
}
clearQueue(){
//Clear out queue container
this.queueContainer.innerHTML = '';;
//Clear out queue marker container
this.queueMarkerContainer.innerHTML = '';
//Grab all related tooltips
const tooltips = this.ownerDoc.body.querySelectorAll('.media-tooltip');
//clear them out since we're clearing out the elements with the event to remove them
//These should clear out on their own on mousemove but this makes things look a *little* prettier :)
for(let tooltip of tooltips){
tooltip.parentNode.remove();
}
//Clear any marker timers
clearTimeout(this.timeMarkerTimer);
//If we have an existing time marker
if(this.timeMarker != null){
//Clear it out
this.timeMarker.remove();
this.timeMarker = null;
}
}
async fullRender(date = new Date()){
//Clear the queue
this.clearQueue();
//If we have a scale label
if(this.scaleLabel != null){
//Take it out
this.scaleLabel.remove();
this.scaleLabel = null;
}
//Render out time scale
this.renderQueueScale(date);
//wait a few frames so the scale can finish rendering, because dom function aren't async for some fucking reason
for(let i = 0; i <= 2; i++){
await utils.ux.awaitNextFrame();
}
//render the time marker
this.renderTimeMarker(date, true);
//render out the queue
this.renderQueue(date);
}
renderQueue(date = new Date()){
//Clear out queue container
this.queueContainer.innerHTML = '';
//for every entry in the queue
for(let entry of this.client.queue){
//Check if item starts today
const startsToday = utils.isSameDate(this.day, new Date(entry[1].startTime));
//Check if item ends today
const endsToday = utils.isSameDate(this.day, new Date(entry[1].startTime + (entry[1].duration * 1000)));
//If the item either starts or ends today
var playsToday = (startsToday || endsToday);
//If the media neither starts nor ends today
if(!playsToday){
//set playsToday based on whether or not we're playing something fucking huge and it's covering all of today
playsToday = utils.dateWithinRange(new Date(entry[1].startTime), new Date(entry[1].startTime + (entry[1].duration * 1000)), this.day);
}
//If part of the current item plays today
if(playsToday){
//Create entry div
const entryDiv = document.createElement('div');
entryDiv.classList.add('queue-entry');
//If this item starts today
if(startsToday){
//Place entry div based on time
entryDiv.style.top = `${this.offsetByDate(new Date(entry[1].startTime))}px`;
}else{
//Get dawn
const dawn = new Date();
dawn.setHours(0,0,0,0);
//Place item beginning at dawn
entryDiv.style.top = `${this.offsetByDate(dawn)}px`;
//Run entry from top
entryDiv.classList.add('started-before-today');
}
//If the item ends today
if(endsToday){
//Place entry div based on time
entryDiv.style.bottom = `${this.offsetByDate(new Date(entry[1].startTime + (entry[1].duration * 1000)), true)}px`;
}else{
//Get midnight
const dusk = new Date();
dusk.setHours(23,59,59,999);
//Place item beginning at dawn
entryDiv.style.bottom = `${this.offsetByDate(dusk, true)}px`;
//Run entry to bottom
entryDiv.classList.add('ends-after-today');
}
//Create entry title
const entryTitle = document.createElement('p');
entryTitle.textContent = entry[1].title;
//Tooltip generation
//tooltip div
const tooltipDiv = document.createElement('div');
tooltipDiv.classList.add('media-tooltip');
//tooltip title
const tooltipTitle = document.createElement('p');
tooltipTitle.textContent = `Title: ${entry[1].title}`;
//tooltip filename
const tooltipFilename = document.createElement('p');
tooltipFilename.textContent = `File Name: ${entry[1].fileName}`;
//tooltip source
const tooltipSource = document.createElement('p');
tooltipSource.textContent = `Source: ${entry[1].type}`;
//tooltip duration
const tooltipDuration = document.createElement('p');
tooltipDuration.textContent = `Duration: ${entry[1].duration}`;
//tooltip start
const tooltipStart = document.createElement('p');
tooltipStart.textContent = `Start Time: ${new Date(entry[1].startTime).toLocaleString()}`;
//tooltip end
const tooltipEnd = document.createElement('p');
tooltipEnd.textContent = `End Time: ${new Date(entry[1].startTime + (entry[1].duration * 1000)).toLocaleString()}`;
//append components
for(let component of [
tooltipTitle,
tooltipFilename,
tooltipSource,
tooltipDuration,
tooltipStart,
tooltipEnd
]){
tooltipDiv.append(component);
}
//Setup media tooltip
entryDiv.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, tooltipDiv, false, null, true, this.ownerDoc);});
//context menu
const menuMap = new Map([
["Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})}],
["Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1])}],
["Delete", ()=>{this.client.socket.emit('delete', {uuid: entry[1].uuid})}],
["Open in New Tab", ()=>{window.open(entry[1].url, '_blank').focus();}],
["Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);}],
]);
//Setup context menu
entryDiv.addEventListener('contextmenu', (event)=>{utils.ux.displayContextMenu(event, '', menuMap, this.ownerDoc);});
//Append entry elements
entryDiv.append(entryTitle);
//Append entry
this.queueContainer.append(entryDiv);
}
}
class reschedulePopup{
constructor(event, client, media, cb){
//Set Client
this.client = client;
//Set media
this.media = media;
//Set callback
this.cb = cb;
//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('/scheduleMedia', true, this.asyncConstructor.bind(this));
}
asyncConstructor(){
//Grab required UI elements
this.scheduleButton = this.popup.contentDiv.querySelector('#schedule-media-popup-schedule-button');
this.datePrompt = this.popup.contentDiv.querySelector('#schedule-media-popup-time-prompt');
//getCurrentDate
const curDate = new Date();
//Zero out current date to midnight
curDate.setSeconds(0,0);
//Set the date prompt to the next minute, adjusted to display local time
this.datePrompt.valueAsDate = utils.ux.localizeDate(curDate);
//Setup input
this.setupInput();
//If we have a function
if(typeof cb == "function"){
//Call any callbacks we where given
this.cb();
}
}
setupInput(){
//Setup input
this.scheduleButton.addEventListener('click', this.schedule.bind(this));
this.popup.popupDiv.addEventListener('keydown', this.schedule.bind(this));
}
schedule(event){
//If we clicked or hit enter
if(event.key == null || event.key == "Enter"){
//Get localized input date
const inputDate = utils.ux.normalizeDate(this.datePrompt.valueAsDate);
//If someone is trying to schedule in the past
if(inputDate < new Date().getTime()){
//Schedule now
this.client.socket.emit('move', {uuid: this.media.uuid});
//Otherwise
}else{
//Tell the server to move the media
this.client.socket.emit('move', {uuid: this.media.uuid, start: inputDate.getTime()});
}
//Close the popup
this.popup.closePopup();
}
}
}
}
renderTimeMarker(date = new Date(), forceScroll = false){
//Calculate marker position by date
const markerPosition = Math.round(this.offsetByDate(date));
//If markers are null
if(markerPosition == null){
//Try again in a second since the user probably just popped the panel or something :P
(smackTimer.bind(this))();
}
//If we're not looking at today
if(!utils.isSameDate(this.day, new Date())){
//If we still have at time marker
if(this.timeMarker != null){
this.timeMarker.remove();
this.timeMarker = null
}
//Stop here
return;
}
//if we need to make the time marker
if(this.timeMarker == null){
//Create the time marker
this.timeMarker = document.createElement('span');
//Add time marker class
this.timeMarker.id = 'time-marker';
//Append time marker
this.queue.appendChild(this.timeMarker);
}
//Set time marker position
this.timeMarker.style.top = `${markerPosition}px`;
//If the panel document isn't null (we're not actively switching panels)
if(this.panelDocument != null && (this.autoscroll || forceScroll)){
//Get height difference between window and queue layout controller
const docDifference = this.ownerDoc.defaultView.innerHeight - this.queueLayoutController.getBoundingClientRect().height;
//Calculate scroll target by body difference and marker position
const scrollTarget = (markerPosition - (this.queueLayoutController.getBoundingClientRect().height - docDifference) / 2) + docDifference;
//Calculate scroll behavior by distance
const scrollBehavior = Math.abs(scrollTarget - this.queueLayoutController.scrollTop) > 10 ? "smooth" : "instant";
//Scroll to the marker
this.queueLayoutController.scroll({
left: 0,
top: scrollTarget,
behavior: scrollBehavior
});
}
//Set the timer to run the function again
(smackTimer.bind(this))();
function smackTimer(){
//Clear update timer
clearTimeout(this.timeMarkerTimer);
//Set timer to update marker every second
this.timeMarkerTimer = setTimeout(this.renderTimeMarker.bind(this), 1000);
}
}
renderQueueScale(inputDate = new Date()){
//Clear out queue marker container
this.queueMarkerContainer.innerHTML = '';
//Make sure we don't modify the date we're handed
const date = structuredClone(inputDate);
//Zero out time to midnight on the morning of the input date
date.setHours(0,0,0,0);
//Store epoch of current date at midnight for later user
const dateEpoch = date.getTime();
//Create array to hold entries for post processing
const entries = [];
//while we've counted less than the amount of time in the day, count up by scale
for(let time = 0; time <= 86400; time += this.scale){
//Get index of current chunk by dividing time by scale
const index = time / this.scale;
//Set time by scale, we could do this against this.scale and date.getTime(), but this seemed cleaner :P
date.setTime(dateEpoch + (time * 1000))
//Create marker span
const markerDiv = document.createElement('div');
markerDiv.classList.add('queue-marker');
//Create marker line (unfortunately <hr> tags don't seem to play nice with parents who have display:flex)
const marker = document.createElement('span');
marker.classList.add('queue-marker');
//If it's even/zero
if(index % 2 == 0){
const markerLabel = document.createElement('p');
//If scale is over a minute then we don't need to display seconds
const seconds = this.scale > 60 ? '' : `:${('0' + date.getSeconds()).slice(-2)}`
//If we're counting AM
if(date.getHours() < 12){
//Display as AM
markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM`
//If we're cointing noon
}else if(date.getHours() == 12){
//display as noon
markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
//if we're counting pm
}else{
//display as pm
markerLabel.textContent = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
}
//Add marker label to marker span
markerDiv.appendChild(markerLabel);
}
//Append marker to marker span
markerDiv.appendChild(marker);
//Append marker span to queue container
this.queueMarkerContainer.appendChild(markerDiv);
//Add it to our postprocessing list
entries.push(markerDiv);
}
//If we made anything (should always be true :P)
if(entries.length > 0){
//Set the margin for the first queue marker
entries[0].classList.add('queue-marker-first');
//Set the margin for the last queue marker
entries[entries.length - 1].classList.add('queue-marker-last');
}
}
offsetByDate(date = new Date(), bottomOffset = false){
//Pull start of day epoch from given date, make sure to use a new date object so we don't fuck up any date objects passed to us
const dayEpoch = new Date(date).setHours(0,0,0,0);
//Get difference between now and day epoch to get time since the start of the current day in milliseconds
const curTime = date.getTime() - dayEpoch;
//Devide by amount of milliseconds in a day to convert time over to a floating point number between 0 and 1
//Make sure to reverse it if bottomOffset is set to true
const timeFloat = bottomOffset ? 1 - (curTime / 86400000) : curTime / 86400000;
//Get queue markers
const markers = this.panelDocument.querySelectorAll('span.queue-marker');
//If the marker is null for some reason
if(markers[0] == null){
return null;
}
//Get marker position range
const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop]
//Get maximum position relative to the range itself
const offsetMax = range[1] - range[0];
//return position relative to parent
return (offsetMax * timeFloat) + range[0];
}
}