Added schedule clearing and scroll to drag to schedule panel.
This commit is contained in:
parent
56ab5a16ec
commit
c04edb6691
|
|
@ -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){
|
||||||
|
|
|
||||||
28
src/views/partial/popup/clearMedia.ejs
Normal file
28
src/views/partial/popup/clearMedia.ejs
Normal 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>
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
29
www/css/popup/clearMedia.css
Normal file
29
www/css/popup/clearMedia.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -497,7 +497,7 @@ div.queue-entry{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-tooltip p{
|
.media-tooltip{
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue