From 340dfdf4ace5ebc45000cb95b4a65cf12e7cc532 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 8 Nov 2026 23:26:18 -0500 Subject: [PATCH 1/7] Fixed bug with navbar links --- src/views/partial/navbar.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %> <% } } %>

From 0291904534814d3f4d0a2b8ab4f1fbc3a823aa39 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 9 Nov 2026 02:44:06 -0500 Subject: [PATCH 2/7] Updated public channel list API endpoint to provide next 90 minutes of scheduled media --- src/controllers/api/channel/listController.js | 1 + src/routers/api/channelRouter.js | 2 +- src/schemas/channel/channelSchema.js | 29 ++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) 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/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); } }); From c3483627d8235022d6c13b5225b04c201a22d8ef Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 9 Nov 2026 04:05:55 -0500 Subject: [PATCH 3/7] Started work on improved channel guide rendering --- src/controllers/indexController.js | 3 +- src/views/index.ejs | 15 +----- src/views/partial/channelGuide.ejs | 29 +++++++++++ www/css/{index.css => channelGuide.css} | 64 ++++++++++--------------- www/js/channelGuide.js | 56 ++++++++++++++++++++++ www/js/utils.js | 12 +++++ 6 files changed, 125 insertions(+), 54 deletions(-) create mode 100644 src/views/partial/channelGuide.ejs rename www/css/{index.css => channelGuide.css} (51%) create mode 100644 www/js/channelGuide.js 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/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'); %>
<%- include('partial/scripts', {user}); %> +
diff --git a/src/views/partial/channelGuide.ejs b/src/views/partial/channelGuide.ejs new file mode 100644 index 0000000..834c105 --- /dev/null +++ b/src/views/partial/channelGuide.ejs @@ -0,0 +1,29 @@ +<%# 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/www/css/index.css b/www/css/channelGuide.css similarity index 51% rename from www/css/index.css rename to www/css/channelGuide.css index 6512046..85140ee 100644 --- a/www/css/index.css +++ b/www/css/channelGuide.css @@ -14,53 +14,39 @@ 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-schedule-container { + flex: 1; } -.channel-guide-entry-item img{ - max-width: 7em; - max-height: 7em; + +#channel-guide-schedule-index { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--accent0); } -span.channel-guide-entry-item{ - overflow-y: scroll; - height: 2em; - scrollbar-width: none; +.channel-guide-timeslot-label { + width: 100%; + text-align: center; + padding: 0; + margin: 0.3em 0; } -p.channel-guide-entry{ - font-size: 0.8em; - margin-left: 0; +/*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{ + padding: 0.3em 0.5em; + margin: 0; + border-right: 1px solid var(--accent0); +} + +#channel-guide-new-channel-button{ + border-bottom: 1px solid var(--accent0); } \ No newline at end of file diff --git a/www/js/channelGuide.js b/www/js/channelGuide.js new file mode 100644 index 0000000..50447f1 --- /dev/null +++ b/www/js/channelGuide.js @@ -0,0 +1,56 @@ +/*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(){ + 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(){ + //Fetch latest channel list and render it to the schedule page + this.renderList(await utils.ajax.getChannels()); + } + + renderList(schedule, now = new Date()){ + this.renderIndex(now); + } + + renderIndex(now = new Date()){ + //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); + } + } +} + +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", From 5e83e646b28bab81581125209aa12a92213c08d7 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 9 Nov 2026 04:23:31 -0500 Subject: [PATCH 4/7] Added automatic refreshing to channel guide --- www/js/channelGuide.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/www/js/channelGuide.js b/www/js/channelGuide.js index 50447f1..30cbaf2 100644 --- a/www/js/channelGuide.js +++ b/www/js/channelGuide.js @@ -16,6 +16,10 @@ along with this program. If not, see .*/ class ChannelGuide{ constructor(){ + //define property to hold refresh timer + this.refreshTimer; + + //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'); @@ -30,10 +34,22 @@ class ChannelGuide{ } renderList(schedule, now = new Date()){ - this.renderIndex(now); + this.renderIndex(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 + setTimeout(this.refreshList.bind(this), 1000 * 60); } 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 From 263acf250b07b85d6d48ed0c103c6ad271838bf4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 10 Nov 2026 04:15:23 -0500 Subject: [PATCH 5/7] Continued work on improved channel guide --- src/views/partial/channelGuide.ejs | 6 ++- www/css/channelGuide.css | 49 ++++++++++++++++---- www/css/theme/movie-night.css | 28 ++++-------- www/css/theme/the-network-classic.css | 34 ++++++++------ www/js/channelGuide.js | 66 +++++++++++++++++++++++++-- 5 files changed, 135 insertions(+), 48 deletions(-) diff --git a/src/views/partial/channelGuide.ejs b/src/views/partial/channelGuide.ejs index 834c105..2786c9c 100644 --- a/src/views/partial/channelGuide.ejs +++ b/src/views/partial/channelGuide.ejs @@ -15,10 +15,12 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . %>
-
-
+
diff --git a/www/css/channelGuide.css b/www/css/channelGuide.css index 85140ee..531e4b5 100644 --- a/www/css/channelGuide.css +++ b/www/css/channelGuide.css @@ -14,19 +14,24 @@ 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; - border-bottom: 1px solid var(--accent0); } .channel-guide-timeslot-label { @@ -36,17 +41,41 @@ along with this program. If not, see .*/ margin: 0.3em 0; } -/*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{ - padding: 0.3em 0.5em; + padding: 0.3em 0; margin: 0; - border-right: 1px solid var(--accent0); } -#channel-guide-new-channel-button{ - border-bottom: 1px solid var(--accent0); +.channel-guide-schedule-row{ + height: 2.2em; +} + +.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; +} + +.channel-guide-schedule-entry { + display: flex; + justify-content: center; + align-items: center; +} + +.channel-guide-schedule-entry h3 { + margin: 0; + text-wrap: nowrap; } \ No newline at end of file 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..5d760f6 100644 --- a/www/css/theme/the-network-classic.css +++ b/www/css/theme/the-network-classic.css @@ -364,27 +364,33 @@ input.control-prompt, input.control-prompt:focus{ display: none; } -/* index */ -.channel-guide-entry{ - background-color: var(--bg1); - color: var(--accent1); +/* chan guide */ +.channel-guide-channel-list-entry { + background-color: var(--bg2-alt1); } -div.channel-guide-entry{ - border-radius: 0.3em; - box-shadow: 0.2em 0.2em 0.1em var(--bg1-alt0) inset; +.channel-guide-channel-list-entry::before { + content: ''; + background: var(--media-header-gradient); + 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; +#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); +} -p.channel-guide-entry-item{ - background-color: var(--bg1-alt0); +#channel-guide-new-channel-button{ + border-bottom: 1px solid var(--accent0); } /* channel settings */ diff --git a/www/js/channelGuide.js b/www/js/channelGuide.js index 30cbaf2..b2fe286 100644 --- a/www/js/channelGuide.js +++ b/www/js/channelGuide.js @@ -34,7 +34,9 @@ class ChannelGuide{ } renderList(schedule, now = new Date()){ - this.renderIndex(now) + this.renderIndex(now); + this.renderChannelList(schedule, now); + this.renderSchedule(schedule, now); //if we've got a job to refresh the list if(this.refreshTimer != null){ @@ -42,8 +44,8 @@ class ChannelGuide{ clearTimeout(this.refreshTimer); } - //Set timer to refresh the thing erry minute - setTimeout(this.refreshList.bind(this), 1000 * 60); + //Set timer to refresh the thing erry minute on the minute + setTimeout(this.refreshList.bind(this), 1000 * (60 - now.getSeconds())); } renderIndex(now = new Date()){ @@ -67,6 +69,64 @@ class ChannelGuide{ this.scheduleIndex.appendChild(timeslotLabel); } } + + renderChannelList(schedule, now = new Date()){ + //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 = ''; + + //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'); + + //For every scheduled video + for(const entry of channel.schedule){ + //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(entry.title); + + //Assemble the entry + entryDiv.appendChild(entryLabel); + + //slot entry into schedule track + channelTrack.appendChild(entryDiv); + } + + //Inject channel track into schedule div + this.scheduledMediaDiv.appendChild(channelTrack); + } + } + + renderScheduleItem(item){ + } + } const channelGuide = new ChannelGuide(); \ No newline at end of file From c78b056cabed0a474c7a77594c3fee3c2951b62c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 10 Nov 2026 04:34:59 -0500 Subject: [PATCH 6/7] Small improvement to queue rendering --- www/css/panel/queue.css | 2 ++ 1 file changed, 2 insertions(+) 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{ From f7db5ea31d64a633eaea980a82e09b671a75ebbc Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 11 Nov 2026 16:26:33 -0500 Subject: [PATCH 7/7] Implemented basic channel guide, needs moar polish --- www/css/channelGuide.css | 7 +- www/css/theme/the-network-classic.css | 14 ++++ www/js/channelGuide.js | 108 +++++++++++++++++++++----- 3 files changed, 109 insertions(+), 20 deletions(-) diff --git a/www/css/channelGuide.css b/www/css/channelGuide.css index 531e4b5..f30075c 100644 --- a/www/css/channelGuide.css +++ b/www/css/channelGuide.css @@ -47,7 +47,7 @@ along with this program. If not, see .*/ } .channel-guide-schedule-row{ - height: 2.2em; + height: 3.4em; } .channel-guide-channel-list-entry { @@ -67,15 +67,20 @@ along with this program. If not, see .*/ .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/theme/the-network-classic.css b/www/css/theme/the-network-classic.css index 5d760f6..2ab84d6 100644 --- a/www/css/theme/the-network-classic.css +++ b/www/css/theme/the-network-classic.css @@ -393,6 +393,20 @@ input.control-prompt, input.control-prompt:focus{ border-bottom: 1px solid var(--accent0); } +.channel-guide-schedule-entry{ + padding: 0 0.7em; + background-color: var(--bg1); + border-left: 1px solid var(--accent0); +} + +.channel-guide-schedule-entry.starts-before{ + border-left: none; +} + +.channel-guide-schedule-entry.last-ends-before{ + border-right: 1px solid var(--accent0); +} + /* channel settings */ .channel-info-label{ color: var(--accent1-alt0); diff --git a/www/js/channelGuide.js b/www/js/channelGuide.js index b2fe286..cc9039e 100644 --- a/www/js/channelGuide.js +++ b/www/js/channelGuide.js @@ -19,6 +19,9 @@ class ChannelGuide{ //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'); @@ -28,14 +31,17 @@ class ChannelGuide{ this.refreshList(); } - async refreshList(){ - //Fetch latest channel list and render it to the schedule page - this.renderList(await utils.ajax.getChannels()); + 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, now); + this.renderChannelList(schedule); this.renderSchedule(schedule, now); //if we've got a job to refresh the list @@ -70,7 +76,7 @@ class ChannelGuide{ } } - renderChannelList(schedule, now = new Date()){ + renderChannelList(schedule){ //Clean out channel list this.channelList.innerHTML = ''; @@ -96,27 +102,24 @@ class ChannelGuide{ //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'); + channelTrack.classList.add('channel-guide-schedule-track', 'channel-guide-schedule-row'); //For every scheduled video - for(const entry of channel.schedule){ - //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(entry.title); - - //Assemble the entry - entryDiv.appendChild(entryLabel); + 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(entryDiv); + channelTrack.appendChild(this.renderScheduleItem(entry, now, last)); } //Inject channel track into schedule div @@ -124,7 +127,74 @@ class ChannelGuide{ } } - renderScheduleItem(item){ + + 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; + } }