diff --git a/src/views/partial/panels/queue.ejs b/src/views/partial/panels/queue.ejs index 08043fe..e7b5ed5 100644 --- a/src/views/partial/panels/queue.ejs +++ b/src/views/partial/panels/queue.ejs @@ -14,17 +14,21 @@ 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 . %> -
- -
- -
-
- <%# Probably not the cleanest way to do this but fuggit %> - - -
-
-
-
+
+
+ +
+ +
+
+ <%# Probably not the cleanest way to do this but fuggit %> + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/www/css/channel.css b/www/css/channel.css index f959e32..3be0ab3 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -242,14 +242,12 @@ span.user-entry{ top: 0; bottom: 0; left: 0; - overflow-y: auto; scrollbar-width: thin; width: 75%; } #cpanel-pinned-div{ position: relative; - overflow-y: auto; scrollbar-width: thin; } diff --git a/www/css/panel.css b/www/css/panel.css index a85d2ff..b99a4d6 100644 --- a/www/css/panel.css +++ b/www/css/panel.css @@ -16,6 +16,7 @@ along with this program. If not, see .*/ .cpanel-div{ flex-direction: column; overflow: hidden; + flex: 1; } .cpanel-header-div{ @@ -37,4 +38,19 @@ along with this program. If not, see .*/ .cpanel-header-icon{ cursor: pointer; +} + +.cpanel-doc{ + flex: 1; + overflow: auto; +} + +.cpanel-body{ + display: block; + height: 100%; +} + +#cpanel-div{ + display: flex; + height: 100%; } \ No newline at end of file diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index d9d370f..d7f9410 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -13,18 +13,36 @@ 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 .*/ -#queue{ - position: relative; - display: grid; - grid-template-columns: auto 75%; +#queue-panel-layout-controller{ + display: flex; + flex-direction: column; + overflow-y: auto; + height: 100%; } +#queue{ + display: flex; + flex: 1; + flex-direction: row; + position: relative; +} + +#queue-container{ + position: relative; + flex-grow: 1; +} + +#queue-marker-container{ + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 0.1; +} div.queue-marker{ display: flex; flex-direction: row; align-items: center; - grid-column: 1; z-index: 2; } @@ -36,12 +54,29 @@ span.queue-marker{ div.queue-entry{ overflow: hidden; - margin: 1.5em 0.5em; - grid-column: 2; + position: absolute; + margin: 0 1em; + right: 0; + left: 0; +} + +div.queue-entry a{ + position: relative; + z-index: 2; } #time-marker{ position: absolute; height: 1px; width: 100%; + z-index: 1; +} + +#queue-marker-scale-label{ + position: absolute; + top: 25%; + left: 0; + right: 0; + text-align: center; + font-size: 3em; } \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index bb32e30..b4c66fb 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -47,6 +47,8 @@ along with this program. If not, see .*/ --danger-glow0: 2px 2px 3px var(--danger0), -2px 2px 3px var(--danger0), 2px -2px 3px var(--danger0), -2px -2px 3px var(--danger0); --danger-glow0-alt1: 2px 2px 3px var(--danger0-alt1), -2px 2px 3px var(--danger0-alt1), 2px -2px 3px var(--danger0-alt1), -2px -2px 3px var(--danger0-alt1); + --timer-glow: -2px 1px 3px var(--danger0-alt1), 2px -1px 3px var(--danger0-alt1); + --userlist-color0:rgb(63, 121, 71); --userlist-color1:rgb(143, 46, 26); --userlist-color2:rgb(51, 101, 161); @@ -464,12 +466,13 @@ span.queue-marker{ #time-marker{ background-color: var(--danger0); + box-shadow: var(--timer-glow); } div.queue-entry{ margin: 0.2em; padding: 0 0.7em; - border-radius: 1em; + border-radius: 0.3em; background-color: var(--bg1); border: 1px solid var(--accent1); } diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js index e73133b..faf4ec6 100644 --- a/www/js/channel/panels/queuePanel.js +++ b/www/js/channel/panels/queuePanel.js @@ -4,11 +4,178 @@ class queuePanel extends panelObj{ //Store releative scale of items in seconds, defaulting to 30 minute chunks this.scale = 30 * 60; + + //Create variable to hold rescale timer + this.rescaleTimer = null; + + //Define non-input event listeners + this.defineListeners(); } docSwitch(){ + //Get queue div + this.queue = this.panelDocument.querySelector('#queue'); //Get queue container - this.queueContainer = this.panelDocument.querySelector('#queue'); + this.queueContainer = this.queue.querySelector("#queue-container"); + //Get queue marker contianer + this.queueMarkerContainer = this.queue.querySelector('#queue-marker-container'); + //Get queue layout controller + this.queueLayoutController = this.panelDocument.querySelector('#queue-panel-layout-controller'); + //Re-acquire time marker + this.timeMarker = this.panelDocument.querySelector('#time-marker'); + + //Render out the queue + this.fullRender(); + + //Setup panel input + this.setupInput(); + } + + closer(){ + //Clear any remaining timers + clearTimeout(this.timeMarkerTimer); + } + + + defineListeners(){ + this.client.socket.on("queue", (data) => { + this.renderQueue(); + }) + } + + setupInput(){ + this.queue.addEventListener('wheel', this.scaleScroll.bind(this)); + } + + scaleScroll(event){ + if(event.ctrlKey){ + //Capture inverse scroll wheel direction + const scrollDirection = event.wheelDeltaY / Math.abs(event.wheelDeltaY) * -1; + + //Default scale factor to 5 seconds + let scaleFactor = 5; + + //Tried to do this with math but couldnt because im bad at math so heres the if statement of shame :( + if(this.scale >= 7200){ + scaleFactor = 3600 + }else if(this.scale >= 3600){ + scaleFactor = scrollDirection > 0 ? 3600 : 1800; + }else if(this.scale >= 1800){ + scaleFactor = scrollDirection > 0 ? 1800 : 900; + }else if(this.scale == 900){ + scaleFactor = scrollDirection > 0 ? 900 : 300; + }else if(this.scale > 300){ + //If we're above five minutes use five minutes + scaleFactor = 300; + }else if(this.scale == 300){ + //If we're at five minutes scroll up by five minutes or scroll down to one minute + scaleFactor = scrollDirection > 0 ? 300 : 240; + }else if(this.scale == 60){ + //If we're at one minutes scroll up by four minutes or scroll down by 10 seconds + scaleFactor = scrollDirection > 0 ? 240 : 10; + }else if(this.scale > 10){ + scaleFactor = 10; + }else if(this.scale == 10){ + scaleFactor = scrollDirection > 0 ? 10 : 5; + } + + //Prevent page-wide zoom in/out + event.preventDefault(); + + //Clear out the queue UI + this.clearQueue(); + + + //Calculate new scale + const newScale = this.scale + (scaleFactor * scrollDirection); + + //Clamp scale between 10 seconds and half a day + this.scale = Math.max(5, Math.min(43200, newScale)); + + //If we have no scale label + if(this.scaleLabel == null){ + //Make it + this.scaleLabel = document.createElement('p'); + this.scaleLabel.id = 'queue-marker-scale-label'; + this.queue.appendChild(this.scaleLabel); + } + + //Set scale label text to humie readable time scale + this.scaleLabel.innerHTML = `Time Scale:
${this.humieFriendlyDuration(this.scale)}` + + //Clear any previous rescale timer + clearTimeout(this.rescaleTimer); + //Set timeout to re-render after input stops + this.rescaleTimer = setTimeout(this.fullRender.bind(this), 500); + } + } + + humieFriendlyDuration(seconds){ + //If we have an invalid duration + if(seconds <= 0){ + //bitch, moan, and complain! + return('Invalid input duration'); + } + + //Create an empty array to hold the time strings + const timeStrings = []; + + //Pull hours from time + const hours = Math.floor(seconds / 3600); + //Remove recorded hours + seconds -= hours * 3600; + + //Pull minutes from time + const minutes = Math.floor(seconds / 60); + //Remove recorded minutes + seconds -= minutes * 60; + + //If we have an hour + if(hours == 1){ + //Add the string + timeStrings.push('1 Hour'); + //If we have hours + }else if(hours > 0){ + //Add the string + timeStrings.push(`${hours} Hours`); + } + + //If we have a minute + if(minutes == 1){ + //Add the string + timeStrings.push('1 Minute'); + //If we have minutes + }else if(minutes > 0){ + //Add the string + timeStrings.push(`${minutes} Minutes`); + } + + //Add the 'and ' if we need it + const secondsPrefix = timeStrings.length > 0 ? 'and ' : ''; + + //If we have a second + if(seconds == 1){ + //Add the string + timeStrings.push(`${secondsPrefix}1 Second`); + //If we have more than a second + }else if(seconds > 1){ + //Add the string + timeStrings.push(`${secondsPrefix}${seconds} Seconds`); + } + + //Join the time strings together + return timeStrings.join(', '); + } + + clearQueue(){ + //Clear out queue container + this.queueContainer.innerHTML = ''; + //Clear out queue marker container + this.queueMarkerContainer.innerHTML = ''; + + + //Clear any marker timers + clearTimeout(this.timeMarkerTimer); //If we have an existing time marker if(this.timeMarker != null){ @@ -16,28 +183,46 @@ class queuePanel extends panelObj{ this.timeMarker.remove(); this.timeMarker = null; } - - //Render out the queue - this.renderQueue(); } - closer(){ - //Clear any remaining timers - clearTimeout(this.renderTimeMarker); - } + async fullRender(date = new Date()){ - renderQueue(date = new Date()){ - //Clear out queue container - this.queueContainer.innerHTML = ''; + //Clear the queue + this.clearQueue(); + + //If we have a scale label + if(this.scaleLabel != null){ + //Take it out + this.scaleLabel.remove(); + this.scaleLabel = null; + } //Render out time scale this.renderQueueScale(date); + //wait a few frames so the scale can finish rendering, because dom function aren't async for some fucking reason + for(let i = 0; i <= 2; i++){ + await utils.awaitNextFrame(); + } + + //render the time marker + this.renderTimeMarker(date); + + //render out the queue + this.renderQueue(date); + } + + renderQueue(date = new Date()){ + //for every entry in the queue for(let entry of this.client.queue){ //Create entry div const entryDiv = document.createElement('div'); entryDiv.classList.add('queue-entry'); + //Place entry div + entryDiv.style.top = `${this.offsetByDate(new Date(entry[1].startTime))}px`; + entryDiv.style.bottom = `${this.offsetByDate(new Date(entry[1].startTime + (entry[1].duration * 1000)), true)}px`; + //Create entry title const entryTitle = document.createElement('a'); entryTitle.textContent = entry[1].title; @@ -52,42 +237,50 @@ class queuePanel extends panelObj{ } renderTimeMarker(date = new Date()){ - //Pull start of day epoch from given date - const dayEpoch = structuredClone(date).setHours(0,0,0,0); - //Get difference to get time since the start of the current day in seconds - const curTime = date.getTime() - dayEpoch; - //Get time in day as a float between 0 and 1 - const timeFloat = curTime / 86400000; - //Get queue markers - const markers = this.panelDocument.querySelectorAll('span.queue-marker'); + //Calculate marker position by date + const markerPosition = Math.round(this.offsetByDate(date)); - //If the marker is null for some reason - if(markers[0] == null){ + //If markers are null + if(markerPosition == null){ //Try again in a second since the user probably just popped the panel or something :P (smackTimer.bind(this))(); - return; } - //Get marker position range - const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop] - //Get maximum position relative to the range itself - const offsetMax = range[1] - range[0]; - //Get marker position relative to parent - const markerPosition = (offsetMax * timeFloat) + range[0]; - //if we need to make the time marker if(this.timeMarker == null){ - //Create time marker + //Create the time marker this.timeMarker = document.createElement('span'); //Add time marker class this.timeMarker.id = 'time-marker'; //Append time marker - this.queueContainer.appendChild(this.timeMarker); + this.queue.appendChild(this.timeMarker); } //Set time marker position this.timeMarker.style.top = `${markerPosition}px`; + //If the panel document isn't null (we're not actively switching panels) + if(this.panelDocument != null){ + //Grab the current owner document object + const ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; + + //Get height difference between window and queue layout controller + const docDifference = ownerDoc.defaultView.innerHeight - this.queueLayoutController.getBoundingClientRect().height; + //Calculate scroll target by body difference and marker position + const scrollTarget = (markerPosition - (this.queueLayoutController.getBoundingClientRect().height - docDifference) / 2) + docDifference; + //Calculate scroll behavior by distance + const scrollBehavior = Math.abs(scrollTarget - this.queueLayoutController.scrollTop) > 10 ? "smooth" : "instant"; + + + //Scroll to the marker + this.queueLayoutController.scroll({ + left: 0, + top: scrollTarget, + behavior: scrollBehavior + }); + } + + //Set the timer to run the function again (smackTimer.bind(this))(); function smackTimer(){ @@ -102,7 +295,7 @@ class queuePanel extends panelObj{ //Make sure we don't modify the date we're handed const date = structuredClone(inputDate); - //Zero out date to midnight + //Zero out time to midnight on the morning of the input date date.setHours(0,0,0,0); //Store epoch of current date at midnight for later user @@ -152,18 +345,32 @@ class queuePanel extends panelObj{ markerSpan.appendChild(marker); //Append marker span to queue container - this.queueContainer.appendChild(markerSpan); + this.queueMarkerContainer.appendChild(markerSpan); + } + } + + offsetByDate(date = new Date(), bottomOffset = false){ + //Pull start of day epoch from given date + const dayEpoch = structuredClone(date).setHours(0,0,0,0); + //Get difference between now and day epoch to get time since the start of the current day in milliseconds + const curTime = date.getTime() - dayEpoch; + //Devide by amount of milliseconds in a day to convert time over to a floating point number between 0 and 1 + //Make sure to reverse it if bottomOffset is set to true + const timeFloat = bottomOffset ? 1 - (curTime / 86400000) : curTime / 86400000; + //Get queue markers + const markers = this.panelDocument.querySelectorAll('span.queue-marker'); + + //If the marker is null for some reason + if(markers[0] == null){ + + return null; } - //Easiest way to wait for DOM to do it's thing is to: - //fires before next frame - requestAnimationFrame(()=>{ - //fires before next-next frame (after next frame) - requestAnimationFrame(()=>{ - //render the time marker - this.renderTimeMarker(inputDate); - }); - }); - + //Get marker position range + const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop] + //Get maximum position relative to the range itself + const offsetMax = range[1] - range[0]; + //return position relative to parent + return (offsetMax * timeFloat) + range[0]; } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index adeb85e..9616bd7 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -209,7 +209,7 @@ class player{ this.toggleUI(true); clearTimeout(this.uiTimer); if(!this.onUI){ - this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false); + //this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false); } } diff --git a/www/js/utils.js b/www/js/utils.js index 12a3750..bb415d5 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -35,6 +35,20 @@ class canopyUtils{ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + + async awaitNextFrame(){ + //return a new promise + return new Promise((resolve)=>{ + //Before the next frame + requestAnimationFrame(()=>{ + //fires before next-next frame (after next frame) + requestAnimationFrame(()=>{ + //resolve the promise + resolve(); + }); + }); + }) + } } class canopyUXUtils{