Compare commits

..

No commits in common. "f7db5ea31d64a633eaea980a82e09b671a75ebbc" and "c2bbf0b6f8283b4384f28d37fa38c7fad6021bcf" have entirely different histories.

14 changed files with 122 additions and 409 deletions

View file

@ -21,7 +21,6 @@ const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
//api account functions //api account functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){
try{ try{
//We pull this from the DB insteada activeChannels because we want to list channels that arent active after server restarts
const chanGuide = await channelModel.getChannelList(); const chanGuide = await channelModel.getChannelList();
res.status(200); res.status(200);

View file

@ -25,7 +25,8 @@ const {exceptionHandler, errorHandler} = require('../utils/loggerUtils');
//root index functions //root index functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){
try{ try{
return navRender.render(req, res, 'index', {unescape: validator.unescape}) const chanGuide = await channelModel.getChannelList();
return navRender.render(req, res, 'index', {chanGuide: chanGuide, unescape: validator.unescape})
}catch(err){ }catch(err){
return exceptionHandler(res, err); return exceptionHandler(res, err);
} }

View file

@ -66,7 +66,7 @@ router.post('/thumbnail', channelValidator.thumbnail(), thumbnailController.post
router.get('/description', descriptionController.get); router.get('/description', descriptionController.get);
router.post('/description', channelValidator.description(), descriptionController.post); router.post('/description', channelValidator.description(), descriptionController.post);
//list //list
router.get('/list', listController.get); router.get('/list', channelModel.reqPermCheck("manageChannel"), listController.get);
//settings //settings
router.get('/settings', channelModel.reqPermCheck("manageChannel"), settingsController.get); router.get('/settings', channelModel.reqPermCheck("manageChannel"), settingsController.get);
router.post('/settings', channelModel.reqPermCheck("changeSettings"), channelValidator.settingsMap(), settingsController.post); router.post('/settings', channelModel.reqPermCheck("changeSettings"), channelValidator.settingsMap(), settingsController.post);

View file

@ -284,10 +284,9 @@ channelSchema.statics.register = async function(channelObj, ownerObj){
/** /**
* Generates Network-Friendly Browser-Digestable list of channels * Generates Network-Friendly Browser-Digestable list of channels
* @param {Boolean} includeHidden - Whether or not to include hidden channels within the list * @param {Boolean} includeHidden - Whether or not to include hidden channels within the list
* @param {Date} endDate - Limits amount of scheduled items sent, defaults to 90 minutes
* @returns {Array} List of Network-Friendly Browser-Digestable Objects representing channels on the server * @returns {Array} List of Network-Friendly Browser-Digestable Objects representing channels on the server
*/ */
channelSchema.statics.getChannelList = async function(includeHidden = false, endDate = new Date(Date.now() + (1000 * 60 * 90))){ channelSchema.statics.getChannelList = async function(includeHidden = false){
const chanDB = await this.find({}); const chanDB = await this.find({});
var chanGuide = []; var chanGuide = [];
@ -295,32 +294,12 @@ channelSchema.statics.getChannelList = async function(includeHidden = false, end
chanDB.forEach((channel) => { chanDB.forEach((channel) => {
//For each channel, push an object with only the information we need to the channel guide //For each channel, push an object with only the information we need to the channel guide
if(!channel.settings.hidden || includeHidden){ if(!channel.settings.hidden || includeHidden){
//Create freeze-dried channel entry object (just add bong water) chanGuide.push({
const entry = {
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,
description: channel.description, description: channel.description,
thumbnail: channel.thumbnail, thumbnail: channel.thumbnail
schedule: [] });
}
//If we're currently playing something
if(channel.media.nowPlaying != null){
//Toss the currently playing item on
entry.schedule.push(channel.media.nowPlaying);
}
//For every piece of scehduled media for this channel
for(const media of channel.media.scheduled){
//If it starts before our cut-off
if(endDate.getTime() > media.startTime){
//Add that shit, son!
entry.schedule.push(media);
}
}
//add the channel entry to our return array
chanGuide.push(entry);
} }
}); });

View file

@ -18,14 +18,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<head> <head>
<%- include('partial/styles', {instance, user}); %> <%- include('partial/styles', {instance, user}); %>
<%- include('partial/csrfToken', {csrfToken}); %> <%- include('partial/csrfToken', {csrfToken}); %>
<link rel="stylesheet" type="text/css" href="css/index.css">
<title><%= instance %></title> <title><%= instance %></title>
</head> </head>
<body> <body>
<%- include('partial/navbar', {user}); %> <%- include('partial/navbar', {user}); %>
<%- include('partial/channelGuide'); %> <h3><a href="/newchannel">Start a new channel...</a></h3>
<div id="channel-guide-div" class="channel-guide">
<% chanGuide.forEach((channel) => { %>
<div id="channel-guide-entry-<%= unescape(channel.name) %>" class="channel-guide-entry">
<a href="/c/<%= unescape(channel.name) %>" class="channel-guide-entry channel-guide-entry-item"><img id="channel-guide-entry-img-<%= unescape(channel.name) %>" class="channel-guide-entry channel-guide-entry-item" src="<%= encodeURI(unescape(channel.thumbnail)) %>"></a>
<h3 id="channel-guide-entry-name-<%= unescape(channel.name) %>" class="channel-guide-entry channel-guide-entry-item"><a href="/c/<%= encodeURI(unescape(channel.name)) %>" class="channel-guide-entry channel-guide-entry-item"><%= unescape(channel.name) %></a></h3>
<span id="channel-guide-entry-description-span-<%= unescape(channel.name) %>" class="channel-guide-entry channel-guide-entry-item">
<p id="channel-guide-entry-description-<%= unescape(channel.name) %>" class="channel-guide-entry channel-guide-entry-item"><%= unescape(channel.description) %></h3>
</span>
</div>
<% }); %>
</div>
</body> </body>
<footer> <footer>
<%- include('partial/scripts', {user}); %> <%- include('partial/scripts', {user}); %>
<script src="/js/channelGuide.js"></script>
</footer> </footer>
</html> </html>

View file

@ -1,31 +0,0 @@
<%# Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2026 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/channelGuide.css">
<div id="channel-guide-div" class="channel-guide">
<div id="channel-guide-channel-list-container">
<div id="channel-guide-new-channel-button" class="positive-button channel-guide-channel-list-column">
<h3><a href="/newchannel">Create Channel</a></h3>
</div>
<div id="channel-guide-channel-list">
</div>
</div>
<div id="channel-guide-schedule-container">
<div id="channel-guide-schedule-index">
</div>
<div id="channel-guide-scheduled-media">
</div>
</div>
</div>

View file

@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
const linkObj = links[link]; const linkObj = links[link];
if(!linkObj.registeredOnly || user != null){ %> if(!linkObj.registeredOnly || user != null){ %>
- -
<a target="_blank" class="navbar-item" href="<%- links[link].link %>"><%= link %></a> <a target="_blank" class="navbar-item" href="<%- links[link] %>"><%= link %></a>
<% } <% }
} %> } %>
</p> </p>

View file

@ -1,86 +0,0 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2026 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/>.*/
.channel-guide-channel-list-column{
width: 11em;
text-align: center;
}
#channel-guide-div{
display: flex;
}
#channel-guide-schedule-container {
flex: 1;
width: calc(100% - 11em);
}
#channel-guide-schedule-index {
display: flex;
justify-content: space-between;
}
.channel-guide-timeslot-label {
width: 100%;
text-align: center;
padding: 0;
margin: 0.3em 0;
}
#channel-guide-new-channel-button h3{
padding: 0.3em 0;
margin: 0;
}
.channel-guide-schedule-row{
height: 3.4em;
}
.channel-guide-channel-list-entry {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-repeat: no-repeat;
background-size: cover;
background-position-y: center;
}
.channel-guide-channel-list-entry h3 {
margin: 0;
z-index: 1;
}
.channel-guide-schedule-track {
display: flex;
position: relative;
}
.channel-guide-schedule-entry {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: absolute;
}
.channel-guide-schedule-entry h3 {
margin: 0;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}

