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 += '