import escapeHtml from 'escape-html'; import inputManager from '../../scripts/inputManager'; import browser from '../../scripts/browser'; import globalize from '../../lib/globalize'; import Events from '../../utils/events.ts'; import scrollHelper from '../../scripts/scrollHelper'; import serverNotifications from '../../scripts/serverNotifications'; import loading from '../loading/loading'; import datetime from '../../scripts/datetime'; import focusManager from '../focusManager'; import { playbackManager } from '../playback/playbackmanager'; import * as userSettings from '../../scripts/settings/userSettings'; import imageLoader from '../images/imageLoader'; import layoutManager from '../layoutManager'; import itemShortcuts from '../shortcuts'; import dom from '../../scripts/dom'; import './guide.scss'; import './programs.scss'; import 'material-design-icons-iconfont'; import '../../styles/scrollstyles.scss'; import '../../elements/emby-programcell/emby-programcell'; import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-tabs/emby-tabs'; import '../../elements/emby-scroller/emby-scroller'; import '../../styles/flexstyles.scss'; import 'webcomponents.js/webcomponents-lite'; import ServerConnections from '../ServerConnections'; import template from './tvguide.template.html'; function showViewSettings(instance) { import('./guide-settings').then(({ default: guideSettingsDialog }) => { guideSettingsDialog.show(instance.categoryOptions).then(function () { instance.refresh(); }); }); } function updateProgramCellOnScroll(cell, scrollPct) { let left = cell.posLeft; if (!left) { left = parseFloat(cell.style.left.replace('%', '')); cell.posLeft = left; } let width = cell.posWidth; if (!width) { width = parseFloat(cell.style.width.replace('%', '')); cell.posWidth = width; } const right = left + width; const newPct = Math.max(Math.min(scrollPct, right), left); const offset = newPct - left; const pctOfWidth = (offset / width) * 100; let guideProgramName = cell.guideProgramName; if (!guideProgramName) { guideProgramName = cell.querySelector('.guideProgramName'); cell.guideProgramName = guideProgramName; } let caret = cell.caret; if (!caret) { caret = cell.querySelector('.guide-programNameCaret'); cell.caret = caret; } if (guideProgramName) { if (pctOfWidth > 0 && pctOfWidth <= 100) { guideProgramName.style.transform = 'translateX(' + pctOfWidth + '%)'; caret.classList.remove('hide'); } else { guideProgramName.style.transform = 'none'; caret.classList.add('hide'); } } } let isUpdatingProgramCellScroll = false; function updateProgramCellsOnScroll(programGrid, programCells) { if (isUpdatingProgramCellScroll) { return; } isUpdatingProgramCellScroll = true; requestAnimationFrame(function () { const scrollLeft = programGrid.scrollLeft; const scrollPct = scrollLeft ? (scrollLeft / programGrid.scrollWidth) * 100 : 0; for (const programCell of programCells) { updateProgramCellOnScroll(programCell, scrollPct); } isUpdatingProgramCellScroll = false; }); } function onProgramGridClick(e) { if (!layoutManager.tv) { return; } const programCell = dom.parentWithClass(e.target, 'programCell'); if (programCell) { let startDate = programCell.getAttribute('data-startdate'); let endDate = programCell.getAttribute('data-enddate'); startDate = datetime.parseISO8601Date(startDate, { toLocal: true }).getTime(); endDate = datetime.parseISO8601Date(endDate, { toLocal: true }).getTime(); const now = new Date().getTime(); if (now >= startDate && now < endDate) { const channelId = programCell.getAttribute('data-channelid'); const serverId = programCell.getAttribute('data-serverid'); e.preventDefault(); e.stopPropagation(); playbackManager.play({ ids: [channelId], serverId: serverId }); } } } function Guide(options) { const self = this; let items = {}; self.options = options; self.categoryOptions = { categories: [] }; // 30 mins const cellCurationMinutes = 30; const cellDurationMs = cellCurationMinutes * 60 * 1000; const msPerDay = 86400000; let currentDate; let currentStartIndex = 0; let currentChannelLimit = 0; let autoRefreshInterval; let programCells; let lastFocusDirection; self.refresh = function () { currentDate = null; reloadPage(options.element); restartAutoRefresh(); }; self.pause = function () { stopAutoRefresh(); }; self.resume = function (refreshData) { if (refreshData) { self.refresh(); } else { restartAutoRefresh(); } }; self.destroy = function () { stopAutoRefresh(); Events.off(serverNotifications, 'TimerCreated', onTimerCreated); Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled); Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); setScrollEvents(options.element, false); itemShortcuts.off(options.element); items = {}; }; function restartAutoRefresh() { stopAutoRefresh(); const intervalMs = 60000 * 15; // (minutes) autoRefreshInterval = setInterval(function () { self.refresh(); }, intervalMs); } function stopAutoRefresh() { if (autoRefreshInterval) { clearInterval(autoRefreshInterval); autoRefreshInterval = null; } } function normalizeDateToTimeslot(date) { const minutesOffset = date.getMinutes() - cellCurationMinutes; if (minutesOffset >= 0) { date.setHours(date.getHours(), cellCurationMinutes, 0, 0); } else { date.setHours(date.getHours(), 0, 0, 0); } return date; } function showLoading() { loading.show(); } function hideLoading() { loading.hide(); } function reloadGuide(context, newStartDate, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender) { const apiClient = ServerConnections.getApiClient(options.serverId); const channelQuery = { StartIndex: 0, EnableFavoriteSorting: userSettings.get('livetv-favoritechannelsattop') !== 'false' }; channelQuery.UserId = apiClient.getCurrentUserId(); const channelLimit = 500; currentChannelLimit = channelLimit; showLoading(); channelQuery.StartIndex = currentStartIndex; channelQuery.Limit = channelLimit; channelQuery.AddCurrentProgram = false; channelQuery.EnableUserData = false; channelQuery.EnableImageTypes = 'Primary'; const categories = self.categoryOptions.categories || []; const displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; const displaySportsContent = !categories.length || categories.indexOf('sports') !== -1; const displayNewsContent = !categories.length || categories.indexOf('news') !== -1; const displayKidsContent = !categories.length || categories.indexOf('kids') !== -1; const displaySeriesContent = !categories.length || categories.indexOf('series') !== -1; if (displayMovieContent && displaySportsContent && displayNewsContent && displayKidsContent) { channelQuery.IsMovie = null; channelQuery.IsSports = null; channelQuery.IsKids = null; channelQuery.IsNews = null; channelQuery.IsSeries = null; } else { if (displayNewsContent) { channelQuery.IsNews = true; } if (displaySportsContent) { channelQuery.IsSports = true; } if (displayKidsContent) { channelQuery.IsKids = true; } if (displayMovieContent) { channelQuery.IsMovie = true; } if (displaySeriesContent) { channelQuery.IsSeries = true; } } if (userSettings.get('livetv-channelorder') === 'DatePlayed') { channelQuery.SortBy = 'DatePlayed'; channelQuery.SortOrder = 'Descending'; } else { channelQuery.SortBy = null; channelQuery.SortOrder = null; } let date = newStartDate; // Add one second to avoid getting programs that are just ending date = new Date(date.getTime() + 1000); // Subtract to avoid getting programs that are starting when the grid ends const nextDay = new Date(date.getTime() + msPerDay - 2000); // Normally we'd want to just let responsive css handle this, // but since mobile browsers are often underpowered, // it can help performance to get them out of the markup const allowIndicators = dom.getWindowSize().innerWidth >= 600; const renderOptions = { showHdIcon: allowIndicators && userSettings.get('guide-indicator-hd') === 'true', showLiveIndicator: allowIndicators && userSettings.get('guide-indicator-live') !== 'false', showPremiereIndicator: allowIndicators && userSettings.get('guide-indicator-premiere') !== 'false', showNewIndicator: allowIndicators && userSettings.get('guide-indicator-new') !== 'false', showRepeatIndicator: allowIndicators && userSettings.get('guide-indicator-repeat') === 'true', showEpisodeTitle: !layoutManager.tv }; apiClient.getLiveTvChannels(channelQuery).then(function (channelsResult) { const btnPreviousPage = context.querySelector('.btnPreviousPage'); const btnNextPage = context.querySelector('.btnNextPage'); if (channelsResult.TotalRecordCount > channelLimit) { context.querySelector('.guideOptions').classList.remove('hide'); btnPreviousPage.classList.remove('hide'); btnNextPage.classList.remove('hide'); if (channelQuery.StartIndex) { context.querySelector('.btnPreviousPage').disabled = false; } else { context.querySelector('.btnPreviousPage').disabled = true; } if ((channelQuery.StartIndex + channelLimit) < channelsResult.TotalRecordCount) { btnNextPage.disabled = false; } else { btnNextPage.disabled = true; } } else { context.querySelector('.guideOptions').classList.add('hide'); } const programFields = []; const programQuery = { UserId: apiClient.getCurrentUserId(), MaxStartDate: nextDay.toISOString(), MinEndDate: date.toISOString(), channelIds: channelsResult.Items.map(function (c) { return c.Id; }).join(','), ImageTypeLimit: 1, EnableImages: false, //EnableImageTypes: layoutManager.tv ? "Primary,Backdrop" : "Primary", SortBy: 'StartDate', EnableTotalRecordCount: false, EnableUserData: false }; if (renderOptions.showHdIcon) { programFields.push('IsHD'); } if (programFields.length) { programQuery.Fields = programFields.join(''); } apiClient.getLiveTvPrograms(programQuery).then(function (programsResult) { const guideOptions = { focusProgramOnRender, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs }; renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, guideOptions, apiClient); hideLoading(); }); }); } function getDisplayTime(date) { if ((typeof date).toString().toLowerCase() === 'string') { try { date = datetime.parseISO8601Date(date, { toLocal: true }); } catch (err) { return date; } } return datetime.getDisplayTime(date).toLowerCase(); } function getTimeslotHeadersHtml(startDate, endDateTime) { let html = ''; // clone startDate = new Date(startDate.getTime()); html += '
'; while (startDate.getTime() < endDateTime) { html += '
'; html += getDisplayTime(startDate); html += '
'; // Add 30 mins startDate.setTime(startDate.getTime() + cellDurationMs); } return html; } function parseDates(program) { if (!program.StartDateLocal) { try { program.StartDateLocal = datetime.parseISO8601Date(program.StartDate, { toLocal: true }); } catch (err) { console.error('error parsing timestamp for start date'); } } if (!program.EndDateLocal) { try { program.EndDateLocal = datetime.parseISO8601Date(program.EndDate, { toLocal: true }); } catch (err) { console.error('error parsing timestamp for end date'); } } return null; } function getTimerIndicator(item) { let status; if (item.Type === 'SeriesTimer') { return ''; } else if (item.TimerId || item.SeriesTimerId) { status = item.Status || 'Cancelled'; } else if (item.Type === 'Timer') { status = item.Status; } else { return ''; } if (item.SeriesTimerId) { if (status !== 'Cancelled') { return ''; } return ''; } return ''; } function getChannelProgramsHtml(context, date, channel, programs, programOptions, listInfo) { let html = ''; const startMs = date.getTime(); const endMs = startMs + msPerDay - 1; const outerCssClass = layoutManager.tv ? 'channelPrograms channelPrograms-tv' : 'channelPrograms'; html += '
'; const clickAction = layoutManager.tv ? 'link' : 'programdialog'; const categories = self.categoryOptions.categories || []; const displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; const displaySportsContent = !categories.length || categories.indexOf('sports') !== -1; const displayNewsContent = !categories.length || categories.indexOf('news') !== -1; const displayKidsContent = !categories.length || categories.indexOf('kids') !== -1; const displaySeriesContent = !categories.length || categories.indexOf('series') !== -1; const enableColorCodedBackgrounds = userSettings.get('guide-colorcodedbackgrounds') === 'true'; let programsFound; const now = new Date().getTime(); for (let i = listInfo.startIndex, length = programs.length; i < length; i++) { const program = programs[i]; if (program.ChannelId !== channel.Id) { if (programsFound) { break; } continue; } programsFound = true; listInfo.startIndex++; parseDates(program); const startDateLocalMs = program.StartDateLocal.getTime(); const endDateLocalMs = program.EndDateLocal.getTime(); if (endDateLocalMs < startMs) { continue; } if (startDateLocalMs > endMs) { break; } items[program.Id] = program; const renderStartMs = Math.max(startDateLocalMs, startMs); let startPercent = (startDateLocalMs - startMs) / msPerDay; startPercent *= 100; startPercent = Math.max(startPercent, 0); const renderEndMs = Math.min(endDateLocalMs, endMs); let endPercent = (renderEndMs - renderStartMs) / msPerDay; endPercent *= 100; let cssClass = 'programCell itemAction'; let accentCssClass = null; let displayInnerContent = true; if (program.IsKids) { displayInnerContent = displayKidsContent; accentCssClass = 'kids'; } else if (program.IsSports) { displayInnerContent = displaySportsContent; accentCssClass = 'sports'; } else if (program.IsNews) { displayInnerContent = displayNewsContent; accentCssClass = 'news'; } else if (program.IsMovie) { displayInnerContent = displayMovieContent; accentCssClass = 'movie'; } else if (program.IsSeries) { displayInnerContent = displaySeriesContent; } else { displayInnerContent = displayMovieContent && displayNewsContent && displaySportsContent && displayKidsContent && displaySeriesContent; } if (displayInnerContent && enableColorCodedBackgrounds && accentCssClass) { cssClass += ' programCell-' + accentCssClass; } if (now >= startDateLocalMs && now < endDateLocalMs) { cssClass += ' programCell-active'; } let timerAttributes = ''; if (program.TimerId) { timerAttributes += ' data-timerid="' + program.TimerId + '"'; } if (program.SeriesTimerId) { timerAttributes += ' data-seriestimerid="' + program.SeriesTimerId + '"'; } const isAttribute = endPercent >= 2 ? ' is="emby-programcell"' : ''; html += ''; if (displayInnerContent) { const guideProgramNameClass = 'guideProgramName'; html += '
'; html += '
'; html += '
' + escapeHtml(program.Name); let indicatorHtml = null; if (program.IsLive && programOptions.showLiveIndicator) { indicatorHtml = '' + globalize.translate('Live') + ''; } else if (program.IsPremiere && programOptions.showPremiereIndicator) { indicatorHtml = '' + globalize.translate('Premiere') + ''; } else if (program.IsSeries && !program.IsRepeat && programOptions.showNewIndicator) { indicatorHtml = '' + globalize.translate('New') + ''; } else if (program.IsSeries && program.IsRepeat && programOptions.showRepeatIndicator) { indicatorHtml = '' + globalize.translate('Repeat') + ''; } html += indicatorHtml || ''; if ((program.EpisodeTitle && programOptions.showEpisodeTitle)) { html += '
'; if (program.EpisodeTitle && programOptions.showEpisodeTitle) { html += '' + escapeHtml(program.EpisodeTitle) + ''; } html += '
'; } html += '
'; if (program.IsHD && programOptions.showHdIcon) { if (layoutManager.tv) { html += '
HD
'; } else { html += '
HD
'; } } html += getTimerIndicator(program); html += '
'; } html += ''; } html += '
'; return html; } function renderChannelHeaders(context, channels, apiClient) { let html = ''; for (const channel of channels) { const hasChannelImage = channel.ImageTags.Primary; let cssClass = 'guide-channelHeaderCell itemAction'; if (layoutManager.tv) { cssClass += ' guide-channelHeaderCell-tv'; } const title = []; if (channel.ChannelNumber) { title.push(channel.ChannelNumber); } if (channel.Name) { title.push(channel.Name); } html += ''; } const channelList = context.querySelector('.channelsContainer'); channelList.innerHTML = html; imageLoader.lazyChildren(channelList); } function renderPrograms(context, date, channels, programs, programOptions) { const listInfo = { startIndex: 0 }; const html = []; for (const channel of channels) { html.push(getChannelProgramsHtml(context, date, channel, programs, programOptions, listInfo)); } programGrid.innerHTML = html.join(''); programCells = programGrid.querySelectorAll('[is=emby-programcell]'); updateProgramCellsOnScroll(programGrid, programCells); } function getProgramSortOrder(program, channels) { const channelId = program.ChannelId; let channelIndex = -1; for (let i = 0, length = channels.length; i < length; i++) { if (channelId === channels[i].Id) { channelIndex = i; break; } } const start = datetime.parseISO8601Date(program.StartDate, { toLocal: true }); return (channelIndex * 10000000) + (start.getTime() / 60000); } function renderGuide(context, date, channels, programs, renderOptions, guideOptions, apiClient) { programs.sort(function (a, b) { return getProgramSortOrder(a, channels) - getProgramSortOrder(b, channels); }); const activeElement = document.activeElement; const itemId = activeElement?.getAttribute ? activeElement.getAttribute('data-id') : null; let channelRowId = null; if (activeElement) { channelRowId = dom.parentWithClass(activeElement, 'channelPrograms'); channelRowId = channelRowId?.getAttribute ? channelRowId.getAttribute('data-channelid') : null; } renderChannelHeaders(context, channels, apiClient); const startDate = date; const endDate = new Date(startDate.getTime() + msPerDay); context.querySelector('.timeslotHeaders').innerHTML = getTimeslotHeadersHtml(startDate, endDate); items = {}; renderPrograms(context, date, channels, programs, renderOptions); if (guideOptions.focusProgramOnRender) { focusProgram(context, itemId, channelRowId, guideOptions.focusToTimeMs, guideOptions.startTimeOfDayMs); } scrollProgramGridToTimeMs(context, guideOptions.scrollToTimeMs, guideOptions.startTimeOfDayMs); } function scrollProgramGridToTimeMs(context, scrollToTimeMs, startTimeOfDayMs) { scrollToTimeMs -= startTimeOfDayMs; const pct = scrollToTimeMs / msPerDay; programGrid.scrollTop = 0; const scrollPos = pct * programGrid.scrollWidth; nativeScrollTo(programGrid, scrollPos, true); } function focusProgram(context, itemId, channelRowId, focusToTimeMs, startTimeOfDayMs) { let focusElem; if (itemId) { focusElem = context.querySelector('[data-id="' + itemId + '"]'); } if (focusElem) { focusManager.focus(focusElem); } else { let autoFocusParent; if (channelRowId) { autoFocusParent = context.querySelector('[data-channelid="' + channelRowId + '"]'); } if (!autoFocusParent) { autoFocusParent = programGrid; } focusToTimeMs -= startTimeOfDayMs; const pct = (focusToTimeMs / msPerDay) * 100; let programCell = autoFocusParent.querySelector('.programCell'); while (programCell) { let left = (programCell.style.left || '').replace('%', ''); left = left ? parseFloat(left) : 0; let width = (programCell.style.width || '').replace('%', ''); width = width ? parseFloat(width) : 0; if (left >= pct || (left + width) >= pct) { break; } programCell = programCell.nextSibling; } if (programCell) { focusManager.focus(programCell); } else { focusManager.autoFocus(autoFocusParent, true); } } } function nativeScrollTo(container, pos, horizontal) { if (container.scrollTo) { if (horizontal) { container.scrollTo(pos, 0); } else { container.scrollTo(0, pos); } } else if (horizontal) { container.scrollLeft = Math.round(pos); } else { container.scrollTop = Math.round(pos); } } let lastGridScroll = 0; let lastHeaderScroll = 0; let scrollXPct = 0; function onProgramGridScroll(context, elem, headers) { if ((new Date().getTime() - lastHeaderScroll) >= 1000) { lastGridScroll = new Date().getTime(); const scrollLeft = elem.scrollLeft; scrollXPct = (scrollLeft * 100) / elem.scrollWidth; nativeScrollTo(headers, scrollLeft, true); } updateProgramCellsOnScroll(elem, programCells); } function onTimeslotHeadersScroll(context, elem) { if ((new Date().getTime() - lastGridScroll) >= 1000) { lastHeaderScroll = new Date().getTime(); nativeScrollTo(programGrid, elem.scrollLeft, true); } } function changeDate(page, date, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender) { const newStartDate = normalizeDateToTimeslot(date); currentDate = newStartDate; reloadGuide(page, newStartDate, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender); } function getDateTabText(date, isActive, tabIndex) { const cssClass = isActive ? 'emby-tab-button guide-date-tab-button emby-tab-button-active' : 'emby-tab-button guide-date-tab-button'; let html = ''; return html; } function setDateRange(page, guideInfo) { const today = new Date(); const nowHours = today.getHours(); today.setHours(nowHours, 0, 0, 0); let start = datetime.parseISO8601Date(guideInfo.StartDate, { toLocal: true }); const end = datetime.parseISO8601Date(guideInfo.EndDate, { toLocal: true }); start.setHours(nowHours, 0, 0, 0); end.setHours(0, 0, 0, 0); if (start.getTime() >= end.getTime()) { end.setDate(start.getDate() + 1); } start = new Date(Math.max(today, start)); let dateTabsHtml = ''; let tabIndex = 0; // TODO: Use date-fns const date = new Date(); if (currentDate) { date.setTime(currentDate.getTime()); } date.setHours(nowHours, 0, 0, 0); let startTimeOfDayMs = (start.getHours() * 60 * 60 * 1000); startTimeOfDayMs += start.getMinutes() * 60 * 1000; while (start <= end) { const isActive = date.getDate() === start.getDate() && date.getMonth() === start.getMonth() && date.getFullYear() === start.getFullYear(); dateTabsHtml += getDateTabText(start, isActive, tabIndex); start.setDate(start.getDate() + 1); start.setHours(0, 0, 0, 0); tabIndex++; } page.querySelector('.emby-tabs-slider').innerHTML = dateTabsHtml; page.querySelector('.guideDateTabs').refresh(); const newDate = new Date(); const newDateHours = newDate.getHours(); let scrollToTimeMs = newDateHours * 60 * 60 * 1000; const minutes = newDate.getMinutes(); if (minutes >= 30) { scrollToTimeMs += 30 * 60 * 1000; } const focusToTimeMs = ((newDateHours * 60) + minutes) * 60 * 1000; changeDate(page, date, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, layoutManager.tv); } function reloadPage(page) { showLoading(); const apiClient = ServerConnections.getApiClient(options.serverId); apiClient.getLiveTvGuideInfo().then(function (guideInfo) { setDateRange(page, guideInfo); }); } function getChannelProgramsFocusableElements(container) { const elements = container.querySelectorAll('.programCell'); const list = []; // add 1 to avoid programs that are out of view to the left const currentScrollXPct = scrollXPct + 1; for (const elem of elements) { let left = (elem.style.left || '').replace('%', ''); left = left ? parseFloat(left) : 0; let width = (elem.style.width || '').replace('%', ''); width = width ? parseFloat(width) : 0; if ((left + width) >= currentScrollXPct) { list.push(elem); } } return list; } function onInputCommand(e) { const target = e.target; const programCell = dom.parentWithClass(target, 'programCell'); let container; let channelPrograms; let focusableElements; let newRow; switch (e.detail.command) { case 'up': if (programCell) { container = programGrid; channelPrograms = dom.parentWithClass(programCell, 'channelPrograms'); newRow = channelPrograms.previousSibling; if (newRow) { focusableElements = getChannelProgramsFocusableElements(newRow); if (focusableElements.length) { container = newRow; } else { focusableElements = null; } } else { container = null; } } else { container = null; } lastFocusDirection = e.detail.command; focusManager.moveUp(target, { container: container, focusableElements: focusableElements }); break; case 'down': if (programCell) { container = programGrid; channelPrograms = dom.parentWithClass(programCell, 'channelPrograms'); newRow = channelPrograms.nextSibling; if (newRow) { focusableElements = getChannelProgramsFocusableElements(newRow); if (focusableElements.length) { container = newRow; } else { focusableElements = null; } } else { container = null; } } else { container = null; } lastFocusDirection = e.detail.command; focusManager.moveDown(target, { container: container, focusableElements: focusableElements }); break; case 'left': container = programCell ? dom.parentWithClass(programCell, 'channelPrograms') : null; // allow left outside the channelProgramsContainer when the first child is currently focused if (container && !programCell.previousSibling) { container = null; } lastFocusDirection = e.detail.command; focusManager.moveLeft(target, { container: container }); break; case 'right': container = programCell ? dom.parentWithClass(programCell, 'channelPrograms') : null; lastFocusDirection = e.detail.command; focusManager.moveRight(target, { container: container }); break; default: return; } e.preventDefault(); e.stopPropagation(); } function onScrollerFocus(e) { const target = e.target; const programCell = dom.parentWithClass(target, 'programCell'); if (programCell) { const focused = target; const id = focused.getAttribute('data-id'); const item = items[id]; if (item) { Events.trigger(self, 'focus', [ { item: item }]); } } if (lastFocusDirection === 'left') { if (programCell) { scrollHelper.toStart(programGrid, programCell, true, true); } } else if (lastFocusDirection === 'right') { if (programCell) { scrollHelper.toCenter(programGrid, programCell, true, true); } } else if (lastFocusDirection === 'up' || lastFocusDirection === 'down') { const verticalScroller = dom.parentWithClass(target, 'guideVerticalScroller'); if (verticalScroller) { const focusedElement = programCell || dom.parentWithTag(target, 'BUTTON'); verticalScroller.toCenter(focusedElement, true); } } } function setScrollEvents(view, enabled) { if (layoutManager.tv) { const guideVerticalScroller = view.querySelector('.guideVerticalScroller'); if (enabled) { inputManager.on(guideVerticalScroller, onInputCommand); } else { inputManager.off(guideVerticalScroller, onInputCommand); } } } function onTimerCreated(e, apiClient, data) { const programId = data.ProgramId; // This could be null, not supported by all tv providers const newTimerId = data.Id; // find guide cells by program id, ensure timer icon const cells = options.element.querySelectorAll('.programCell[data-id="' + programId + '"]'); for (const cell of cells) { const icon = cell.querySelector('.timerIcon'); if (!icon) { cell.querySelector('.guideProgramName').insertAdjacentHTML('beforeend', ''); } if (newTimerId) { cell.setAttribute('data-timerid', newTimerId); } } } function onTimerCancelled(e, apiClient, data) { const id = data.Id; // find guide cells by timer id, remove timer icon const cells = options.element.querySelectorAll('.programCell[data-timerid="' + id + '"]'); for (const cell of cells) { const icon = cell.querySelector('.timerIcon'); if (icon) { icon.parentNode.removeChild(icon); } cell.removeAttribute('data-timerid'); } } function onSeriesTimerCancelled(e, apiClient, data) { const id = data.Id; // find guide cells by timer id, remove timer icon const cells = options.element.querySelectorAll('.programCell[data-seriestimerid="' + id + '"]'); for (const cell of cells) { const icon = cell.querySelector('.seriesTimerIcon'); if (icon) { icon.parentNode.removeChild(icon); } cell.removeAttribute('data-seriestimerid'); } } const guideContext = options.element; guideContext.classList.add('tvguide'); guideContext.innerHTML = globalize.translateHtml(template, 'core'); const programGrid = guideContext.querySelector('.programGrid'); const timeslotHeaders = guideContext.querySelector('.timeslotHeaders'); if (layoutManager.tv) { dom.addEventListener(guideContext.querySelector('.guideVerticalScroller'), 'focus', onScrollerFocus, { capture: true, passive: true }); } else if (layoutManager.desktop) { timeslotHeaders.classList.add('timeslotHeaders-desktop'); } if (browser.iOS || browser.osx) { guideContext.querySelector('.channelsContainer').classList.add('noRubberBanding'); programGrid.classList.add('noRubberBanding'); } dom.addEventListener(programGrid, 'scroll', function () { onProgramGridScroll(guideContext, this, timeslotHeaders); }, { passive: true }); dom.addEventListener(timeslotHeaders, 'scroll', function () { onTimeslotHeadersScroll(guideContext, this); }, { passive: true }); programGrid.addEventListener('click', onProgramGridClick); guideContext.querySelector('.btnNextPage').addEventListener('click', function () { currentStartIndex += currentChannelLimit; reloadPage(guideContext); restartAutoRefresh(); }); guideContext.querySelector('.btnPreviousPage').addEventListener('click', function () { currentStartIndex = Math.max(currentStartIndex - currentChannelLimit, 0); reloadPage(guideContext); restartAutoRefresh(); }); guideContext.querySelector('.btnGuideViewSettings').addEventListener('click', function () { showViewSettings(self); restartAutoRefresh(); }); guideContext.querySelector('.guideDateTabs').addEventListener('tabchange', function (e) { const allTabButtons = e.target.querySelectorAll('.guide-date-tab-button'); const tabButton = allTabButtons[parseInt(e.detail.selectedTabIndex, 10)]; if (tabButton) { const previousButton = e.detail.previousIndex == null ? null : allTabButtons[parseInt(e.detail.previousIndex, 10)]; const date = new Date(); date.setTime(parseInt(tabButton.getAttribute('data-date'), 10)); const scrollWidth = programGrid.scrollWidth; let scrollToTimeMs; if (scrollWidth) { scrollToTimeMs = (programGrid.scrollLeft / scrollWidth) * msPerDay; } else { scrollToTimeMs = 0; } if (previousButton) { const previousDate = new Date(); previousDate.setTime(parseInt(previousButton.getAttribute('data-date'), 10)); scrollToTimeMs += (previousDate.getHours() * 60 * 60 * 1000); scrollToTimeMs += (previousDate.getMinutes() * 60 * 1000); } let startTimeOfDayMs = (date.getHours() * 60 * 60 * 1000); startTimeOfDayMs += (date.getMinutes() * 60 * 1000); changeDate(guideContext, date, scrollToTimeMs, scrollToTimeMs, startTimeOfDayMs, false); } }); setScrollEvents(guideContext, true); itemShortcuts.on(guideContext); Events.trigger(self, 'load'); Events.on(serverNotifications, 'TimerCreated', onTimerCreated); Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled); Events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); self.refresh(); } export default Guide;