66
www/css/index.css Normal file
View file

@ -0,0 +1,66 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2026 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/>.*/
@media (orientation: landscape){
#channel-guide-div{
max-width: 100vh;
}
}
@media (orientation: portrait){
#channel-guide-div{
max-width: 80vw;
}
}
#channel-guide-div{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11em, 1fr));
margin: 0 auto 0 auto;
}
div.channel-guide-entry{
overflow: hidden;
width: 10em;
margin: 0.5em;
}
.channel-guide-entry{
margin: 0.5em auto 0.5em auto;
padding: 0.2em;
display: flex;
flex-direction: column;
}
.channel-guide-entry-item{
margin: 0.1em auto 0.1em auto;
}
.channel-guide-entry-item img{
max-width: 7em;
max-height: 7em;
}
span.channel-guide-entry-item{
overflow-y: scroll;
height: 2em;
scrollbar-width: none;
}
p.channel-guide-entry{
font-size: 0.8em;
margin-left: 0;
}

View file

@ -112,8 +112,6 @@ div.queue-entry.live{
div.queue-entry p{ div.queue-entry p{
z-index: 2; z-index: 2;
pointer-events: none; pointer-events: none;
overflow: hidden;
text-overflow: ellipsis;
} }
div.dragging-queue-entry{ div.dragging-queue-entry{

View file

@ -324,17 +324,27 @@ input.control-prompt, input.control-prompt:focus{
background-color: var(--bg1-alt0); background-color: var(--bg1-alt0);
} }
/* chan guide */ /* index */
.channel-guide-channel-list-entry { .channel-guide-entry{
background-color: var(--bg2); background-color: var(--bg1);
color: var(--accent1);
} }
.channel-guide-channel-list-entry::before { div.channel-guide-entry{
content: ''; border-radius: 0.3em;
background: var(--media-header-gradient); box-shadow: 0.2em 0.2em 0.1em var(--bg1-alt0) inset;
position: absolute; }
width: 100%;
height: 100%; span.channel-guide-entry-item{
background-color: var(--bg1-alt0);
box-shadow: 0.2em 0.2em 0.1em black inset;
border-radius: 0.3em;
margin: 0 0.1em 0 0.1em;
}
p.channel-guide-entry-item{
background-color: var(--bg1-alt0);
} }
/* channel settings */ /* channel settings */

View file

@ -364,47 +364,27 @@ input.control-prompt, input.control-prompt:focus{
display: none; display: none;
} }
/* chan guide */ /* index */
.channel-guide-channel-list-entry { .channel-guide-entry{
background-color: var(--bg2-alt1);
}
.channel-guide-channel-list-entry::before {
content: '';
background: var(--media-header-gradient);
position: absolute;
width: 100%;
height: 100%;
}
#channel-guide-schedule-index {
border-bottom: 1px solid var(--accent0);
}
/*EWWIE WHY DOES CSS COUNTING START AT ONE?!?!*/
.channel-guide-timeslot-label:not(:nth-child(1)){
border-left: 1px solid var(--accent0);
}
#channel-guide-new-channel-button h3{
border-right: 1px solid var(--accent0);
}
#channel-guide-new-channel-button{
border-bottom: 1px solid var(--accent0);
}
.channel-guide-schedule-entry{
padding: 0 0.7em;
background-color: var(--bg1); background-color: var(--bg1);
border-left: 1px solid var(--accent0); color: var(--accent1);
} }
.channel-guide-schedule-entry.starts-before{ div.channel-guide-entry{
border-left: none; border-radius: 0.3em;
box-shadow: 0.2em 0.2em 0.1em var(--bg1-alt0) inset;
} }
.channel-guide-schedule-entry.last-ends-before{ span.channel-guide-entry-item{
border-right: 1px solid var(--accent0); background-color: var(--bg1-alt0);
box-shadow: 0.2em 0.2em 0.1em black inset;
border-radius: 0.3em;
margin: 0 0.1em 0 0.1em;
}
p.channel-guide-entry-item{
background-color: var(--bg1-alt0);
} }
/* channel settings */ /* channel settings */

