diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 431c10a..a7248c9 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -136,8 +136,11 @@ module.exports = class{ } }); + //Get schedule as a temporary array + const queue = Array.from(this.channel.queue.schedule); + //Send off the metadata to our user's clients - this.emit("clientMetadata", {user: userObj, flairList}); + this.emit("clientMetadata", {user: userObj, flairList, queue}); } async sendSiteEmotes(){ diff --git a/src/app/channel/media/media.js b/src/app/channel/media/media.js index ddf1f3d..97dfeee 100644 --- a/src/app/channel/media/media.js +++ b/src/app/channel/media/media.js @@ -18,9 +18,10 @@ along with this program. If not, see .*/ const crypto = require('node:crypto'); module.exports = class{ - constructor(title, fileName, id, type, duration){ + constructor(title, fileName, url, id, type, duration){ this.title = title; this.fileName = fileName + this.url = url; this.id = id; this.type = type; this.duration = duration; diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 27ebf38..107119d 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -156,6 +156,9 @@ module.exports = class{ //Replace the existing schedule map with our new one this.schedule = newSchedule; + + //Broadcast the channel queue + this.broadcastQueue(); } start(mediaObj){ @@ -285,4 +288,8 @@ module.exports = class{ } } + + broadcastQueue(){ + this.server.io.in(this.channel.name).emit('queue',{queue: Array.from(this.schedule)}) + } } \ No newline at end of file diff --git a/src/app/channel/media/queuedMedia.js b/src/app/channel/media/queuedMedia.js index 4dc015d..71525af 100644 --- a/src/app/channel/media/queuedMedia.js +++ b/src/app/channel/media/queuedMedia.js @@ -18,9 +18,9 @@ along with this program. If not, see .*/ const media = require('./media'); module.exports = class extends media{ - constructor(title, fileName, id, type, duration, startTime){ + constructor(title, fileName, url, id, type, duration, startTime){ //Call derived constructor - super(title, fileName, id, type, duration); + super(title, fileName, url, id, type, duration); //Set media start time this.startTime = startTime; @@ -32,7 +32,7 @@ module.exports = class extends media{ //statics static fromMedia(media, startTime){ //Create and return queuedMedia object from given media object and arguments - return new this(media.title, media.fileName, media.id, media.type, media.duration, startTime); + return new this(media.title, media.fileName, media.url, media.id, media.type, media.duration, startTime); } //methods diff --git a/src/controllers/panel/queueController.js b/src/controllers/panel/queueController.js new file mode 100644 index 0000000..295ff4c --- /dev/null +++ b/src/controllers/panel/queueController.js @@ -0,0 +1,20 @@ +/*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 .*/ + +//root index functions +module.exports.get = async function(req, res){ + res.render('partial/panels/queue', {}); +} \ No newline at end of file diff --git a/src/routers/panelRouter.js b/src/routers/panelRouter.js index 908cdbd..0efba65 100644 --- a/src/routers/panelRouter.js +++ b/src/routers/panelRouter.js @@ -23,6 +23,7 @@ const placeholderController = require("../controllers/panel/placeholderControlle const emoteController = require("../controllers/panel/emoteController"); const popoutContainerController = require("../controllers/panel/popoutContainerController"); const profileController = require("../controllers/panel/profileController"); +const queueController = require("../controllers/panel/queueController"); //Validators const accountValidator = require("../validators/accountValidator"); @@ -34,5 +35,6 @@ router.get('/placeholder', placeholderController.get); router.get('/emote', emoteController.get); router.get('/popoutContainer', popoutContainerController.get); router.get('/profile', accountValidator.user(), profileController.get); +router.get('/queue', queueController.get); module.exports = router; diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 790b5f2..638e0fa 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -40,7 +40,7 @@ module.exports.yankMedia = async function(url){ const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`; //Create new media object from file info - mediaList.push(new media(name, name, link, 'ia', Number(file.length))); + mediaList.push(new media(name, name, link, link, 'ia', Number(file.length))); } //return media object list diff --git a/src/views/channel.ejs b/src/views/channel.ejs index 4f89406..f923223 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -32,17 +32,25 @@ along with this program. If not, see . %>
<%- include('partial/scripts', {user}); %> + <%# 3rd party code %> + <%# 1st party code %> + <%# admin gunk %> + <%# command/chat processing %> + <%# client children %> + <%# panels %> + + <%# main client %>
\ No newline at end of file diff --git a/src/views/partial/panels/queue.ejs b/src/views/partial/panels/queue.ejs new file mode 100644 index 0000000..08043fe --- /dev/null +++ b/src/views/partial/panels/queue.ejs @@ -0,0 +1,30 @@ +<%# 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 . %> + +
+ +
+ +
+
+ <%# Probably not the cleanest way to do this but fuggit %> + + +
+
+
+
+
\ No newline at end of file diff --git a/src/views/popoutContainer.ejs b/src/views/popoutContainer.ejs index 68140e4..a56a51b 100644 --- a/src/views/popoutContainer.ejs +++ b/src/views/popoutContainer.ejs @@ -20,7 +20,7 @@ along with this program. If not, see . %> <%= instance %> - NULL_POPOUT - +
diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css new file mode 100644 index 0000000..d9d370f --- /dev/null +++ b/www/css/panel/queue.css @@ -0,0 +1,47 @@ +/*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 .*/ +#queue{ + position: relative; + display: grid; + grid-template-columns: auto 75%; +} + + +div.queue-marker{ + display: flex; + flex-direction: row; + align-items: center; + grid-column: 1; + z-index: 2; +} + +span.queue-marker{ + height: 1px; + min-width: 5px; + flex: 1; +} + +div.queue-entry{ + overflow: hidden; + margin: 1.5em 0.5em; + grid-column: 2; +} + +#time-marker{ + position: absolute; + height: 1px; + width: 100%; +} \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index d9eb117..bb32e30 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -55,6 +55,8 @@ along with this program. If not, see .*/ --userlist-color5:rgb(150, 64, 6); --userlist-color6:rgb(111, 61, 204); + --userlist-contrast-glow: 2px 2px 3px var(--bg0), 2px -2px 3px var(--bg0), -2px 2px 3px var(--bg0), -2px -2px 3px var(--bg0); + --media-header-gradient: linear-gradient(180deg, var(--bg1-alt0) 0%, #FFFFFF00 76%); --background-panel-effect-pretty: blur(4px); @@ -75,6 +77,8 @@ body{ background-color: var(--bg0); font-family: var(--main-font); color: var(--accent0); + background-image: url('/img/background.png'); + background-size: 5em; } a{ @@ -291,6 +295,15 @@ p.channel-guide-entry-item{ #chat-area{ background-color: var(--bg2); } +#chat-panel-head-div{ + background-color: var(--bg0); +} + +#chat-panel-head-spacer-span, #chat-panel-users-div{ + background-color: var(--bg0); + background-image: url("/img/background.png"); + background-size: 2.3em +} #chat-panel-prompt-autocomplete{ color: var(--accent0-alt1); @@ -308,37 +321,34 @@ p.channel-guide-entry-item{ .userlist-color0{/*green0*/ color: var(--userlist-color0); - text-shadow: none; } .userlist-color1{/*red0*/ color: var(--userlist-color1); - text-shadow: none; } .userlist-color2{/*blue0*/ color: var(--userlist-color2); - text-shadow: none; } .userlist-color3{/*tan0*/ color: var(--userlist-color3); - text-shadow: none; } .userlist-color4{/*pink0*/ color: var(--userlist-color4); - text-shadow: none; } .userlist-color5{/*orange*/ color: var(--userlist-color5); - text-shadow: none; } .userlist-color6{/*violet*/ color: var(--userlist-color6); - text-shadow: none; +} + +[class*="userlist-color"].chat-panel-users{ + text-shadow: var(--userlist-contrast-glow); } .high-level{ @@ -417,6 +427,10 @@ div.tooltip{ } /* panel */ +.cpanel-body{ + background-image: none; +} + .title-filler-span{ background-color: var(--accent0); } @@ -444,6 +458,26 @@ span.emote-list-trash-icon{ border: 1px solid var(--accent0) } +span.queue-marker{ + background-color: var(--accent0); +} + +#time-marker{ + background-color: var(--danger0); +} + +div.queue-entry{ + margin: 0.2em; + padding: 0 0.7em; + border-radius: 1em; + background-color: var(--bg1); + border: 1px solid var(--accent1); +} + +.queue-entry a{ + color: var(--accent1); +} + /* altcha theming*/ div.altcha{ box-shadow: 4px 4px 1px var(--bg1-alt0) inset; diff --git a/www/img/background.png b/www/img/background.png new file mode 100644 index 0000000..35c9115 Binary files /dev/null and b/www/img/background.png differ diff --git a/www/img/background_ikd.png b/www/img/background_ikd.png new file mode 100644 index 0000000..58cb8e2 Binary files /dev/null and b/www/img/background_ikd.png differ diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index cf23311..87a4ad8 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -55,6 +55,10 @@ class channel{ this.socket.on("clientMetadata", this.handleClientInfo.bind(this)); this.socket.on("error", console.log); + + this.socket.on("queue", (data) => { + this.queue = data.queue; + }) } handleClientInfo(data){ @@ -68,6 +72,9 @@ class channel{ //Tell the chatbox to handle client info //should it have its own event listener instead? Guess it's a stylistic choice :P this.chatBox.handleClientInfo(data); + + //Store queue for use by the queue panel + this.queue = data.queue; } } diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js new file mode 100644 index 0000000..e73133b --- /dev/null +++ b/www/js/channel/panels/queuePanel.js @@ -0,0 +1,169 @@ +class queuePanel extends panelObj{ + constructor(client, panelDocument){ + super(client, "Media Queue", "/panel/queue", panelDocument); + + //Store releative scale of items in seconds, defaulting to 30 minute chunks + this.scale = 30 * 60; + } + + docSwitch(){ + //Get queue container + this.queueContainer = this.panelDocument.querySelector('#queue'); + + //If we have an existing time marker + if(this.timeMarker != null){ + //Clear it out + this.timeMarker.remove(); + this.timeMarker = null; + } + + //Render out the queue + this.renderQueue(); + } + + closer(){ + //Clear any remaining timers + clearTimeout(this.renderTimeMarker); + } + + renderQueue(date = new Date()){ + //Clear out queue container + this.queueContainer.innerHTML = ''; + + //Render out time scale + this.renderQueueScale(date); + + for(let entry of this.client.queue){ + //Create entry div + const entryDiv = document.createElement('div'); + entryDiv.classList.add('queue-entry'); + + //Create entry title + const entryTitle = document.createElement('a'); + entryTitle.textContent = entry[1].title; + entryTitle.href = entry[1].url; + + //Append entry elements + entryDiv.append(entryTitle); + + //Append entry + this.queueContainer.append(entryDiv); + } + } + + 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'); + + //If the marker is null for some reason + if(markers[0] == 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 + this.timeMarker = document.createElement('span'); + //Add time marker class + this.timeMarker.id = 'time-marker'; + //Append time marker + this.queueContainer.appendChild(this.timeMarker); + } + + //Set time marker position + this.timeMarker.style.top = `${markerPosition}px`; + + (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()){ + //Make sure we don't modify the date we're handed + const date = structuredClone(inputDate); + + //Zero out date to midnight + date.setHours(0,0,0,0); + + //Store epoch of current date at midnight for later user + const dateEpoch = date.getTime(); + + //while we've counted less than the amount of time in the day, count up by scale + for(let time = 0; time <= 86400; time += this.scale){ + //Get index of current chunk by dividing time by scale + const index = time / this.scale; + + //Set time by scale, we could do this against this.scale and date.getTime(), but this seemed cleaner :P + date.setTime(dateEpoch + (time * 1000)) + + //Create marker span + const markerSpan = document.createElement('div'); + markerSpan.classList.add('queue-marker'); + + //Create marker line (unfortunately
tags don't seem to play nice with parents who have display:flex) + const marker = document.createElement('span'); + marker.classList.add('queue-marker'); + + //If it's even/zero + if(index % 2 == 0){ + const markerLabel = document.createElement('p'); + //If scale is over a minute then we don't need to display seconds + const seconds = this.scale > 60 ? '' : `:${('0' + date.getSeconds()).slice(-2)}` + + //If we're counting AM + if(date.getHours() < 12){ + //Display as AM + markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM` + //If we're cointing noon + }else if(date.getHours() == 12){ + //display as noon + markerLabel.textContent = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` + //if we're counting pm + }else{ + //display as pm + markerLabel.textContent = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM` + } + + //Add marker label to marker span + markerSpan.appendChild(markerLabel); + } + + //Append marker to marker span + markerSpan.appendChild(marker); + + //Append marker span to queue container + this.queueContainer.appendChild(markerSpan); + } + + //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); + }); + }); + + } +} \ No newline at end of file