diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index d593f78..9b859c4 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -49,6 +49,7 @@ module.exports = class{ socket.on("queue", (data) => {this.queueURL(socket, data)}); socket.on("delete", (data => {this.deleteMedia(socket, data)})); socket.on("move", (data => {this.moveMedia(socket, data)})); + socket.on("clear", (data => {this.deleteRange(socket, data)})); } @@ -57,8 +58,6 @@ module.exports = class{ //Set url var url = data.url; - //pull URL and start time from data - //let {url, start, title} = data; //If we where given a bad URL if(!validator.isURL(url)){ //Attempt to fix the situation by encoding it @@ -122,6 +121,30 @@ module.exports = class{ } } + deleteRange(socket, data){ + try{ + //If start time isn't an integer + if(data.start != null && !validator.isInt(String(data.start))){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Bad start date!", "queue"); + //and ignore it! + return; + } + + //If end time isn't an integer + if(data.end != null && !validator.isInt(String(data.end))){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Bad end date!", "queue"); + //and ignore it! + return; + } + + this.removeRange(data.start, data.end, socket); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } + } + deleteMedia(socket, data){ try{ //If we don't have a valid UUID @@ -150,9 +173,9 @@ module.exports = class{ } //If start time isn't an integer after the current epoch - if(data.start != null && !validator.isInt(String(data.start), new Date().getTime())){ + if(data.start != null && !validator.isInt(String(data.start))){ //Null out time to tell the later parts of the function to start it now - data.start = null; + data.start = undefined; } //Move media by UUID @@ -177,17 +200,35 @@ module.exports = class{ //If we have no next item if(nextItem == null){ - //Fuck off and die - return; + //Get current item + const currentItem = this.getItemAtEpoch() + + //If we have a current item and it isn't currently playing + if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ + //Start the found item at w/ a pre-calculated time stamp to reflect the given start time + this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000)); + } + //otherwise if we have an item + }else{ + //Calculate the amount of time in ms that the next item will start in + const startsIn = nextItem.startTime - new Date().getTime(); + + //Clear out any item that might be up next + clearTimeout(this.nextTimer); + //Set the next timer + this.nextTimer = setTimeout(()=>{this.start(nextItem)}, startsIn); } + } - //Calculate the amount of time in ms that the next item will start in - const startsIn = nextItem.startTime - new Date().getTime(); + removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){ + //Find items within given range + const foundItems = this.getItemsBetweenEpochs(start, end); - //Clear out any item that might be up next - clearTimeout(this.nextTimer); - //Set the next timer - this.nextTimer = setTimeout(()=>{this.start(nextItem)}, startsIn); + //For each item + for(let item of foundItems){ + //Remove media + this.removeMedia(item.uuid, socket); + } } rescheduleMedia(uuid, start = new Date().getTime() + 50, socket){ @@ -205,11 +246,21 @@ module.exports = class{ return; } + //Grab the old start time for safe keeping + const oldStart = media.startTime; + //Set media time media.startTime = start; - //Re-schedule the media for the given time - this.scheduleMedia(media, socket); + //Attempt to schedule media at given time + //Otherwise, if it returns false for fuckup + if(!this.scheduleMedia(media, socket)){ + //Reset start time + media.startTime = oldStart; + + //Schedule in old slot + this.scheduleMedia(media, socket); + } } removeMedia(uuid, socket){ @@ -224,7 +275,7 @@ module.exports = class{ loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue"); } //Ignore it - return; + return false; } //If we're currently playing the requested item. @@ -264,11 +315,11 @@ module.exports = class{ that, no matter what, re-ordering the schedule map would've required us to iterate through and convert it to an array and back anyways... - Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them as seconds. + Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - If for some reason they haven't we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication + If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication Further Reading: https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays @@ -276,14 +327,14 @@ module.exports = class{ */ //If there's already something queued right now - if(this.getItemAtEpoch(mediaObj.startTime) != null){ + if(this.getItemAtEpoch(mediaObj.startTime) != null || this.getItemAtEpoch(mediaObj.startTime + (mediaObj.duration * 1000))){ //If an originating socket was provided for this request if(socket != null){ //Yell at the user for being an asshole loggerUtils.socketErrorHandler(socket, "This time slot has already been taken in the queue!", "queue"); } //Ignore it - return; + return false; } @@ -315,14 +366,17 @@ module.exports = class{ //Refresh the next timer to ensure whatever comes on next is right this.refreshNextTimer(); + + //return media object for use + return mediaObj; } - start(mediaObj){ + start(mediaObj, timestamp = 0){ //Silently end the media this.end(true); //reset current timestamp - this.timestamp = 0; + this.timestamp = timestamp; //Set current playing media this.nowPlaying = mediaObj; @@ -335,6 +389,9 @@ module.exports = class{ //Setup the next video this.refreshNextTimer(); + + //return media object for use + return mediaObj; } sync(){ @@ -377,6 +434,23 @@ module.exports = class{ } } + getItemsBetweenEpochs(start, end){ + //Create an empty array to hold found items + const foundItems = []; + + //Loop through scheduled items + for(let item of this.schedule){ + //If the item starts after our start date and before our end date + if(item[0] >= start && item[0] <= end ){ + //Add the current item to the list + foundItems.push(item[1]); + } + } + + //Return any found items + return foundItems; + } + getItemAtEpoch(epoch = new Date().getTime()){ //Loop through scheduled items for(let item of this.schedule){ diff --git a/src/views/partial/popup/clearMedia.ejs b/src/views/partial/popup/clearMedia.ejs new file mode 100644 index 0000000..0b331cf --- /dev/null +++ b/src/views/partial/popup/clearMedia.ejs @@ -0,0 +1,28 @@ +<%# 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 . %> + + +
+ + + + + + + + + +
\ No newline at end of file diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index be7b4dc..eae0a46 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -29,7 +29,8 @@ along with this program. If not, see .*/ } #queue-control-offset{ - margin-bottom: 2.15em; + height: 2.15em; + flex-shrink: 0 } #queue-control-buttons{ @@ -54,6 +55,7 @@ along with this program. If not, see .*/ #queue-container{ position: relative; flex-grow: 1; + overflow: hidden; } #queue-marker-container{ diff --git a/www/css/popup/clearMedia.css b/www/css/popup/clearMedia.css new file mode 100644 index 0000000..74016df --- /dev/null +++ b/www/css/popup/clearMedia.css @@ -0,0 +1,29 @@ +/*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 .*/ +.clear-media-popup-div{ + display: flex; + flex-direction: column; +} + +.clear-media-input-span{ + display: flex; + justify-content: space-between; + margin-bottom: 0.5em; +} + +.clear-media-input-span input{ + margin-left: 1em; +} \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 5c60777..f40706a 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -497,7 +497,7 @@ div.queue-entry{ text-align: center; } -.media-tooltip p{ +.media-tooltip{ font-family: monospace; } diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js index 13b2a5a..c0d861a 100644 --- a/www/js/channel/panels/queuePanel.js +++ b/www/js/channel/panels/queuePanel.js @@ -33,13 +33,18 @@ class queuePanel extends panelObj{ 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.clearMediaButton = this.panelDocument.querySelector('#queue-clear'); //Get control divs this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); @@ -86,10 +91,12 @@ class queuePanel extends panelObj{ 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.clearMediaButton.addEventListener('click', this.clearMedia.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)); @@ -147,14 +154,30 @@ class queuePanel extends panelObj{ } } + clearMedia(event){ + //Call up the popup + new clearPopup(event, this.client, null); + } + /* 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); + + //Clear out prompts + this.addMediaLinkPrompt.value = ''; + this.addMediaNamePrompt.value = ''; + } + /* set date controls */ incrementDate(event){ //Increment day @@ -264,14 +287,19 @@ class queuePanel extends panelObj{ }else{ //If we're looking at today if(utils.isSameDate(new Date(), this.day)){ - //Disable scroll lock - this.autoscroll = false; - //Unlight the indicator - this.scrollLockButton.classList.remove('positive-button'); + //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){ @@ -413,7 +441,12 @@ class queuePanel extends panelObj{ //Create entry div const entryDiv = document.createElement('div'); entryDiv.classList.add('queue-entry'); - entryDiv.dataset['uuid'] = entry[1].uuid; + + //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){ @@ -528,41 +561,125 @@ class queuePanel extends panelObj{ 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; - //Drag entry with mouse - this.ownerDoc.body.addEventListener('mousemove', (nestedEvent)=>{(dragEntry.bind(this))(nestedEvent, event.target)}); + //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'); - //Drop on moust up - this.ownerDoc.body.addEventListener('mouseup', (nestedEvent)=>{(dropEntry.bind(this))(nestedEvent, event.target)}); + //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.getBoundingClientRect().top + this.ownerDoc.defaultView.scrollY) - event.clientY); - event.target.dataset['dragoffset'] = event.clientY - (event.target.getBoundingClientRect().top + this.ownerDoc.defaultView.scrollY); + event.target.dataset['dragoffset'] = (event.target.offsetTop + this.ownerDoc.defaultView.scrollY) - event.clientY; //Call the drag entry function to move the entry on click without re-writing the wheel - (dragEntry.bind(this))(event, event.target); + (dragEntry.bind(this))(event, event.target, timetip); + + //Start dragscroll loop + this.dragScrollTimer = setInterval(()=>{(dragScroll.bind(this))(event.target)}, 10); } - function dragEntry(event, target){ + 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) + 10))){ + //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.getBoundingClientRect().top + this.ownerDoc.defaultView.scrollY; + 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 - target.style.top = `${event.clientY - Number(target.dataset['dragoffset']) - windowOffset}px`; + const entryTop = event.clientY + Number(target.dataset['dragoffset']) - windowOffset; + + //Set target vertical position + target.style.top = `${entryTop}px`; } - function dropEntry(event, target){ + function dropEntry(event, target, timetip){ //Gross but works :P if(!target.isConnected){ return; @@ -574,74 +691,12 @@ class queuePanel extends panelObj{ //allow selection on body this.ownerDoc.body.style.userSelect = 'none'; + //Remove timetip + timetip.remove(); //Finish dragging target.dataset['drag'] = false; } - - class reschedulePopup{ - constructor(event, client, media, cb){ - //Set Client - this.client = client; - //Set media - this.media = media; - //Set callback - this.cb = cb; - - //Create media popup and call async constructor when done - //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :( - this.popup = new canopyUXUtils.popup('/scheduleMedia', true, this.asyncConstructor.bind(this)); - } - - asyncConstructor(){ - //Grab required UI elements - this.scheduleButton = this.popup.contentDiv.querySelector('#schedule-media-popup-schedule-button'); - this.datePrompt = this.popup.contentDiv.querySelector('#schedule-media-popup-time-prompt'); - - //getCurrentDate - const curDate = new Date(); - //Zero out current date to midnight - curDate.setSeconds(0,0); - //Set the date prompt to the next minute, adjusted to display local time - this.datePrompt.valueAsDate = utils.ux.localizeDate(curDate); - - //Setup input - this.setupInput(); - - //If we have a function - if(typeof cb == "function"){ - //Call any callbacks we where given - this.cb(); - } - } - - setupInput(){ - //Setup input - this.scheduleButton.addEventListener('click', this.schedule.bind(this)); - this.popup.popupDiv.addEventListener('keydown', this.schedule.bind(this)); - } - - schedule(event){ - //If we clicked or hit enter - if(event.key == null || event.key == "Enter"){ - //Get localized input date - const inputDate = utils.ux.normalizeDate(this.datePrompt.valueAsDate); - - //If someone is trying to schedule in the past - if(inputDate < new Date().getTime()){ - //Schedule now - this.client.socket.emit('move', {uuid: this.media.uuid}); - //Otherwise - }else{ - //Tell the server to move the media - this.client.socket.emit('move', {uuid: this.media.uuid, start: inputDate.getTime()}); - } - - //Close the popup - this.popup.closePopup(); - } - } - } } renderTimeMarker(date = new Date(), forceScroll = false){ @@ -742,23 +797,11 @@ class queuePanel extends panelObj{ //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 - const seconds = this.scale > 60 ? '' : `:${('0' + date.getSeconds()).slice(-2)}` - //If we're counting AM - if(date.getHours() < 12){ - //Display as AM - markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM` - //If we're cointing noon - }else if(date.getHours() == 12){ - //display as noon - markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` - //if we're counting pm - }else{ - //display as pm - markerLabel.textContent = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` - } + //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); @@ -820,8 +863,8 @@ class queuePanel extends panelObj{ //save as 'float' between 0 and 1 const relativeInput = ((input - range[0]) / offsetMax); - //Get the current date - const date = new Date(); + //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); @@ -829,4 +872,148 @@ class queuePanel extends panelObj{ //return our date return date; } +} + +class schedulePopup{ + constructor(event, client, url, title, cb){ + //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)); + } + + 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){ + //Call derived constructor + super(event, client, null, null, cb); + + //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){ + //Set Client + this.client = client; + //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('/clearMedia', true, this.asyncConstructor.bind(this)); + } + + 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(); + } + } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index ec9b74f..36014bc 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -101,6 +101,9 @@ class player{ //Re-size to aspect since video may now be a different size this.client.chatBox.resizeAspect(); + + //Sync off of starter time stamp + this.mediaHandler.sync(data.timestamp); } sync(data){ diff --git a/www/js/utils.js b/www/js/utils.js index 4ef54fc..763fc19 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -76,6 +76,29 @@ class canopyUXUtils{ return new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); } + timeStringFromDate(date, displaySeconds = true){ + let outString = '' + + //If scale is over a minute then we don't need to display seconds + const seconds = displaySeconds ? `:${('0' + date.getSeconds()).slice(-2)}` : '' + + //If we're counting AM + if(date.getHours() < 12){ + //Display as AM + outString = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM` + //If we're cointing noon + }else if(date.getHours() == 12){ + //display as noon + outString = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` + //if we're counting pm + }else{ + //display as pm + outString = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` + } + + return outString; + } + //Update this and popup class to use nodes //and display multiple errors in one popup displayResponseError(body){