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; //Setup child classes this.playlistManager = new playlistManager(client, panelDocument, this); //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'); //Get queue control offset this.queueControlOffset = this.panelDocument.querySelector('#queue-control-offset'); //Re-acquire time marker this.timeMarker = this.panelDocument.querySelector('#time-marker'); //Dragscroll timer this.dragScrollTimer = null; //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'); this.playlistMenuButton = this.panelDocument.querySelector("#queue-playlists"); this.clearMediaButton = this.panelDocument.querySelector('#queue-clear'); this.queueLockButton = this.panelDocument.querySelector('#queue-lock'); //Get control divs this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); this.queueDateDiv = this.panelDocument.querySelector('#queue-control-date'); this.playlistDiv = this.panelDocument.querySelector('#queue-playlist-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'); //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'); //Display lock status this.handleScheduleLock(); //Render out the queue this.fullRender(); //Setup panel input this.setupInput(); //Pass the new panelDoc and docSwitch() call down to child classes this.playlistManager.panelDocument = this.panelDocument; this.playlistManager.docSwitch(); } 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", () => {this.renderQueue();}); this.client.socket.on("queue", () => {this.renderQueue();}); this.client.socket.on("start", () => {this.renderQueue();}); this.client.socket.on("end", () => {this.renderQueue();}); this.client.socket.on("lock", this.handleScheduleLock.bind(this)); this.client.socket.on("error", this.handleQueueError.bind(this)); } 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)); this.playlistMenuButton.addEventListener('click', this.togglePlaylistDiv.bind(this)); this.clearMediaButton.addEventListener('click', this.clearMedia.bind(this)); this.queueLockButton.addEventListener('click', this.lockSchedule.bind(this)); //control bar divs //Add Media this.queueLastButton.addEventListener('click', this.queueLast.bind(this)) this.queueAtButton.addEventListener('click', this.queueAt.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)); } /* 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'); } } handleQueueError(data){ //Create a flag to reload the queue let reloadQueue = false; //Check errors for(let error of data.errors){ //If we have a queue error if(error.type == "queue"){ //Throw the reload flag reloadQueue = true; } } //If we need to reload the queue if(reloadQueue){ //Do so this.renderQueue(); } } /* 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, jumpToDay = true){ //If we're supposed to jump to the current day if(jumpToDay){ //Set schedule to current day this.setDay(); } //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'; } } togglePlaylistDiv(event){ //If the div is hidden if(this.playlistDiv.style.display == 'none'){ //Query playlists client.socket.emit('getChannelPlaylists'); //Light up the button this.playlistMenuButton.classList.add('positive-button'); //Show the div this.playlistDiv.style.display = ''; }else{ //Unlight the button this.playlistMenuButton.classList.remove('positive-button'); //Hide the div this.playlistDiv.style.display = 'none'; } } clearMedia(event){ //Call up the popup new clearPopup(event, this.client, null, this.ownerDoc); } lockSchedule(event){ client.socket.emit('lock'); } /* add queue controls */ queueLast(event){ //Send off the request this.client.socket.emit("queue",{url:this.addMediaLinkPrompt.value, title:this.addMediaNamePrompt.value}); //Clear out prompts this.addMediaLinkPrompt.value = ''; this.addMediaNamePrompt.value = ''; } queueAt(event){ //Call up the popup new schedulePopup(event, this.client, this.addMediaLinkPrompt.value, this.addMediaNamePrompt.value, null, this.ownerDoc); //Clear out prompts 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 = new 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(null, false); } } 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{ //If we're looking at today if(utils.isSameDate(new Date(), this.day)){ //Unlock auto scroll this.unlockScroll(); } } } unlockScroll(){ //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(){ //If we have no body if(this.ownerDoc.body == null){ //We have bigger issues return; } //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'); //For each property of the media object for(let key of Object.keys(entry[1])){ //add it to its given dataset entryDiv.dataset[key] = entry[1][key]; } //If this item starts today if(startsToday){ //Place the top of the 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 apply the rest of the styling rules entryDiv.classList.add('started-yesterday'); } //If the item ends today if(endsToday){ //Place the bottom of the entry div based on time entryDiv.style.bottom = `${this.offsetByDate(new Date(this.getMediaEnd(entry[1])), 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 apply the rest of the styling rules entryDiv.classList.add('ends-tomorrow'); } //If we started in the middle of the video if(entry[1].startTimeStamp > 0){ entryDiv.classList.add('started-late'); } //If we ended early if(entry[1].earlyEnd != null){ entryDiv.classList.add('ended-early'); } //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 components //For each string for(let string of [ `Title: ${entry[1].title}`, `File Name: ${entry[1].fileName}`, `Source: ${entry[1].type}`, `Duration: ${entry[1].duration}`, `Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`, `End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}${entry[1].earlyEnd == null ? '' : ' (Ended Early)'}` ]){ //Create a 'p' node const component = document.createElement('p'); //Fill it with the string component.textContent = string; //Append it to the tooltip div tooltipDiv.append(component); } //Setup media tooltip entryDiv.addEventListener('mouseenter',(event)=>{ //If we're not dragging if(event.target.dataset['drag'] != 'true'){ //Display tooltip utils.ux.displayTooltip(event, tooltipDiv, false, null, true, this.ownerDoc); } }); //Create context menu map const menuMap = new Map(); const now = new Date(); //If the item hasn't started yet if(entry[1].startTime > now.getTime()){ //Add 'Play' option to context menu menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})}); //Add 'Move To...' option to context menu menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)}); //Otherwise, if the item is currently playing }else if(this.getMediaEnd(entry[1]) > now.getTime()){ //Add 'Stop' option to context menu menuMap.set("Stop", ()=>{this.client.socket.emit('stop', {uuid: entry[1].uuid})}); //Add the Now Playing glow, not the prettiest place to add this, but why let a good conditional go to waste? entryDiv.classList.add('now-playing'); //Otherwise, if the item has been archived }else{ entryDiv.classList.add('archived'); } //Add 'Delete' option to context menu menuMap.set("Delete", ()=>{this.client.socket.emit('delete', {uuid: entry[1].uuid})}) //Add 'New Tab' option to context menu menuMap.set("Open in New Tab", ()=>{window.open(entry[1].url, '_blank').focus();}) //Add 'Copy URL' option to context menu menuMap.set("Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);}) //If the item hasn't yet ended if(this.getMediaEnd(entry[1]) > now.getTime()){ //Setup drag n drop entryDiv.addEventListener('mousedown', clickEntry.bind(this)); } //Setup context menu entryDiv.addEventListener('contextmenu', (event)=>{ //If we're not dragging if(event.target.dataset['drag'] != 'true'){ //Display context menu utils.ux.displayContextMenu(event, '', menuMap, this.ownerDoc); } }); //Append entry elements entryDiv.append(entryTitle); //Append entry this.queueContainer.append(entryDiv); } } function clickEntry(event){ //If it's not a left click if(event.buttons != 1){ //fuck off return; } //Grab existing height let height = event.target.offsetHeight; let cutoffOffset = 0; //If the item got cut-off at the bottom if(event.target.classList.contains("ends-tomorrow") || event.target.classList.contains("ended-early")){ //Calculate height from duration height = this.offsetByMilliseconds(Number(event.target.dataset['duration']) * 1000); //If the item got cut-off at the top }else if(event.target.classList.contains('started-yesterday') || event.target.classList.contains("started-late")){ //Keep old height for now const oldHeight = height; //Calculate height from duration height = this.offsetByMilliseconds(Number(event.target.dataset['duration']) * 1000); //Calculate the mouse offset needed to keep it properly placed relative to the original click point cutoffOffset = height - oldHeight; } //Remove any cut-off borders event.target.classList.remove('ends-tomorrow', 'started-yesterday', 'ended-early', 'started-late'); //If we havent set height or width if(event.target.style.height == ""){ //Preserve calculated entry height event.target.style.height = `${height}px`; } //Add set dragging CSS class to target event.target.classList.add('dragging-queue-entry'); //enable drag on target dataset event.target.dataset['drag'] = true; //Create a tooltip to show the time we're dragging to const timetip = new canopyUXUtils.tooltip('', false, null, this.ownerDoc); timetip.tooltip.classList.add('media-tooltip'); //Drag entry with mouse this.ownerDoc.body.addEventListener('mousemove', (nestedEvent)=>{(dragEntry.bind(this))(nestedEvent, event.target, timetip)}); //Drop on mouse up this.ownerDoc.body.addEventListener('mouseup', (nestedEvent)=>{(dropEntry.bind(this))(nestedEvent, event.target, timetip)}); //Disable selection on body this.ownerDoc.body.style.userSelect = 'none'; //Save top of target relative to window minus the mouse position as our drag offset event.target.dataset['dragoffset'] = (event.target.offsetTop + this.ownerDoc.defaultView.scrollY) - event.clientY - cutoffOffset; //Call the drag entry function to move the entry on click without re-writing the wheel (dragEntry.bind(this))(event, event.target, timetip); //Start dragscroll loop this.dragScrollTimer = setInterval(()=>{(dragScroll.bind(this))(event.target)}, 10); } function dragScroll(target){ //Stop timeout loop if(!target.isConnected || target.dataset['drag'] != "true"){ //Clear the interval clearInterval(this.dragScrollTimer); //Fuck off and die! return; } //Set minimum distance to detect const detectionDistance = 70; //Set value to devide distance from edge during scroll speed calculation const speedDevider = 6; //Get top boundaries distance from the top relative to the scroll top and set as top input let topInput = target.offsetTop - this.queueLayoutController.scrollTop; //Get bottom boundaries distance from the top relative to the scroll top and set as bottom input 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(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 topInput = Math.round(target.offsetTop - Number(target.dataset['dragoffset']) - (this.queueLayoutController.getBoundingClientRect().top + this.queueControlOffset.offsetHeight)); bottomInput = this.queueLayoutController.offsetHeight - (topInput + this.queueControlOffset.offsetHeight); } //If the top of the entry is within five pixels of the top of the parent and we have room to scroll up if(topInput < detectionDistance && this.queueLayoutController.scrollTop > 0){ //Unlock auto scroll this.unlockScroll(); //Filter out less than 0 from relative entry top to calculate speed const speed = Math.round(((detectionDistance) - (topInput < 0 ? 0 : topInput)) / speedDevider); //Scroll queue by distance to top this.queueLayoutController.scrollBy(0, -speed); //Add scroll amount to drag offset to keep entry aligned with mouse target.dataset['dragoffset'] = Number(target.dataset['dragoffset']) - speed //Move entry by speed to match new drag offset target.style.top = `${target.offsetTop - speed}px` //Otherwise if the bottom of the entry is within five pixels the bottom of the parent and we have room to scroll down }else if(bottomInput < (detectionDistance) && this.queueLayoutController.scrollTop < this.queueLayoutController.scrollTopMax){ //Unlock auto scroll this.unlockScroll(); //Calculate speed by distance to bottom const offsetBottom = bottomInput; //Filter out less than 0, reverse the range, and apply scroll dampen to avoid scrolling off the edge const speed = Math.round((detectionDistance - (offsetBottom < 0 ? 0 : offsetBottom)) / speedDevider) //Scroll queue by calculated speed this.queueLayoutController.scrollBy(0, speed); //Subtract speed from drag offset to keep aligned with mouse target.dataset['dragoffset'] = Number(target.dataset['dragoffset']) + speed; //Move entry by speed to match new drag offset target.style.top = `${target.offsetTop + speed}px` } } function dragEntry(event, target, timetip){ //Gross but works :P if(!target.isConnected || target.dataset['drag'] != "true"){ return; } //Get current start time const start = this.dateByOffset(target.offsetTop); //Position timetip timetip.moveToMouse(event); //Inject timetip label timetip.tooltip.innerHTML = [ `Start Time: ${utils.ux.timeStringFromDate(start, true)}`, `End Time: ${utils.ux.timeStringFromDate(new Date(start.getTime() + (target.dataset['duration'] * 1000)), true)}` ].join('
'); //Calculate offset from rest of window const windowOffset = this.queueContainer.offsetTop + this.ownerDoc.defaultView.scrollY; //Move the entry to the mouse offset by the target nodes height and the queue layouts scroll const entryTop = event.clientY + Number(target.dataset['dragoffset']) - windowOffset; //Set target vertical position target.style.top = `${entryTop}px`; } function dropEntry(event, target, timetip){ //Gross but works :P if(!target.isConnected || target.dataset['drag'] != "true"){ return; } //Asynchronously send move command item by calculated time to server this.client.socket.emit('move', {uuid: target.dataset['uuid'], start: this.dateByOffset(target.offsetTop).getTime()}); //allow selection on body this.ownerDoc.body.style.userSelect = 'none'; //Remove timetip timetip.remove(); //Finish dragging target.dataset['drag'] = false; } } 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
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){ //Create marker label const markerLabel = document.createElement('p'); //If scale is over a minute then we don't need to display seconds markerLabel.textContent = utils.ux.timeStringFromDate(date, this.scale < 60) //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; //Calculate the offset from todays milliseconds return this.offsetByMilliseconds(curTime, bottomOffset); } dateByOffset(input = 0){ //Get markers const markers = this.panelDocument.querySelectorAll('span.queue-marker'); //get range of position from top to bottom marker const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop] //get max offset relative to markers const offsetMax = range[1] - range[0]; //input offset + relative difference between top marker and parent devided by offset max //save as 'float' between 0 and 1 const relativeInput = ((input - range[0]) / offsetMax); //Get the currently viewed day const date = new Date(this.day); //Convert our 'float' from 0-1 to a time between 0-24 date.setHours(0,0,0,relativeInput * 86400000); //return our date return date; } offsetByMilliseconds(input = 0, bottomOffset = false){ //Convert amount of milliseconds to a float, 0 representing the start of the day and 1 representing the end. //Make sure to reverse it if bottomOffset is set to true const timeFloat = bottomOffset ? 1 - (input / 86400000) : input / 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]; } getMediaEnd(media){ //If we have an early end if(media.earlyEnd != null){ return media.startTime + (media.earlyEnd * 1000); //Otherwise }else{ return media.startTime + ((media.duration - media.startTimeStamp) * 1000); } } } 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'); //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'; } } 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 const playlist = data[playlistIndex]; //--Create Base Structure--- //Create a new playlist div const playlistDiv = document.createElement('div'); //Set it's class playlistDiv.classList.add('queue-playlist-div'); //Set playlist div dataset playlistDiv.dataset.name = playlist.name; //If this isn't our first rodeo if(playlistIndex != 0){ //make note playlistDiv.classList.add('not-first-queue-playlist-div'); } //Create span to hold playlist entry line contents const playlistSpan = document.createElement('span'); //Set classes playlistSpan.classList.add('queue-playlist-span'); //--Create Labels--- //Create playlist label span const playlistLabels = document.createElement('span'); //Set it's class playlistLabels.classList.add('queue-playlist-labels-span'); //Create playlist title label const playlistTitle = document.createElement('p'); //Set it's class playlistTitle.classList.add('queue-playlist-title'); //Create playlist count label const playlistCount = document.createElement('p'); //Set it's class playlistCount.classList.add('queue-playlist-count'); //List video count playlistCount.innerText = `Count: ${playlist.media.length}`; //--Create Controls-- //Create playlist control span const playlistControls = document.createElement('span'); //Set it's class playlistControls.classList.add('queue-playlist-control-span'); //Unescape Sanatized Enteties and safely inject as plaintext playlistTitle.innerText = utils.unescapeEntities(playlist.name); //Create queue all button const playlistQueueRandomButton = document.createElement('button'); //Set it's classes playlistQueueRandomButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control'); //Inject text content playlistQueueRandomButton.textContent = 'Queue Random'; //Create queue all button const playlistQueueAllButton = document.createElement('button'); //Set it's classes playlistQueueAllButton.classList.add('queue-playlist-queue-all-button', 'queue-playlist-control'); //Inject text content playlistQueueAllButton.textContent = 'Queue All'; //Create delete button const playlistDeleteButton = document.createElement('button'); //Set it's classes playlistDeleteButton.classList.add('queue-playlist-delete-button', 'queue-playlist-control', 'danger-button', 'bi-trash-fill'); //--Create Media Elements-- //Create Media Container Div const mediaDiv = renderMedia(); //Append items to playlist labels span playlistLabels.appendChild(playlistTitle); playlistLabels.appendChild(playlistCount); //Append items to playlist control span playlistControls.appendChild(playlistQueueRandomButton); playlistControls.appendChild(playlistQueueAllButton); playlistControls.appendChild(playlistDeleteButton); //Append items to playlist span playlistSpan.appendChild(playlistLabels); playlistSpan.appendChild(playlistControls); //Append items to playlist div playlistDiv.appendChild(playlistSpan); playlistDiv.appendChild(mediaDiv); //Append current playlist span to the channel playlist div this.channelPlaylistDiv.appendChild(playlistDiv); //Define input event listeners playlistQueueAllButton.addEventListener('click', queueAll); playlistDeleteButton.addEventListener('click', deletePlaylist); //aux rendering functions function renderMedia(){ //Create media container div const mediaContainer = document.createElement('div'); //Set classes mediaContainer.classList.add('queue-playlist-media-div'); //return media container return mediaContainer; } //playlist control functions function queueAll(){ client.socket.emit('queueChannelPlaylist', {playlist: playlist.name}); } function deletePlaylist(){ client.socket.emit('deleteChannelPlaylist', {playlist: playlist.name}); } } } } 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); } 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"){ //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 schedulePopup{ constructor(event, client, url, title, cb, doc){ //Set Client this.client = client; //Set link this.url = url; //Set title this.title = title; //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), doc); } 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 time to the nearest minute 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); //Tell the server to move the media this.client.socket.emit("queue",{url: this.url, title: this.title, start: inputDate.getTime()}); //Close the popup this.popup.closePopup(); } } } class reschedulePopup extends schedulePopup{ constructor(event, client, media, cb, doc){ //Call derived constructor super(event, client, null, null, cb, doc); //Set media this.media = media; } 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); //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(); } } } class clearPopup{ constructor(event, client, cb, doc){ //Set Client this.client = client; //Set callback this.cb = cb; console.log(doc); //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('/clearMedia', true, this.asyncConstructor.bind(this), doc); } asyncConstructor(){ //Grab required UI elements this.clearButton = this.popup.contentDiv.querySelector('#clear-media-popup-clear-button'); this.startDatePrompt = this.popup.contentDiv.querySelector('#clear-media-popup-start-time-prompt'); this.endDatePrompt = this.popup.contentDiv.querySelector('#clear-media-popup-end-time-prompt'); //getCurrentDate const curDate = new Date(); //Zero out current time to the nearest minute curDate.setSeconds(0,0); //Set the start date prompt to the next minute, adjusted to display local time this.startDatePrompt.valueAsDate = utils.ux.localizeDate(curDate); //Add 30 minutes curDate.setMinutes(curDate.getMinutes() + 30); //Set the end date prompt to 30 minutes in the futre, adjusted to display local time this.endDatePrompt.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.clearButton.addEventListener('click', this.clear.bind(this)); this.popup.popupDiv.addEventListener('keydown', this.clear.bind(this)); } clear(event){ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ //Get localized input date const inputStartDate = utils.ux.normalizeDate(this.startDatePrompt.valueAsDate); const inputEndDate = utils.ux.normalizeDate(this.endDatePrompt.valueAsDate); //Tell the server to clear media between the input range this.client.socket.emit("clear",{start: inputStartDate.getTime(), end: inputEndDate.getTime()}); //Close the popup this.popup.closePopup(); } } }