- /*Canopy - The next generation of stoner streaming software
-Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.*/
-
-/**
- * Class representing Queue Panel UX
- */
-class queuePanel extends panelObj{
- /**
- * Instantiates a new Queue Panel object
- * @param {channel} client - Parent Client Management Object
- * @param {Document} panelDocument - Panel Document
- */
- constructor(client, panelDocument){
- //Call derived constructor
- super(client, "Media Schedule", "/panel/queue", panelDocument);
-
- /**
- * Current day, zero'd out to midnight
- */
- this.day = new Date();
- //Zero out day to midnight
- this.day.setHours(0,0,0,0);
-
- /**
- * Schedule time scale in seconds, defaults to 30 minutes
- */
- this.scale = 30 * 60;
-
- /**
- * Re-scale timer, counts down after re-sizing to clear re-size UI and show schedule again
- */
- this.rescaleTimer = null;
-
- /**
- * Enables auto-scroll on schedule UX
- */
- this.autoscroll = true;
-
- /**
- * Child Playlist Manager Object
- */
- this.playlistManager = new playlistManager(client, panelDocument, this);
-
- //Define non-input event listeners
- this.defineListeners();
- }
-
- docSwitch(){
- //Clear timetips
- this.killTimetips();
-
- //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.goLiveButton = this.panelDocument.querySelector('#queue-go-live');
- 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.goLiveDiv = this.panelDocument.querySelector('#queue-live-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');
- //Go Live
- this.goLiveNamePrompt = this.panelDocument.querySelector("#queue-live-prompts-name-input");
- this.goLiveOverwriteButton = this.panelDocument.querySelector("#queue-live-prompts-overwrite-button");
- this.goLivePushbackButton = this.panelDocument.querySelector("#queue-live-prompts-pushback-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.renderIntervalTimer);
- //Clear timetips
- this.killTimetips();
- }
-
- /**
- * Handles Network-Related Event Listeners
- */
- defineListeners(){
- //Render queue when we receive a new copy of the queue data from the server
- //Render queue should be called within an arrow function so that it's called with default parameters, and not handed an event as a date
- this.client.socket.on("clientMetadata", () => {this.renderQueue();});
- this.client.socket.on("queue", () => {this.renderQueue();});
- this.client.socket.on("start", this.handleStart.bind(this));
- this.client.socket.on("end", this.handleEnd.bind(this));
- this.client.socket.on("lock", this.handleScheduleLock.bind(this));
- this.client.socket.on("error", this.handleQueueError.bind(this));
- }
-
- /**
- * Handles Input-Related Event Listeners
- */
- 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.goLiveButton.addEventListener('click', this.toggleGoLive.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))
- //Go Live
- this.goLiveOverwriteButton.addEventListener('click', this.goLive.bind(this));
- this.goLivePushbackButton.addEventListener('click', this.goLive.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 */
- /**
- * Handles call from server to start media
- * @param {Object} data - Data from server
- */
- handleStart(data){
- //If we're starting an HLS Livestream
- if(data.media != null && data.media.type == 'livehls'){
- //Hide the 'goLive' controls
- this.goLiveDiv.style.display = 'none';
- }
-
- this.renderQueue();
- }
-
- /**
- * Handles call from server to end media
- */
- handleEnd(){
- //Reset Go Live button
- this.goLiveButton.classList.replace('critical-danger-button', 'danger-button');
- this.goLiveButton.classList.replace('bi-record', 'bi-record2');
- this.goLiveButton.title = "Go Live";
-
- this.renderQueue();
- }
-
- /**
- * Handles call from server to lock schedule
- */
- 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');
- }
- }
-
- /**
- * Handles queue-related error from the server
- * @param {Object} data - Data from server
- */
- 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 */
-
- /**
- * Toggles add media UX
- * @param {Event} event - Event passed down from Event Listener
- */
- 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';
- }
- }
-
- /**
- * Toggles Go-Live UX
- * @param {Event} event - Event passed down from Event Listener
- */
- toggleGoLive(event){
- //If we're not livestreaming
- if(client.player.mediaHandler.type != "livehls"){
- //If the div is hidden
- if(this.goLiveDiv.style.display == 'none'){
- //Show the div
- this.goLiveDiv.style.display = '';
- }else{
- //Hide the div
- this.goLiveDiv.style.display = 'none';
- }
- //Otherwise
- }else{
- //Hide the div, if it isn't already
- this.goLiveDiv.style.display = 'none';
-
- //Stop the livestream
- client.socket.emit('stop');
- }
-
- }
-
- /**
- * Handles sending request to server to start a live stream
- * @param {Event} event - Event passed down from Event Listener
- */
- goLive(event){
- //Start a livestream
- client.socket.emit('goLive',{title: this.goLiveNamePrompt.value, mode: event.target.dataset['mode']});
- }
-
- /**
- * Locks schedule scroll to current time marker
- * @param {Event} event - Event passed down from Event Listener
- * @param {Boolean} jumpToDay - whether or not to jump schedule to the current day
- */
- 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');
- }
- }
-
- /**
- * Toggles schedule date control
- * @param {Event} event - Event passed down from Event Listener
- */
- 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';
- }
- }
-
- /**
- * Toggles playlist management UX
- * @param {Event} event - Event passed down from Event Listener
- */
- 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';
- }
- }
-
- /**
- * Sends request to server to clear media between a range of two dates
- * @param {Event} event - Event passed down from Event Listener
- */
- clearMedia(event){
- //Call up the popup
- new clearPopup(event, this.client, null, this.ownerDoc);
- }
-
- /**
- * Sends request to lock the schedule
- * @param {Event} event - Event passed down from Event Listener
- */
- lockSchedule(event){
- client.socket.emit('lock');
- }
-
- /* add queue controls */
- /**
- * Sends request to queue current URL after the last item
- * @param {Event} event - Event passed down from Event Listener
- */
- 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 = '';
- }
-
- /**
- * Sends request to queue current URL at the current time
- * @param {Event} event - Event passed down from Event Listener
- */
- 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 */
- /**
- * Increments displayed schedule date
- * @param {Event} event - Event passed down from Event Listener
- */
- incrementDate(event){
- //Increment day
- this.day.setDate(this.day.getDate() + 1);
- //Set day
- this.setDay(this.day);
- }
-
- /**
- * Decrements displayed schedule date
- * @param {Event} event - Event passed down from Event Listener
- */
- decrementDate(event){
- //Decrement day
- this.day.setDate(this.day.getDate() - 1);
-
- //Set day
- this.setDay(this.day);
- }
-
- /**
- * Validates and sets display schedule date from user input
- * @param {Event} event - Event passed down from Event Listener
- */
- setQueueDate(event){
- //If we have a valid date
- if(this.queueDatePrompt.valueAsDate != null){
- //Set the day
- this.setDay(utils.ux.normalizeDate(this.queueDatePrompt.valueAsDate));
-
- }
- }
-
- /**
- * Directly sets schedule display date
- * @param {Date} date - Date to set schedule to
- */
- 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);
- }
- }
-
- /**
- * Handles time-scale changes on crtl+scroll
- * @param {Event} event - Event passed down from Event Listener
- */
- scaleScroll(event){
- if(event.ctrlKey){
- //I could sit here and work out why timetips break everything on scalescroll
- //Then again you wouldn't want phantom timetips surviving the re-scale so you might as well get two birds stoned at once :P
- this.killTimetips();
-
- //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>${utils.ux.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();
- }
- }
- }
-
- /**
- * Un-locks scroll from curren time marker
- */
- unlockScroll(){
- //Disable scroll lock
- this.autoscroll = false;
- //Unlight the indicator
- this.scrollLockButton.classList.remove('positive-button');
- }
-
- /**
- * Handles re-rendering schedule upon window re-size
- * @param {Event} event - Event passed down from Event Listener
- */
- resizeRender(event){
- const date = new Date();
- this.renderQueue(date);
- this.renderTimeMarker(date);
- }
-
- /**
- * Clears out queue container for re-render
- */
- 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.renderIntervalTimer);
-
- //If we have an existing time marker
- if(this.timeMarker != null){
- //Clear it out
- this.timeMarker.remove();
- this.timeMarker = null;
- }
- }
-
- /**
- *
- * @param {Date} date - Current time,
- */
- 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);
-
- //Grab the first marker
- let firstMarker = this.panelDocument.querySelector('.queue-marker-first');
-
- //Loop until first marker is properly positioned
- while(firstMarker.offsetTop > 0){
- //wait a few frames so the scale can finish rendering, because dom function aren't async for some fucking reason
- await utils.ux.awaitNextFrame();
- }
-
- //render the time marker w/ force scroll
- this.renderTimeMarker(date, true);
-
- //Kick off render interval
- this.renderInterval(date);
-
- //render out the queue
- this.renderQueue(date);
- }
-
- /**
- * Renders out schedule
- * @param {Date} date - Date representing current time, defaults to new date object
- */
- 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`;
-
- //Apply style rules for items that starrted yesterday
- 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 = utils.unescapeEntities(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: ${utils.ux.humieFriendlyDuration(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 = utils.unescapeEntities(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 (confirm with UUID since time might not always be reliable, such as during livestreams)
- }else if(entry[1].uuid == this.client.player.mediaHandler.nowPlaying.uuid){
- //Add 'Stop' option to context menu
- menuMap.set("Stop", ()=>{this.client.socket.emit('stop')});
- //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);
- }
-
- }
-
- //Render out any playing livestreams
- this.renderLiveStream(date);
-
- 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;
-
- //Kill existing timetips
- this.killTimetips();
-
- //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','media-timetip');
- timetip.tooltip.addEventListener('mousemove', this.killTimetips.bind(this));
-
-
- //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)});
-
- //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
- //Normally wouldn't do innerHTML but these values are calculated serverside and it saves us making a <br> element
- 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('<br>');
-
- //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){
- //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';
-
- //kill timetips
- this.killTimetips();
-
- //Finish dragging
- target.dataset['drag'] = false;
- }
-
- }
-
- /**
- * Kills off hung tooltips
- * @param {Event} event - Event passed down from Event Listener
- */
- killTimetips(event){
- //If we have an event and it's holding any mouse buttons
- if(event != null && event.buttons != 0){
- //Fuck off and die
- return;
- }
-
- //Find any existing timetips
- for(const timetip of this.ownerDoc.querySelectorAll('.media-timetip')){
- //nukem
- timetip.remove();
- }
- }
-
- /**
- * Render call called at 1-second intervals, handles time and livestream markers
- * @param {Date} date - Date representing current time, defaults to new date object
- */
- renderInterval(date = new Date()){
- this.renderTimeMarker(date);
- this.renderLiveStream(date, true);
-
-
- //Clear update timer
- clearTimeout(this.renderIntervalTimer);
-
- //Set timer to update marker every second
- this.renderIntervalTimer = setTimeout(this.renderInterval.bind(this), 1000);
- }
-
- /**
- * Renders current time marker on to the schedule
- * @param {Date} date - Date representing current time, defaults to new date object
- * @param {Boolean} forceScroll - Whether or not to scroll the schedule on increment
- */
- 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
- return;
- }
-
- //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
- });
- }
- }
-
- /**
- * Renders queue scale markers
- * @param {Date} inputDate - Date representing current time, defaults to new date object
- */
- 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){
- //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');
- }
- }
-
- /**
- * Renders live-stream marker
- * @param {Date} inputDate - Date representing current time, defaults to new date object
- * @param {Boolean} intervalRun - Whether or not this was run by renderInterval, defaults to false
- */
- renderLiveStream(date = new Date(), intervalRun = false){
- //Grab all live queue entries
- const staleEntry = this.queueContainer.querySelector('.queue-entry.live');
-
- //If we're not livestreaming
- if(client.player.mediaHandler.type != "livehls"){
- //If we have a stale entry
- if(staleEntry != null){
- //Remove stale entry since we're no longer streaming
- staleEntry.remove();
- }
-
- //Fuck off and die
- return;
- }
-
- //If we haven't updated the Go-Live button yet
- //This kinda needs to be done here since this might be opened mid-stream :P
- if(this.goLiveButton.title != "Stop Stream"){
- this.goLiveButton.classList.replace('danger-button', 'critical-danger-button');
- this.goLiveButton.title = "Stop Stream";
- }
-
-
- //If this was called by the timer function, and not some other queue-rendering related function
- if(intervalRun){
- //Though there is reason to run an if statement here since we dont want the latter over-writing the former every run...
- //If we're showing the regular icon
- if(this.goLiveButton.classList.contains('bi-record2')){
- //Animated it with the other live icon
- this.goLiveButton.classList.replace('bi-record2', 'bi-record');
- //otherwise
- }else{
- //Show the standard one we always do
- this.goLiveButton.classList.replace('bi-record', 'bi-record2');
- }
- }
-
- //Grab currently playing media
- const nowPlaying = client.player.mediaHandler.nowPlaying;
-
- //If we don't have a good stale entry to re-use
- if(staleEntry == null || staleEntry.dataset.uuid != nowPlaying.uuid){
- //If it wasn't null but just old
- if(staleEntry != null){
- //Nukem
- staleEntry.remove();
- }
-
- //Create entry div
- const entryDiv = document.createElement('div');
- entryDiv.classList.add('queue-entry', 'live');
-
- //Iterate through nowPlaying's properties
- for(let key of Object.keys(nowPlaying)){
- //Add them to the entry div's dataset
- entryDiv.dataset[key] = nowPlaying[key];
- }
-
- //Convert start epoch to JS date object
- const started = new Date(nowPlaying.startTime);
-
- //If this started today
- if(utils.isSameDate(this.day, started)){
- //Set entryDiv top-border location based on start time
- entryDiv.style.top = `${this.offsetByDate(started)}px`
- }else{
- //Get dawn
- const dawn = new Date();
- dawn.setHours(0,0,0,0)
-
- //Place item at the beginning of dawn
- entryDiv.style.top = `${this.offsetByDate(dawn)}px`;
-
- //Apply rest of the styling rules for items that started yestarday
- entryDiv.classList.add('started-yesterday')
- }
-
- //Create entry title
- const entryTitle = document.createElement('p');
- entryTitle.textContent = utils.unescapeEntities(nowPlaying.title);
-
- //Set entry div bottom-border location based on current time, round to match time marker
- entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
-
- //Assembly entryDiv
- entryDiv.appendChild(entryTitle);
-
- //Setup media tooltip
- entryDiv.addEventListener('mouseenter',(event)=>{
- //Construct tooltip on mouseover to re-calculate duration for live media
- const tooltipDiv = buildTooltip();
-
- //Display tooltip
- utils.ux.displayTooltip(event, tooltipDiv, false, null, true, this.ownerDoc);
- });
-
- const menuMap = new Map([
- ["Stop", ()=>{this.client.socket.emit('stop');}],
- ["Delete", ()=>{this.client.socket.emit('delete', {uuid: nowPlaying.uuid});}],
- ["Open in New Tab", ()=>{window.open(nowPlaying.url, '_blank').focus();}],
- ["Copy URL", ()=>{navigator.clipboard.writeText(nowPlaying.url);}],
-
- ]);
-
- //Setup context menu
- entryDiv.addEventListener('contextmenu', (event)=>{
- //Display context menu
- utils.ux.displayContextMenu(event, '', menuMap, this.ownerDoc);
- });
-
- //Append entry div to queue container
- this.queueContainer.appendChild(entryDiv);
- }else{
- //Update existing entry, round offset to match time marker
- staleEntry.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
- }
-
- //Keep tooltip date seperate so it re-calculates live duration properly
- function buildTooltip(date = new Date()){
- //Tooltip generation
- //tooltip div
- const tooltipDiv = document.createElement('div');
- tooltipDiv.classList.add('media-tooltip');
-
- //tooltip components
- const tooltipStrings = [
- `Title: ${nowPlaying.title}`,
- `File Name: ${nowPlaying.fileName}`,
- `Source: HLS Livestream (${nowPlaying.url})`,
- `Duration: ${utils.ux.humieFriendlyDuration((date.getTime() - nowPlaying.startTime) / 1000)}`,
- `Start Time: ${new Date(nowPlaying.startTime).toLocaleString()}`,
- ];
-
- //For each string in the tooltip
- for(const string of tooltipStrings){
-
- //Create a 'p' node
- const component = document.createElement('p');
- //Fill it with the string
- component.textContent = utils.unescapeEntities(string);
-
- //Append it to the tooltip div
- tooltipDiv.append(component);
- }
-
- //Create End Date
- const source = document.createElement('p');
- const liveSpan = document.createElement('span');
-
- //Fill end date label
- source.textContent = "End Time: "
- liveSpan.textContent = 'LIVE'
-
- //Set class
- liveSpan.classList.add('critical-danger-text');
-
- //Assemble end date
- source.appendChild(liveSpan);
- tooltipDiv.appendChild(source);
-
- return tooltipDiv;
- }
- }
-
- /**
- * Calculate schedule offset from top or bottom of div from date
- * @param {Date} date - Date to calculate from, defaults to now
- * @param {Boolean} bottomOffset - Whether or not offset should be calculated from top or bottom
- * @returns {Number} Offset from top/bottom of div in PX relative to time markers, calculated from given date
- */
- 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);
- }
-
- /**
- * Calculates date by offset pixels relative to schedule div top
- * @param {Number} input - Pixels date is away from schedule div top
- * @returns {Date} Date object representing date which was calculated from given pixel offset
- */
- 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;
- }
-
- /**
- * Calculate schedule offset from top or bottom of div from JS Epoch (millis)
- * @param {Number} input - JS Epoch (millis) to calculate from, defaults to 0
- * @param {Boolean} bottomOffset - Whether or not offset should be calculated from top or bottom
- * @returns {Number} Offset from top/bottom of div in PX relative to time markers, calculated from given 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];
- }
-
- /**
- * Returns media end time
- * @param {Object} media - media object to get end time from
- * @returns {Number} Media end time as JS Epoch (millis)
- */
- 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 representing pop-up dialogue to schedule a piece of media
- */
-class schedulePopup{
- /**
- * Instantiates a new schedule media Pop-up
- * @param {Event} event - Event passed down from Event Listener
- * @param {channel} client - Parent Client Management Object
- * @param {String} url - URL/link to media to queue
- * @param {String} title - Title of media to queue
- * @param {Function} cb - Callback function, passed upon pop-up creation
- * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
- */
- constructor(event, client, url, title, cb, doc){
- /**
- * Parent Client Management Object
- */
- this.client = client;
-
- /**
- * URL/Link to media to queue
- */
- this.url = url;
-
- /**
- * Title of media to queue
- */
- this.title = title;
-
- /**
- * Callback function, passed upon pop-up creation
- */
- 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 :(
- /**
- * canopyUXUtils.popup() object
- */
- this.popup = new canopyUXUtils.popup('/scheduleMedia', true, this.asyncConstructor.bind(this), doc);
- }
-
- /**
- * Continuation of object construction, called after child popup object construction
- */
- 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();
- }
- }
-
- /**
- * Defines input-related Event Handlers
- */
- setupInput(){
- //Setup input
- this.scheduleButton.addEventListener('click', this.schedule.bind(this));
- this.popup.popupDiv.addEventListener('keydown', this.schedule.bind(this));
- }
-
- /**
- * Handles sending request to schedule item to the queue
- * @param {Event} event - Event passed down from Event Listener
- */
- 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 representing pop-up dialogue for reschedule queue media
- * @extends schedulePopup
- */
-class reschedulePopup extends schedulePopup{
- /**
- * Instantiates a new schedule media Pop-up
- * @param {Event} event - Event passed down from Event Listener
- * @param {channel} client - Parent Client Management Object
- * @param {Object} media - Media object to re-schedule
- * @param {Function} cb - Callback function, passed upon pop-up creation
- * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
- */
- constructor(event, client, media, cb, doc){
- //Call derived constructor
- super(event, client, null, null, cb, doc);
-
- /**
- * Media Object to Re-Schedule
- */
- 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 represneting pop-up dialogue for clearing queue between a range of two dates
- */
-class clearPopup{
- /**
- * Instantiates a new queue Clear Popup
- * @param {Event} event - Event passed down from Event Listener
- * @param {channel} client - Parent Client Management Object
- * @param {Function} cb - Callback function, passed upon pop-up creation
- * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
- */
- constructor(event, client, cb, doc){
- /**
- * Parent Client Management Object
- */
- this.client = client;
-
- /**
- * Callback function, passed upon pop-up creation
- */
- 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 :(
- /**
- * canopyUXUtils.popup() object
- */
- this.popup = new canopyUXUtils.popup('/clearMedia', true, this.asyncConstructor.bind(this), doc);
- }
-
- /**
- * Continuation of object construction, called after child popup object construction
- */
- 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();
- }
- }
-
- /**
- * Defines input-related Event Handlers
- */
- setupInput(){
- //Setup input
- this.clearButton.addEventListener('click', this.clear.bind(this));
- this.popup.popupDiv.addEventListener('keydown', this.clear.bind(this));
- }
-
- /**
- * Handles sending request to clear playlist between two dates to the server
- * @param {Event} event - Event passed down from Event Listener
- */
- 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();
- }
- }
-}
-
-