View file

@ -1,202 +0,0 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2026 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/>.*/
class ChannelGuide{
constructor(){
//define property to hold refresh timer
this.refreshTimer;
//define property to hold schedule cache
this.cache;
//Grab DOM Nodes
this.scheduleIndex = document.querySelector('#channel-guide-schedule-index');
this.channelList = document.querySelector('#channel-guide-channel-list');
this.scheduledMediaDiv = document.querySelector('#channel-guide-scheduled-media');
//Refresh the schedule
this.refreshList();
}
async refreshList(now = new Date()){
//Freshen up the local schedule cache
this.cache = await utils.ajax.getChannels();
//render the freshly pulled schedule data to the page
this.renderList(this.cache, now);
}
renderList(schedule, now = new Date()){
this.renderIndex(now);
this.renderChannelList(schedule);
this.renderSchedule(schedule, now);
//if we've got a job to refresh the list
if(this.refreshTimer != null){
//cancel it
clearTimeout(this.refreshTimer);
}
//Set timer to refresh the thing erry minute on the minute
setTimeout(this.refreshList.bind(this), 1000 * (60 - now.getSeconds()));
}
renderIndex(now = new Date()){
//Clear schedule index
this.scheduleIndex.innerHTML = '';
//DO EM 3 TEIMS!
for(let slot = 0; slot < 3; slot++){
//Clone passed date object to prevent over-writing
const setDate = new Date(now.getTime());
//Up timeslot by 30 minutes for each timeslot in the loop
setDate.setMinutes(setDate.getMinutes() + (slot * 30));
//Generate timeslot label
const timeslotLabel = document.createElement('h3');
timeslotLabel.classList.add('channel-guide-timeslot-label');
timeslotLabel.textContent = utils.ux.timeStringFromDate(setDate, false);
//Append timeslot label to schedule index
this.scheduleIndex.appendChild(timeslotLabel);
}
}
renderChannelList(schedule){
//Clean out channel list
this.channelList.innerHTML = '';
for(const channel of schedule){
//Create entry for channel
const entry = document.createElement('div');
entry.classList.add('channel-guide-channel-list-entry', 'channel-guide-schedule-row');
entry.style.backgroundImage = `Url("${channel.thumbnail}")`;
//Throw together a channel label
const entryLabel = document.createElement('h3');
entryLabel.textContent = utils.unescapeEntities(channel.name);
//Some assembly required
entry.appendChild(entryLabel);
//Add channel entry to the list
this.channelList.appendChild(entry);
}
}
renderSchedule(schedule, now = new Date()){
//Clear out the schedule
this.scheduledMediaDiv.innerHTML = '';
schedule = this.fillDeadAir(schedule, now);
//Iterate through all of the received channels
for(const channel of schedule){
//Create a div to hold all of the current channels entries
const channelTrack = document.createElement('div');
channelTrack.classList.add('channel-guide-schedule-track', 'channel-guide-schedule-row');
//For every scheduled video
for(const entryIndex in channel.schedule){
//Get entry by index
const entry = channel.schedule[entryIndex];
//Check if this is the last item
const last = entryIndex == channel.schedule.length - 1;
//slot entry into schedule track
channelTrack.appendChild(this.renderScheduleItem(entry, now, last));
}
//Inject channel track into schedule div
this.scheduledMediaDiv.appendChild(channelTrack);
}
}
fillDeadAir(schedule){
//For each video in the schedule
for(const video of schedule){
}
//Return processed schedule
return schedule;
}
renderScheduleItem(item, now = new Date(), last = false){
//Create div for current schedule entry
const entryDiv = document.createElement('div');
entryDiv.classList.add('channel-guide-schedule-entry', 'channel-guide-schedule-row');
//Throw down an entry label
const entryLabel = document.createElement('h3');
entryLabel.textContent = utils.unescapeEntities(item.title);
//calculate end epoch
const end = item.startTime + (item.duration * 1000);
//Place entryDiv by scheduled time
entryDiv.style.left = `${this.offsetByEpoch(item.startTime, now)}px`;
entryDiv.style.right = `${this.offsetByEpoch(end, now, true)}px`;
//if this started before the schedule time range
if(item.startTime < now.getTime()){
//mark it for css
entryDiv.classList.add('starts-before');
}
//if this is the last item and it ends before the schedule time range
if(last && end < now.getTime() + 5400000){
//mark it for css
entryDiv.classList.add('last-ends-before');
}
//Assemble the entry
entryDiv.appendChild(entryLabel);
//Return assembled entry div
return entryDiv;
}
offsetByEpoch(scheduleEpoch, now = new Date(), rightOffset = false){
//Calculate how many milliseconds after the current time our scheduled item is
const ms = scheduleEpoch - now.getTime();
//Calculate and return offset by milliseconds
return this.offsetByMilliseconds(ms, rightOffset, rightOffset);
}
offsetByMilliseconds(ms, rightOffset = false){
//Calculate place in schedule as float between 0 and 1 by deviding input by schedules range (90 minutes)
//Making sure to reverse value if rightOffset is thrown
const timeFloat = rightOffset ? 1 - ms / 5400000 : ms / 5400000;
//Get maximum position relative to the range itself
const offsetMax = this.scheduledMediaDiv.getBoundingClientRect().width;
//Calculate offset by timefloat and offset max
const offset = (offsetMax * timeFloat) < 0 ? 0 : (offsetMax * timeFloat);
//return position relative to parent, adding channel list width to left side to handle offset
return offset;
}
}
const channelGuide = new ChannelGuide();

View file

@ -1012,18 +1012,6 @@ class canopyAjaxUtils{
} }
//Channel //Channel
async getChannels(){
var response = await fetch(`/api/channel/list`,{
method: "GET"
});
if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async newChannel(name, description, thumbnail, verification){ async newChannel(name, description, thumbnail, verification){
var response = await fetch(`/api/channel/register`,{ var response = await fetch(`/api/channel/register`,{
method: "POST", method: "POST",