diff --git a/src/controllers/api/channel/listController.js b/src/controllers/api/channel/listController.js index 378df55..e55ea61 100644 --- a/src/controllers/api/channel/listController.js +++ b/src/controllers/api/channel/listController.js @@ -21,6 +21,7 @@ const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); //api account functions module.exports.get = async function(req, res){ 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(); res.status(200); diff --git a/src/controllers/indexController.js b/src/controllers/indexController.js index 537656f..7c96e82 100644 --- a/src/controllers/indexController.js +++ b/src/controllers/indexController.js @@ -25,8 +25,7 @@ const {exceptionHandler, errorHandler} = require('../utils/loggerUtils'); //root index functions module.exports.get = async function(req, res){ try{ - const chanGuide = await channelModel.getChannelList(); - return navRender.render(req, res, 'index', {chanGuide: chanGuide, unescape: validator.unescape}) + return navRender.render(req, res, 'index', {unescape: validator.unescape}) }catch(err){ return exceptionHandler(res, err); } diff --git a/src/routers/api/channelRouter.js b/src/routers/api/channelRouter.js index 0e1a1ed..41290ed 100644 --- a/src/routers/api/channelRouter.js +++ b/src/routers/api/channelRouter.js @@ -66,7 +66,7 @@ router.post('/thumbnail', channelValidator.thumbnail(), thumbnailController.post router.get('/description', descriptionController.get); router.post('/description', channelValidator.description(), descriptionController.post); //list -router.get('/list', channelModel.reqPermCheck("manageChannel"), listController.get); +router.get('/list', listController.get); //settings router.get('/settings', channelModel.reqPermCheck("manageChannel"), settingsController.get); router.post('/settings', channelModel.reqPermCheck("changeSettings"), channelValidator.settingsMap(), settingsController.post); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index bd02fe0..3976870 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -284,9 +284,10 @@ channelSchema.statics.register = async function(channelObj, ownerObj){ /** * Generates Network-Friendly Browser-Digestable list of channels * @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 */ -channelSchema.statics.getChannelList = async function(includeHidden = false){ +channelSchema.statics.getChannelList = async function(includeHidden = false, endDate = new Date(Date.now() + (1000 * 60 * 90))){ const chanDB = await this.find({}); var chanGuide = []; @@ -294,12 +295,32 @@ channelSchema.statics.getChannelList = async function(includeHidden = false){ chanDB.forEach((channel) => { //For each channel, push an object with only the information we need to the channel guide if(!channel.settings.hidden || includeHidden){ - chanGuide.push({ + //Create freeze-dried channel entry object (just add bong water) + const entry = { id: channel.id, name: channel.name, 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); } }); diff --git a/src/views/index.ejs b/src/views/index.ejs index b2932b1..637b545 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -18,25 +18,14 @@ along with this program. If not, see . %> <%- include('partial/styles', {instance, user}); %> <%- include('partial/csrfToken', {csrfToken}); %> - <%= instance %> <%- include('partial/navbar', {user}); %> -

Start a new channel...

-
- <% chanGuide.forEach((channel) => { %> -
- -

<%= unescape(channel.name) %>

- -

<%= unescape(channel.description) %> - -

- <% }); %> -
+ <%- include('partial/channelGuide'); %> diff --git a/src/views/partial/channelGuide.ejs b/src/views/partial/channelGuide.ejs new file mode 100644 index 0000000..2786c9c --- /dev/null +++ b/src/views/partial/channelGuide.ejs @@ -0,0 +1,31 @@ +<%# 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 . %> + +
+
+ +
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/views/partial/navbar.ejs b/src/views/partial/navbar.ejs index 21fabcf..3c9a070 100644 --- a/src/views/partial/navbar.ejs +++ b/src/views/partial/navbar.ejs @@ -24,7 +24,7 @@ along with this program. If not, see . %> const linkObj = links[link]; if(!linkObj.registeredOnly || user != null){ %> - - <%= link %> + <%= link %> <% } } %>

diff --git a/www/css/channelGuide.css b/www/css/channelGuide.css new file mode 100644 index 0000000..f30075c --- /dev/null +++ b/www/css/channelGuide.css @@ -0,0 +1,86 @@ +/*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 .*/ + +.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; +} \ No newline at end of file diff --git a/www/css/index.css b/www/css/index.css deleted file mode 100644 index 6512046..0000000 --- a/www/css/index.css +++ /dev/null @@ -1,66 +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 .*/ - -@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; -} \ No newline at end of file diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index d0e1597..8a8410c 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -112,6 +112,8 @@ div.queue-entry.live{ div.queue-entry p{ z-index: 2; pointer-events: none; + overflow: hidden; + text-overflow: ellipsis; } div.dragging-queue-entry{ diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 2f7b2df..4ad6774 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -324,27 +324,17 @@ input.control-prompt, input.control-prompt:focus{ background-color: var(--bg1-alt0); } -/* index */ -.channel-guide-entry{ - background-color: var(--bg1); - color: var(--accent1); +/* chan guide */ +.channel-guide-channel-list-entry { + background-color: var(--bg2); } -div.channel-guide-entry{ - border-radius: 0.3em; - box-shadow: 0.2em 0.2em 0.1em var(--bg1-alt0) inset; -} - -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-guide-channel-list-entry::before { + content: ''; + background: var(--media-header-gradient); + position: absolute; + width: 100%; + height: 100%; } /* channel settings */ diff --git a/www/css/theme/the-network-classic.css b/www/css/theme/the-network-classic.css index 6a16288..2ab84d6 100644 --- a/www/css/theme/the-network-classic.css +++ b/www/css/theme/the-network-classic.css @@ -364,27 +364,47 @@ input.control-prompt, input.control-prompt:focus{ display: none; } -/* index */ -.channel-guide-entry{ +/* chan guide */ +.channel-guide-channel-list-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); - color: var(--accent1); + border-left: 1px solid var(--accent0); } -div.channel-guide-entry{ - border-radius: 0.3em; - box-shadow: 0.2em 0.2em 0.1em var(--bg1-alt0) inset; +.channel-guide-schedule-entry.starts-before{ + border-left: none; } -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-guide-schedule-entry.last-ends-before{ + border-right: 1px solid var(--accent0); } /* channel settings */ diff --git a/www/js/channelGuide.js b/www/js/channelGuide.js new file mode 100644 index 0000000..cc9039e --- /dev/null +++ b/www/js/channelGuide.js @@ -0,0 +1,202 @@ +/*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 .*/ + +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(); \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index 4661e3a..1d042a8 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -1012,6 +1012,18 @@ class canopyAjaxUtils{ } //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){ var response = await fetch(`/api/channel/register`,{ method: "POST",