Started work on queue panel
This commit is contained in:
parent
4f6b3318a0
commit
42c20455e5
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
}
|
||||
}
|
||||
|
|
@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
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
|
||||
|
|
|
|||
20
src/controllers/panel/queueController.js
Normal file
20
src/controllers/panel/queueController.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//root index functions
|
||||
module.exports.get = async function(req, res){
|
||||
res.render('partial/panels/queue', {});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,17 +32,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
|||
</body>
|
||||
<footer>
|
||||
<%- include('partial/scripts', {user}); %>
|
||||
<%# 3rd party code %>
|
||||
<script src="/socket.io/socket.io.min.js"></script>
|
||||
<%# 1st party code %>
|
||||
<%# admin gunk %>
|
||||
<script src="/js/adminUtils.js"></script>
|
||||
<script src="/js/popup/banPopup.js"></script>
|
||||
<%# command/chat processing %>
|
||||
<script src="/js/channel/commandPreprocessor.js"></script>
|
||||
<script src="/js/channel/chatPostprocessor.js"></script>
|
||||
<%# client children %>
|
||||
<script src="/js/channel/chat.js"></script>
|
||||
<script src="/js/channel/userlist.js"></script>
|
||||
<script src="/js/channel/mediaHandler.js"></script>
|
||||
<script src="/js/channel/player.js"></script>
|
||||
<script src="/js/channel/cpanel.js"></script>
|
||||
<%# panels %>
|
||||
<script src="/js/channel/panels/emotePanel.js"></script>
|
||||
<script src="/js/channel/panels/queuePanel.js"></script>
|
||||
<%# main client %>
|
||||
<script src="/js/channel/channel.js"></script>
|
||||
</footer>
|
||||
</html>
|
||||
30
src/views/partial/panels/queue.ejs
Normal file
30
src/views/partial/panels/queue.ejs
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>. %>
|
||||
<link rel="stylesheet" type="text/css" href="/css/panel/queue.css">
|
||||
<div id="queue-controls">
|
||||
<span id="queue-media-prompts">
|
||||
<div class="panel-control-prompt control-prompt">
|
||||
<input placeholder="Media Link..." id="media-link-input" class="control-prompt">
|
||||
</div>
|
||||
<div class="panel-control-prompt control-prompt">
|
||||
<%# Probably not the cleanest way to do this but fuggit %>
|
||||
<input placeholder="Media Name..." id="media-name-input" class="control-prompt">
|
||||
<button id="queue-last-button" class="positive-button">Queue Last</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div id="queue">
|
||||
</div>
|
||||
|
|
@ -20,7 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
|||
<link rel="stylesheet" type="text/css" href="/css/panel.css">
|
||||
<title><%= instance %> - NULL_POPOUT</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="cpanel-body">
|
||||
<div class="cpanel-div" id="cpanel-div">
|
||||
<div class="cpanel-header-div" id="cpanel-header-div">
|
||||
<i class="cpanel cpanel-header-icon cpanel-pin-icon bi-pin-angle-fill" id="cpanel-pin-icon"></i>
|
||||
|
|
|
|||
47
www/css/panel/queue.css
Normal file
47
www/css/panel/queue.css
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
#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%;
|
||||
}
|
||||
|
|
@ -55,6 +55,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
--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;
|
||||
|
|
|
|||
BIN
www/img/background.png
Normal file
BIN
www/img/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
BIN
www/img/background_ikd.png
Normal file
BIN
www/img/background_ikd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
169
www/js/channel/panels/queuePanel.js
Normal file
169
www/js/channel/panels/queuePanel.js
Normal file
|
|
@ -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 <hr> 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);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue