/*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();