Added schedule clearing and scroll to drag to schedule panel.

This commit is contained in:
rainbow napkin 2025-02-07 06:01:20 -05:00
parent 56ab5a16ec
commit c04edb6691
8 changed files with 466 additions and 120 deletions

View file

@ -49,6 +49,7 @@ module.exports = class{
socket.on("queue", (data) => {this.queueURL(socket, data)}); socket.on("queue", (data) => {this.queueURL(socket, data)});
socket.on("delete", (data => {this.deleteMedia(socket, data)})); socket.on("delete", (data => {this.deleteMedia(socket, data)}));
socket.on("move", (data => {this.moveMedia(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 //Set url
var url = data.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 we where given a bad URL
if(!validator.isURL(url)){ if(!validator.isURL(url)){
//Attempt to fix the situation by encoding it //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){ deleteMedia(socket, data){
try{ try{
//If we don't have a valid UUID //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 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 //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 //Move media by UUID
@ -177,17 +200,35 @@ module.exports = class{
//If we have no next item //If we have no next item
if(nextItem == null){ if(nextItem == null){
//Fuck off and die //Get current item
return; 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 removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){
const startsIn = nextItem.startTime - new Date().getTime(); //Find items within given range
const foundItems = this.getItemsBetweenEpochs(start, end);
//Clear out any item that might be up next //For each item
clearTimeout(this.nextTimer); for(let item of foundItems){
//Set the next timer //Remove media
this.nextTimer = setTimeout(()=>{this.start(nextItem)}, startsIn); this.removeMedia(item.uuid, socket);
}
} }
rescheduleMedia(uuid, start = new Date().getTime() + 50, socket){ rescheduleMedia(uuid, start = new Date().getTime() + 50, socket){
@ -205,11 +246,21 @@ module.exports = class{
return; return;
} }
//Grab the old start time for safe keeping
const oldStart = media.startTime;
//Set media time //Set media time
media.startTime = start; media.startTime = start;
//Re-schedule the media for the given time //Attempt to schedule media at given time
this.scheduleMedia(media, socket); //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){ removeMedia(uuid, socket){
@ -224,7 +275,7 @@ module.exports = class{
loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue"); loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue");
} }
//Ignore it //Ignore it
return; return false;
} }
//If we're currently playing the requested item. //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... 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) 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 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: Further Reading:
https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays 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 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 an originating socket was provided for this request
if(socket != null){ if(socket != null){
//Yell at the user for being an asshole //Yell at the user for being an asshole
loggerUtils.socketErrorHandler(socket, "This time slot has already been taken in the queue!", "queue"); loggerUtils.socketErrorHandler(socket, "This time slot has already been taken in the queue!", "queue");
} }
//Ignore it //Ignore it
return; return false;
} }
@ -315,14 +366,17 @@ module.exports = class{
//Refresh the next timer to ensure whatever comes on next is right //Refresh the next timer to ensure whatever comes on next is right
this.refreshNextTimer(); this.refreshNextTimer();
//return media object for use
return mediaObj;
} }
start(mediaObj){ start(mediaObj, timestamp = 0){
//Silently end the media //Silently end the media
this.end(true); this.end(true);
//reset current timestamp //reset current timestamp
this.timestamp = 0; this.timestamp = timestamp;
//Set current playing media //Set current playing media
this.nowPlaying = mediaObj; this.nowPlaying = mediaObj;
@ -335,6 +389,9 @@ module.exports = class{
//Setup the next video //Setup the next video
this.refreshNextTimer(); this.refreshNextTimer();
//return media object for use
return mediaObj;
} }
sync(){ 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()){ getItemAtEpoch(epoch = new Date().getTime()){
//Loop through scheduled items //Loop through scheduled items
for(let item of this.schedule){ for(let item of this.schedule){

View file

@ -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 <https://www.gnu.org/licenses/>. %>
<link rel="stylesheet" type="text/css" href="/css/popup/clearMedia.css">
<h3 class="popup-title">Clear Media</h3>
<div class="clear-media-popup-div">
<span class="clear-media-input-span">
<label for="clear-media-popup-start-time-prompt">Start:</label>
<input name="clear-media-popup-start-time-prompt" type="datetime-local" id="clear-media-popup-start-time-prompt">
</span>
<span class="clear-media-input-span">
<label for="clear-media-popup-end-time-prompt">End:</label>
<input name="clear-media-popup-end-time-prompt" type="datetime-local" id="clear-media-popup-end-time-prompt">
</span>
<button class="danger-button" id="clear-media-popup-clear-button">Clear</button>
</div>

View file

@ -29,7 +29,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
} }
#queue-control-offset{ #queue-control-offset{
margin-bottom: 2.15em; height: 2.15em;
flex-shrink: 0
} }
#queue-control-buttons{ #queue-control-buttons{
@ -54,6 +55,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
#queue-container{ #queue-container{
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
overflow: hidden;
} }
#queue-marker-container{ #queue-marker-container{

View file

@ -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 <https://www.gnu.org/licenses/>.*/
.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;
}

View file

@ -497,7 +497,7 @@ div.queue-entry{
text-align: center; text-align: center;
} }
.media-tooltip p{ .media-tooltip{
font-family: monospace; font-family: monospace;
} }

View file

@ -33,13 +33,18 @@ class queuePanel extends panelObj{
this.queueMarkerContainer = this.queue.querySelector('#queue-marker-container'); this.queueMarkerContainer = this.queue.querySelector('#queue-marker-container');
//Get queue layout controller //Get queue layout controller
this.queueLayoutController = this.panelDocument.querySelector('#queue-panel-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 //Re-acquire time marker
this.timeMarker = this.panelDocument.querySelector('#time-marker'); this.timeMarker = this.panelDocument.querySelector('#time-marker');
//Dragscroll timer
this.dragScrollTimer = null;
//Get main control buttons //Get main control buttons
this.addMediaButton = this.panelDocument.querySelector('#queue-add-media'); this.addMediaButton = this.panelDocument.querySelector('#queue-add-media');
this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock'); this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock');
this.queueDateButton = this.panelDocument.querySelector('#queue-date') this.queueDateButton = this.panelDocument.querySelector('#queue-date')
this.clearMediaButton = this.panelDocument.querySelector('#queue-clear');
//Get control divs //Get control divs
this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts'); 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.addMediaButton.addEventListener('click', this.toggleAddMedia.bind(this));
this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this)); this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this));
this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this)); this.queueDateButton.addEventListener('click', this.toggleDateControl.bind(this));
this.clearMediaButton.addEventListener('click', this.clearMedia.bind(this));
//control bar divs //control bar divs
//Add Media //Add Media
this.queueLastButton.addEventListener('click', this.queueLast.bind(this)) this.queueLastButton.addEventListener('click', this.queueLast.bind(this))
this.queueAtButton.addEventListener('click', this.queueAt.bind(this))
//Queue Date //Queue Date
this.queueDateDecrement.addEventListener('click', this.decrementDate.bind(this)); this.queueDateDecrement.addEventListener('click', this.decrementDate.bind(this));
this.queueDateIncrement.addEventListener('click', this.incrementDate.bind(this)); this.queueDateIncrement.addEventListener('click', this.incrementDate.bind(this));
@ -147,10 +154,26 @@ class queuePanel extends panelObj{
} }
} }
clearMedia(event){
//Call up the popup
new clearPopup(event, this.client, null);
}
/* add queue controls */ /* add queue controls */
queueLast(event){ queueLast(event){
//Send off the request //Send off the request
this.client.socket.emit("queue",{url:this.addMediaLinkPrompt.value, title:this.addMediaNamePrompt.value}); 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.addMediaLinkPrompt.value = '';
this.addMediaNamePrompt.value = ''; this.addMediaNamePrompt.value = '';
} }
@ -264,14 +287,19 @@ class queuePanel extends panelObj{
}else{ }else{
//If we're looking at today //If we're looking at today
if(utils.isSameDate(new Date(), this.day)){ if(utils.isSameDate(new Date(), this.day)){
//Disable scroll lock //Unlock auto scroll
this.autoscroll = false; this.unlockScroll();
//Unlight the indicator
this.scrollLockButton.classList.remove('positive-button');
} }
} }
} }
unlockScroll(){
//Disable scroll lock
this.autoscroll = false;
//Unlight the indicator
this.scrollLockButton.classList.remove('positive-button');
}
humieFriendlyDuration(seconds){ humieFriendlyDuration(seconds){
//If we have an invalid duration //If we have an invalid duration
if(seconds <= 0){ if(seconds <= 0){
@ -413,7 +441,12 @@ class queuePanel extends panelObj{
//Create entry div //Create entry div
const entryDiv = document.createElement('div'); const entryDiv = document.createElement('div');
entryDiv.classList.add('queue-entry'); 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 this item starts today
if(startsToday){ if(startsToday){
@ -528,41 +561,125 @@ class queuePanel extends panelObj{
event.target.style.height = `${height}px`; event.target.style.height = `${height}px`;
} }
//Add set dragging CSS class to target
event.target.classList.add('dragging-queue-entry'); event.target.classList.add('dragging-queue-entry');
//enable drag on target dataset
event.target.dataset['drag'] = true; event.target.dataset['drag'] = true;
//Drag entry with mouse //Create a tooltip to show the time we're dragging to
this.ownerDoc.body.addEventListener('mousemove', (nestedEvent)=>{(dragEntry.bind(this))(nestedEvent, event.target)}); const timetip = new canopyUXUtils.tooltip('', false, null, this.ownerDoc);
timetip.tooltip.classList.add('media-tooltip');
//Drop on moust up //Drag entry with mouse
this.ownerDoc.body.addEventListener('mouseup', (nestedEvent)=>{(dropEntry.bind(this))(nestedEvent, event.target)}); 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 //Disable selection on body
this.ownerDoc.body.style.userSelect = 'none'; this.ownerDoc.body.style.userSelect = 'none';
//Save top of target relative to window minus the mouse position as our drag offset //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.target.offsetTop + this.ownerDoc.defaultView.scrollY) - event.clientY;
event.target.dataset['dragoffset'] = event.clientY - (event.target.getBoundingClientRect().top + this.ownerDoc.defaultView.scrollY);
//Call the drag entry function to move the entry on click without re-writing the wheel //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 //Gross but works :P
if(!target.isConnected || target.dataset['drag'] != "true"){ if(!target.isConnected || target.dataset['drag'] != "true"){
return; 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('<br>');
//Calculate offset from rest of window //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 //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 //Gross but works :P
if(!target.isConnected){ if(!target.isConnected){
return; return;
@ -574,74 +691,12 @@ class queuePanel extends panelObj{
//allow selection on body //allow selection on body
this.ownerDoc.body.style.userSelect = 'none'; this.ownerDoc.body.style.userSelect = 'none';
//Remove timetip
timetip.remove();
//Finish dragging //Finish dragging
target.dataset['drag'] = false; 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){ renderTimeMarker(date = new Date(), forceScroll = false){
@ -742,23 +797,11 @@ class queuePanel extends panelObj{
//If it's even/zero //If it's even/zero
if(index % 2 == 0){ if(index % 2 == 0){
//Create marker label
const markerLabel = document.createElement('p'); 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 scale is over a minute then we don't need to display seconds
if(date.getHours() < 12){ markerLabel.textContent = utils.ux.timeStringFromDate(date, this.scale < 60)
//Display as AM
markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM`
//If we're cointing noon
}else if(date.getHours() == 12){
//display as noon
markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
//if we're counting pm
}else{
//display as pm
markerLabel.textContent = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
}
//Add marker label to marker span //Add marker label to marker span
markerDiv.appendChild(markerLabel); markerDiv.appendChild(markerLabel);
@ -820,8 +863,8 @@ class queuePanel extends panelObj{
//save as 'float' between 0 and 1 //save as 'float' between 0 and 1
const relativeInput = ((input - range[0]) / offsetMax); const relativeInput = ((input - range[0]) / offsetMax);
//Get the current date //Get the currently viewed day
const date = new Date(); const date = new Date(this.day);
//Convert our 'float' from 0-1 to a time between 0-24 //Convert our 'float' from 0-1 to a time between 0-24
date.setHours(0,0,0,relativeInput * 86400000); date.setHours(0,0,0,relativeInput * 86400000);
@ -830,3 +873,147 @@ class queuePanel extends panelObj{
return 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();
}
}
}

View file

@ -101,6 +101,9 @@ class player{
//Re-size to aspect since video may now be a different size //Re-size to aspect since video may now be a different size
this.client.chatBox.resizeAspect(); this.client.chatBox.resizeAspect();
//Sync off of starter time stamp
this.mediaHandler.sync(data.timestamp);
} }
sync(data){ sync(data){

View file

@ -76,6 +76,29 @@ class canopyUXUtils{
return new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); 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 //Update this and popup class to use nodes
//and display multiple errors in one popup //and display multiple errors in one popup
displayResponseError(body){ displayResponseError(body){