diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index c97aff3..e700b51 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -49,9 +49,9 @@ module.exports = class{ this.preSwitchTimer = null; //Create variable to hold currently playing media object this.nowPlaying = null; + //Create variable to lock standard queuing functions during livestreams this.streamLock = false; - //create boolean to hold schedule lock this.locked = false; @@ -1179,7 +1179,7 @@ module.exports = class{ } } - //Create a new array to hold the new schedule so we only have to write to the DB once. + //Create a new array to hold the new schedule since we'll only be keeping select items let newSched = []; //For every saved scheduled item diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 6df7a64..84b4564 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -44,6 +44,7 @@ along with this program. If not, see .*/ --danger0: firebrick; --danger0-alt0: rgb(121, 11, 11); --danger0-alt1: rgb(255, 105, 105); + --danger0-alt2: rgb(242, 189, 189); --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); @@ -504,6 +505,17 @@ div.queue-entry{ text-align: center; } +div.queue-entry.live { + background-color: var(--danger0); + border: 1px solid var(--danger0-alt0); + box-shadow: 0px 9px 10px -3px var(--danger0-alt1); +} + + +.queue-entry.live p{ + color: var(--danger0-alt2); +} + .media-tooltip{ font-family: monospace; } diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index d368f57..4567966 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -101,7 +101,7 @@ class queuePanel extends panelObj{ closer(){ //Clear any remaining timers - clearTimeout(this.timeMarkerTimer); + clearTimeout(this.renderIntervalTimer); //Clear timetips this.killTimetips(); } @@ -382,7 +382,7 @@ class queuePanel extends panelObj{ } //Set scale label text to humie readable time scale - this.scaleLabel.innerHTML = `Time Scale:
${this.humieFriendlyDuration(this.scale)}` + this.scaleLabel.innerHTML = `Time Scale:
${utils.ux.humieFriendlyDuration(this.scale)}` //Clear any previous rescale timer clearTimeout(this.rescaleTimer); @@ -403,64 +403,7 @@ class queuePanel extends panelObj{ this.autoscroll = false; //Unlight the indicator this.scrollLockButton.classList.remove('positive-button'); - } - - 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(', '); - } + } resizeRender(event){ const date = new Date(); @@ -490,7 +433,7 @@ class queuePanel extends panelObj{ } //Clear any marker timers - clearTimeout(this.timeMarkerTimer); + clearTimeout(this.renderIntervalTimer); //If we have an existing time marker if(this.timeMarker != null){ @@ -501,7 +444,6 @@ class queuePanel extends panelObj{ } async fullRender(date = new Date()){ - //Clear the queue this.clearQueue(); @@ -524,9 +466,12 @@ class queuePanel extends panelObj{ await utils.ux.awaitNextFrame(); } - //render the time marker + //render the time marker w/ force scroll this.renderTimeMarker(date, true); + //Kick off render interval + this.renderInterval(date); + //render out the queue this.renderQueue(date); } @@ -575,7 +520,7 @@ class queuePanel extends panelObj{ //Place item beginning at dawn entryDiv.style.top = `${this.offsetByDate(dawn)}px`; - //Run apply the rest of the styling rules + //Apply style rules for items that starrted yesterday entryDiv.classList.add('started-yesterday'); } @@ -620,14 +565,14 @@ class queuePanel extends panelObj{ `Title: ${entry[1].title}`, `File Name: ${entry[1].fileName}`, `Source: ${entry[1].type}`, - `Duration: ${entry[1].duration}`, + `Duration: ${utils.ux.humieFriendlyDuration(entry[1].duration)}`, `Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`, `End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}${entry[1].earlyEnd == null ? '' : ' (Ended Early)'}` ]){ //Create a 'p' node const component = document.createElement('p'); //Fill it with the string - component.textContent = string; + component.textContent = utils.unescapeEntities(string); //Append it to the tooltip div tooltipDiv.append(component); } @@ -690,6 +635,9 @@ class queuePanel extends panelObj{ //Append entry this.queueContainer.append(entryDiv); } + + //Render out any playing livestreams + this.renderLiveStream(date); } function clickEntry(event){ @@ -888,6 +836,18 @@ class queuePanel extends panelObj{ } } + renderInterval(date = new Date()){ + this.renderTimeMarker(date); + this.renderLiveStream(date); + + + //Clear update timer + clearTimeout(this.renderIntervalTimer); + + //Set timer to update marker every second + this.renderIntervalTimer = setTimeout(this.renderInterval.bind(this), 1000); + } + renderTimeMarker(date = new Date(), forceScroll = false){ //Calculate marker position by date const markerPosition = Math.round(this.offsetByDate(date)); @@ -895,7 +855,7 @@ class queuePanel extends panelObj{ //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; } //If we're not looking at today @@ -940,16 +900,6 @@ class queuePanel extends panelObj{ behavior: scrollBehavior }); } - - //Set the timer to run the function again - (smackTimer.bind(this))(); - - function smackTimer(){ - //Clear update timer - clearTimeout(this.timeMarkerTimer); - //Set timer to update marker every second - this.timeMarkerTimer = setTimeout(this.renderTimeMarker.bind(this), 1000); - } } renderQueueScale(inputDate = new Date()){ @@ -1015,6 +965,135 @@ class queuePanel extends panelObj{ } } + renderLiveStream(date = new Date()){ + //Grab all live queue entries + const staleEntry = this.queueContainer.querySelector('.queue-entry.live'); + + //If we're not livestreaming + if(client.player.mediaHandler.type != "livehls"){ + //If we have a stale entry + if(staleEntry != null){ + //Remove stale entry since we're no longer streaming + staleEntry.remove(); + } + + //Fuck off and die + return; + } + + //Grab currently playing media + const nowPlaying = client.player.mediaHandler.nowPlaying; + + //If we don't have a good stale entry to re-use + if(staleEntry == null || staleEntry.dataset.uuid != nowPlaying.uuid){ + //If it wasn't null but just old + if(staleEntry != null){ + //Nukem + staleEntry.remove(); + } + + //Create entry div + const entryDiv = document.createElement('div'); + entryDiv.classList.add('queue-entry', 'live'); + + //Iterate through nowPlaying's properties + for(let key of Object.keys(nowPlaying)){ + //Add them to the entry div's dataset + entryDiv.dataset[key] = nowPlaying[key]; + } + + //Convert start epoch to JS date object + const started = new Date(nowPlaying.startTime); + + //If this started today + if(utils.isSameDate(this.day, started)){ + //Set entryDiv top-border location based on start time + entryDiv.style.top = `${this.offsetByDate(started)}px` + }else{ + //Get dawn + const dawn = new Date(); + dawn.setHours(0,0,0,0) + + //Place item at the beginning of dawn + entryDiv.style.top = `${this.offsetByDate(dawn)}px`; + + //Apply rest of the styling rules for items that started yestarday + entryDiv.classList.add('started-yesterday') + } + + //Create entry title + const entryTitle = document.createElement('p'); + entryTitle.textContent = utils.unescapeEntities(nowPlaying.title); + + //Set entry div bottom-border location based on current time + entryDiv.style.bottom = `${this.offsetByDate(date, true)}px` + + //Assembly entryDiv + entryDiv.appendChild(entryTitle); + + //Setup media tooltip + entryDiv.addEventListener('mouseenter',(event)=>{ + //Construct tooltip on mouseover to re-calculate duration for live media + const tooltipDiv = buildTooltip(); + + //Display tooltip + utils.ux.displayTooltip(event, tooltipDiv, false, null, true, this.ownerDoc); + }); + + //Append entry div to queue container + this.queueContainer.appendChild(entryDiv); + }else{ + //Update existing entry + staleEntry.style.bottom = `${this.offsetByDate(date, true)}px` + } + + //Keep tooltip date seperate so it re-calculates live duration properly + function buildTooltip(date = new Date()){ + //Tooltip generation + //tooltip div + const tooltipDiv = document.createElement('div'); + tooltipDiv.classList.add('media-tooltip'); + + //tooltip components + const tooltipStrings = [ + `Title: ${nowPlaying.title}`, + `File Name: ${nowPlaying.fileName}`, + `Source: HLS Livestream (${nowPlaying.url})`, + `Duration: ${utils.ux.humieFriendlyDuration((date.getTime() - nowPlaying.startTime) / 1000)}`, + `Start Time: ${new Date(nowPlaying.startTime).toLocaleString()}`, + ]; + + //For each string in the tooltip + for(const string of tooltipStrings){ + + //Create a 'p' node + const component = document.createElement('p'); + //Fill it with the string + component.textContent = utils.unescapeEntities(string); + + //Append it to the tooltip div + tooltipDiv.append(component); + } + + //Create End Date + const source = document.createElement('p'); + const liveSpan = document.createElement('span'); + + //Fill end date label + source.textContent = "End Time: " + liveSpan.textContent = 'LIVE' + + //Set class + liveSpan.classList.add('critical-danger-text'); + + //Assemble end date + source.appendChild(liveSpan); + tooltipDiv.appendChild(source); + + return tooltipDiv; + } + } + offsetByDate(date = new Date(), bottomOffset = false){ //Pull start of day epoch from given date, make sure to use a new date object so we don't fuck up any date objects passed to us const dayEpoch = new Date(date).setHours(0,0,0,0); diff --git a/www/js/utils.js b/www/js/utils.js index 7685b17..b43c5a6 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -99,6 +99,63 @@ class canopyUXUtils{ return outString; } + 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}${Math.round(seconds)} Seconds`); + } + + //Join the time strings together + return timeStrings.join(', '); + } + //Update this and popup class to use nodes //and display multiple errors in one popup displayResponseError(body){