class queuePanel extends panelObj{ constructor(client, panelDocument){ super(client, "Media Queue", "/panel/queue", panelDocument); //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; 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'); //Get control divs //Add Media this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); //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'); //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)); //control bar divs this.queueLastButton.addEventListener('click', this.queueLast.bind(this)) } toggleAddMedia(event){ if(this.addMediaDiv.style.display == 'none'){ this.addMediaDiv.style.display = ''; }else{ this.addMediaDiv.style.display = 'none'; } } 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 = ''; } lockScroll(event){ //Enable scroll lock this.autoscroll = true; //Light the indicator this.scrollLockButton.classList.add('positive-button'); //Render the marker early to insta-jump this.renderTimeMarker(); } 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:
${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{ //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.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){ //Create entry div const entryDiv = document.createElement('div'); entryDiv.classList.add('queue-entry'); //Place entry div entryDiv.style.top = `${this.offsetByDate(new Date(entry[1].startTime))}px`; entryDiv.style.bottom = `${this.offsetByDate(new Date(entry[1].startTime + (entry[1].duration * 1000)), true)}px`; //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 = `Start 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...", ()=>{}], ["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); } } 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 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(); //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 markerSpan = document.createElement('div'); markerSpan.classList.add('queue-marker'); //Create marker line (unfortunately
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 markerSpan.appendChild(markerLabel); } //Append marker to marker span markerSpan.appendChild(marker); //Append marker span to queue container this.queueMarkerContainer.appendChild(markerSpan); } } 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]; } }