diff --git a/package.json b/package.json index 82f73826f0..db2dd72826 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,8 @@ "src/components/filterdialog/filterdialog.js", "src/components/focusManager.js", "src/components/groupedcards.js", + "src/components/guide/guide.js", + "src/components/guide/guide-settings.js", "src/components/homeScreenSettings/homeScreenSettings.js", "src/components/homesections/homesections.js", "src/components/htmlMediaHelper.js", @@ -158,8 +160,10 @@ "src/components/recordingcreator/seriesrecordingeditor.js", "src/components/recordingcreator/recordinghelper.js", "src/components/refreshdialog/refreshdialog.js", + "src/components/remotecontrol/remotecontrol.js", "src/components/sanatizefilename.js", "src/components/scrollManager.js", + "src/plugins/chromecastPlayer/plugin.js", "src/components/slideshow/slideshow.js", "src/components/sortmenu/sortmenu.js", "src/plugins/htmlVideoPlayer/plugin.js", @@ -177,6 +181,7 @@ "src/components/syncPlay/playbackPermissionManager.js", "src/components/syncPlay/syncPlayManager.js", "src/components/syncPlay/timeSyncManager.js", + "src/components/tabbedview/tabbedview.js", "src/components/viewManager/viewManager.js", "src/components/tvproviders/schedulesdirect.js", "src/components/tvproviders/xmltv.js", @@ -206,7 +211,7 @@ "src/controllers/music/musicplaylists.js", "src/controllers/music/musicrecommended.js", "src/controllers/music/songs.js", - "src/controllers/dashboard/mediaLibrary.js", + "src/controllers/dashboard/library.js", "src/controllers/dashboard/metadataImages.js", "src/controllers/dashboard/metadatanfo.js", "src/controllers/dashboard/networking.js", @@ -227,6 +232,7 @@ "src/controllers/dashboard/users/userparentalcontrol.js", "src/controllers/dashboard/users/userpasswordpage.js", "src/controllers/dashboard/users/userprofilespage.js", + "src/controllers/home.js", "src/controllers/list.js", "src/controllers/edititemmetadata.js", "src/controllers/favorites.js", @@ -313,6 +319,7 @@ "src/scripts/autoThemes.js", "src/scripts/themeManager.js", "src/scripts/keyboardNavigation.js", + "src/scripts/libraryMenu.js", "src/scripts/libraryBrowser.js", "src/scripts/livetvcomponents.js", "src/scripts/mouseManager.js", diff --git a/src/components/guide/guide-settings.js b/src/components/guide/guide-settings.js index b3133ec272..35f0d3e06e 100644 --- a/src/components/guide/guide-settings.js +++ b/src/components/guide/guide-settings.js @@ -1,150 +1,149 @@ -define(['dialogHelper', 'globalize', 'userSettings', 'layoutManager', 'connectionManager', 'require', 'loading', 'scrollHelper', 'emby-checkbox', 'emby-radio', 'css!./../formdialog', 'material-icons'], function (dialogHelper, globalize, userSettings, layoutManager, connectionManager, require, loading, scrollHelper) { - 'use strict'; +import dialogHelper from 'dialogHelper'; +import globalize from 'globalize'; +import * as userSettings from 'userSettings'; +import layoutManager from 'layoutManager'; +import scrollHelper from 'scrollHelper'; +import 'emby-checkbox'; +import 'emby-radio'; +import 'css!./../formdialog'; +import 'material-icons'; - layoutManager = layoutManager.default || layoutManager; - scrollHelper = scrollHelper.default || scrollHelper; +function saveCategories(context, options) { + const categories = []; - function saveCategories(context, options) { - var categories = []; + const chkCategorys = context.querySelectorAll('.chkCategory'); + for (const chkCategory of chkCategorys) { + const type = chkCategory.getAttribute('data-type'); - var chkCategorys = context.querySelectorAll('.chkCategory'); - for (var i = 0, length = chkCategorys.length; i < length; i++) { - var type = chkCategorys[i].getAttribute('data-type'); - - if (chkCategorys[i].checked) { - categories.push(type); - } - } - - if (categories.length >= 4) { - categories.push('series'); - } - - // differentiate between none and all - categories.push('all'); - options.categories = categories; - } - - function loadCategories(context, options) { - var selectedCategories = options.categories || []; - - var chkCategorys = context.querySelectorAll('.chkCategory'); - for (var i = 0, length = chkCategorys.length; i < length; i++) { - var type = chkCategorys[i].getAttribute('data-type'); - - chkCategorys[i].checked = !selectedCategories.length || selectedCategories.indexOf(type) !== -1; + if (chkCategory.checked) { + categories.push(type); } } - function save(context) { - var i; - var length; + if (categories.length >= 4) { + categories.push('series'); + } - var chkIndicators = context.querySelectorAll('.chkIndicator'); - for (i = 0, length = chkIndicators.length; i < length; i++) { - var type = chkIndicators[i].getAttribute('data-type'); - userSettings.set('guide-indicator-' + type, chkIndicators[i].checked); + // differentiate between none and all + categories.push('all'); + options.categories = categories; +} + +function loadCategories(context, options) { + const selectedCategories = options.categories || []; + + const chkCategorys = context.querySelectorAll('.chkCategory'); + for (const chkCategory of chkCategorys) { + const type = chkCategory.getAttribute('data-type'); + + chkCategory.checked = !selectedCategories.length || selectedCategories.indexOf(type) !== -1; + } +} + +function save(context) { + const chkIndicators = context.querySelectorAll('.chkIndicator'); + + for (const chkIndicator of chkIndicators) { + const type = chkIndicator.getAttribute('data-type'); + userSettings.set('guide-indicator-' + type, chkIndicator.checked); + } + + userSettings.set('guide-colorcodedbackgrounds', context.querySelector('.chkColorCodedBackgrounds').checked); + userSettings.set('livetv-favoritechannelsattop', context.querySelector('.chkFavoriteChannelsAtTop').checked); + + const sortBys = context.querySelectorAll('.chkSortOrder'); + for (const sortBy of sortBys) { + if (sortBy.checked) { + userSettings.set('livetv-channelorder', sortBy.value); + break; } + } +} - userSettings.set('guide-colorcodedbackgrounds', context.querySelector('.chkColorCodedBackgrounds').checked); - userSettings.set('livetv-favoritechannelsattop', context.querySelector('.chkFavoriteChannelsAtTop').checked); +function load(context) { + const chkIndicators = context.querySelectorAll('.chkIndicator'); - var sortBys = context.querySelectorAll('.chkSortOrder'); - for (i = 0, length = sortBys.length; i < length; i++) { - if (sortBys[i].checked) { - userSettings.set('livetv-channelorder', sortBys[i].value); - break; - } + for (const chkIndicator of chkIndicators) { + const type = chkIndicator.getAttribute('data-type'); + + if (chkIndicator.getAttribute('data-default') === 'true') { + chkIndicator.checked = userSettings.get('guide-indicator-' + type) !== 'false'; + } else { + chkIndicator.checked = userSettings.get('guide-indicator-' + type) === 'true'; } } - function load(context) { - var i; - var length; + context.querySelector('.chkColorCodedBackgrounds').checked = userSettings.get('guide-colorcodedbackgrounds') === 'true'; + context.querySelector('.chkFavoriteChannelsAtTop').checked = userSettings.get('livetv-favoritechannelsattop') !== 'false'; - var chkIndicators = context.querySelectorAll('.chkIndicator'); - for (i = 0, length = chkIndicators.length; i < length; i++) { - var type = chkIndicators[i].getAttribute('data-type'); + const sortByValue = userSettings.get('livetv-channelorder') || 'Number'; - if (chkIndicators[i].getAttribute('data-default') === 'true') { - chkIndicators[i].checked = userSettings.get('guide-indicator-' + type) !== 'false'; + const sortBys = context.querySelectorAll('.chkSortOrder'); + for (const sortBy of sortBys) { + sortBy.checked = sortBy.value === sortByValue; + } +} + +function showEditor(options) { + return new Promise(function (resolve, reject) { + let settingsChanged = false; + + import('text!./guide-settings.template.html').then(({ default: template }) => { + const dialogOptions = { + removeOnClose: true, + scrollY: false + }; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; } else { - chkIndicators[i].checked = userSettings.get('guide-indicator-' + type) === 'true'; + dialogOptions.size = 'small'; } - } - context.querySelector('.chkColorCodedBackgrounds').checked = userSettings.get('guide-colorcodedbackgrounds') === 'true'; - context.querySelector('.chkFavoriteChannelsAtTop').checked = userSettings.get('livetv-favoritechannelsattop') !== 'false'; + const dlg = dialogHelper.createDialog(dialogOptions); - var sortByValue = userSettings.get('livetv-channelorder') || 'Number'; + dlg.classList.add('formDialog'); - var sortBys = context.querySelectorAll('.chkSortOrder'); - for (i = 0, length = sortBys.length; i < length; i++) { - sortBys[i].checked = sortBys[i].value === sortByValue; - } - } + let html = ''; - function showEditor(options) { - return new Promise(function (resolve, reject) { - var settingsChanged = false; + html += globalize.translateHtml(template, 'core'); - require(['text!./guide-settings.template.html'], function (template) { - var dialogOptions = { - removeOnClose: true, - scrollY: false - }; + dlg.innerHTML = html; - if (layoutManager.tv) { - dialogOptions.size = 'fullscreen'; - } else { - dialogOptions.size = 'small'; - } - - var dlg = dialogHelper.createDialog(dialogOptions); - - dlg.classList.add('formDialog'); - - var html = ''; - - html += globalize.translateHtml(template, 'core'); - - dlg.innerHTML = html; - - dlg.addEventListener('change', function () { - settingsChanged = true; - }); - - dlg.addEventListener('close', function () { - if (layoutManager.tv) { - scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false); - } - - save(dlg); - saveCategories(dlg, options); - - if (settingsChanged) { - resolve(); - } else { - reject(); - } - }); - - dlg.querySelector('.btnCancel').addEventListener('click', function () { - dialogHelper.close(dlg); - }); - - if (layoutManager.tv) { - scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false); - } - - load(dlg); - loadCategories(dlg, options); - dialogHelper.open(dlg); + dlg.addEventListener('change', function () { + settingsChanged = true; }); - }); - } - return { - show: showEditor - }; -}); + dlg.addEventListener('close', function () { + if (layoutManager.tv) { + scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false); + } + + save(dlg); + saveCategories(dlg, options); + + if (settingsChanged) { + resolve(); + } else { + reject(); + } + }); + + dlg.querySelector('.btnCancel').addEventListener('click', function () { + dialogHelper.close(dlg); + }); + + if (layoutManager.tv) { + scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false); + } + + load(dlg); + loadCategories(dlg, options); + dialogHelper.open(dlg); + }); + }); +} + +export default { + show: showEditor +}; diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js index c8fd8021e3..a7b32d887d 100644 --- a/src/components/guide/guide.js +++ b/src/components/guide/guide.js @@ -1,1182 +1,1198 @@ -define(['require', 'inputManager', 'browser', 'globalize', 'connectionManager', 'scrollHelper', 'serverNotifications', 'loading', 'datetime', 'focusManager', 'playbackManager', 'userSettings', 'imageLoader', 'events', 'layoutManager', 'itemShortcuts', 'dom', 'css!./guide.css', 'programStyles', 'material-icons', 'scrollStyles', 'emby-programcell', 'emby-button', 'paper-icon-button-light', 'emby-tabs', 'emby-scroller', 'flexStyles', 'webcomponents'], function (require, inputManager, browser, globalize, connectionManager, scrollHelper, serverNotifications, loading, datetime, focusManager, playbackManager, userSettings, imageLoader, events, layoutManager, itemShortcuts, dom) { - 'use strict'; +import inputManager from 'inputManager'; +import browser from 'browser'; +import globalize from 'globalize'; +import connectionManager from 'connectionManager'; +import scrollHelper from 'scrollHelper'; +import serverNotifications from 'serverNotifications'; +import loading from 'loading'; +import datetime from 'datetime'; +import focusManager from 'focusManager'; +import playbackManager from 'playbackManager'; +import * as userSettings from 'userSettings'; +import imageLoader from 'imageLoader'; +import events from 'events'; +import layoutManager from 'layoutManager'; +import itemShortcuts from 'itemShortcuts'; +import dom from 'dom'; +import 'css!./guide.css'; +import 'programStyles'; +import 'material-icons'; +import 'scrollStyles'; +import 'emby-programcell'; +import 'emby-button'; +import 'paper-icon-button-light'; +import 'emby-tabs'; +import 'emby-scroller'; +import 'flexStyles'; +import 'webcomponents'; - playbackManager = playbackManager.default || playbackManager; - browser = browser.default || browser; - loading = loading.default || loading; - layoutManager = layoutManager.default || layoutManager; - focusManager = focusManager.default || focusManager; - scrollHelper = scrollHelper.default || scrollHelper; - serverNotifications = serverNotifications.default || serverNotifications; - - function showViewSettings(instance) { - require(['guide-settings-dialog'], function (guideSettingsDialog) { - guideSettingsDialog.show(instance.categoryOptions).then(function () { - instance.refresh(); - }); +function showViewSettings(instance) { + import('guide-settings-dialog').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; } - function updateProgramCellOnScroll(cell, scrollPct) { - var left = cell.posLeft; - if (!left) { - left = parseFloat(cell.style.left.replace('%', '')); - cell.posLeft = left; - } - var 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); - var right = left + width; - var newPct = Math.max(Math.min(scrollPct, right), left); + const offset = newPct - left; + const pctOfWidth = (offset / width) * 100; - var offset = newPct - left; - var pctOfWidth = (offset / width) * 100; - - var guideProgramName = cell.guideProgramName; - if (!guideProgramName) { - guideProgramName = cell.querySelector('.guideProgramName'); - cell.guideProgramName = guideProgramName; - } - - var 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 guideProgramName = cell.guideProgramName; + if (!guideProgramName) { + guideProgramName = cell.querySelector('.guideProgramName'); + cell.guideProgramName = guideProgramName; } - var isUpdatingProgramCellScroll = false; - function updateProgramCellsOnScroll(programGrid, programCells) { - if (isUpdatingProgramCellScroll) { - return; - } - - isUpdatingProgramCellScroll = true; - - requestAnimationFrame(function () { - var scrollLeft = programGrid.scrollLeft; - - var scrollPct = scrollLeft ? (scrollLeft / programGrid.scrollWidth) * 100 : 0; - - for (var i = 0, length = programCells.length; i < length; i++) { - updateProgramCellOnScroll(programCells[i], scrollPct); - } - - isUpdatingProgramCellScroll = false; - }); + let caret = cell.caret; + if (!caret) { + caret = cell.querySelector('.guide-programNameCaret'); + cell.caret = caret; } - function onProgramGridClick(e) { - if (!layoutManager.tv) { - return; - } - - var programCell = dom.parentWithClass(e.target, 'programCell'); - if (programCell) { - var startDate = programCell.getAttribute('data-startdate'); - var endDate = programCell.getAttribute('data-enddate'); - startDate = datetime.parseISO8601Date(startDate, { toLocal: true }).getTime(); - endDate = datetime.parseISO8601Date(endDate, { toLocal: true }).getTime(); - - var now = new Date().getTime(); - if (now >= startDate && now < endDate) { - var channelId = programCell.getAttribute('data-channelid'); - var serverId = programCell.getAttribute('data-serverid'); - - e.preventDefault(); - e.stopPropagation(); - - playbackManager.play({ - ids: [channelId], - serverId: serverId - }); - } + 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'); } } +} - function Guide(options) { - var self = this; - var items = {}; +let isUpdatingProgramCellScroll = false; +function updateProgramCellsOnScroll(programGrid, programCells) { + if (isUpdatingProgramCellScroll) { + return; + } - self.options = options; - self.categoryOptions = { categories: [] }; + isUpdatingProgramCellScroll = true; - // 30 mins - var cellCurationMinutes = 30; - var cellDurationMs = cellCurationMinutes * 60 * 1000; - var msPerDay = 86400000; + requestAnimationFrame(function () { + const scrollLeft = programGrid.scrollLeft; - var currentDate; - var currentStartIndex = 0; - var currentChannelLimit = 0; - var autoRefreshInterval; - var programCells; - var lastFocusDirection; - var programGrid; + const scrollPct = scrollLeft ? (scrollLeft / programGrid.scrollWidth) * 100 : 0; - 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, 'SeriesTimerCreated', onSeriesTimerCreated); - events.off(serverNotifications, 'TimerCancelled', onTimerCancelled); - events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); - - setScrollEvents(options.element, false); - itemShortcuts.off(options.element); - items = {}; - }; - - function restartAutoRefresh() { - stopAutoRefresh(); - - var intervalMs = 60000 * 15; // (minutes) - - autoRefreshInterval = setInterval(function () { - self.refresh(); - }, intervalMs); + for (const programCell of programCells) { + updateProgramCellOnScroll(programCell, scrollPct); } - function stopAutoRefresh() { - if (autoRefreshInterval) { - clearInterval(autoRefreshInterval); - autoRefreshInterval = null; - } - } - - function normalizeDateToTimeslot(date) { - var 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) { - var apiClient = connectionManager.getApiClient(options.serverId); - - var channelQuery = { - - StartIndex: 0, - EnableFavoriteSorting: userSettings.get('livetv-favoritechannelsattop') !== 'false' - }; - - channelQuery.UserId = apiClient.getCurrentUserId(); - - var channelLimit = 500; - currentChannelLimit = channelLimit; - - showLoading(); - - channelQuery.StartIndex = currentStartIndex; - channelQuery.Limit = channelLimit; - channelQuery.AddCurrentProgram = false; - channelQuery.EnableUserData = false; - channelQuery.EnableImageTypes = 'Primary'; - - var categories = self.categoryOptions.categories || []; - var displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; - var displaySportsContent = !categories.length || categories.indexOf('sports') !== -1; - var displayNewsContent = !categories.length || categories.indexOf('news') !== -1; - var displayKidsContent = !categories.length || categories.indexOf('kids') !== -1; - var 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; - } - - var 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 - var 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 - var allowIndicators = dom.getWindowSize().innerWidth >= 600; - - var 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 ? false : true - }; - - apiClient.getLiveTvChannels(channelQuery).then(function (channelsResult) { - var btnPreviousPage = context.querySelector('.btnPreviousPage'); - var 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'); - } - - var programFields = []; - - var 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) { - renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender); - - 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) { - var 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) { - var 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, options, listInfo) { - var html = ''; - - var startMs = date.getTime(); - var endMs = startMs + msPerDay - 1; - - var outerCssClass = layoutManager.tv ? 'channelPrograms channelPrograms-tv' : 'channelPrograms'; - - html += '
'; - - var clickAction = layoutManager.tv ? 'link' : 'programdialog'; - - var categories = self.categoryOptions.categories || []; - var displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; - var displaySportsContent = !categories.length || categories.indexOf('sports') !== -1; - var displayNewsContent = !categories.length || categories.indexOf('news') !== -1; - var displayKidsContent = !categories.length || categories.indexOf('kids') !== -1; - var displaySeriesContent = !categories.length || categories.indexOf('series') !== -1; - var enableColorCodedBackgrounds = userSettings.get('guide-colorcodedbackgrounds') === 'true'; - - var programsFound; - var now = new Date().getTime(); - - for (var i = listInfo.startIndex, length = programs.length; i < length; i++) { - var program = programs[i]; - - if (program.ChannelId !== channel.Id) { - if (programsFound) { - break; - } - - continue; - } - - programsFound = true; - listInfo.startIndex++; - - parseDates(program); - - var startDateLocalMs = program.StartDateLocal.getTime(); - var endDateLocalMs = program.EndDateLocal.getTime(); - - if (endDateLocalMs < startMs) { - continue; - } - - if (startDateLocalMs > endMs) { - break; - } - - items[program.Id] = program; - - var renderStartMs = Math.max(startDateLocalMs, startMs); - var startPercent = (startDateLocalMs - startMs) / msPerDay; - startPercent *= 100; - startPercent = Math.max(startPercent, 0); - - var renderEndMs = Math.min(endDateLocalMs, endMs); - var endPercent = (renderEndMs - renderStartMs) / msPerDay; - endPercent *= 100; - - var cssClass = 'programCell itemAction'; - var accentCssClass = null; - var 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'; - } - - var timerAttributes = ''; - if (program.TimerId) { - timerAttributes += ' data-timerid="' + program.TimerId + '"'; - } - if (program.SeriesTimerId) { - timerAttributes += ' data-seriestimerid="' + program.SeriesTimerId + '"'; - } - - var isAttribute = endPercent >= 2 ? ' is="emby-programcell"' : ''; - - html += ''; - - if (displayInnerContent) { - var guideProgramNameClass = 'guideProgramName'; - - html += '
'; - - html += '
'; - - html += '
' + program.Name; - - var indicatorHtml = null; - if (program.IsLive && options.showLiveIndicator) { - indicatorHtml = '' + globalize.translate('Live') + ''; - } else if (program.IsPremiere && options.showPremiereIndicator) { - indicatorHtml = '' + globalize.translate('Premiere') + ''; - } else if (program.IsSeries && !program.IsRepeat && options.showNewIndicator) { - indicatorHtml = '' + globalize.translate('AttributeNew') + ''; - } else if (program.IsSeries && program.IsRepeat && options.showRepeatIndicator) { - indicatorHtml = '' + globalize.translate('Repeat') + ''; - } - html += indicatorHtml || ''; - - if ((program.EpisodeTitle && options.showEpisodeTitle)) { - html += '
'; - - if (program.EpisodeTitle && options.showEpisodeTitle) { - html += '' + program.EpisodeTitle + ''; - } - html += '
'; - } - - html += '
'; - - if (program.IsHD && options.showHdIcon) { - if (layoutManager.tv) { - html += '
HD
'; - } else { - html += '
HD
'; - } - } - - html += getTimerIndicator(program); - - html += '
'; - } - - html += ''; - } - - html += '
'; - - return html; - } - - function renderChannelHeaders(context, channels, apiClient) { - var html = ''; - - for (var i = 0, length = channels.length; i < length; i++) { - var channel = channels[i]; - var hasChannelImage = channel.ImageTags.Primary; - - var cssClass = 'guide-channelHeaderCell itemAction'; - - if (layoutManager.tv) { - cssClass += ' guide-channelHeaderCell-tv'; - } - - var title = []; - if (channel.ChannelNumber) { - title.push(channel.ChannelNumber); - } - if (channel.Name) { - title.push(channel.Name); - } - - html += ''; - } - - var channelList = context.querySelector('.channelsContainer'); - channelList.innerHTML = html; - imageLoader.lazyChildren(channelList); - } - - function renderPrograms(context, date, channels, programs, options) { - var listInfo = { - startIndex: 0 - }; - - var html = []; - - for (var i = 0, length = channels.length; i < length; i++) { - html.push(getChannelProgramsHtml(context, date, channels[i], programs, options, listInfo)); - } - - programGrid.innerHTML = html.join(''); - - programCells = programGrid.querySelectorAll('[is=emby-programcell]'); - - updateProgramCellsOnScroll(programGrid, programCells); - } - - function getProgramSortOrder(program, channels) { - var channelId = program.ChannelId; - var channelIndex = -1; - - for (var i = 0, length = channels.length; i < length; i++) { - if (channelId === channels[i].Id) { - channelIndex = i; - break; - } - } - - var start = datetime.parseISO8601Date(program.StartDate, { toLocal: true }); - - return (channelIndex * 10000000) + (start.getTime() / 60000); - } - - function renderGuide(context, date, channels, programs, renderOptions, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender) { - programs.sort(function (a, b) { - return getProgramSortOrder(a, channels) - getProgramSortOrder(b, channels); - }); - - var activeElement = document.activeElement; - var itemId = activeElement && activeElement.getAttribute ? activeElement.getAttribute('data-id') : null; - var channelRowId = null; - - if (activeElement) { - channelRowId = dom.parentWithClass(activeElement, 'channelPrograms'); - channelRowId = channelRowId && channelRowId.getAttribute ? channelRowId.getAttribute('data-channelid') : null; - } - - renderChannelHeaders(context, channels, apiClient); - - var startDate = date; - var endDate = new Date(startDate.getTime() + msPerDay); - context.querySelector('.timeslotHeaders').innerHTML = getTimeslotHeadersHtml(startDate, endDate); - items = {}; - renderPrograms(context, date, channels, programs, renderOptions); - - if (focusProgramOnRender) { - focusProgram(context, itemId, channelRowId, focusToTimeMs, startTimeOfDayMs); - } - - scrollProgramGridToTimeMs(context, scrollToTimeMs, startTimeOfDayMs); - } - - function scrollProgramGridToTimeMs(context, scrollToTimeMs, startTimeOfDayMs) { - scrollToTimeMs -= startTimeOfDayMs; - - var pct = scrollToTimeMs / msPerDay; - - programGrid.scrollTop = 0; - - var scrollPos = pct * programGrid.scrollWidth; - - nativeScrollTo(programGrid, scrollPos, true); - } - - function focusProgram(context, itemId, channelRowId, focusToTimeMs, startTimeOfDayMs) { - var focusElem; - if (itemId) { - focusElem = context.querySelector('[data-id="' + itemId + '"]'); - } - - if (focusElem) { - focusManager.focus(focusElem); - } else { - var autoFocusParent; - - if (channelRowId) { - autoFocusParent = context.querySelector('[data-channelid="' + channelRowId + '"]'); - } - - if (!autoFocusParent) { - autoFocusParent = programGrid; - } - - focusToTimeMs -= startTimeOfDayMs; - - var pct = (focusToTimeMs / msPerDay) * 100; - - var programCell = autoFocusParent.querySelector('.programCell'); - - while (programCell) { - var left = (programCell.style.left || '').replace('%', ''); - left = left ? parseFloat(left) : 0; - var 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); - } - } - } - - var lastGridScroll = 0; - var lastHeaderScroll = 0; - var scrollXPct = 0; - function onProgramGridScroll(context, elem, timeslotHeaders) { - if ((new Date().getTime() - lastHeaderScroll) >= 1000) { - lastGridScroll = new Date().getTime(); - - var scrollLeft = elem.scrollLeft; - scrollXPct = (scrollLeft * 100) / elem.scrollWidth; - nativeScrollTo(timeslotHeaders, 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) { - var newStartDate = normalizeDateToTimeslot(date); - currentDate = newStartDate; - - reloadGuide(page, newStartDate, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender); - } - - function getDateTabText(date, isActive, tabIndex) { - var cssClass = isActive ? 'emby-tab-button guide-date-tab-button emby-tab-button-active' : 'emby-tab-button guide-date-tab-button'; - - var html = ''; - - return html; - } - - function setDateRange(page, guideInfo) { - var today = new Date(); - var nowHours = today.getHours(); - today.setHours(nowHours, 0, 0, 0); - - var start = datetime.parseISO8601Date(guideInfo.StartDate, { toLocal: true }); - var 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)); - - var dateTabsHtml = ''; - var tabIndex = 0; - - // TODO: Use date-fns - var date = new Date(); - - if (currentDate) { - date.setTime(currentDate.getTime()); - } - - date.setHours(nowHours, 0, 0, 0); - - var startTimeOfDayMs = (start.getHours() * 60 * 60 * 1000); - startTimeOfDayMs += start.getMinutes() * 60 * 1000; - - while (start <= end) { - var 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(); - - var newDate = new Date(); - var newDateHours = newDate.getHours(); - var scrollToTimeMs = newDateHours * 60 * 60 * 1000; - - var minutes = newDate.getMinutes(); - if (minutes >= 30) { - scrollToTimeMs += 30 * 60 * 1000; - } - - var focusToTimeMs = ((newDateHours * 60) + minutes) * 60 * 1000; - changeDate(page, date, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, layoutManager.tv); - } - - function reloadPage(page) { - showLoading(); - - var apiClient = connectionManager.getApiClient(options.serverId); - - apiClient.getLiveTvGuideInfo().then(function (guideInfo) { - setDateRange(page, guideInfo); - }); - } - - function getChannelProgramsFocusableElements(container) { - var elements = container.querySelectorAll('.programCell'); - - var list = []; - // add 1 to avoid programs that are out of view to the left - var currentScrollXPct = scrollXPct + 1; - - for (var i = 0, length = elements.length; i < length; i++) { - var elem = elements[i]; - - var left = (elem.style.left || '').replace('%', ''); - left = left ? parseFloat(left) : 0; - var width = (elem.style.width || '').replace('%', ''); - width = width ? parseFloat(width) : 0; - - if ((left + width) >= currentScrollXPct) { - list.push(elem); - } - } - - return list; - } - - function onInputCommand(e) { - var target = e.target; - var programCell = dom.parentWithClass(target, 'programCell'); - var container; - var channelPrograms; - var focusableElements; - var 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; - } + 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(); - } - function onScrollerFocus(e) { - var target = e.target; - var programCell = dom.parentWithClass(target, 'programCell'); - - if (programCell) { - var focused = target; - - var id = focused.getAttribute('data-id'); - var 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') { - var verticalScroller = dom.parentWithClass(target, 'guideVerticalScroller'); - if (verticalScroller) { - var focusedElement = programCell || dom.parentWithTag(target, 'BUTTON'); - verticalScroller.toCenter(focusedElement, true); - } - } - } - - function setScrollEvents(view, enabled) { - if (layoutManager.tv) { - var guideVerticalScroller = view.querySelector('.guideVerticalScroller'); - - if (enabled) { - inputManager.on(guideVerticalScroller, onInputCommand); - } else { - inputManager.off(guideVerticalScroller, onInputCommand); - } - } - } - - function onTimerCreated(e, apiClient, data) { - var programId = data.ProgramId; - // This could be null, not supported by all tv providers - var newTimerId = data.Id; - - // find guide cells by program id, ensure timer icon - var cells = options.element.querySelectorAll('.programCell[data-id="' + programId + '"]'); - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - - var icon = cell.querySelector('.timerIcon'); - if (!icon) { - cell.querySelector('.guideProgramName').insertAdjacentHTML('beforeend', ''); - } - - if (newTimerId) { - cell.setAttribute('data-timerid', newTimerId); - } - } - } - - function onSeriesTimerCreated(e, apiClient, data) { - } - - function onTimerCancelled(e, apiClient, data) { - var id = data.Id; - // find guide cells by timer id, remove timer icon - var cells = options.element.querySelectorAll('.programCell[data-timerid="' + id + '"]'); - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - var icon = cell.querySelector('.timerIcon'); - if (icon) { - icon.parentNode.removeChild(icon); - } - cell.removeAttribute('data-timerid'); - } - } - - function onSeriesTimerCancelled(e, apiClient, data) { - var id = data.Id; - // find guide cells by timer id, remove timer icon - var cells = options.element.querySelectorAll('.programCell[data-seriestimerid="' + id + '"]'); - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - var icon = cell.querySelector('.seriesTimerIcon'); - if (icon) { - icon.parentNode.removeChild(icon); - } - cell.removeAttribute('data-seriestimerid'); - } - } - - require(['text!./tvguide.template.html'], function (template) { - var context = options.element; - - context.classList.add('tvguide'); - - context.innerHTML = globalize.translateHtml(template, 'core'); - - programGrid = context.querySelector('.programGrid'); - var timeslotHeaders = context.querySelector('.timeslotHeaders'); - - if (layoutManager.tv) { - dom.addEventListener(context.querySelector('.guideVerticalScroller'), 'focus', onScrollerFocus, { - capture: true, - passive: true - }); - } else if (layoutManager.desktop) { - timeslotHeaders.classList.add('timeslotHeaders-desktop'); - } - - if (browser.iOS || browser.osx) { - context.querySelector('.channelsContainer').classList.add('noRubberBanding'); - - programGrid.classList.add('noRubberBanding'); - } - - dom.addEventListener(programGrid, 'scroll', function (e) { - onProgramGridScroll(context, this, timeslotHeaders); - }, { - passive: true + playbackManager.play({ + ids: [channelId], + serverId: serverId }); + } + } +} - dom.addEventListener(timeslotHeaders, 'scroll', function () { - onTimeslotHeadersScroll(context, this); - }, { - passive: true - }); +function Guide(options) { + const self = this; + let items = {}; - programGrid.addEventListener('click', onProgramGridClick); + self.options = options; + self.categoryOptions = { categories: [] }; - context.querySelector('.btnNextPage').addEventListener('click', function () { - currentStartIndex += currentChannelLimit; - reloadPage(context); - restartAutoRefresh(); - }); + // 30 mins + const cellCurationMinutes = 30; + const cellDurationMs = cellCurationMinutes * 60 * 1000; + const msPerDay = 86400000; - context.querySelector('.btnPreviousPage').addEventListener('click', function () { - currentStartIndex = Math.max(currentStartIndex - currentChannelLimit, 0); - reloadPage(context); - restartAutoRefresh(); - }); + let currentDate; + let currentStartIndex = 0; + let currentChannelLimit = 0; + let autoRefreshInterval; + let programCells; + let lastFocusDirection; + let programGrid; - context.querySelector('.btnGuideViewSettings').addEventListener('click', function () { - showViewSettings(self); - restartAutoRefresh(); - }); + self.refresh = function () { + currentDate = null; + reloadPage(options.element); + restartAutoRefresh(); + }; - context.querySelector('.guideDateTabs').addEventListener('tabchange', function (e) { - var allTabButtons = e.target.querySelectorAll('.guide-date-tab-button'); - - var tabButton = allTabButtons[parseInt(e.detail.selectedTabIndex)]; - if (tabButton) { - var previousButton = e.detail.previousIndex == null ? null : allTabButtons[parseInt(e.detail.previousIndex)]; - - var date = new Date(); - date.setTime(parseInt(tabButton.getAttribute('data-date'))); - - var scrollWidth = programGrid.scrollWidth; - var scrollToTimeMs; - if (scrollWidth) { - scrollToTimeMs = (programGrid.scrollLeft / scrollWidth) * msPerDay; - } else { - scrollToTimeMs = 0; - } - - if (previousButton) { - var previousDate = new Date(); - previousDate.setTime(parseInt(previousButton.getAttribute('data-date'))); - - scrollToTimeMs += (previousDate.getHours() * 60 * 60 * 1000); - scrollToTimeMs += (previousDate.getMinutes() * 60 * 1000); - } - - var startTimeOfDayMs = (date.getHours() * 60 * 60 * 1000); - startTimeOfDayMs += (date.getMinutes() * 60 * 1000); - - changeDate(context, date, scrollToTimeMs, scrollToTimeMs, startTimeOfDayMs, false); - } - }); - - setScrollEvents(context, true); - itemShortcuts.on(context); - - events.trigger(self, 'load'); - - events.on(serverNotifications, 'TimerCreated', onTimerCreated); - events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated); - events.on(serverNotifications, 'TimerCancelled', onTimerCancelled); - events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); + 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, 'SeriesTimerCreated', onSeriesTimerCreated); + 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 = connectionManager.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 ? false : true + }; + + 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) { + renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender); + + hideLoading(); + }); }); } - return Guide; -}); + 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, options, 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 += '
' + program.Name; + + let indicatorHtml = null; + if (program.IsLive && options.showLiveIndicator) { + indicatorHtml = '' + globalize.translate('Live') + ''; + } else if (program.IsPremiere && options.showPremiereIndicator) { + indicatorHtml = '' + globalize.translate('Premiere') + ''; + } else if (program.IsSeries && !program.IsRepeat && options.showNewIndicator) { + indicatorHtml = '' + globalize.translate('AttributeNew') + ''; + } else if (program.IsSeries && program.IsRepeat && options.showRepeatIndicator) { + indicatorHtml = '' + globalize.translate('Repeat') + ''; + } + html += indicatorHtml || ''; + + if ((program.EpisodeTitle && options.showEpisodeTitle)) { + html += '
'; + + if (program.EpisodeTitle && options.showEpisodeTitle) { + html += '' + program.EpisodeTitle + ''; + } + html += '
'; + } + + html += '
'; + + if (program.IsHD && options.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, options) { + const listInfo = { + startIndex: 0 + }; + + const html = []; + + for (const channel of channels) { + html.push(getChannelProgramsHtml(context, date, channel, programs, options, 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, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender) { + programs.sort(function (a, b) { + return getProgramSortOrder(a, channels) - getProgramSortOrder(b, channels); + }); + + const activeElement = document.activeElement; + const itemId = activeElement && activeElement.getAttribute ? activeElement.getAttribute('data-id') : null; + let channelRowId = null; + + if (activeElement) { + channelRowId = dom.parentWithClass(activeElement, 'channelPrograms'); + channelRowId = 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 (focusProgramOnRender) { + focusProgram(context, itemId, channelRowId, focusToTimeMs, startTimeOfDayMs); + } + + scrollProgramGridToTimeMs(context, scrollToTimeMs, 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, timeslotHeaders) { + if ((new Date().getTime() - lastHeaderScroll) >= 1000) { + lastGridScroll = new Date().getTime(); + + const scrollLeft = elem.scrollLeft; + scrollXPct = (scrollLeft * 100) / elem.scrollWidth; + nativeScrollTo(timeslotHeaders, 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 = connectionManager.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 onSeriesTimerCreated(e, apiClient, data) { + } + + 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'); + } + } + + import('text!./tvguide.template.html').then(({default: template}) => { + const context = options.element; + + context.classList.add('tvguide'); + + context.innerHTML = globalize.translateHtml(template, 'core'); + + programGrid = context.querySelector('.programGrid'); + const timeslotHeaders = context.querySelector('.timeslotHeaders'); + + if (layoutManager.tv) { + dom.addEventListener(context.querySelector('.guideVerticalScroller'), 'focus', onScrollerFocus, { + capture: true, + passive: true + }); + } else if (layoutManager.desktop) { + timeslotHeaders.classList.add('timeslotHeaders-desktop'); + } + + if (browser.iOS || browser.osx) { + context.querySelector('.channelsContainer').classList.add('noRubberBanding'); + + programGrid.classList.add('noRubberBanding'); + } + + dom.addEventListener(programGrid, 'scroll', function (e) { + onProgramGridScroll(context, this, timeslotHeaders); + }, { + passive: true + }); + + dom.addEventListener(timeslotHeaders, 'scroll', function () { + onTimeslotHeadersScroll(context, this); + }, { + passive: true + }); + + programGrid.addEventListener('click', onProgramGridClick); + + context.querySelector('.btnNextPage').addEventListener('click', function () { + currentStartIndex += currentChannelLimit; + reloadPage(context); + restartAutoRefresh(); + }); + + context.querySelector('.btnPreviousPage').addEventListener('click', function () { + currentStartIndex = Math.max(currentStartIndex - currentChannelLimit, 0); + reloadPage(context); + restartAutoRefresh(); + }); + + context.querySelector('.btnGuideViewSettings').addEventListener('click', function () { + showViewSettings(self); + restartAutoRefresh(); + }); + + context.querySelector('.guideDateTabs').addEventListener('tabchange', function (e) { + const allTabButtons = e.target.querySelectorAll('.guide-date-tab-button'); + + const tabButton = allTabButtons[parseInt(e.detail.selectedTabIndex)]; + if (tabButton) { + const previousButton = e.detail.previousIndex == null ? null : allTabButtons[parseInt(e.detail.previousIndex)]; + + const date = new Date(); + date.setTime(parseInt(tabButton.getAttribute('data-date'))); + + 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'))); + + scrollToTimeMs += (previousDate.getHours() * 60 * 60 * 1000); + scrollToTimeMs += (previousDate.getMinutes() * 60 * 1000); + } + + let startTimeOfDayMs = (date.getHours() * 60 * 60 * 1000); + startTimeOfDayMs += (date.getMinutes() * 60 * 1000); + + changeDate(context, date, scrollToTimeMs, scrollToTimeMs, startTimeOfDayMs, false); + } + }); + + setScrollEvents(context, true); + itemShortcuts.on(context); + + events.trigger(self, 'load'); + + events.on(serverNotifications, 'TimerCreated', onTimerCreated); + events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated); + events.on(serverNotifications, 'TimerCancelled', onTimerCancelled); + events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); + + self.refresh(); + }); +} + +export default Guide; diff --git a/src/components/imageDownloader/imageDownloader.js b/src/components/imageDownloader/imageDownloader.js index 2be2ef09b2..3f78a5ecbb 100644 --- a/src/components/imageDownloader/imageDownloader.js +++ b/src/components/imageDownloader/imageDownloader.js @@ -8,7 +8,6 @@ import browser from 'browser'; import layoutManager from 'layoutManager'; import scrollHelper from 'scrollHelper'; import globalize from 'globalize'; -import require from 'require'; import 'emby-checkbox'; import 'paper-icon-button-light'; import 'emby-button'; @@ -317,7 +316,7 @@ import 'cardStyle'; function showEditor(itemId, serverId, itemType) { loading.show(); - require(['text!./imageDownloader.template.html'], function (template) { + import('text!./imageDownloader.template.html').then(({default: template}) => { const apiClient = connectionManager.getApiClient(serverId); currentItemId = itemId; diff --git a/src/components/metadataEditor/metadataEditor.js b/src/components/metadataEditor/metadataEditor.js index be44e86b44..162da8984a 100644 --- a/src/components/metadataEditor/metadataEditor.js +++ b/src/components/metadataEditor/metadataEditor.js @@ -6,7 +6,6 @@ import loading from 'loading'; import focusManager from 'focusManager'; import connectionManager from 'connectionManager'; import globalize from 'globalize'; -import require from 'require'; import shell from 'shell'; import 'emby-checkbox'; import 'emby-input'; @@ -37,7 +36,7 @@ import 'flexStyles'; function submitUpdatedItem(form, item) { function afterContentTypeUpdated() { - require(['toast'], function (toast) { + import('toast').then(({default: toast}) => { toast(globalize.translate('MessageItemSaved')); }); @@ -227,7 +226,7 @@ import 'flexStyles'; } function editPerson(context, person, index) { - require(['personEditor'], function (personEditor) { + import('personEditor').then(({default: personEditor}) => { personEditor.show(person).then(function (updatedPerson) { const isNew = index === -1; @@ -246,14 +245,14 @@ import 'flexStyles'; if (parentId) { reload(context, parentId, item.ServerId); } else { - require(['appRouter'], function (appRouter) { + import('appRouter').then(({default: appRouter}) => { appRouter.goHome(); }); } } function showMoreMenu(context, button, user) { - require(['itemContextMenu'], function (itemContextMenu) { + import('itemContextMenu').then(({default: itemContextMenu}) => { var item = currentItem; itemContextMenu.show({ diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index c6c2a123b4..6048c918c7 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -1,940 +1,954 @@ -define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageLoader', 'playbackManager', 'nowPlayingHelper', 'events', 'connectionManager', 'apphost', 'globalize', 'layoutManager', 'userSettings', 'cardBuilder', 'itemContextMenu', 'cardStyle', 'emby-itemscontainer', 'css!./remotecontrol.css', 'emby-ratingbutton'], function (browser, datetime, backdrop, libraryBrowser, listView, imageLoader, playbackManager, nowPlayingHelper, events, connectionManager, appHost, globalize, layoutManager, userSettings, cardBuilder, itemContextMenu) { - 'use strict'; +import datetime from 'datetime'; +import backdrop from 'backdrop'; +import listView from 'listView'; +import imageLoader from 'imageLoader'; +import playbackManager from 'playbackManager'; +import nowPlayingHelper from 'nowPlayingHelper'; +import events from 'events'; +import connectionManager from 'connectionManager'; +import appHost from 'apphost'; +import globalize from 'globalize'; +import layoutManager from 'layoutManager'; +import * as userSettings from 'userSettings'; +import cardBuilder from 'cardBuilder'; +import itemContextMenu from 'itemContextMenu'; +import 'cardStyle'; +import 'emby-itemscontainer'; +import 'css!./remotecontrol.css'; +import 'emby-ratingbutton'; - playbackManager = playbackManager.default || playbackManager; - layoutManager = layoutManager.default || layoutManager; +/*eslint prefer-const: "error"*/ - var showMuteButton = true; - var showVolumeSlider = true; +let showMuteButton = true; +let showVolumeSlider = true; - function showAudioMenu(context, player, button, item) { - var currentIndex = playbackManager.getAudioStreamIndex(player); - var streams = playbackManager.audioTracks(player); - var menuItems = streams.map(function (s) { - var menuItem = { - name: s.DisplayTitle, - id: s.Index - }; +function showAudioMenu(context, player, button, item) { + const currentIndex = playbackManager.getAudioStreamIndex(player); + const streams = playbackManager.audioTracks(player); + const menuItems = streams.map(function (s) { + const menuItem = { + name: s.DisplayTitle, + id: s.Index + }; - if (s.Index == currentIndex) { - menuItem.selected = true; - } - - return menuItem; - }); - - require(['actionsheet'], function (actionsheet) { - actionsheet.show({ - items: menuItems, - positionTo: button, - callback: function (id) { - playbackManager.setAudioStreamIndex(parseInt(id), player); - } - }); - }); - } - - function showSubtitleMenu(context, player, button, item) { - var currentIndex = playbackManager.getSubtitleStreamIndex(player); - var streams = playbackManager.subtitleTracks(player); - var menuItems = streams.map(function (s) { - var menuItem = { - name: s.DisplayTitle, - id: s.Index - }; - - if (s.Index == currentIndex) { - menuItem.selected = true; - } - - return menuItem; - }); - menuItems.unshift({ - id: -1, - name: globalize.translate('ButtonOff'), - selected: currentIndex == null - }); - - require(['actionsheet'], function (actionsheet) { - actionsheet.show({ - items: menuItems, - positionTo: button, - callback: function (id) { - playbackManager.setSubtitleStreamIndex(parseInt(id), player); - } - }); - }); - } - - function getNowPlayingNameHtml(nowPlayingItem, includeNonNameInfo) { - return nowPlayingHelper.getNowPlayingNames(nowPlayingItem, includeNonNameInfo).map(function (i) { - return i.text; - }).join('
'); - } - - function seriesImageUrl(item, options) { - if (item.Type !== 'Episode') { - return null; + if (s.Index == currentIndex) { + menuItem.selected = true; } - options = options || {}; - options.type = options.type || 'Primary'; - if (options.type === 'Primary' && item.SeriesPrimaryImageTag) { - options.tag = item.SeriesPrimaryImageTag; + return menuItem; + }); + + import('actionsheet').then(({ default: actionsheet }) => { + actionsheet.show({ + items: menuItems, + positionTo: button, + callback: function (id) { + playbackManager.setAudioStreamIndex(parseInt(id), player); + } + }); + }); +} + +function showSubtitleMenu(context, player, button, item) { + const currentIndex = playbackManager.getSubtitleStreamIndex(player); + const streams = playbackManager.subtitleTracks(player); + const menuItems = streams.map(function (s) { + const menuItem = { + name: s.DisplayTitle, + id: s.Index + }; + + if (s.Index == currentIndex) { + menuItem.selected = true; + } + + return menuItem; + }); + menuItems.unshift({ + id: -1, + name: globalize.translate('ButtonOff'), + selected: currentIndex == null + }); + + import('actionsheet').then(({ default: actionsheet }) => { + actionsheet.show({ + items: menuItems, + positionTo: button, + callback: function (id) { + playbackManager.setSubtitleStreamIndex(parseInt(id), player); + } + }); + }); +} + +function getNowPlayingNameHtml(nowPlayingItem, includeNonNameInfo) { + return nowPlayingHelper.getNowPlayingNames(nowPlayingItem, includeNonNameInfo).map(function (i) { + return i.text; + }).join('
'); +} + +function seriesImageUrl(item, options) { + if (item.Type !== 'Episode') { + return null; + } + + options = options || {}; + options.type = options.type || 'Primary'; + if (options.type === 'Primary' && item.SeriesPrimaryImageTag) { + options.tag = item.SeriesPrimaryImageTag; + return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); + } + + if (options.type === 'Thumb') { + if (item.SeriesThumbImageTag) { + options.tag = item.SeriesThumbImageTag; return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); } - if (options.type === 'Thumb') { - if (item.SeriesThumbImageTag) { - options.tag = item.SeriesThumbImageTag; - return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } - - if (item.ParentThumbImageTag) { - options.tag = item.ParentThumbImageTag; - return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); - } + if (item.ParentThumbImageTag) { + options.tag = item.ParentThumbImageTag; + return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); } - - return null; } - function imageUrl(item, options) { - options = options || {}; - options.type = options.type || 'Primary'; + return null; +} - if (item.ImageTags && item.ImageTags[options.type]) { - options.tag = item.ImageTags[options.type]; - return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.PrimaryImageItemId || item.Id, options); - } +function imageUrl(item, options) { + options = options || {}; + options.type = options.type || 'Primary'; - if (item.AlbumId && item.AlbumPrimaryImageTag) { - options.tag = item.AlbumPrimaryImageTag; - return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); - } - - return null; + if (item.ImageTags && item.ImageTags[options.type]) { + options.tag = item.ImageTags[options.type]; + return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.PrimaryImageItemId || item.Id, options); } - function updateNowPlayingInfo(context, state, serverId) { - var item = state.NowPlayingItem; - var displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; - if (typeof item !== 'undefined') { - var nowPlayingServerId = (item.ServerId || serverId); - if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { - var songName = item.Name; - var artistsSeries = ''; - var albumName = ''; - if (item.Artists != null) { - if (item.ArtistItems != null) { - for (const artist of item.ArtistItems) { - let artistName = artist.Name; - let artistId = artist.Id; - artistsSeries += `${artistName}`; - if (artist !== item.ArtistItems.slice(-1)[0]) { - artistsSeries += ', '; - } + if (item.AlbumId && item.AlbumPrimaryImageTag) { + options.tag = item.AlbumPrimaryImageTag; + return connectionManager.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); + } + + return null; +} + +function updateNowPlayingInfo(context, state, serverId) { + const item = state.NowPlayingItem; + const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; + if (typeof item !== 'undefined') { + const nowPlayingServerId = (item.ServerId || serverId); + if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { + const songName = item.Name; + let artistsSeries = ''; + let albumName = ''; + if (item.Artists != null) { + if (item.ArtistItems != null) { + for (const artist of item.ArtistItems) { + const artistName = artist.Name; + const artistId = artist.Id; + artistsSeries += `${artistName}`; + if (artist !== item.ArtistItems.slice(-1)[0]) { + artistsSeries += ', '; } - } else if (item.Artists) { - // For some reason, Chromecast Player doesn't return a item.ArtistItems object, so we need to fallback - // to normal item.Artists item. - // TODO: Normalise fields returned by all the players - for (const artist of item.Artists) { - artistsSeries += `${artist}`; - if (artist !== item.Artists.slice(-1)[0]) { - artistsSeries += ', '; - } + } + } else if (item.Artists) { + // For some reason, Chromecast Player doesn't return a item.ArtistItems object, so we need to fallback + // to normal item.Artists item. + // TODO: Normalise fields returned by all the players + for (const artist of item.Artists) { + artistsSeries += `${artist}`; + if (artist !== item.Artists.slice(-1)[0]) { + artistsSeries += ', '; } } } - if (item.Album != null) { - albumName = '` + item.Album + ''; - } - context.querySelector('.nowPlayingAlbum').innerHTML = albumName; - context.querySelector('.nowPlayingArtist').innerHTML = artistsSeries; - context.querySelector('.nowPlayingSongName').innerHTML = songName; - } else if (item.Type == 'Episode') { - if (item.SeasonName != null) { - var seasonName = item.SeasonName; - context.querySelector('.nowPlayingSeason').innerHTML = '${seasonName}`; - } - if (item.SeriesName != null) { - var seriesName = item.SeriesName; - if (item.SeriesId != null) { - context.querySelector('.nowPlayingSerie').innerHTML = '${seriesName}`; - } else { - context.querySelector('.nowPlayingSerie').innerHTML = seriesName; - } - } - context.querySelector('.nowPlayingEpisode').innerHTML = item.Name; - } else { - context.querySelector('.nowPlayingPageTitle').innerHTML = displayName; } - - if (displayName.length > 0 && item.Type != 'Audio' && item.Type != 'Episode') { - context.querySelector('.nowPlayingPageTitle').classList.remove('hide'); - } else { - context.querySelector('.nowPlayingPageTitle').classList.add('hide'); + if (item.Album != null) { + albumName = '` + item.Album + ''; } - - var url = item ? seriesImageUrl(item, { - maxHeight: 300 - }) || imageUrl(item, { - maxHeight: 300 - }) : null; - - let contextButton = context.querySelector('.btnToggleContextMenu'); - // We remove the previous event listener by replacing the item in each update event - const autoFocusContextButton = document.activeElement === contextButton; - let contextButtonClone = contextButton.cloneNode(true); - contextButton.parentNode.replaceChild(contextButtonClone, contextButton); - contextButton = context.querySelector('.btnToggleContextMenu'); - if (autoFocusContextButton) { - contextButton.focus(); + context.querySelector('.nowPlayingAlbum').innerHTML = albumName; + context.querySelector('.nowPlayingArtist').innerHTML = artistsSeries; + context.querySelector('.nowPlayingSongName').innerHTML = songName; + } else if (item.Type == 'Episode') { + if (item.SeasonName != null) { + const seasonName = item.SeasonName; + context.querySelector('.nowPlayingSeason').innerHTML = '${seasonName}`; } - const stopPlayback = !!layoutManager.mobile; - var options = { - play: false, - queue: false, - stopPlayback: stopPlayback, - clearQueue: true, - openAlbum: false, - positionTo: contextButton - }; - var apiClient = connectionManager.getApiClient(item.ServerId); - apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { - apiClient.getCurrentUser().then(function (user) { - contextButton.addEventListener('click', function () { - itemContextMenu.show(Object.assign({ - item: fullItem, - user: user - }, options)); - }); - }); - }); - setImageUrl(context, state, url); - if (item) { - backdrop.setBackdrops([item]); - apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { - var userData = fullItem.UserData || {}; - var likes = userData.Likes == null ? '' : userData.Likes; - context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - }); - } else { - backdrop.clearBackdrop(); - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - } - } - } - - function setImageUrl(context, state, url) { - var item = state.NowPlayingItem; - var imgContainer = context.querySelector('.nowPlayingPageImageContainer'); - - if (url) { - imgContainer.innerHTML = ''; - if (item.Type == 'Audio') { - context.querySelector('.nowPlayingPageImage').classList.add('nowPlayingPageImageAudio'); - context.querySelector('.nowPlayingPageImageContainer').classList.remove('nowPlayingPageImageAudio'); - } else { - context.querySelector('.nowPlayingPageImageContainer').classList.add('nowPlayingPageImagePoster'); - context.querySelector('.nowPlayingPageImage').classList.remove('nowPlayingPageImageAudio'); - } - } else { - imgContainer.innerHTML = '
'; - } - } - - function buttonVisible(btn, enabled) { - if (enabled) { - btn.classList.remove('hide'); - } else { - btn.classList.add('hide'); - } - } - - function updateSupportedCommands(context, commands) { - var all = context.querySelectorAll('.btnCommand'); - - for (var i = 0, length = all.length; i < length; i++) { - var enableButton = commands.indexOf(all[i].getAttribute('data-command')) !== -1; - all[i].disabled = !enableButton; - } - } - - return function () { - function toggleRepeat() { - switch (playbackManager.getRepeatMode()) { - case 'RepeatAll': - playbackManager.setRepeatMode('RepeatOne'); - break; - case 'RepeatOne': - playbackManager.setRepeatMode('RepeatNone'); - break; - case 'RepeatNone': - playbackManager.setRepeatMode('RepeatAll'); - } - } - - function updatePlayerState(player, context, state) { - lastPlayerState = state; - var item = state.NowPlayingItem; - var playerInfo = playbackManager.getPlayerInfo(); - var supportedCommands = playerInfo.supportedCommands; - currentPlayerSupportedCommands = supportedCommands; - var playState = state.PlayState || {}; - var isSupportedCommands = supportedCommands.includes('DisplayMessage') || supportedCommands.includes('SendString') || supportedCommands.includes('Select'); - buttonVisible(context.querySelector('.btnToggleFullscreen'), item && item.MediaType == 'Video' && supportedCommands.includes('ToggleFullscreen')); - updateAudioTracksDisplay(player, context); - updateSubtitleTracksDisplay(player, context); - - if (supportedCommands.includes('DisplayMessage') && !currentPlayer.isLocalPlayer) { - context.querySelector('.sendMessageSection').classList.remove('hide'); - } else { - context.querySelector('.sendMessageSection').classList.add('hide'); - } - - if (supportedCommands.includes('SendString') && !currentPlayer.isLocalPlayer) { - context.querySelector('.sendTextSection').classList.remove('hide'); - } else { - context.querySelector('.sendTextSection').classList.add('hide'); - } - - if (supportedCommands.includes('Select') && !currentPlayer.isLocalPlayer) { - context.querySelector('.navigationSection').classList.remove('hide'); - } else { - context.querySelector('.navigationSection').classList.add('hide'); - } - - if (isSupportedCommands && !currentPlayer.isLocalPlayer) { - context.querySelector('.remoteControlSection').classList.remove('hide'); - } else { - context.querySelector('.remoteControlSection').classList.add('hide'); - } - - buttonVisible(context.querySelector('.btnStop'), item != null); - buttonVisible(context.querySelector('.btnNextTrack'), item != null); - buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); - if (layoutManager.mobile) { - buttonVisible(context.querySelector('.btnRewind'), false); - buttonVisible(context.querySelector('.btnFastForward'), false); - } else { - buttonVisible(context.querySelector('.btnRewind'), item != null); - buttonVisible(context.querySelector('.btnFastForward'), item != null); - } - var positionSlider = context.querySelector('.nowPlayingPositionSlider'); - - if (positionSlider && item && item.RunTimeTicks) { - positionSlider.setKeyboardSteps(userSettings.skipBackLength() * 1000000 / item.RunTimeTicks, - userSettings.skipForwardLength() * 1000000 / item.RunTimeTicks); - } - - if (positionSlider && !positionSlider.dragging) { - positionSlider.disabled = !playState.CanSeek; - var isProgressClear = state.MediaSource && state.MediaSource.RunTimeTicks == null; - positionSlider.setIsClear(isProgressClear); - } - - updatePlayPauseState(playState.IsPaused, item != null); - updateTimeDisplay(playState.PositionTicks, item ? item.RunTimeTicks : null); - updatePlayerVolumeState(context, playState.IsMuted, playState.VolumeLevel); - - if (item && item.MediaType == 'Video') { - context.classList.remove('hideVideoButtons'); - } else { - context.classList.add('hideVideoButtons'); - } - - updateRepeatModeDisplay(playbackManager.getRepeatMode()); - onShuffleQueueModeChange(false); - updateNowPlayingInfo(context, state); - } - - function updateAudioTracksDisplay(player, context) { - var supportedCommands = currentPlayerSupportedCommands; - buttonVisible(context.querySelector('.btnAudioTracks'), playbackManager.audioTracks(player).length > 1 && supportedCommands.indexOf('SetAudioStreamIndex') != -1); - } - - function updateSubtitleTracksDisplay(player, context) { - var supportedCommands = currentPlayerSupportedCommands; - buttonVisible(context.querySelector('.btnSubtitles'), playbackManager.subtitleTracks(player).length && supportedCommands.indexOf('SetSubtitleStreamIndex') != -1); - } - - function updateRepeatModeDisplay(repeatMode) { - var context = dlg; - let toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton'); - const cssClass = 'buttonActive'; - let innHtml = ''; - let repeatOn = true; - - switch (repeatMode) { - case 'RepeatAll': - break; - case 'RepeatOne': - innHtml = ''; - break; - case 'RepeatNone': - default: - repeatOn = false; - break; - } - - for (const toggleRepeatButton of toggleRepeatButtons) { - toggleRepeatButton.classList.toggle(cssClass, repeatOn); - toggleRepeatButton.innerHTML = innHtml; - } - } - - function updatePlayerVolumeState(context, isMuted, volumeLevel) { - var view = context; - var supportedCommands = currentPlayerSupportedCommands; - - if (supportedCommands.indexOf('Mute') === -1) { - showMuteButton = false; - } - - if (supportedCommands.indexOf('SetVolume') === -1) { - showVolumeSlider = false; - } - - if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) { - showMuteButton = false; - showVolumeSlider = false; - } - - const buttonMute = view.querySelector('.buttonMute'); - const buttonMuteIcon = buttonMute.querySelector('.material-icons'); - - buttonMuteIcon.classList.remove('volume_off', 'volume_up'); - - if (isMuted) { - buttonMute.setAttribute('title', globalize.translate('Unmute')); - buttonMuteIcon.classList.add('volume_off'); - } else { - buttonMute.setAttribute('title', globalize.translate('Mute')); - buttonMuteIcon.classList.add('volume_up'); - } - - if (!showMuteButton && !showVolumeSlider) { - context.querySelector('.volumecontrol').classList.add('hide'); - } else { - buttonMute.classList.toggle('hide', !showMuteButton); - - var nowPlayingVolumeSlider = context.querySelector('.nowPlayingVolumeSlider'); - var nowPlayingVolumeSliderContainer = context.querySelector('.nowPlayingVolumeSliderContainer'); - - if (nowPlayingVolumeSlider) { - nowPlayingVolumeSliderContainer.classList.toggle('hide', !showVolumeSlider); - - if (!nowPlayingVolumeSlider.dragging) { - nowPlayingVolumeSlider.value = volumeLevel || 0; - } - } - } - } - - function updatePlayPauseState(isPaused, isActive) { - var context = dlg; - var btnPlayPause = context.querySelector('.btnPlayPause'); - const btnPlayPauseIcon = btnPlayPause.querySelector('.material-icons'); - - btnPlayPauseIcon.classList.remove('play_circle_filled', 'pause_circle_filled'); - btnPlayPauseIcon.classList.add(isPaused ? 'play_circle_filled' : 'pause_circle_filled'); - - buttonVisible(btnPlayPause, isActive); - } - - function updateTimeDisplay(positionTicks, runtimeTicks) { - var context = dlg; - var positionSlider = context.querySelector('.nowPlayingPositionSlider'); - - if (positionSlider && !positionSlider.dragging) { - if (runtimeTicks) { - var pct = positionTicks / runtimeTicks; - pct *= 100; - positionSlider.value = pct; + if (item.SeriesName != null) { + const seriesName = item.SeriesName; + if (item.SeriesId != null) { + context.querySelector('.nowPlayingSerie').innerHTML = '${seriesName}`; } else { - positionSlider.value = 0; + context.querySelector('.nowPlayingSerie').innerHTML = seriesName; } } - - context.querySelector('.positionTime').innerHTML = positionTicks == null ? '--:--' : datetime.getDisplayRunningTime(positionTicks); - context.querySelector('.runtime').innerHTML = runtimeTicks != null ? datetime.getDisplayRunningTime(runtimeTicks) : '--:--'; + context.querySelector('.nowPlayingEpisode').innerHTML = item.Name; + } else { + context.querySelector('.nowPlayingPageTitle').innerHTML = displayName; } - function getPlaylistItems(player) { - return playbackManager.getPlaylist(player); + if (displayName.length > 0 && item.Type != 'Audio' && item.Type != 'Episode') { + context.querySelector('.nowPlayingPageTitle').classList.remove('hide'); + } else { + context.querySelector('.nowPlayingPageTitle').classList.add('hide'); } - function loadPlaylist(context, player) { - getPlaylistItems(player).then(function (items) { - var html = ''; - let favoritesEnabled = true; - if (layoutManager.mobile) { - if (items.length > 0) { - context.querySelector('.btnTogglePlaylist').classList.remove('hide'); - } else { - context.querySelector('.btnTogglePlaylist').classList.add('hide'); - } - favoritesEnabled = false; - } + const url = item ? seriesImageUrl(item, { + maxHeight: 300 + }) || imageUrl(item, { + maxHeight: 300 + }) : null; - html += listView.getListViewHtml({ - items: items, - smallIcon: true, - action: 'setplaylistindex', - enableUserDataButtons: favoritesEnabled, - rightButtons: [{ - icon: 'remove_circle_outline', - title: globalize.translate('ButtonRemove'), - id: 'remove' - }], - dragHandle: true + let contextButton = context.querySelector('.btnToggleContextMenu'); + // We remove the previous event listener by replacing the item in each update event + const autoFocusContextButton = document.activeElement === contextButton; + const contextButtonClone = contextButton.cloneNode(true); + contextButton.parentNode.replaceChild(contextButtonClone, contextButton); + contextButton = context.querySelector('.btnToggleContextMenu'); + if (autoFocusContextButton) { + contextButton.focus(); + } + const stopPlayback = !!layoutManager.mobile; + const options = { + play: false, + queue: false, + stopPlayback: stopPlayback, + clearQueue: true, + openAlbum: false, + positionTo: contextButton + }; + const apiClient = connectionManager.getApiClient(item.ServerId); + apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { + apiClient.getCurrentUser().then(function (user) { + contextButton.addEventListener('click', function () { + itemContextMenu.show(Object.assign({ + item: fullItem, + user: user + }, options)); }); - - var itemsContainer = context.querySelector('.playlist'); - let focusedItemPlaylistId = itemsContainer.querySelector('button:focus'); - itemsContainer.innerHTML = html; - if (focusedItemPlaylistId !== null) { - focusedItemPlaylistId = focusedItemPlaylistId.getAttribute('data-playlistitemid'); - const newFocusedItem = itemsContainer.querySelector(`button[data-playlistitemid="${focusedItemPlaylistId}"]`); - if (newFocusedItem !== null) { - newFocusedItem.focus(); - } - } - - var playlistItemId = playbackManager.getCurrentPlaylistItemId(player); - - if (playlistItemId) { - var img = itemsContainer.querySelector(`.listItem[data-playlistItemId="${playlistItemId}"] .listItemImage`); - - if (img) { - img.classList.remove('lazy'); - img.classList.add('playlistIndexIndicatorImage'); - } - } - - imageLoader.lazyChildren(itemsContainer); }); + }); + setImageUrl(context, state, url); + if (item) { + backdrop.setBackdrops([item]); + apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { + const userData = fullItem.UserData || {}; + const likes = userData.Likes == null ? '' : userData.Likes; + context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; + }); + } else { + backdrop.clearBackdrop(); + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; + } + } +} + +function setImageUrl(context, state, url) { + const item = state.NowPlayingItem; + const imgContainer = context.querySelector('.nowPlayingPageImageContainer'); + + if (url) { + imgContainer.innerHTML = ''; + if (item.Type == 'Audio') { + context.querySelector('.nowPlayingPageImage').classList.add('nowPlayingPageImageAudio'); + context.querySelector('.nowPlayingPageImageContainer').classList.remove('nowPlayingPageImageAudio'); + } else { + context.querySelector('.nowPlayingPageImageContainer').classList.add('nowPlayingPageImagePoster'); + context.querySelector('.nowPlayingPageImage').classList.remove('nowPlayingPageImageAudio'); + } + } else { + imgContainer.innerHTML = '
'; + } +} + +function buttonVisible(btn, enabled) { + if (enabled) { + btn.classList.remove('hide'); + } else { + btn.classList.add('hide'); + } +} + +function updateSupportedCommands(context, commands) { + const all = context.querySelectorAll('.btnCommand'); + + for (let i = 0, length = all.length; i < length; i++) { + const enableButton = commands.indexOf(all[i].getAttribute('data-command')) !== -1; + all[i].disabled = !enableButton; + } +} + +export default function () { + function toggleRepeat() { + switch (playbackManager.getRepeatMode()) { + case 'RepeatAll': + playbackManager.setRepeatMode('RepeatOne'); + break; + case 'RepeatOne': + playbackManager.setRepeatMode('RepeatNone'); + break; + case 'RepeatNone': + playbackManager.setRepeatMode('RepeatAll'); + } + } + + function updatePlayerState(player, context, state) { + lastPlayerState = state; + const item = state.NowPlayingItem; + const playerInfo = playbackManager.getPlayerInfo(); + const supportedCommands = playerInfo.supportedCommands; + currentPlayerSupportedCommands = supportedCommands; + const playState = state.PlayState || {}; + const isSupportedCommands = supportedCommands.includes('DisplayMessage') || supportedCommands.includes('SendString') || supportedCommands.includes('Select'); + buttonVisible(context.querySelector('.btnToggleFullscreen'), item && item.MediaType == 'Video' && supportedCommands.includes('ToggleFullscreen')); + updateAudioTracksDisplay(player, context); + updateSubtitleTracksDisplay(player, context); + + if (supportedCommands.includes('DisplayMessage') && !currentPlayer.isLocalPlayer) { + context.querySelector('.sendMessageSection').classList.remove('hide'); + } else { + context.querySelector('.sendMessageSection').classList.add('hide'); } - function onPlaybackStart(e, state) { - console.debug('remotecontrol event: ' + e.type); - var player = this; - onStateChanged.call(player, e, state); + if (supportedCommands.includes('SendString') && !currentPlayer.isLocalPlayer) { + context.querySelector('.sendTextSection').classList.remove('hide'); + } else { + context.querySelector('.sendTextSection').classList.add('hide'); } - function onRepeatModeChange() { - updateRepeatModeDisplay(playbackManager.getRepeatMode()); + if (supportedCommands.includes('Select') && !currentPlayer.isLocalPlayer) { + context.querySelector('.navigationSection').classList.remove('hide'); + } else { + context.querySelector('.navigationSection').classList.add('hide'); } - function onShuffleQueueModeChange(updateView = true) { - let shuffleMode = playbackManager.getQueueShuffleMode(this); - let context = dlg; - const cssClass = 'buttonActive'; - let shuffleButtons = context.querySelectorAll('.btnShuffleQueue'); + if (isSupportedCommands && !currentPlayer.isLocalPlayer) { + context.querySelector('.remoteControlSection').classList.remove('hide'); + } else { + context.querySelector('.remoteControlSection').classList.add('hide'); + } - for (let shuffleButton of shuffleButtons) { - switch (shuffleMode) { - case 'Shuffle': - shuffleButton.classList.add(cssClass); - break; - case 'Sorted': - default: - shuffleButton.classList.remove(cssClass); - break; + buttonVisible(context.querySelector('.btnStop'), item != null); + buttonVisible(context.querySelector('.btnNextTrack'), item != null); + buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); + if (layoutManager.mobile) { + buttonVisible(context.querySelector('.btnRewind'), false); + buttonVisible(context.querySelector('.btnFastForward'), false); + } else { + buttonVisible(context.querySelector('.btnRewind'), item != null); + buttonVisible(context.querySelector('.btnFastForward'), item != null); + } + const positionSlider = context.querySelector('.nowPlayingPositionSlider'); + + if (positionSlider && item && item.RunTimeTicks) { + positionSlider.setKeyboardSteps(userSettings.skipBackLength() * 1000000 / item.RunTimeTicks, + userSettings.skipForwardLength() * 1000000 / item.RunTimeTicks); + } + + if (positionSlider && !positionSlider.dragging) { + positionSlider.disabled = !playState.CanSeek; + const isProgressClear = state.MediaSource && state.MediaSource.RunTimeTicks == null; + positionSlider.setIsClear(isProgressClear); + } + + updatePlayPauseState(playState.IsPaused, item != null); + updateTimeDisplay(playState.PositionTicks, item ? item.RunTimeTicks : null); + updatePlayerVolumeState(context, playState.IsMuted, playState.VolumeLevel); + + if (item && item.MediaType == 'Video') { + context.classList.remove('hideVideoButtons'); + } else { + context.classList.add('hideVideoButtons'); + } + + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + onShuffleQueueModeChange(false); + updateNowPlayingInfo(context, state); + } + + function updateAudioTracksDisplay(player, context) { + const supportedCommands = currentPlayerSupportedCommands; + buttonVisible(context.querySelector('.btnAudioTracks'), playbackManager.audioTracks(player).length > 1 && supportedCommands.indexOf('SetAudioStreamIndex') != -1); + } + + function updateSubtitleTracksDisplay(player, context) { + const supportedCommands = currentPlayerSupportedCommands; + buttonVisible(context.querySelector('.btnSubtitles'), playbackManager.subtitleTracks(player).length && supportedCommands.indexOf('SetSubtitleStreamIndex') != -1); + } + + function updateRepeatModeDisplay(repeatMode) { + const context = dlg; + const toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton'); + const cssClass = 'buttonActive'; + let innHtml = ''; + let repeatOn = true; + + switch (repeatMode) { + case 'RepeatAll': + break; + case 'RepeatOne': + innHtml = ''; + break; + case 'RepeatNone': + default: + repeatOn = false; + break; + } + + for (const toggleRepeatButton of toggleRepeatButtons) { + toggleRepeatButton.classList.toggle(cssClass, repeatOn); + toggleRepeatButton.innerHTML = innHtml; + } + } + + function updatePlayerVolumeState(context, isMuted, volumeLevel) { + const view = context; + const supportedCommands = currentPlayerSupportedCommands; + + if (supportedCommands.indexOf('Mute') === -1) { + showMuteButton = false; + } + + if (supportedCommands.indexOf('SetVolume') === -1) { + showVolumeSlider = false; + } + + if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) { + showMuteButton = false; + showVolumeSlider = false; + } + + const buttonMute = view.querySelector('.buttonMute'); + const buttonMuteIcon = buttonMute.querySelector('.material-icons'); + + buttonMuteIcon.classList.remove('volume_off', 'volume_up'); + + if (isMuted) { + buttonMute.setAttribute('title', globalize.translate('Unmute')); + buttonMuteIcon.classList.add('volume_off'); + } else { + buttonMute.setAttribute('title', globalize.translate('Mute')); + buttonMuteIcon.classList.add('volume_up'); + } + + if (!showMuteButton && !showVolumeSlider) { + context.querySelector('.volumecontrol').classList.add('hide'); + } else { + buttonMute.classList.toggle('hide', !showMuteButton); + + const nowPlayingVolumeSlider = context.querySelector('.nowPlayingVolumeSlider'); + const nowPlayingVolumeSliderContainer = context.querySelector('.nowPlayingVolumeSliderContainer'); + + if (nowPlayingVolumeSlider) { + nowPlayingVolumeSliderContainer.classList.toggle('hide', !showVolumeSlider); + + if (!nowPlayingVolumeSlider.dragging) { + nowPlayingVolumeSlider.value = volumeLevel || 0; } } - - if (updateView) { - onPlaylistUpdate(); - } } + } - function onPlaylistUpdate(e) { - loadPlaylist(dlg, this); - } + function updatePlayPauseState(isPaused, isActive) { + const context = dlg; + const btnPlayPause = context.querySelector('.btnPlayPause'); + const btnPlayPauseIcon = btnPlayPause.querySelector('.material-icons'); - function onPlaylistItemRemoved(e, info) { - var context = dlg; - if (info !== undefined) { - var playlistItemIds = info.playlistItemIds; + btnPlayPauseIcon.classList.remove('play_circle_filled', 'pause_circle_filled'); + btnPlayPauseIcon.classList.add(isPaused ? 'play_circle_filled' : 'pause_circle_filled'); - for (var i = 0, length = playlistItemIds.length; i < length; i++) { - var listItem = context.querySelector('.listItem[data-playlistItemId="' + playlistItemIds[i] + '"]'); + buttonVisible(btnPlayPause, isActive); + } - if (listItem) { - listItem.parentNode.removeChild(listItem); - } - } + function updateTimeDisplay(positionTicks, runtimeTicks) { + const context = dlg; + const positionSlider = context.querySelector('.nowPlayingPositionSlider'); + + if (positionSlider && !positionSlider.dragging) { + if (runtimeTicks) { + let pct = positionTicks / runtimeTicks; + pct *= 100; + positionSlider.value = pct; } else { - onPlaylistUpdate(); + positionSlider.value = 0; } } - function onPlaybackStopped(e, state) { - console.debug('remotecontrol event: ' + e.type); - var player = this; + context.querySelector('.positionTime').innerHTML = positionTicks == null ? '--:--' : datetime.getDisplayRunningTime(positionTicks); + context.querySelector('.runtime').innerHTML = runtimeTicks != null ? datetime.getDisplayRunningTime(runtimeTicks) : '--:--'; + } - if (!state.NextMediaType) { - updatePlayerState(player, dlg, {}); - Emby.Page.back(); + function getPlaylistItems(player) { + return playbackManager.getPlaylist(player); + } + + function loadPlaylist(context, player) { + getPlaylistItems(player).then(function (items) { + let html = ''; + let favoritesEnabled = true; + if (layoutManager.mobile) { + if (items.length > 0) { + context.querySelector('.btnTogglePlaylist').classList.remove('hide'); + } else { + context.querySelector('.btnTogglePlaylist').classList.add('hide'); + } + favoritesEnabled = false; + } + + html += listView.getListViewHtml({ + items: items, + smallIcon: true, + action: 'setplaylistindex', + enableUserDataButtons: favoritesEnabled, + rightButtons: [{ + icon: 'remove_circle_outline', + title: globalize.translate('ButtonRemove'), + id: 'remove' + }], + dragHandle: true + }); + + const itemsContainer = context.querySelector('.playlist'); + let focusedItemPlaylistId = itemsContainer.querySelector('button:focus'); + itemsContainer.innerHTML = html; + if (focusedItemPlaylistId !== null) { + focusedItemPlaylistId = focusedItemPlaylistId.getAttribute('data-playlistitemid'); + const newFocusedItem = itemsContainer.querySelector(`button[data-playlistitemid="${focusedItemPlaylistId}"]`); + if (newFocusedItem !== null) { + newFocusedItem.focus(); + } + } + + const playlistItemId = playbackManager.getCurrentPlaylistItemId(player); + + if (playlistItemId) { + const img = itemsContainer.querySelector(`.listItem[data-playlistItemId="${playlistItemId}"] .listItemImage`); + + if (img) { + img.classList.remove('lazy'); + img.classList.add('playlistIndexIndicatorImage'); + } + } + + imageLoader.lazyChildren(itemsContainer); + }); + } + + function onPlaybackStart(e, state) { + console.debug('remotecontrol event: ' + e.type); + const player = this; + onStateChanged.call(player, e, state); + } + + function onRepeatModeChange() { + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + } + + function onShuffleQueueModeChange(updateView = true) { + const shuffleMode = playbackManager.getQueueShuffleMode(this); + const context = dlg; + const cssClass = 'buttonActive'; + const shuffleButtons = context.querySelectorAll('.btnShuffleQueue'); + + for (const shuffleButton of shuffleButtons) { + switch (shuffleMode) { + case 'Shuffle': + shuffleButton.classList.add(cssClass); + break; + case 'Sorted': + default: + shuffleButton.classList.remove(cssClass); + break; } } - function onPlayPauseStateChanged(e) { - updatePlayPauseState(this.paused(), true); - } - - function onStateChanged(event, state) { - var player = this; - updatePlayerState(player, dlg, state); + if (updateView) { onPlaylistUpdate(); } + } - function onTimeUpdate(e) { - var now = new Date().getTime(); + function onPlaylistUpdate(e) { + loadPlaylist(dlg, this); + } - if (!(now - lastUpdateTime < 700)) { - lastUpdateTime = now; - var player = this; - currentRuntimeTicks = playbackManager.duration(player); - updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks); + function onPlaylistItemRemoved(e, info) { + const context = dlg; + if (info !== undefined) { + const playlistItemIds = info.playlistItemIds; + + for (let i = 0, length = playlistItemIds.length; i < length; i++) { + const listItem = context.querySelector('.listItem[data-playlistItemId="' + playlistItemIds[i] + '"]'); + + if (listItem) { + listItem.parentNode.removeChild(listItem); + } + } + } else { + onPlaylistUpdate(); + } + } + + function onPlaybackStopped(e, state) { + console.debug('remotecontrol event: ' + e.type); + const player = this; + + if (!state.NextMediaType) { + updatePlayerState(player, dlg, {}); + Emby.Page.back(); + } + } + + function onPlayPauseStateChanged(e) { + updatePlayPauseState(this.paused(), true); + } + + function onStateChanged(event, state) { + const player = this; + updatePlayerState(player, dlg, state); + onPlaylistUpdate(); + } + + function onTimeUpdate(e) { + const now = new Date().getTime(); + + if (!(now - lastUpdateTime < 700)) { + lastUpdateTime = now; + const player = this; + currentRuntimeTicks = playbackManager.duration(player); + updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks); + } + } + + function onVolumeChanged(e) { + const player = this; + updatePlayerVolumeState(dlg, player.isMuted(), player.getVolume()); + } + + function releaseCurrentPlayer() { + const player = currentPlayer; + + if (player) { + events.off(player, 'playbackstart', onPlaybackStart); + events.off(player, 'statechange', onStateChanged); + events.off(player, 'repeatmodechange', onRepeatModeChange); + events.off(player, 'shufflequeuemodechange', onShuffleQueueModeChange); + events.off(player, 'playlistitemremove', onPlaylistItemRemoved); + events.off(player, 'playlistitemmove', onPlaylistUpdate); + events.off(player, 'playlistitemadd', onPlaylistUpdate); + events.off(player, 'playbackstop', onPlaybackStopped); + events.off(player, 'volumechange', onVolumeChanged); + events.off(player, 'pause', onPlayPauseStateChanged); + events.off(player, 'unpause', onPlayPauseStateChanged); + events.off(player, 'timeupdate', onTimeUpdate); + currentPlayer = null; + } + } + + function bindToPlayer(context, player) { + if (releaseCurrentPlayer(), currentPlayer = player, player) { + const state = playbackManager.getPlayerState(player); + onStateChanged.call(player, { + type: 'init' + }, state); + events.on(player, 'playbackstart', onPlaybackStart); + events.on(player, 'statechange', onStateChanged); + events.on(player, 'repeatmodechange', onRepeatModeChange); + events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange); + events.on(player, 'playlistitemremove', onPlaylistItemRemoved); + events.on(player, 'playlistitemmove', onPlaylistUpdate); + events.on(player, 'playlistitemadd', onPlaylistUpdate); + events.on(player, 'playbackstop', onPlaybackStopped); + events.on(player, 'volumechange', onVolumeChanged); + events.on(player, 'pause', onPlayPauseStateChanged); + events.on(player, 'unpause', onPlayPauseStateChanged); + events.on(player, 'timeupdate', onTimeUpdate); + const playerInfo = playbackManager.getPlayerInfo(); + const supportedCommands = playerInfo.supportedCommands; + currentPlayerSupportedCommands = supportedCommands; + updateSupportedCommands(context, supportedCommands); + } + } + + function onBtnCommandClick() { + if (currentPlayer) { + if (this.classList.contains('repeatToggleButton')) { + toggleRepeat(); + } else { + playbackManager.sendCommand({ + Name: this.getAttribute('data-command') + }, currentPlayer); } } + } - function onVolumeChanged(e) { - var player = this; - updatePlayerVolumeState(dlg, player.isMuted(), player.getVolume()); + function getSaveablePlaylistItems() { + return getPlaylistItems(currentPlayer).then(function (items) { + return items.filter(function (i) { + return i.Id && i.ServerId; + }); + }); + } + + function savePlaylist() { + import('playlistEditor').then(({ default: playlistEditor }) => { + getSaveablePlaylistItems().then(function (items) { + const serverId = items.length ? items[0].ServerId : ApiClient.serverId(); + new playlistEditor({ + items: items.map(function (i) { + return i.Id; + }), + serverId: serverId, + enableAddToPlayQueue: false, + defaultValue: 'new' + }); + }); + }); + } + + function bindEvents(context) { + const btnCommand = context.querySelectorAll('.btnCommand'); + const positionSlider = context.querySelector('.nowPlayingPositionSlider'); + + for (let i = 0, length = btnCommand.length; i < length; i++) { + btnCommand[i].addEventListener('click', onBtnCommandClick); } - function releaseCurrentPlayer() { - var player = currentPlayer; - - if (player) { - events.off(player, 'playbackstart', onPlaybackStart); - events.off(player, 'statechange', onStateChanged); - events.off(player, 'repeatmodechange', onRepeatModeChange); - events.off(player, 'shufflequeuemodechange', onShuffleQueueModeChange); - events.off(player, 'playlistitemremove', onPlaylistItemRemoved); - events.off(player, 'playlistitemmove', onPlaylistUpdate); - events.off(player, 'playlistitemadd', onPlaylistUpdate); - events.off(player, 'playbackstop', onPlaybackStopped); - events.off(player, 'volumechange', onVolumeChanged); - events.off(player, 'pause', onPlayPauseStateChanged); - events.off(player, 'unpause', onPlayPauseStateChanged); - events.off(player, 'timeupdate', onTimeUpdate); - currentPlayer = null; - } - } - - function bindToPlayer(context, player) { - if (releaseCurrentPlayer(), currentPlayer = player, player) { - var state = playbackManager.getPlayerState(player); - onStateChanged.call(player, { - type: 'init' - }, state); - events.on(player, 'playbackstart', onPlaybackStart); - events.on(player, 'statechange', onStateChanged); - events.on(player, 'repeatmodechange', onRepeatModeChange); - events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange); - events.on(player, 'playlistitemremove', onPlaylistItemRemoved); - events.on(player, 'playlistitemmove', onPlaylistUpdate); - events.on(player, 'playlistitemadd', onPlaylistUpdate); - events.on(player, 'playbackstop', onPlaybackStopped); - events.on(player, 'volumechange', onVolumeChanged); - events.on(player, 'pause', onPlayPauseStateChanged); - events.on(player, 'unpause', onPlayPauseStateChanged); - events.on(player, 'timeupdate', onTimeUpdate); - var playerInfo = playbackManager.getPlayerInfo(); - var supportedCommands = playerInfo.supportedCommands; - currentPlayerSupportedCommands = supportedCommands; - updateSupportedCommands(context, supportedCommands); - } - } - - function onBtnCommandClick() { + context.querySelector('.btnToggleFullscreen').addEventListener('click', function (e) { if (currentPlayer) { - if (this.classList.contains('repeatToggleButton')) { - toggleRepeat(); + playbackManager.sendCommand({ + Name: e.target.getAttribute('data-command') + }, currentPlayer); + } + }); + context.querySelector('.btnAudioTracks').addEventListener('click', function (e) { + if (currentPlayer && lastPlayerState && lastPlayerState.NowPlayingItem) { + showAudioMenu(context, currentPlayer, e.target, lastPlayerState.NowPlayingItem); + } + }); + context.querySelector('.btnSubtitles').addEventListener('click', function (e) { + if (currentPlayer && lastPlayerState && lastPlayerState.NowPlayingItem) { + showSubtitleMenu(context, currentPlayer, e.target, lastPlayerState.NowPlayingItem); + } + }); + context.querySelector('.btnStop').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.stop(currentPlayer); + } + }); + context.querySelector('.btnPlayPause').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.playPause(currentPlayer); + } + }); + context.querySelector('.btnNextTrack').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.nextTrack(currentPlayer); + } + }); + context.querySelector('.btnRewind').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.rewind(currentPlayer); + } + }); + context.querySelector('.btnFastForward').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.fastForward(currentPlayer); + } + }); + for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) { + shuffleButton.addEventListener('click', function () { + if (currentPlayer) { + playbackManager.toggleQueueShuffleMode(currentPlayer); + } + }); + } + + context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) { + if (currentPlayer) { + if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) { + // Cancel this event if doubleclick is fired + if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) { + return; + } + playbackManager.seekPercent(0, currentPlayer); + // This is done automatically by playbackManager. However, setting this here gives instant visual feedback. + // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. + positionSlider.value = 0; } else { - playbackManager.sendCommand({ - Name: this.getAttribute('data-command') - }, currentPlayer); - } - } - } - - function getSaveablePlaylistItems() { - return getPlaylistItems(currentPlayer).then(function (items) { - return items.filter(function (i) { - return i.Id && i.ServerId; - }); - }); - } - - function savePlaylist() { - require(['playlistEditor'], function (playlistEditor) { - getSaveablePlaylistItems().then(function (items) { - var serverId = items.length ? items[0].ServerId : ApiClient.serverId(); - new playlistEditor.showEditor({ - items: items.map(function (i) { - return i.Id; - }), - serverId: serverId, - enableAddToPlayQueue: false, - defaultValue: 'new' - }); - }); - }); - } - - function bindEvents(context) { - var btnCommand = context.querySelectorAll('.btnCommand'); - var positionSlider = context.querySelector('.nowPlayingPositionSlider'); - - for (var i = 0, length = btnCommand.length; i < length; i++) { - btnCommand[i].addEventListener('click', onBtnCommandClick); - } - - context.querySelector('.btnToggleFullscreen').addEventListener('click', function (e) { - if (currentPlayer) { - playbackManager.sendCommand({ - Name: e.target.getAttribute('data-command') - }, currentPlayer); - } - }); - context.querySelector('.btnAudioTracks').addEventListener('click', function (e) { - if (currentPlayer && lastPlayerState && lastPlayerState.NowPlayingItem) { - showAudioMenu(context, currentPlayer, e.target, lastPlayerState.NowPlayingItem); - } - }); - context.querySelector('.btnSubtitles').addEventListener('click', function (e) { - if (currentPlayer && lastPlayerState && lastPlayerState.NowPlayingItem) { - showSubtitleMenu(context, currentPlayer, e.target, lastPlayerState.NowPlayingItem); - } - }); - context.querySelector('.btnStop').addEventListener('click', function () { - if (currentPlayer) { - playbackManager.stop(currentPlayer); - } - }); - context.querySelector('.btnPlayPause').addEventListener('click', function () { - if (currentPlayer) { - playbackManager.playPause(currentPlayer); - } - }); - context.querySelector('.btnNextTrack').addEventListener('click', function () { - if (currentPlayer) { - playbackManager.nextTrack(currentPlayer); - } - }); - context.querySelector('.btnRewind').addEventListener('click', function () { - if (currentPlayer) { - playbackManager.rewind(currentPlayer); - } - }); - context.querySelector('.btnFastForward').addEventListener('click', function () { - if (currentPlayer) { - playbackManager.fastForward(currentPlayer); - } - }); - for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) { - shuffleButton.addEventListener('click', function () { - if (currentPlayer) { - playbackManager.toggleQueueShuffleMode(currentPlayer); - } - }); - } - - context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) { - if (currentPlayer) { - if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) { - // Cancel this event if doubleclick is fired - if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) { - return; - } - playbackManager.seekPercent(0, currentPlayer); - // This is done automatically by playbackManager. However, setting this here gives instant visual feedback. - // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. - positionSlider.value = 0; - } else { - playbackManager.previousTrack(currentPlayer); - } - } - }); - - context.querySelector('.btnPreviousTrack').addEventListener('dblclick', function () { - if (currentPlayer) { playbackManager.previousTrack(currentPlayer); } - }); - positionSlider.addEventListener('change', function () { - var value = this.value; + } + }); - if (currentPlayer) { - var newPercent = parseFloat(value); - playbackManager.seekPercent(newPercent, currentPlayer); - } - }); + context.querySelector('.btnPreviousTrack').addEventListener('dblclick', function () { + if (currentPlayer) { + playbackManager.previousTrack(currentPlayer); + } + }); + positionSlider.addEventListener('change', function () { + const value = this.value; - positionSlider.getBubbleText = function (value) { - var state = lastPlayerState; + if (currentPlayer) { + const newPercent = parseFloat(value); + playbackManager.seekPercent(newPercent, currentPlayer); + } + }); - if (!state || !state.NowPlayingItem || !currentRuntimeTicks) { - return '--:--'; - } + positionSlider.getBubbleText = function (value) { + const state = lastPlayerState; - var ticks = currentRuntimeTicks; - ticks /= 100; - ticks *= value; - return datetime.getDisplayRunningTime(ticks); - }; + if (!state || !state.NowPlayingItem || !currentRuntimeTicks) { + return '--:--'; + } - context.querySelector('.nowPlayingVolumeSlider').addEventListener('input', (e) => { - playbackManager.setVolume(e.target.value, currentPlayer); - }); + let ticks = currentRuntimeTicks; + ticks /= 100; + ticks *= value; + return datetime.getDisplayRunningTime(ticks); + }; - context.querySelector('.buttonMute').addEventListener('click', function () { - playbackManager.toggleMute(currentPlayer); - }); - var playlistContainer = context.querySelector('.playlist'); - playlistContainer.addEventListener('action-remove', function (e) { - playbackManager.removeFromPlaylist([e.detail.playlistItemId], currentPlayer); - }); - playlistContainer.addEventListener('itemdrop', function (e) { - var newIndex = e.detail.newIndex; - var playlistItemId = e.detail.playlistItemId; - playbackManager.movePlaylistItem(playlistItemId, newIndex, currentPlayer); - }); - context.querySelector('.btnSavePlaylist').addEventListener('click', savePlaylist); - context.querySelector('.btnTogglePlaylist').addEventListener('click', function () { - if (context.querySelector('.playlist').classList.contains('hide')) { - context.querySelector('.playlist').classList.remove('hide'); - context.querySelector('.btnSavePlaylist').classList.remove('hide'); - context.querySelector('.volumecontrol').classList.add('hide'); - if (layoutManager.mobile) { - context.querySelector('.playlistSectionButton').classList.remove('playlistSectionButtonTransparent'); - } - } else { - context.querySelector('.playlist').classList.add('hide'); - context.querySelector('.btnSavePlaylist').classList.add('hide'); - if (showMuteButton || showVolumeSlider) { - context.querySelector('.volumecontrol').classList.remove('hide'); - } - if (layoutManager.mobile) { - context.querySelector('.playlistSectionButton').classList.add('playlistSectionButtonTransparent'); - } - } - }); - } + context.querySelector('.nowPlayingVolumeSlider').addEventListener('input', (e) => { + playbackManager.setVolume(e.target.value, currentPlayer); + }); - function onPlayerChange() { - bindToPlayer(dlg, playbackManager.getCurrentPlayer()); - } - - function onMessageSubmit(e) { - var form = e.target; - playbackManager.sendCommand({ - Name: 'DisplayMessage', - Arguments: { - Header: form.querySelector('#txtMessageTitle').value, - Text: form.querySelector('#txtMessageText', form).value - } - }, currentPlayer); - form.querySelector('input').value = ''; - - require(['toast'], function (toast) { - toast('Message sent.'); - }); - - e.preventDefault(); - e.stopPropagation(); - return false; - } - - function onSendStringSubmit(e) { - var form = e.target; - playbackManager.sendCommand({ - Name: 'SendString', - Arguments: { - String: form.querySelector('#txtTypeText', form).value - } - }, currentPlayer); - form.querySelector('input').value = ''; - - require(['toast'], function (toast) { - toast('Text sent.'); - }); - - e.preventDefault(); - e.stopPropagation(); - return false; - } - - function init(ownerView, context) { - let volumecontrolHtml = '
'; - volumecontrolHtml += ``; - volumecontrolHtml += '
'; - volumecontrolHtml += '
'; - let optionsSection = context.querySelector('.playlistSectionButton'); - if (!layoutManager.mobile) { - context.querySelector('.nowPlayingSecondaryButtons').insertAdjacentHTML('beforeend', volumecontrolHtml); - optionsSection.classList.remove('align-items-center', 'justify-content-center'); - optionsSection.classList.add('align-items-right', 'justify-content-flex-end'); + context.querySelector('.buttonMute').addEventListener('click', function () { + playbackManager.toggleMute(currentPlayer); + }); + const playlistContainer = context.querySelector('.playlist'); + playlistContainer.addEventListener('action-remove', function (e) { + playbackManager.removeFromPlaylist([e.detail.playlistItemId], currentPlayer); + }); + playlistContainer.addEventListener('itemdrop', function (e) { + const newIndex = e.detail.newIndex; + const playlistItemId = e.detail.playlistItemId; + playbackManager.movePlaylistItem(playlistItemId, newIndex, currentPlayer); + }); + context.querySelector('.btnSavePlaylist').addEventListener('click', savePlaylist); + context.querySelector('.btnTogglePlaylist').addEventListener('click', function () { + if (context.querySelector('.playlist').classList.contains('hide')) { context.querySelector('.playlist').classList.remove('hide'); context.querySelector('.btnSavePlaylist').classList.remove('hide'); - context.classList.add('padded-bottom'); + context.querySelector('.volumecontrol').classList.add('hide'); + if (layoutManager.mobile) { + context.querySelector('.playlistSectionButton').classList.remove('playlistSectionButtonTransparent'); + } } else { - optionsSection.querySelector('.btnTogglePlaylist').insertAdjacentHTML('afterend', volumecontrolHtml); - optionsSection.classList.add('playlistSectionButtonTransparent'); - context.querySelector('.btnTogglePlaylist').classList.remove('hide'); - context.querySelector('.playlistSectionButton').classList.remove('justify-content-center'); - context.querySelector('.playlistSectionButton').classList.add('justify-content-space-between'); + context.querySelector('.playlist').classList.add('hide'); + context.querySelector('.btnSavePlaylist').classList.add('hide'); + if (showMuteButton || showVolumeSlider) { + context.querySelector('.volumecontrol').classList.remove('hide'); + } + if (layoutManager.mobile) { + context.querySelector('.playlistSectionButton').classList.add('playlistSectionButtonTransparent'); + } } + }); + } - bindEvents(context); - context.querySelector('.sendMessageForm').addEventListener('submit', onMessageSubmit); - context.querySelector('.typeTextForm').addEventListener('submit', onSendStringSubmit); - events.on(playbackManager, 'playerchange', onPlayerChange); + function onPlayerChange() { + bindToPlayer(dlg, playbackManager.getCurrentPlayer()); + } - if (layoutManager.tv) { - var positionSlider = context.querySelector('.nowPlayingPositionSlider'); - positionSlider.classList.add('focusable'); - positionSlider.enableKeyboardDragging(); + function onMessageSubmit(e) { + const form = e.target; + playbackManager.sendCommand({ + Name: 'DisplayMessage', + Arguments: { + Header: form.querySelector('#txtMessageTitle').value, + Text: form.querySelector('#txtMessageText', form).value } + }, currentPlayer); + form.querySelector('input').value = ''; + + import('toast').then(({ default: toast }) => { + toast('Message sent.'); + }); + + e.preventDefault(); + e.stopPropagation(); + return false; + } + + function onSendStringSubmit(e) { + const form = e.target; + playbackManager.sendCommand({ + Name: 'SendString', + Arguments: { + String: form.querySelector('#txtTypeText', form).value + } + }, currentPlayer); + form.querySelector('input').value = ''; + + import('toast').then(({ default: toast }) => { + toast('Text sent.'); + }); + + e.preventDefault(); + e.stopPropagation(); + return false; + } + + function init(ownerView, context) { + let volumecontrolHtml = '
'; + volumecontrolHtml += ``; + volumecontrolHtml += '
'; + volumecontrolHtml += '
'; + const optionsSection = context.querySelector('.playlistSectionButton'); + if (!layoutManager.mobile) { + context.querySelector('.nowPlayingSecondaryButtons').insertAdjacentHTML('beforeend', volumecontrolHtml); + optionsSection.classList.remove('align-items-center', 'justify-content-center'); + optionsSection.classList.add('align-items-right', 'justify-content-flex-end'); + context.querySelector('.playlist').classList.remove('hide'); + context.querySelector('.btnSavePlaylist').classList.remove('hide'); + context.classList.add('padded-bottom'); + } else { + optionsSection.querySelector('.btnTogglePlaylist').insertAdjacentHTML('afterend', volumecontrolHtml); + optionsSection.classList.add('playlistSectionButtonTransparent'); + context.querySelector('.btnTogglePlaylist').classList.remove('hide'); + context.querySelector('.playlistSectionButton').classList.remove('justify-content-center'); + context.querySelector('.playlistSectionButton').classList.add('justify-content-space-between'); } - function onDialogClosed(e) { - releaseCurrentPlayer(); - events.off(playbackManager, 'playerchange', onPlayerChange); - lastPlayerState = null; + bindEvents(context); + context.querySelector('.sendMessageForm').addEventListener('submit', onMessageSubmit); + context.querySelector('.typeTextForm').addEventListener('submit', onSendStringSubmit); + events.on(playbackManager, 'playerchange', onPlayerChange); + + if (layoutManager.tv) { + const positionSlider = context.querySelector('.nowPlayingPositionSlider'); + positionSlider.classList.add('focusable'); + positionSlider.enableKeyboardDragging(); } + } - function onShow(context, tab) { - bindToPlayer(context, playbackManager.getCurrentPlayer()); - } + function onDialogClosed(e) { + releaseCurrentPlayer(); + events.off(playbackManager, 'playerchange', onPlayerChange); + lastPlayerState = null; + } - var dlg; - var currentPlayer; - var lastPlayerState; - var currentPlayerSupportedCommands = []; - var lastUpdateTime = 0; - var currentRuntimeTicks = 0; - var self = this; + function onShow(context, tab) { + bindToPlayer(context, playbackManager.getCurrentPlayer()); + } - self.init = function (ownerView, context) { - dlg = context; - init(ownerView, dlg); - }; + let dlg; + let currentPlayer; + let lastPlayerState; + let currentPlayerSupportedCommands = []; + let lastUpdateTime = 0; + let currentRuntimeTicks = 0; + const self = this; - self.onShow = function () { - onShow(dlg, window.location.hash); - }; - - self.destroy = function () { - onDialogClosed(); - }; + self.init = function (ownerView, context) { + dlg = context; + init(ownerView, dlg); }; -}); + + self.onShow = function () { + onShow(dlg, window.location.hash); + }; + + self.destroy = function () { + onDialogClosed(); + }; +} diff --git a/src/components/tabbedview/tabbedview.js b/src/components/tabbedview/tabbedview.js index c8c0f232be..710a0e3c40 100644 --- a/src/components/tabbedview/tabbedview.js +++ b/src/components/tabbedview/tabbedview.js @@ -1,32 +1,33 @@ -define(['backdrop', 'mainTabsManager', 'layoutManager', 'emby-tabs'], function (backdrop, mainTabsManager, layoutManager) { - 'use strict'; +import backdrop from 'backdrop'; +import * as mainTabsManager from 'mainTabsManager'; +import layoutManager from 'layoutManager'; +import 'emby-tabs'; - layoutManager = layoutManager.default || layoutManager; +function onViewDestroy(e) { + var tabControllers = this.tabControllers; - function onViewDestroy(e) { - var tabControllers = this.tabControllers; + if (tabControllers) { + tabControllers.forEach(function (t) { + if (t.destroy) { + t.destroy(); + } + }); - if (tabControllers) { - tabControllers.forEach(function (t) { - if (t.destroy) { - t.destroy(); - } - }); - - this.tabControllers = null; - } - - this.view = null; - this.params = null; - this.currentTabController = null; - this.initialTabIndex = null; + this.tabControllers = null; } - function onBeforeTabChange() { + this.view = null; + this.params = null; + this.currentTabController = null; + this.initialTabIndex = null; +} - } +function onBeforeTabChange() { - function TabbedView(view, params) { +} + +class TabbedView { + constructor(view, params) { this.tabControllers = []; this.view = view; this.params = params; @@ -87,7 +88,7 @@ define(['backdrop', 'mainTabsManager', 'layoutManager', 'emby-tabs'], function ( view.addEventListener('viewdestroy', onViewDestroy.bind(this)); } - TabbedView.prototype.onResume = function (options) { + onResume(options) { this.setTitle(); backdrop.clearBackdrop(); @@ -98,19 +99,18 @@ define(['backdrop', 'mainTabsManager', 'layoutManager', 'emby-tabs'], function ( } else if (currentTabController && currentTabController.onResume) { currentTabController.onResume({}); } - }; + } - TabbedView.prototype.onPause = function () { + onPause() { var currentTabController = this.currentTabController; if (currentTabController && currentTabController.onPause) { currentTabController.onPause(); } - }; - - TabbedView.prototype.setTitle = function () { + } + setTitle() { Emby.Page.setTitle(''); - }; + } +} - return TabbedView; -}); +export default TabbedView; diff --git a/src/apikeys.html b/src/controllers/dashboard/apikeys.html similarity index 100% rename from src/apikeys.html rename to src/controllers/dashboard/apikeys.html diff --git a/src/dashboard.html b/src/controllers/dashboard/dashboard.html similarity index 100% rename from src/dashboard.html rename to src/controllers/dashboard/dashboard.html diff --git a/src/device.html b/src/controllers/dashboard/devices/device.html similarity index 100% rename from src/device.html rename to src/controllers/dashboard/devices/device.html diff --git a/src/devices.html b/src/controllers/dashboard/devices/devices.html similarity index 100% rename from src/devices.html rename to src/controllers/dashboard/devices/devices.html diff --git a/src/dlnaprofile.html b/src/controllers/dashboard/dlna/profile.html similarity index 100% rename from src/dlnaprofile.html rename to src/controllers/dashboard/dlna/profile.html diff --git a/src/dlnaprofiles.html b/src/controllers/dashboard/dlna/profiles.html similarity index 100% rename from src/dlnaprofiles.html rename to src/controllers/dashboard/dlna/profiles.html diff --git a/src/dlnasettings.html b/src/controllers/dashboard/dlna/settings.html similarity index 100% rename from src/dlnasettings.html rename to src/controllers/dashboard/dlna/settings.html diff --git a/src/encodingsettings.html b/src/controllers/dashboard/encodingsettings.html similarity index 100% rename from src/encodingsettings.html rename to src/controllers/dashboard/encodingsettings.html diff --git a/src/dashboardgeneral.html b/src/controllers/dashboard/general.html similarity index 100% rename from src/dashboardgeneral.html rename to src/controllers/dashboard/general.html diff --git a/src/library.html b/src/controllers/dashboard/library.html similarity index 100% rename from src/library.html rename to src/controllers/dashboard/library.html diff --git a/src/controllers/dashboard/mediaLibrary.js b/src/controllers/dashboard/library.js similarity index 100% rename from src/controllers/dashboard/mediaLibrary.js rename to src/controllers/dashboard/library.js diff --git a/src/librarydisplay.html b/src/controllers/dashboard/librarydisplay.html similarity index 100% rename from src/librarydisplay.html rename to src/controllers/dashboard/librarydisplay.html diff --git a/src/log.html b/src/controllers/dashboard/logs.html similarity index 100% rename from src/log.html rename to src/controllers/dashboard/logs.html diff --git a/src/metadataimages.html b/src/controllers/dashboard/metadataimages.html similarity index 100% rename from src/metadataimages.html rename to src/controllers/dashboard/metadataimages.html diff --git a/src/metadatanfo.html b/src/controllers/dashboard/metadatanfo.html similarity index 100% rename from src/metadatanfo.html rename to src/controllers/dashboard/metadatanfo.html diff --git a/src/networking.html b/src/controllers/dashboard/networking.html similarity index 100% rename from src/networking.html rename to src/controllers/dashboard/networking.html diff --git a/src/playbackconfiguration.html b/src/controllers/dashboard/playback.html similarity index 100% rename from src/playbackconfiguration.html rename to src/controllers/dashboard/playback.html diff --git a/src/scheduledtask.html b/src/controllers/dashboard/scheduledtasks/scheduledtask.html similarity index 100% rename from src/scheduledtask.html rename to src/controllers/dashboard/scheduledtasks/scheduledtask.html diff --git a/src/scheduledtasks.html b/src/controllers/dashboard/scheduledtasks/scheduledtasks.html similarity index 100% rename from src/scheduledtasks.html rename to src/controllers/dashboard/scheduledtasks/scheduledtasks.html diff --git a/src/serveractivity.html b/src/controllers/dashboard/serveractivity.html similarity index 100% rename from src/serveractivity.html rename to src/controllers/dashboard/serveractivity.html diff --git a/src/streamingsettings.html b/src/controllers/dashboard/streaming.html similarity index 100% rename from src/streamingsettings.html rename to src/controllers/dashboard/streaming.html diff --git a/src/useredit.html b/src/controllers/dashboard/users/useredit.html similarity index 100% rename from src/useredit.html rename to src/controllers/dashboard/users/useredit.html diff --git a/src/userlibraryaccess.html b/src/controllers/dashboard/users/userlibraryaccess.html similarity index 100% rename from src/userlibraryaccess.html rename to src/controllers/dashboard/users/userlibraryaccess.html diff --git a/src/usernew.html b/src/controllers/dashboard/users/usernew.html similarity index 100% rename from src/usernew.html rename to src/controllers/dashboard/users/usernew.html diff --git a/src/userparentalcontrol.html b/src/controllers/dashboard/users/userparentalcontrol.html similarity index 100% rename from src/userparentalcontrol.html rename to src/controllers/dashboard/users/userparentalcontrol.html diff --git a/src/userpassword.html b/src/controllers/dashboard/users/userpassword.html similarity index 100% rename from src/userpassword.html rename to src/controllers/dashboard/users/userpassword.html diff --git a/src/userprofiles.html b/src/controllers/dashboard/users/userprofiles.html similarity index 100% rename from src/userprofiles.html rename to src/controllers/dashboard/users/userprofiles.html diff --git a/src/edititemmetadata.html b/src/controllers/edititemmetadata.html similarity index 100% rename from src/edititemmetadata.html rename to src/controllers/edititemmetadata.html diff --git a/src/home.html b/src/controllers/home.html similarity index 100% rename from src/home.html rename to src/controllers/home.html diff --git a/src/controllers/home.js b/src/controllers/home.js index b615b3609d..72e326e46b 100644 --- a/src/controllers/home.js +++ b/src/controllers/home.js @@ -1,7 +1,33 @@ -define(['tabbedView', 'globalize', 'require', 'emby-tabs', 'emby-button', 'emby-scroller'], function (TabbedView, globalize, require) { - 'use strict'; +import TabbedView from 'tabbedView'; +import globalize from 'globalize'; +import 'emby-tabs'; +import 'emby-button'; +import 'emby-scroller'; - function getTabs() { +class HomeView extends TabbedView { + constructor(view, params) { + super(view, params); + } + + setTitle() { + Emby.Page.setTitle(null); + } + + onPause() { + super.onPause(this); + document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader'); + } + + onResume(options) { + super.onResume(this, options); + document.querySelector('.skinHeader').classList.add('noHomeButtonHeader'); + } + + getDefaultTabIndex() { + return 0; + } + + getTabs() { return [{ name: globalize.translate('Home') }, { @@ -9,67 +35,34 @@ define(['tabbedView', 'globalize', 'require', 'emby-tabs', 'emby-button', 'emby- }]; } - function getDefaultTabIndex() { - return 0; - } - - function getRequirePromise(deps) { - return new Promise(function (resolve, reject) { - require(deps, resolve); - }); - } - - function getTabController(index) { + getTabController(index) { if (index == null) { throw new Error('index cannot be null'); } - var depends = []; + let depends = ''; switch (index) { case 0: - depends.push('controllers/hometab'); + depends = 'controllers/hometab'; break; case 1: - depends.push('controllers/favorites'); + depends = 'controllers/favorites'; } - var instance = this; - return getRequirePromise(depends).then(function (controllerFactory) { - var controller = instance.tabControllers[index]; + const instance = this; + return import(depends).then(({ default: controllerFactory }) => { + let controller = instance.tabControllers[index]; if (!controller) { - controller = new controllerFactory.default(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params); + controller = new controllerFactory(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params); instance.tabControllers[index] = controller; } return controller; }); } +} - function HomeView(view, params) { - TabbedView.call(this, view, params); - } - - Object.assign(HomeView.prototype, TabbedView.prototype); - HomeView.prototype.getTabs = getTabs; - HomeView.prototype.getDefaultTabIndex = getDefaultTabIndex; - HomeView.prototype.getTabController = getTabController; - - HomeView.prototype.setTitle = function () { - Emby.Page.setTitle(null); - }; - - HomeView.prototype.onPause = function () { - TabbedView.prototype.onPause.call(this); - document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader'); - }; - - HomeView.prototype.onResume = function (options) { - TabbedView.prototype.onResume.call(this, options); - document.querySelector('.skinHeader').classList.add('noHomeButtonHeader'); - }; - - return HomeView; -}); +export default HomeView; diff --git a/src/list.html b/src/controllers/list.html similarity index 100% rename from src/list.html rename to src/controllers/list.html diff --git a/src/livetv.html b/src/controllers/livetv.html similarity index 100% rename from src/livetv.html rename to src/controllers/livetv.html diff --git a/src/livetvguideprovider.html b/src/controllers/livetvguideprovider.html similarity index 100% rename from src/livetvguideprovider.html rename to src/controllers/livetvguideprovider.html diff --git a/src/livetvsettings.html b/src/controllers/livetvsettings.html similarity index 100% rename from src/livetvsettings.html rename to src/controllers/livetvsettings.html diff --git a/src/livetvstatus.html b/src/controllers/livetvstatus.html similarity index 100% rename from src/livetvstatus.html rename to src/controllers/livetvstatus.html diff --git a/src/livetvtuner.html b/src/controllers/livetvtuner.html similarity index 100% rename from src/livetvtuner.html rename to src/controllers/livetvtuner.html diff --git a/src/movies.html b/src/controllers/movies/movies.html similarity index 100% rename from src/movies.html rename to src/controllers/movies/movies.html diff --git a/src/music.html b/src/controllers/music/music.html similarity index 100% rename from src/music.html rename to src/controllers/music/music.html diff --git a/src/search.html b/src/controllers/search.html similarity index 100% rename from src/search.html rename to src/controllers/search.html diff --git a/src/tv.html b/src/controllers/shows/tvrecommended.html similarity index 100% rename from src/tv.html rename to src/controllers/shows/tvrecommended.html diff --git a/src/wizardlibrary.html b/src/controllers/wizard/library.html similarity index 100% rename from src/wizardlibrary.html rename to src/controllers/wizard/library.html diff --git a/src/index.html b/src/index.html index 797fce8a94..c689a42f30 100644 --- a/src/index.html +++ b/src/index.html @@ -137,8 +137,7 @@ } @media screen - and (min-device-width: 992px) - and (-webkit-min-device-pixel-ratio: 1) { + and (min-device-width: 992px) { .splashLogo { background-image: url(assets/img/banner-light.png); } diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index 22b49d6faf..b7e6d05969 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -1,66 +1,71 @@ -define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', 'globalize', 'events', 'require', 'castSenderApiLoader'], function (appSettings, userSettings, playbackManager, connectionManager, globalize, events, require, castSenderApiLoader) { - 'use strict'; +import appSettings from 'appSettings'; +import * as userSettings from 'userSettings'; +import playbackManager from 'playbackManager'; +import connectionManager from 'connectionManager'; +import globalize from 'globalize'; +import events from 'events'; +import castSenderApiLoader from 'castSenderApiLoader'; - playbackManager = playbackManager.default || playbackManager; +// Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js - // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js - var currentResolve; - var currentReject; +let currentResolve; +let currentReject; - var PlayerName = 'Google Cast'; +const PlayerName = 'Google Cast'; - function sendConnectionResult(isOk) { - var resolve = currentResolve; - var reject = currentReject; +function sendConnectionResult(isOk) { + const resolve = currentResolve; + const reject = currentReject; - currentResolve = null; - currentReject = null; + currentResolve = null; + currentReject = null; - if (isOk) { - if (resolve) { - resolve(); - } + if (isOk) { + if (resolve) { + resolve(); + } + } else { + if (reject) { + reject(); } else { - if (reject) { - reject(); - } else { - playbackManager.removeActivePlayer(PlayerName); - } + playbackManager.removeActivePlayer(PlayerName); } } +} - /** - * Constants of states for Chromecast device - **/ - var DEVICE_STATE = { - 'IDLE': 0, - 'ACTIVE': 1, - 'WARNING': 2, - 'ERROR': 3 - }; +/** + * Constants of states for Chromecast device + **/ +const DEVICE_STATE = { + 'IDLE': 0, + 'ACTIVE': 1, + 'WARNING': 2, + 'ERROR': 3 +}; - /** - * Constants of states for CastPlayer - **/ - var PLAYER_STATE = { - 'IDLE': 'IDLE', - 'LOADING': 'LOADING', - 'LOADED': 'LOADED', - 'PLAYING': 'PLAYING', - 'PAUSED': 'PAUSED', - 'STOPPED': 'STOPPED', - 'SEEKING': 'SEEKING', - 'ERROR': 'ERROR' - }; +/** + * Constants of states for CastPlayer + **/ +const PLAYER_STATE = { + 'IDLE': 'IDLE', + 'LOADING': 'LOADING', + 'LOADED': 'LOADED', + 'PLAYING': 'PLAYING', + 'PAUSED': 'PAUSED', + 'STOPPED': 'STOPPED', + 'SEEKING': 'SEEKING', + 'ERROR': 'ERROR' +}; - // production version registered with google - // replace this value if you want to test changes on another instance - var applicationStable = 'F007D354'; - var applicationUnstable = '6F511C87'; +// production version registered with google +// replace this value if you want to test changes on another instance +const applicationStable = 'F007D354'; +const applicationUnstable = '6F511C87'; - var messageNamespace = 'urn:x-cast:com.connectsdk'; +const messageNamespace = 'urn:x-cast:com.connectsdk'; - var CastPlayer = function () { +class CastPlayer { + constructor() { /* device variables */ // @type {DEVICE_STATE} A state for device this.deviceState = DEVICE_STATE.IDLE; @@ -81,7 +86,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' this.mediaStatusUpdateHandler = this.onMediaStatusUpdate.bind(this); this.initializeCastPlayer(); - }; + } /** * Initialize Cast media player @@ -89,8 +94,8 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' * invoked once the API has finished initialization. The sessionListener and * receiverListener may be invoked at any time afterwards, and possibly more than once. */ - CastPlayer.prototype.initializeCastPlayer = function () { - var chrome = window.chrome; + initializeCastPlayer() { + const chrome = window.chrome; if (!chrome) { return; } @@ -100,35 +105,35 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' return; } - var applicationID = applicationStable; + let applicationID = applicationStable; if (userSettings.chromecastVersion() === 'unstable') { applicationID = applicationUnstable; } // request session - var sessionRequest = new chrome.cast.SessionRequest(applicationID); - var apiConfig = new chrome.cast.ApiConfig(sessionRequest, + const sessionRequest = new chrome.cast.SessionRequest(applicationID); + const apiConfig = new chrome.cast.ApiConfig(sessionRequest, this.sessionListener.bind(this), this.receiverListener.bind(this)); console.debug('chromecast.initialize'); chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler); - }; + } /** * Callback function for init success */ - CastPlayer.prototype.onInitSuccess = function () { + onInitSuccess() { this.isInitialized = true; console.debug('chromecast init success'); - }; + } /** * Generic error callback function */ - CastPlayer.prototype.onError = function () { + onError() { console.debug('chromecast error'); - }; + } /** * @param {!Object} e A new session @@ -137,7 +142,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' * join existing session and occur in Cast mode and media * status gets synced up with current media of the session */ - CastPlayer.prototype.sessionListener = function (e) { + sessionListener(e) { this.session = e; if (this.session) { if (this.session.media[0]) { @@ -146,24 +151,15 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' this.onSessionConnected(e); } - }; - - function alertText(text, title) { - require(['alert'], function (alert) { - alert.default({ - text: text, - title: title - }); - }); } - CastPlayer.prototype.messageListener = function (namespace, message) { + messageListener(namespace, message) { if (typeof (message) === 'string') { message = JSON.parse(message); } if (message.type === 'playbackerror') { - var errorCode = message.data; + const errorCode = message.data; setTimeout(function () { alertText(globalize.translate('MessagePlaybackError' + errorCode), globalize.translate('HeaderPlaybackError')); }, 300); @@ -174,14 +170,14 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' } else if (message.type) { events.trigger(this, message.type, [message.data]); } - }; + } /** * @param {string} e Receiver availability * This indicates availability of receivers but * does not provide a list of device IDs */ - CastPlayer.prototype.receiverListener = function (e) { + receiverListener(e) { if (e === 'available') { console.debug('chromecast receiver found'); this.hasReceivers = true; @@ -189,12 +185,12 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' console.debug('chromecast receiver list empty'); this.hasReceivers = false; } - }; + } /** * session update listener */ - CastPlayer.prototype.sessionUpdateListener = function (isAlive) { + sessionUpdateListener(isAlive) { if (isAlive) { console.debug('sessionUpdateListener: already alive'); } else { @@ -209,28 +205,28 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' sendConnectionResult(false); } - }; + } /** * Requests that a receiver application session be created or joined. By default, the SessionRequest * passed to the API at initialization time is used; this may be overridden by passing a different * session request in opt_sessionRequest. */ - CastPlayer.prototype.launchApp = function () { + launchApp() { console.debug('chromecast launching app...'); chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this)); - }; + } /** * Callback function for request session success * @param {Object} e A chrome.cast.Session object */ - CastPlayer.prototype.onRequestSessionSuccess = function (e) { + onRequestSessionSuccess(e) { console.debug('chromecast session success: ' + e.sessionId); this.onSessionConnected(e); - }; + } - CastPlayer.prototype.onSessionConnected = function (session) { + onSessionConnected(session) { this.session = session; this.deviceState = DEVICE_STATE.ACTIVE; @@ -246,46 +242,38 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' options: {}, command: 'Identify' }); - }; - - function onVolumeUpKeyDown() { - playbackManager.volumeUp(); - } - - function onVolumeDownKeyDown() { - playbackManager.volumeDown(); } /** * session update listener */ - CastPlayer.prototype.sessionMediaListener = function (e) { + sessionMediaListener(e) { this.currentMediaSession = e; this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); - }; + } /** * Callback function for launch error */ - CastPlayer.prototype.onLaunchError = function () { + onLaunchError() { console.debug('chromecast launch error'); this.deviceState = DEVICE_STATE.ERROR; sendConnectionResult(false); - }; + } /** * Stops the running receiver application associated with the session. */ - CastPlayer.prototype.stopApp = function () { + stopApp() { if (this.session) { this.session.stop(this.onStopAppSuccess.bind(this, 'Session stopped'), this.errorHandler); } - }; + } /** * Callback function for stop app success */ - CastPlayer.prototype.onStopAppSuccess = function (message) { + onStopAppSuccess(message) { console.debug(message); this.deviceState = DEVICE_STATE.IDLE; @@ -294,13 +282,13 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' document.removeEventListener('volumedownbutton', onVolumeDownKeyDown, false); this.currentMediaSession = null; - }; + } /** * Loads media into a running receiver application * @param {Number} mediaIndex An index number to indicate current media content */ - CastPlayer.prototype.loadMedia = function (options, command) { + loadMedia(options, command) { if (!this.session) { console.debug('no session'); return Promise.reject(); @@ -322,20 +310,20 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' options: options, command: command }); - }; + } - CastPlayer.prototype.sendMessage = function (message) { - var player = this; + sendMessage(message) { + const player = this; - var receiverName = null; + let receiverName = null; - var session = player.session; + const session = player.session; if (session && session.receiver && session.receiver.friendlyName) { receiverName = session.receiver.friendlyName; } - var apiClient; + let apiClient; if (message.options && message.options.ServerId) { apiClient = connectionManager.getApiClient(message.options.ServerId); } else if (message.options && message.options.items && message.options.items.length) { @@ -354,7 +342,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' receiverName: receiverName }); - var bitrateSetting = appSettings.maxChromecastBitrate(); + const bitrateSetting = appSettings.maxChromecastBitrate(); if (bitrateSetting) { message.maxBitrate = bitrateSetting; } @@ -365,31 +353,31 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' } return new Promise(function (resolve, reject) { - require(['./chromecastHelper'], function (chromecastHelper) { + import('./chromecastHelper').then(({ default: chromecastHelper }) => { chromecastHelper.getServerAddress(apiClient).then(function (serverAddress) { message.serverAddress = serverAddress; player.sendMessageInternal(message).then(resolve, reject); }, reject); }); }); - }; + } - CastPlayer.prototype.sendMessageInternal = function (message) { + sendMessageInternal(message) { message = JSON.stringify(message); this.session.sendMessage(messageNamespace, message, this.onPlayCommandSuccess.bind(this), this.errorHandler); return Promise.resolve(); - }; + } - CastPlayer.prototype.onPlayCommandSuccess = function () { + onPlayCommandSuccess() { console.debug('Message was sent to receiver ok.'); - }; + } /** * Callback function for loadMedia success * @param {Object} mediaSession A new media object. */ - CastPlayer.prototype.onMediaDiscovered = function (how, mediaSession) { + onMediaDiscovered(how, mediaSession) { console.debug('chromecast new media session ID:' + mediaSession.mediaSessionId + ' (' + how + ')'); this.currentMediaSession = mediaSession; @@ -402,24 +390,24 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' } this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); - }; + } /** * Callback function for media status update from receiver * @param {!Boolean} e true/false */ - CastPlayer.prototype.onMediaStatusUpdate = function (e) { + onMediaStatusUpdate(e) { console.debug('chromecast updating media: ' + e); if (e === false) { this.castPlayerState = PLAYER_STATE.IDLE; } - }; + } /** * Set media volume in Cast mode * @param {Boolean} mute A boolean */ - CastPlayer.prototype.setReceiverVolume = function (mute, vol) { + setReceiverVolume(mute, vol) { if (!this.currentMediaSession) { console.debug('this.currentMediaSession is null'); return; @@ -434,142 +422,161 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' this.mediaCommandSuccessCallback.bind(this), this.errorHandler); } - }; + } /** * Mute CC */ - CastPlayer.prototype.mute = function () { + mute() { this.setReceiverVolume(true); - }; + } /** * Callback function for media command success */ - CastPlayer.prototype.mediaCommandSuccessCallback = function (info, e) { + mediaCommandSuccessCallback(info, e) { console.debug(info); - }; + } +} - function normalizeImages(state) { - if (state && state.NowPlayingItem) { - var item = state.NowPlayingItem; +function alertText(text, title) { + import('alert').then(({default: alert}) => { + alert({ + text: text, + title: title + }); + }); +} - if (!item.ImageTags || !item.ImageTags.Primary) { - if (item.PrimaryImageTag) { - item.ImageTags = item.ImageTags || {}; - item.ImageTags.Primary = item.PrimaryImageTag; - } - } - if (item.BackdropImageTag && item.BackdropItemId === item.Id) { - item.BackdropImageTags = [item.BackdropImageTag]; - } - if (item.BackdropImageTag && item.BackdropItemId !== item.Id) { - item.ParentBackdropImageTags = [item.BackdropImageTag]; - item.ParentBackdropItemId = item.BackdropItemId; +function onVolumeUpKeyDown() { + playbackManager.volumeUp(); +} + +function onVolumeDownKeyDown() { + playbackManager.volumeDown(); +} + +function normalizeImages(state) { + if (state && state.NowPlayingItem) { + const item = state.NowPlayingItem; + + if (!item.ImageTags || !item.ImageTags.Primary) { + if (item.PrimaryImageTag) { + item.ImageTags = item.ImageTags || {}; + item.ImageTags.Primary = item.PrimaryImageTag; } } + if (item.BackdropImageTag && item.BackdropItemId === item.Id) { + item.BackdropImageTags = [item.BackdropImageTag]; + } + if (item.BackdropImageTag && item.BackdropItemId !== item.Id) { + item.ParentBackdropImageTags = [item.BackdropImageTag]; + item.ParentBackdropItemId = item.BackdropItemId; + } } +} - function getItemsForPlayback(apiClient, query) { - var userId = apiClient.getCurrentUserId(); +function getItemsForPlayback(apiClient, query) { + const userId = apiClient.getCurrentUserId(); - if (query.Ids && query.Ids.split(',').length === 1) { - return apiClient.getItem(userId, query.Ids.split(',')).then(function (item) { - return { - Items: [item], - TotalRecordCount: 1 - }; - }); + if (query.Ids && query.Ids.split(',').length === 1) { + return apiClient.getItem(userId, query.Ids.split(',')).then(function (item) { + return { + Items: [item], + TotalRecordCount: 1 + }; + }); + } else { + query.Limit = query.Limit || 100; + query.ExcludeLocationTypes = 'Virtual'; + query.EnableTotalRecordCount = false; + + return apiClient.getItems(userId, query); + } +} + +function bindEventForRelay(instance, eventName) { + events.on(instance._castPlayer, eventName, function (e, data) { + console.debug('cc: ' + eventName); + const state = instance.getPlayerStateInternal(data); + + events.trigger(instance, eventName, [state]); + }); +} + +function initializeChromecast() { + const instance = this; + instance._castPlayer = new CastPlayer(); + + // To allow the native android app to override + document.dispatchEvent(new CustomEvent('chromecastloaded', { + detail: { + player: instance + } + })); + + events.on(instance._castPlayer, 'connect', function (e) { + if (currentResolve) { + sendConnectionResult(true); } else { - query.Limit = query.Limit || 100; - query.ExcludeLocationTypes = 'Virtual'; - query.EnableTotalRecordCount = false; - - return apiClient.getItems(userId, query); + playbackManager.setActivePlayer(PlayerName, instance.getCurrentTargetInfo()); } - } - function bindEventForRelay(instance, eventName) { - events.on(instance._castPlayer, eventName, function (e, data) { - console.debug('cc: ' + eventName); - var state = instance.getPlayerStateInternal(data); + console.debug('cc: connect'); + // Reset this so that statechange will fire + instance.lastPlayerData = null; + }); - events.trigger(instance, eventName, [state]); - }); - } + events.on(instance._castPlayer, 'playbackstart', function (e, data) { + console.debug('cc: playbackstart'); - function initializeChromecast() { - var instance = this; - instance._castPlayer = new CastPlayer(); + instance._castPlayer.initializeCastPlayer(); - // To allow the native android app to override - document.dispatchEvent(new CustomEvent('chromecastloaded', { - detail: { - player: instance - } - })); + const state = instance.getPlayerStateInternal(data); + events.trigger(instance, 'playbackstart', [state]); + }); - events.on(instance._castPlayer, 'connect', function (e) { - if (currentResolve) { - sendConnectionResult(true); - } else { - playbackManager.setActivePlayer(PlayerName, instance.getCurrentTargetInfo()); - } + events.on(instance._castPlayer, 'playbackstop', function (e, data) { + console.debug('cc: playbackstop'); + let state = instance.getPlayerStateInternal(data); - console.debug('cc: connect'); - // Reset this so that statechange will fire - instance.lastPlayerData = null; - }); + events.trigger(instance, 'playbackstop', [state]); - events.on(instance._castPlayer, 'playbackstart', function (e, data) { - console.debug('cc: playbackstart'); + state = instance.lastPlayerData.PlayState || {}; + const volume = state.VolumeLevel || 0.5; + const mute = state.IsMuted || false; - instance._castPlayer.initializeCastPlayer(); + // Reset this so the next query doesn't make it appear like content is playing. + instance.lastPlayerData = {}; + instance.lastPlayerData.PlayState = {}; + instance.lastPlayerData.PlayState.VolumeLevel = volume; + instance.lastPlayerData.PlayState.IsMuted = mute; + }); - var state = instance.getPlayerStateInternal(data); - events.trigger(instance, 'playbackstart', [state]); - }); + events.on(instance._castPlayer, 'playbackprogress', function (e, data) { + console.debug('cc: positionchange'); + const state = instance.getPlayerStateInternal(data); - events.on(instance._castPlayer, 'playbackstop', function (e, data) { - console.debug('cc: playbackstop'); - var state = instance.getPlayerStateInternal(data); + events.trigger(instance, 'timeupdate', [state]); + }); - events.trigger(instance, 'playbackstop', [state]); + bindEventForRelay(instance, 'timeupdate'); + bindEventForRelay(instance, 'pause'); + bindEventForRelay(instance, 'unpause'); + bindEventForRelay(instance, 'volumechange'); + bindEventForRelay(instance, 'repeatmodechange'); + bindEventForRelay(instance, 'shufflequeuemodechange'); - state = instance.lastPlayerData.PlayState || {}; - var volume = state.VolumeLevel || 0.5; - var mute = state.IsMuted || false; + events.on(instance._castPlayer, 'playstatechange', function (e, data) { + console.debug('cc: playstatechange'); + const state = instance.getPlayerStateInternal(data); - // Reset this so the next query doesn't make it appear like content is playing. - instance.lastPlayerData = {}; - instance.lastPlayerData.PlayState = {}; - instance.lastPlayerData.PlayState.VolumeLevel = volume; - instance.lastPlayerData.PlayState.IsMuted = mute; - }); + events.trigger(instance, 'pause', [state]); + }); +} - events.on(instance._castPlayer, 'playbackprogress', function (e, data) { - console.debug('cc: positionchange'); - var state = instance.getPlayerStateInternal(data); - - events.trigger(instance, 'timeupdate', [state]); - }); - - bindEventForRelay(instance, 'timeupdate'); - bindEventForRelay(instance, 'pause'); - bindEventForRelay(instance, 'unpause'); - bindEventForRelay(instance, 'volumechange'); - bindEventForRelay(instance, 'repeatmodechange'); - bindEventForRelay(instance, 'shufflequeuemodechange'); - - events.on(instance._castPlayer, 'playstatechange', function (e, data) { - console.debug('cc: playstatechange'); - var state = instance.getPlayerStateInternal(data); - - events.trigger(instance, 'pause', [state]); - }); - } - - function ChromecastPlayer() { +class ChromecastPlayer { + constructor() { // playbackManager needs this this.name = PlayerName; this.type = 'mediaplayer'; @@ -577,11 +584,11 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' this.isLocalPlayer = false; this.lastPlayerData = {}; - new castSenderApiLoader.default().load().then(initializeChromecast.bind(this)); + new castSenderApiLoader().load().then(initializeChromecast.bind(this)); } - ChromecastPlayer.prototype.tryPair = function (target) { - var castPlayer = this._castPlayer; + tryPair(target) { + const castPlayer = this._castPlayer; if (castPlayer.deviceState !== DEVICE_STATE.ACTIVE && castPlayer.isInitialized) { return new Promise(function (resolve, reject) { @@ -595,23 +602,23 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' return Promise.reject(); } - }; + } - ChromecastPlayer.prototype.getTargets = function () { - var targets = []; + getTargets() { + const targets = []; if (this._castPlayer && this._castPlayer.hasReceivers) { targets.push(this.getCurrentTargetInfo()); } return Promise.resolve(targets); - }; + } // This is a privately used method - ChromecastPlayer.prototype.getCurrentTargetInfo = function () { - var appName = null; + getCurrentTargetInfo() { + let appName = null; - var castPlayer = this._castPlayer; + const castPlayer = this._castPlayer; if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) { appName = castPlayer.session.receiver.friendlyName; @@ -642,10 +649,10 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' 'PlayTrailers' ] }; - }; + } - ChromecastPlayer.prototype.getPlayerStateInternal = function (data) { - var triggerStateChange = false; + getPlayerStateInternal(data) { + let triggerStateChange = false; if (data && !this.lastPlayerData) { triggerStateChange = true; } @@ -662,12 +669,12 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' } return data; - }; + } - ChromecastPlayer.prototype.playWithCommand = function (options, command) { + playWithCommand(options, command) { if (!options.items) { - var apiClient = connectionManager.getApiClient(options.serverId); - var instance = this; + const apiClient = connectionManager.getApiClient(options.serverId); + const instance = this; return apiClient.getItem(apiClient.getCurrentUserId(), options.ids[0]).then(function (item) { options.items = [item]; @@ -683,9 +690,9 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' } return this._castPlayer.loadMedia(options, command); - }; + } - ChromecastPlayer.prototype.seek = function (position) { + seek(position) { position = parseInt(position); position = position / 10000000; @@ -696,55 +703,55 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' }, command: 'Seek' }); - }; + } - ChromecastPlayer.prototype.setAudioStreamIndex = function (index) { + setAudioStreamIndex(index) { this._castPlayer.sendMessage({ options: { index: index }, command: 'SetAudioStreamIndex' }); - }; + } - ChromecastPlayer.prototype.setSubtitleStreamIndex = function (index) { + setSubtitleStreamIndex(index) { this._castPlayer.sendMessage({ options: { index: index }, command: 'SetSubtitleStreamIndex' }); - }; + } - ChromecastPlayer.prototype.setMaxStreamingBitrate = function (options) { + setMaxStreamingBitrate(options) { this._castPlayer.sendMessage({ options: options, command: 'SetMaxStreamingBitrate' }); - }; + } - ChromecastPlayer.prototype.isFullscreen = function () { - var state = this.lastPlayerData || {}; + isFullscreen() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.IsFullscreen; - }; + } - ChromecastPlayer.prototype.nextTrack = function () { + nextTrack() { this._castPlayer.sendMessage({ options: {}, command: 'NextTrack' }); - }; + } - ChromecastPlayer.prototype.previousTrack = function () { + previousTrack() { this._castPlayer.sendMessage({ options: {}, command: 'PreviousTrack' }); - }; + } - ChromecastPlayer.prototype.volumeDown = function () { - var vol = this._castPlayer.session.receiver.volume.level; + volumeDown() { + let vol = this._castPlayer.session.receiver.volume.level; if (vol == null) { vol = 0.5; } @@ -752,20 +759,20 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' vol = Math.max(vol, 0); this._castPlayer.session.setReceiverVolumeLevel(vol); - }; + } - ChromecastPlayer.prototype.endSession = function () { - var instance = this; + endSession() { + const instance = this; this.stop().then(function () { setTimeout(function () { instance._castPlayer.stopApp(); }, 1000); }); - }; + } - ChromecastPlayer.prototype.volumeUp = function () { - var vol = this._castPlayer.session.receiver.volume.level; + volumeUp() { + let vol = this._castPlayer.session.receiver.volume.level; if (vol == null) { vol = 0.5; } @@ -773,53 +780,53 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' vol = Math.min(vol, 1); this._castPlayer.session.setReceiverVolumeLevel(vol); - }; + } - ChromecastPlayer.prototype.setVolume = function (vol) { + setVolume(vol) { vol = Math.min(vol, 100); vol = Math.max(vol, 0); vol = vol / 100; this._castPlayer.session.setReceiverVolumeLevel(vol); - }; + } - ChromecastPlayer.prototype.unpause = function () { + unpause() { this._castPlayer.sendMessage({ options: {}, command: 'Unpause' }); - }; + } - ChromecastPlayer.prototype.playPause = function () { + playPause() { this._castPlayer.sendMessage({ options: {}, command: 'PlayPause' }); - }; + } - ChromecastPlayer.prototype.pause = function () { + pause() { this._castPlayer.sendMessage({ options: {}, command: 'Pause' }); - }; + } - ChromecastPlayer.prototype.stop = function () { + stop() { return this._castPlayer.sendMessage({ options: {}, command: 'Stop' }); - }; + } - ChromecastPlayer.prototype.displayContent = function (options) { + displayContent(options) { this._castPlayer.sendMessage({ options: options, command: 'DisplayContent' }); - }; + } - ChromecastPlayer.prototype.setMute = function (isMuted) { - var castPlayer = this._castPlayer; + setMute(isMuted) { + const castPlayer = this._castPlayer; if (isMuted) { castPlayer.sendMessage({ @@ -832,21 +839,21 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' command: 'Unmute' }); } - }; + } - ChromecastPlayer.prototype.getRepeatMode = function () { - var state = this.lastPlayerData || {}; + getRepeatMode() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.RepeatMode; - }; + } - ChromecastPlayer.prototype.getQueueShuffleMode = function () { - var state = this.lastPlayerData || {}; + getQueueShuffleMode() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.ShuffleMode; - }; + } - ChromecastPlayer.prototype.playTrailers = function (item) { + playTrailers(item) { this._castPlayer.sendMessage({ options: { ItemId: item.Id, @@ -854,177 +861,173 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' }, command: 'PlayTrailers' }); - }; + } - ChromecastPlayer.prototype.setRepeatMode = function (mode) { + setRepeatMode(mode) { this._castPlayer.sendMessage({ options: { RepeatMode: mode }, command: 'SetRepeatMode' }); - }; + } - ChromecastPlayer.prototype.setQueueShuffleMode = function (value) { + setQueueShuffleMode(value) { this._castPlayer.sendMessage({ options: { ShuffleMode: value }, command: 'SetShuffleQueue' }); - }; + } - ChromecastPlayer.prototype.toggleMute = function () { + toggleMute() { this._castPlayer.sendMessage({ options: {}, command: 'ToggleMute' }); - }; + } - ChromecastPlayer.prototype.audioTracks = function () { - var state = this.lastPlayerData || {}; + audioTracks() { + let state = this.lastPlayerData || {}; state = state.NowPlayingItem || {}; - var streams = state.MediaStreams || []; + const streams = state.MediaStreams || []; return streams.filter(function (s) { return s.Type === 'Audio'; }); - }; + } - ChromecastPlayer.prototype.getAudioStreamIndex = function () { - var state = this.lastPlayerData || {}; + getAudioStreamIndex() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.AudioStreamIndex; - }; + } - ChromecastPlayer.prototype.subtitleTracks = function () { - var state = this.lastPlayerData || {}; + subtitleTracks() { + let state = this.lastPlayerData || {}; state = state.NowPlayingItem || {}; - var streams = state.MediaStreams || []; + const streams = state.MediaStreams || []; return streams.filter(function (s) { return s.Type === 'Subtitle'; }); - }; + } - ChromecastPlayer.prototype.getSubtitleStreamIndex = function () { - var state = this.lastPlayerData || {}; + getSubtitleStreamIndex() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.SubtitleStreamIndex; - }; + } - ChromecastPlayer.prototype.getMaxStreamingBitrate = function () { - var state = this.lastPlayerData || {}; + getMaxStreamingBitrate() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.MaxStreamingBitrate; - }; + } - ChromecastPlayer.prototype.getVolume = function () { - var state = this.lastPlayerData || {}; + getVolume() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.VolumeLevel == null ? 100 : state.VolumeLevel; - }; + } - ChromecastPlayer.prototype.isPlaying = function () { - var state = this.lastPlayerData || {}; + isPlaying() { + const state = this.lastPlayerData || {}; return state.NowPlayingItem != null; - }; + } - ChromecastPlayer.prototype.isPlayingVideo = function () { - var state = this.lastPlayerData || {}; + isPlayingVideo() { + let state = this.lastPlayerData || {}; state = state.NowPlayingItem || {}; return state.MediaType === 'Video'; - }; + } - ChromecastPlayer.prototype.isPlayingAudio = function () { - var state = this.lastPlayerData || {}; + isPlayingAudio() { + let state = this.lastPlayerData || {}; state = state.NowPlayingItem || {}; return state.MediaType === 'Audio'; - }; + } - ChromecastPlayer.prototype.currentTime = function (val) { + currentTime(val) { if (val != null) { return this.seek(val); } - var state = this.lastPlayerData || {}; + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.PositionTicks; - }; + } - ChromecastPlayer.prototype.duration = function () { - var state = this.lastPlayerData || {}; + duration() { + let state = this.lastPlayerData || {}; state = state.NowPlayingItem || {}; return state.RunTimeTicks; - }; + } - ChromecastPlayer.prototype.getBufferedRanges = function () { - var state = this.lastPlayerData || {}; + getBufferedRanges() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.BufferedRanges || []; - }; + } - ChromecastPlayer.prototype.paused = function () { - var state = this.lastPlayerData || {}; + paused() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.IsPaused; - }; + } - ChromecastPlayer.prototype.isMuted = function () { - var state = this.lastPlayerData || {}; + isMuted() { + let state = this.lastPlayerData || {}; state = state.PlayState || {}; return state.IsMuted; - }; + } - ChromecastPlayer.prototype.shuffle = function (item) { - var apiClient = connectionManager.getApiClient(item.ServerId); - var userId = apiClient.getCurrentUserId(); + shuffle(item) { + const apiClient = connectionManager.getApiClient(item.ServerId); + const userId = apiClient.getCurrentUserId(); - var instance = this; + const instance = this; apiClient.getItem(userId, item.Id).then(function (item) { instance.playWithCommand({ - items: [item] - }, 'Shuffle'); }); - }; + } - ChromecastPlayer.prototype.instantMix = function (item) { - var apiClient = connectionManager.getApiClient(item.ServerId); - var userId = apiClient.getCurrentUserId(); + instantMix(item) { + const apiClient = connectionManager.getApiClient(item.ServerId); + const userId = apiClient.getCurrentUserId(); - var instance = this; + const instance = this; apiClient.getItem(userId, item.Id).then(function (item) { instance.playWithCommand({ - items: [item] - }, 'InstantMix'); }); - }; + } - ChromecastPlayer.prototype.canPlayMediaType = function (mediaType) { + canPlayMediaType(mediaType) { mediaType = (mediaType || '').toLowerCase(); return mediaType === 'audio' || mediaType === 'video'; - }; + } - ChromecastPlayer.prototype.canQueueMediaType = function (mediaType) { + canQueueMediaType(mediaType) { return this.canPlayMediaType(mediaType); - }; + } - ChromecastPlayer.prototype.queue = function (options) { + queue(options) { this.playWithCommand(options, 'PlayLast'); - }; + } - ChromecastPlayer.prototype.queueNext = function (options) { + queueNext(options) { this.playWithCommand(options, 'PlayNext'); - }; + } - ChromecastPlayer.prototype.play = function (options) { + play(options) { if (options.items) { return this.playWithCommand(options, 'PlayNow'); } else { @@ -1032,50 +1035,48 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' throw new Error('serverId required!'); } - var instance = this; - var apiClient = connectionManager.getApiClient(options.serverId); + const instance = this; + const apiClient = connectionManager.getApiClient(options.serverId); return getItemsForPlayback(apiClient, { - Ids: options.ids.join(',') - }).then(function (result) { options.items = result.Items; return instance.playWithCommand(options, 'PlayNow'); }); } - }; + } - ChromecastPlayer.prototype.toggleFullscreen = function () { + toggleFullscreen() { // not supported - }; + } - ChromecastPlayer.prototype.beginPlayerUpdates = function () { + beginPlayerUpdates() { // Setup polling here - }; + } - ChromecastPlayer.prototype.endPlayerUpdates = function () { + endPlayerUpdates() { // Stop polling here - }; + } - ChromecastPlayer.prototype.getPlaylist = function () { + getPlaylist() { return Promise.resolve([]); - }; + } - ChromecastPlayer.prototype.getCurrentPlaylistItemId = function () { - }; + getCurrentPlaylistItemId() { + } - ChromecastPlayer.prototype.setCurrentPlaylistItem = function (playlistItemId) { + setCurrentPlaylistItem(playlistItemId) { return Promise.resolve(); - }; + } - ChromecastPlayer.prototype.removeFromPlaylist = function (playlistItemIds) { + removeFromPlaylist(playlistItemIds) { return Promise.resolve(); - }; + } - ChromecastPlayer.prototype.getPlayerState = function () { + getPlayerState() { return this.getPlayerStateInternal() || {}; - }; + } +} - return ChromecastPlayer; -}); +export default ChromecastPlayer; diff --git a/src/scripts/libraryBrowser.js b/src/scripts/libraryBrowser.js index 46cda51e55..83d683a690 100644 --- a/src/scripts/libraryBrowser.js +++ b/src/scripts/libraryBrowser.js @@ -119,7 +119,10 @@ export function getQueryPagingHtml (options) { } export function showSortMenu (options) { - require(['dialogHelper', 'emby-radio'], function (dialogHelper) { + Promise.all([ + import('dialogHelper'), + import('emby-radio') + ]).then(([{default: dialogHelper}]) => { function onSortByChange() { var newValue = this.value; diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index 049ada6945..ec572752b9 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -1,13 +1,26 @@ -define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncPlayManager', 'groupSelectionMenu', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncPlayManager, groupSelectionMenu, browser, globalize, imageHelper) { - 'use strict'; +import dom from 'dom'; +import layoutManager from 'layoutManager'; +import inputManager from 'inputManager'; +import connectionManager from 'connectionManager'; +import events from 'events'; +import viewManager from 'viewManager'; +import appRouter from 'appRouter'; +import appHost from 'apphost'; +import playbackManager from 'playbackManager'; +import syncPlayManager from 'syncPlayManager'; +import groupSelectionMenu from 'groupSelectionMenu'; +import browser from 'browser'; +import globalize from 'globalize'; +import imageHelper from 'scripts/imagehelper'; +import 'paper-icon-button-light'; +import 'material-icons'; +import 'scrollStyles'; +import 'flexStyles'; - viewManager = viewManager.default || viewManager; - playbackManager = playbackManager.default || playbackManager; - browser = browser.default || browser; - layoutManager = layoutManager.default || layoutManager; +/* eslint-disable indent */ function renderHeader() { - var html = ''; + let html = ''; html += '
'; html += '
'; html += ''; @@ -51,7 +64,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function lazyLoadViewMenuBarImages() { - require(['imageLoader'], function (imageLoader) { + import('imageLoader').then(({default: imageLoader}) => { imageLoader.lazyChildren(skinHeader); }); } @@ -61,11 +74,11 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateUserInHeader(user) { - var hasImage; + let hasImage; if (user && user.name) { if (user.imageUrl) { - var url = user.imageUrl; + const url = user.imageUrl; updateHeaderUserButton(url); hasImage = true; } @@ -92,9 +105,9 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' headerCastButton.classList.remove('hide'); } - var policy = user.Policy ? user.Policy : user.localUser.Policy; + const policy = user.Policy ? user.Policy : user.localUser.Policy; - var apiClient = getCurrentApiClient(); + const apiClient = getCurrentApiClient(); if (headerSyncButton && policy && policy.SyncPlayAccess !== 'None' && apiClient.isMinServerVersion('10.6.0')) { headerSyncButton.classList.remove('hide'); } @@ -144,7 +157,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' mainDrawerButton.addEventListener('click', toggleMainDrawer); } - var headerBackButton = skinHeader.querySelector('.headerBackButton'); + const headerBackButton = skinHeader.querySelector('.headerBackButton'); if (headerBackButton) { headerBackButton.addEventListener('click', onBackClick); @@ -186,20 +199,20 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function onCastButtonClicked() { - var btn = this; + const btn = this; - require(['playerSelectionMenu'], function (playerSelectionMenu) { + import('playerSelectionMenu').then(({default: playerSelectionMenu}) => { playerSelectionMenu.show(btn); }); } function onSyncButtonClicked() { - var btn = this; + const btn = this; groupSelectionMenu.show(btn); } function onSyncPlayEnabled(event, enabled) { - var icon = headerSyncButton.querySelector('span'); + const icon = headerSyncButton.querySelector('span'); icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); if (enabled) { icon.classList.add('sync'); @@ -209,7 +222,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function onSyncPlaySyncing(event, is_syncing, syncMethod) { - var icon = headerSyncButton.querySelector('span'); + const icon = headerSyncButton.querySelector('span'); icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); if (is_syncing) { icon.classList.add('sync_problem'); @@ -255,7 +268,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function refreshLibraryInfoInDrawer(user, drawer) { - var html = ''; + let html = ''; html += '
'; html += '' + globalize.translate('ButtonHome') + ''; @@ -291,12 +304,12 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' // add buttons to navigation drawer navDrawerScrollContainer.innerHTML = html; - var btnSettings = navDrawerScrollContainer.querySelector('.btnSettings'); + const btnSettings = navDrawerScrollContainer.querySelector('.btnSettings'); if (btnSettings) { btnSettings.addEventListener('click', onSettingsClick); } - var btnLogout = navDrawerScrollContainer.querySelector('.btnLogout'); + const btnLogout = navDrawerScrollContainer.querySelector('.btnLogout'); if (btnLogout) { btnLogout.addEventListener('click', onLogoutClick); } @@ -318,20 +331,20 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateDashboardMenuSelectedItem() { - var links = navDrawerScrollContainer.querySelectorAll('.navMenuOption'); - var currentViewId = viewManager.currentView().id; + const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption'); + const currentViewId = viewManager.currentView().id; - for (var i = 0, length = links.length; i < length; i++) { - var link = links[i]; - var selected = false; - var pageIds = link.getAttribute('data-pageids'); + for (let i = 0, length = links.length; i < length; i++) { + let link = links[i]; + let selected = false; + let pageIds = link.getAttribute('data-pageids'); if (pageIds) { pageIds = pageIds.split('|'); selected = pageIds.indexOf(currentViewId) != -1; } - var pageUrls = link.getAttribute('data-pageurls'); + let pageUrls = link.getAttribute('data-pageurls'); if (pageUrls) { pageUrls = pageUrls.split('|'); @@ -340,7 +353,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' if (selected) { link.classList.add('navMenuOption-selected'); - var title = ''; + let title = ''; link = link.querySelector('.navMenuOptionText') || link; title += (link.innerText || link.textContent).trim(); LibraryMenu.setTitle(title); @@ -351,7 +364,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function createToolsMenuList(pluginItems) { - var links = [{ + const links = [{ name: globalize.translate('TabServer') }, { name: globalize.translate('TabDashboard'), @@ -463,8 +476,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function addPluginPagesToMainMenu(links, pluginItems, section) { - for (var i = 0, length = pluginItems.length; i < length; i++) { - var pluginItem = pluginItems[i]; + for (let i = 0, length = pluginItems.length; i < length; i++) { + const pluginItem = pluginItems[i]; if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === section) { links.push({ @@ -484,10 +497,10 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function getToolsLinkHtml(item) { - var menuHtml = ''; - var pageIds = item.pageIds ? item.pageIds.join('|') : ''; + let menuHtml = ''; + let pageIds = item.pageIds ? item.pageIds.join('|') : ''; pageIds = pageIds ? ' data-pageids="' + pageIds + '"' : ''; - var pageUrls = item.pageUrls ? item.pageUrls.join('|') : ''; + let pageUrls = item.pageUrls ? item.pageUrls.join('|') : ''; pageUrls = pageUrls ? ' data-pageurls="' + pageUrls + '"' : ''; menuHtml += ''; @@ -503,11 +516,11 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' function getToolsMenuHtml(apiClient) { return getToolsMenuLinks(apiClient).then(function (items) { - var item; - var menuHtml = ''; + let item; + let menuHtml = ''; menuHtml += '
'; - for (var i = 0; i < items.length; i++) { + for (let i = 0; i < items.length; i++) { item = items[i]; if (item.href) { @@ -525,7 +538,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' function createDashboardMenu(apiClient) { return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) { - var html = ''; + let html = ''; html += ''; @@ -536,24 +549,24 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function onSidebarLinkClick() { - var section = this.getElementsByClassName('sectionName')[0]; - var text = section ? section.innerHTML : this.innerHTML; + const section = this.getElementsByClassName('sectionName')[0]; + const text = section ? section.innerHTML : this.innerHTML; LibraryMenu.setTitle(text); } function getUserViews(apiClient, userId) { return apiClient.getUserViews({}, userId).then(function (result) { - var items = result.Items; - var list = []; + const items = result.Items; + const list = []; - for (var i = 0, length = items.length; i < length; i++) { - var view = items[i]; + for (let i = 0, length = items.length; i < length; i++) { + const view = items[i]; list.push(view); if (view.CollectionType == 'livetv') { view.ImageTags = {}; view.icon = 'live_tv'; - var guideView = Object.assign({}, view); + const guideView = Object.assign({}, view); guideView.Name = globalize.translate('ButtonGuide'); guideView.ImageTags = {}; guideView.icon = 'dvr'; @@ -567,7 +580,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function showBySelector(selector, show) { - var elem = document.querySelector(selector); + const elem = document.querySelector(selector); if (elem) { if (show) { @@ -597,17 +610,17 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' showBySelector('.libraryMenuDownloads', false); } - var userId = Dashboard.getCurrentUserId(); - var apiClient = getCurrentApiClient(); - var libraryMenuOptions = document.querySelector('.libraryMenuOptions'); + const userId = Dashboard.getCurrentUserId(); + const apiClient = getCurrentApiClient(); + const libraryMenuOptions = document.querySelector('.libraryMenuOptions'); if (libraryMenuOptions) { getUserViews(apiClient, userId).then(function (result) { - var items = result; - var html = `

${globalize.translate('HeaderMedia')}

`; + const items = result; + let html = `

${globalize.translate('HeaderMedia')}

`; html += items.map(function (i) { - var icon = i.icon || imageHelper.getLibraryIcon(i.CollectionType); - var itemId = i.Id; + const icon = i.icon || imageHelper.getLibraryIcon(i.CollectionType); + const itemId = i.Id; return ` @@ -615,8 +628,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' `; }).join(''); libraryMenuOptions.innerHTML = html; - var elem = libraryMenuOptions; - var sidebarLinks = elem.querySelectorAll('.navMenuOption'); + const elem = libraryMenuOptions; + const sidebarLinks = elem.querySelectorAll('.navMenuOption'); for (const sidebarLink of sidebarLinks) { sidebarLink.removeEventListener('click', onSidebarLinkClick); @@ -645,9 +658,9 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateCastIcon() { - var context = document; - var info = playbackManager.getPlayerInfo(); - var icon = headerCastButton.querySelector('.material-icons'); + const context = document; + const info = playbackManager.getPlayerInfo(); + const icon = headerCastButton.querySelector('.material-icons'); icon.classList.remove('cast_connected', 'cast'); @@ -663,18 +676,16 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateLibraryNavLinks(page) { - var i; - var length; - var isLiveTvPage = page.classList.contains('liveTvPage'); - var isChannelsPage = page.classList.contains('channelsPage'); - var isEditorPage = page.classList.contains('metadataEditorPage'); - var isMySyncPage = page.classList.contains('mySyncPage'); - var id = isLiveTvPage || isChannelsPage || isEditorPage || isMySyncPage || page.classList.contains('allLibraryPage') ? '' : getTopParentId() || ''; - var elems = document.getElementsByClassName('lnkMediaFolder'); + const isLiveTvPage = page.classList.contains('liveTvPage'); + const isChannelsPage = page.classList.contains('channelsPage'); + const isEditorPage = page.classList.contains('metadataEditorPage'); + const isMySyncPage = page.classList.contains('mySyncPage'); + const id = isLiveTvPage || isChannelsPage || isEditorPage || isMySyncPage || page.classList.contains('allLibraryPage') ? '' : getTopParentId() || ''; + const elems = document.getElementsByClassName('lnkMediaFolder'); - for (var i = 0, length = elems.length; i < length; i++) { - var lnkMediaFolder = elems[i]; - var itemId = lnkMediaFolder.getAttribute('data-itemid'); + for (let i = 0, length = elems.length; i < length; i++) { + const lnkMediaFolder = elems[i]; + const itemId = lnkMediaFolder.getAttribute('data-itemid'); if (isChannelsPage && itemId === 'channels') { lnkMediaFolder.classList.add('navMenuOption-selected'); @@ -695,7 +706,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateMenuForPageType(isDashboardPage, isLibraryPage) { - var newPageType = isDashboardPage ? 2 : isLibraryPage ? 1 : 3; + const newPageType = isDashboardPage ? 2 : isLibraryPage ? 1 : 3; if (currentPageType !== newPageType) { currentPageType = newPageType; @@ -706,7 +717,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' skinHeader.classList.remove('headroomDisabled'); } - var bodyClassList = document.body.classList; + const bodyClassList = document.body.classList; if (isLibraryPage) { bodyClassList.add('libraryDocument'); @@ -743,7 +754,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function updateTitle(page) { - var title = page.getAttribute('data-title'); + const title = page.getAttribute('data-title'); if (title) { LibraryMenu.setTitle(title); @@ -767,8 +778,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function initHeadRoom(elem) { - require(['headroom'], function (Headroom) { - var headroom = new Headroom(elem); + import('headroom').then(({default: Headroom}) => { + const headroom = new Headroom(elem); headroom.init(); }); } @@ -788,7 +799,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } function getNavDrawerOptions() { - var drawerWidth = screen.availWidth - 50; + let drawerWidth = screen.availWidth - 50; drawerWidth = Math.max(drawerWidth, 240); drawerWidth = Math.min(drawerWidth, 320); return { @@ -807,9 +818,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' navDrawerScrollContainer = navDrawerElement.querySelector('.scrollContainer'); navDrawerScrollContainer.addEventListener('click', onMainDrawerClick); return new Promise(function (resolve, reject) { - require(['navdrawer'], function (navdrawer) { - navdrawer = navdrawer.default || navdrawer; - + import('navdrawer').then(({default: navdrawer}) => { navDrawerInstance = new navdrawer(getNavDrawerOptions()); if (!layoutManager.tv) { @@ -821,98 +830,98 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' }); } - var navDrawerElement; - var navDrawerScrollContainer; - var navDrawerInstance; - var mainDrawerButton; - var headerHomeButton; - var currentDrawerType; - var pageTitleElement; - var headerBackButton; - var headerUserButton; - var currentUser; - var headerCastButton; - var headerSearchButton; - var headerAudioPlayerButton; - var headerSyncButton; - var enableLibraryNavDrawer = layoutManager.desktop; - var enableLibraryNavDrawerHome = !layoutManager.tv; - var skinHeader = document.querySelector('.skinHeader'); - var requiresUserRefresh = true; - window.LibraryMenu = { - getTopParentId: getTopParentId, - onHardwareMenuButtonClick: function () { - toggleMainDrawer(); - }, - setTabs: function (type, selectedIndex, builder) { - require(['mainTabsManager'], function (mainTabsManager) { - if (type) { - mainTabsManager.setTabs(viewManager.currentView(), selectedIndex, builder, function () { - return []; - }); - } else { - mainTabsManager.setTabs(null); - } - }); - }, - setDefaultTitle: function () { - if (!pageTitleElement) { - pageTitleElement = document.querySelector('.pageTitle'); - } + let navDrawerElement; + let navDrawerScrollContainer; + let navDrawerInstance; + let mainDrawerButton; + let headerHomeButton; + let currentDrawerType; + let pageTitleElement; + let headerBackButton; + let headerUserButton; + let currentUser; + let headerCastButton; + let headerSearchButton; + let headerAudioPlayerButton; + let headerSyncButton; + const enableLibraryNavDrawer = layoutManager.desktop; + const enableLibraryNavDrawerHome = !layoutManager.tv; + const skinHeader = document.querySelector('.skinHeader'); + let requiresUserRefresh = true; - if (pageTitleElement) { - pageTitleElement.classList.add('pageTitleWithLogo'); - pageTitleElement.classList.add('pageTitleWithDefaultLogo'); - pageTitleElement.style.backgroundImage = null; - pageTitleElement.innerHTML = ''; - } - - document.title = 'Jellyfin'; - }, - setTitle: function (title) { - if (title == null) { - return void LibraryMenu.setDefaultTitle(); - } - - if (title === '-') { - title = ''; - } - - var html = title; - - if (!pageTitleElement) { - pageTitleElement = document.querySelector('.pageTitle'); - } - - if (pageTitleElement) { - pageTitleElement.classList.remove('pageTitleWithLogo'); - pageTitleElement.classList.remove('pageTitleWithDefaultLogo'); - pageTitleElement.style.backgroundImage = null; - pageTitleElement.innerHTML = html || ''; - } - - document.title = title || 'Jellyfin'; - }, - setTransparentMenu: function (transparent) { - if (transparent) { - skinHeader.classList.add('semiTransparent'); + function setTabs (type, selectedIndex, builder) { + import('mainTabsManager').then((mainTabsManager) => { + if (type) { + mainTabsManager.setTabs(viewManager.currentView(), selectedIndex, builder, function () { + return []; + }); } else { - skinHeader.classList.remove('semiTransparent'); + mainTabsManager.setTabs(null); } + }); + } + + function setDefaultTitle () { + if (!pageTitleElement) { + pageTitleElement = document.querySelector('.pageTitle'); } - }; - var currentPageType; + + if (pageTitleElement) { + pageTitleElement.classList.add('pageTitleWithLogo'); + pageTitleElement.classList.add('pageTitleWithDefaultLogo'); + pageTitleElement.style.backgroundImage = null; + pageTitleElement.innerHTML = ''; + } + + document.title = 'Jellyfin'; + } + + function setTitle (title) { + if (title == null) { + return void LibraryMenu.setDefaultTitle(); + } + + if (title === '-') { + title = ''; + } + + const html = title; + + if (!pageTitleElement) { + pageTitleElement = document.querySelector('.pageTitle'); + } + + if (pageTitleElement) { + pageTitleElement.classList.remove('pageTitleWithLogo'); + pageTitleElement.classList.remove('pageTitleWithDefaultLogo'); + pageTitleElement.style.backgroundImage = null; + pageTitleElement.innerHTML = html || ''; + } + + document.title = title || 'Jellyfin'; + } + + function setTransparentMenu (transparent) { + if (transparent) { + skinHeader.classList.add('semiTransparent'); + } else { + skinHeader.classList.remove('semiTransparent'); + } + } + + let currentPageType; pageClassOn('pagebeforeshow', 'page', function (e) { if (!this.classList.contains('withTabs')) { LibraryMenu.setTabs(null); } }); + pageClassOn('pageshow', 'page', function (e) { - var page = this; - var isDashboardPage = page.classList.contains('type-interior'); - var isHomePage = page.classList.contains('homePage'); - var isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage'); - var apiClient = getCurrentApiClient(); + const page = this; + const isDashboardPage = page.classList.contains('type-interior'); + const isHomePage = page.classList.contains('homePage'); + const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage'); + const apiClient = getCurrentApiClient(); if (isDashboardPage) { if (mainDrawerButton) { @@ -949,7 +958,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' renderHeader(); events.on(connectionManager, 'localusersignedin', function (e, user) { - var currentApiClient = connectionManager.getApiClient(user.ServerId); + const currentApiClient = connectionManager.getApiClient(user.ServerId); currentDrawerType = null; currentUser = { @@ -963,15 +972,32 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' updateUserInHeader(user); }); }); + events.on(connectionManager, 'localusersignedout', function () { currentUser = {}; updateUserInHeader(); }); + events.on(playbackManager, 'playerchange', updateCastIcon); events.on(syncPlayManager, 'enabled', onSyncPlayEnabled); events.on(syncPlayManager, 'syncing', onSyncPlaySyncing); loadNavDrawer(); - return LibraryMenu; -}); + + const LibraryMenu = { + getTopParentId: getTopParentId, + onHardwareMenuButtonClick: function () { + toggleMainDrawer(); + }, + setTabs: setTabs, + setDefaultTitle: setDefaultTitle, + setTitle: setTitle, + setTransparentMenu: setTransparentMenu + }; + + window.LibraryMenu = LibraryMenu; + +export default LibraryMenu; + +/* eslint-enable indent */ diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 4094a2552f..4bb3eb25d9 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -112,54 +112,69 @@ import 'detailtablecss'; }); defineRoute({ - path: '/dashboard.html', + alias: '/dashboard.html', + path: '/controllers/dashboard/dashboard.html', autoFocus: false, roles: 'admin', controller: 'dashboard/dashboard' }); defineRoute({ - path: '/dashboardgeneral.html', + alias: '/dashboardgeneral.html', + path: '/controllers/dashboard/general.html', controller: 'dashboard/general', autoFocus: false, roles: 'admin' }); defineRoute({ - path: '/networking.html', + alias: '/networking.html', + path: '/controllers/dashboard/networking.html', autoFocus: false, roles: 'admin', controller: 'dashboard/networking' }); defineRoute({ - path: '/devices.html', + alias: '/devices.html', + path: '/controllers/dashboard/devices/devices.html', autoFocus: false, roles: 'admin', controller: 'dashboard/devices/devices' }); defineRoute({ - path: '/device.html', + alias: '/device.html', + path: '/controllers/dashboard/devices/device.html', autoFocus: false, roles: 'admin', controller: 'dashboard/devices/device' }); defineRoute({ - path: '/dlnaprofile.html', + alias: '/dlnaprofile.html', + path: '/controllers/dashboard/dlna/profile.html', autoFocus: false, roles: 'admin', controller: 'dashboard/dlna/profile' }); defineRoute({ - path: '/dlnaprofiles.html', + alias: '/dlnaprofiles.html', + path: '/controllers/dashboard/dlna/profiles.html', autoFocus: false, roles: 'admin', controller: 'dashboard/dlna/profiles' }); + defineRoute({ + alias: '/dlnasettings.html', + path: '/controllers/dashboard/dlna/settings.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/dlna/settings' + }); + defineRoute({ alias: '/addplugin.html', path: '/controllers/dashboard/plugins/add/index.html', @@ -169,54 +184,54 @@ import 'detailtablecss'; }); defineRoute({ - path: '/library.html', + alias: '/library.html', + path: '/controllers/dashboard/library.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/mediaLibrary' + controller: 'dashboard/library' }); defineRoute({ - path: '/librarydisplay.html', + alias: '/librarydisplay.html', + path: '/controllers/dashboard/librarydisplay.html', autoFocus: false, roles: 'admin', controller: 'dashboard/librarydisplay' }); defineRoute({ - path: '/dlnasettings.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/dlna/settings' - }); - - defineRoute({ - path: '/edititemmetadata.html', + alias: '/edititemmetadata.html', + path: '/controllers/edititemmetadata.html', controller: 'edititemmetadata', autoFocus: false }); defineRoute({ - path: '/encodingsettings.html', + alias: '/encodingsettings.html', + path: '/controllers/dashboard/encodingsettings.html', autoFocus: false, roles: 'admin', controller: 'dashboard/encodingsettings' }); defineRoute({ - path: '/log.html', + alias: '/log.html', + path: '/controllers/dashboard/logs.html', roles: 'admin', controller: 'dashboard/logs' }); defineRoute({ - path: '/metadataimages.html', + alias: '/metadataimages.html', + path: '/controllers/dashboard/metadataimages.html', autoFocus: false, roles: 'admin', controller: 'dashboard/metadataImages' }); defineRoute({ - path: '/metadatanfo.html', + alias: '/metadatanfo.html', + path: '/controllers/dashboard/metadatanfo.html', autoFocus: false, roles: 'admin', controller: 'dashboard/metadatanfo' @@ -239,7 +254,8 @@ import 'detailtablecss'; }); defineRoute({ - path: '/playbackconfiguration.html', + alias: '/playbackconfiguration.html', + path: '/controllers/dashboard/playback.html', autoFocus: false, roles: 'admin', controller: 'dashboard/playback' @@ -262,19 +278,22 @@ import 'detailtablecss'; }); defineRoute({ - path: '/home.html', + alias: '/home.html', + path: '/controllers/home.html', autoFocus: false, controller: 'home', type: 'home' }); defineRoute({ - path: '/search.html', + alias: '/search.html', + path: '/controllers/search.html', controller: 'searchpage' }); defineRoute({ - path: '/list.html', + alias: '/list.html', + path: '/controllers/list.html', autoFocus: false, controller: 'list' }); @@ -287,46 +306,53 @@ import 'detailtablecss'; }); defineRoute({ - path: '/livetv.html', + alias: '/livetv.html', + path: '/controllers/livetv.html', controller: 'livetv/livetvsuggested', autoFocus: false }); defineRoute({ - path: '/livetvguideprovider.html', + alias: '/livetvguideprovider.html', + path: '/controllers/livetvguideprovider.html', autoFocus: false, roles: 'admin', controller: 'livetvguideprovider' }); defineRoute({ - path: '/livetvsettings.html', + alias: '/livetvsettings.html', + path: '/controllers/livetvsettings.html', autoFocus: false, controller: 'livetvsettings' }); defineRoute({ - path: '/livetvstatus.html', + alias: '/livetvstatus.html', + path: '/controllers/livetvstatus.html', autoFocus: false, roles: 'admin', controller: 'livetvstatus' }); defineRoute({ - path: '/livetvtuner.html', + alias: '/livetvtuner.html', + path: '/controllers/livetvtuner.html', autoFocus: false, roles: 'admin', controller: 'livetvtuner' }); defineRoute({ - path: '/movies.html', + alias: '/movies.html', + path: '/controllers/movies/movies.html', autoFocus: false, controller: 'movies/moviesrecommended' }); defineRoute({ - path: '/music.html', + alias: '/music.html', + path: '/controllers/music/music.html', controller: 'music/musicrecommended', autoFocus: false }); @@ -340,82 +366,94 @@ import 'detailtablecss'; }); defineRoute({ - path: '/scheduledtask.html', + alias: '/scheduledtask.html', + path: '/controllers/dashboard/scheduledtasks/scheduledtask.html', autoFocus: false, roles: 'admin', controller: 'dashboard/scheduledtasks/scheduledtask' }); defineRoute({ - path: '/scheduledtasks.html', + alias: '/scheduledtasks.html', + path: '/controllers/dashboard/scheduledtasks/scheduledtasks.html', autoFocus: false, roles: 'admin', controller: 'dashboard/scheduledtasks/scheduledtasks' }); defineRoute({ - path: '/serveractivity.html', + alias: '/serveractivity.html', + path: '/controllers/dashboard/serveractivity.html', autoFocus: false, roles: 'admin', controller: 'dashboard/serveractivity' }); defineRoute({ - path: '/apikeys.html', + alias: '/apikeys.html', + path: '/controllers/dashboard/apikeys.html', autoFocus: false, roles: 'admin', controller: 'dashboard/apikeys' }); defineRoute({ - path: '/streamingsettings.html', + alias: '/streamingsettings.html', + path: '/controllers/dashboard/streaming.html', autoFocus: false, roles: 'admin', controller: 'dashboard/streaming' }); defineRoute({ - path: '/tv.html', + alias: '/tv.html', + path: '/controllers/shows/tvrecommended.html', autoFocus: false, controller: 'shows/tvrecommended' }); defineRoute({ - path: '/useredit.html', + alias: '/useredit.html', + path: '/controllers/dashboard/users/useredit.html', autoFocus: false, roles: 'admin', controller: 'dashboard/users/useredit' }); defineRoute({ - path: '/userlibraryaccess.html', + alias: '/userlibraryaccess.html', + path: '/controllers/dashboard/users/userlibraryaccess.html', autoFocus: false, roles: 'admin', controller: 'dashboard/users/userlibraryaccess' }); defineRoute({ - path: '/usernew.html', + alias: '/usernew.html', + path: '/controllers/dashboard/users/usernew.html', autoFocus: false, roles: 'admin', controller: 'dashboard/users/usernew' }); defineRoute({ - path: '/userparentalcontrol.html', + alias: '/userparentalcontrol.html', + path: '/controllers/dashboard/users/userparentalcontrol.html', autoFocus: false, roles: 'admin', controller: 'dashboard/users/userparentalcontrol' }); defineRoute({ - path: '/userpassword.html', + alias: '/userpassword.html', + path: '/controllers/dashboard/users/userpassword.html', autoFocus: false, controller: 'dashboard/users/userpasswordpage' }); defineRoute({ - path: '/userprofiles.html', + alias: '/userprofiles.html', + path: '/controllers/dashboard/users/userprofiles.html', autoFocus: false, roles: 'admin', controller: 'dashboard/users/userprofilespage' @@ -438,10 +476,11 @@ import 'detailtablecss'; }); defineRoute({ - path: '/wizardlibrary.html', + alias: '/wizardlibrary.html', + path: '/controllers/wizard/library.html', autoFocus: false, anonymous: true, - controller: 'dashboard/mediaLibrary' + controller: 'dashboard/library' }); defineRoute({ diff --git a/src/strings/de.json b/src/strings/de.json index 0aa4d2172f..b167c31707 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -1545,5 +1545,7 @@ "LabelUnstable": "Instabil", "SubtitleVerticalPositionHelp": "Zeilennummer, in der der Text angezeigt wird. Positive Zahlen geben die Zeile von oben an. Negative Zahlen geben die Zeile von unten an.", "Preview": "Vorschau", - "LabelSubtitleVerticalPosition": "Vertikale Position:" + "LabelSubtitleVerticalPosition": "Vertikale Position:", + "MessageGetInstalledPluginsError": "Beim Abrufen der Liste der derzeit installierten Plugins ist ein Fehler aufgetreten.", + "MessagePluginInstallError": "Bei der Installation des Plugins ist ein Fehler aufgetreten." } diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index 553445404a..eed9f95599 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -159,7 +159,7 @@ "DetectingDevices": "正在侦测设备", "DeviceAccessHelp": "这仅适用于可以唯一标识的设备,而不会阻止浏览器访问。限制用户设备访问会阻止使用未在此被批准的新增设备。", "DirectPlaying": "直接播放", - "DirectStreamHelp2": "直接串流只占用占用很少的CPU并且视频的品质不会有任何损失。", + "DirectStreamHelp2": "直接串流只占用占用很少的CPU并且视频的品质只会有极小程度的损失。", "DirectStreaming": "直接串流", "Director": "导演", "Disabled": "已禁用", @@ -261,7 +261,7 @@ "HeaderAllowMediaDeletionFrom": "允许从中删除媒体", "HeaderApiKey": "API 密钥", "HeaderApiKeys": "API 密钥", - "HeaderApiKeysHelp": "外部应用程序需要 API 密钥才能与 Jellyfin Server 进行通信。使用 Jellyfin 账户进行登录时密钥将会自动生成,您也可以手动为某个应用程序分配一个密钥。", + "HeaderApiKeysHelp": "外部应用程序需要 API 密钥才能与服务器进行通信。密钥会在使用普通账户登录时自动生成,或是手动为应用分配。", "HeaderAudioBooks": "有声读物", "HeaderAudioSettings": "声音设置", "HeaderBlockItemsWithNoRating": "通过没有评级和设置不允许的评级锁定内容:", @@ -327,7 +327,7 @@ "HeaderInstall": "安装", "HeaderInstantMix": "速成合辑", "HeaderItems": "项目", - "HeaderKodiMetadataHelp": "要启用或禁用 NFO 元数据, 请在 Jellyfin 库安装程序中编辑库, 然后找到“元数据储户”部分。", + "HeaderKodiMetadataHelp": "要启用或禁用 NFO 元数据, 请编辑库, 然后找到“元数据储户”部分。", "HeaderLatestEpisodes": "最新剧集", "HeaderLatestMedia": "最新媒体", "HeaderLatestMovies": "最新电影", @@ -372,7 +372,7 @@ "HeaderPreferredMetadataLanguage": "首选元数据语言", "HeaderProfile": "配置", "HeaderProfileInformation": "配置信息", - "HeaderProfileServerSettingsHelp": "这些参数将控制 Jellyfin 媒体服务器如何呈现给设备。", + "HeaderProfileServerSettingsHelp": "这些参数将控制服务器如何将自己呈现给客户端。", "HeaderRecentlyPlayed": "最近播放", "HeaderRecordingOptions": "录制选项", "HeaderRecordingPostProcessing": "记录后处理", @@ -396,7 +396,7 @@ "HeaderSelectServerCachePath": "选择服务器缓存路径", "HeaderSelectServerCachePathHelp": "浏览或输入一个路径用于服务器缓存文件,此文件夹必须可写。", "HeaderSelectTranscodingPath": "选择临时解码路径", - "HeaderSelectTranscodingPathHelp": "浏览或输入一个路径用于临时转码,此文件夹必须可写。", + "HeaderSelectTranscodingPathHelp": "浏览或输入一个路径用于转码文件,此文件夹必须可写。", "HeaderSendMessage": "发送消息", "HeaderSeries": "电视剧", "HeaderSeriesOptions": "系列选项", @@ -445,8 +445,8 @@ "HttpsRequiresCert": "要启用安全连接, 您需要提供一个受信任的 SSL 证书, 例如 Let's Encrypt 。请提供证书或禁用安全连接。", "Identify": "识别", "Images": "图片", - "ImportFavoriteChannelsHelp": "如果启用,只有在协调器设备中被标记为我的最爱的频道才会被导入。", - "ImportMissingEpisodesHelp": "如果启用,会将缺少的剧集信息导入到你的 Jellyfin 数据库并分季分剧显示。可能会大大延长媒体库扫描时间。", + "ImportFavoriteChannelsHelp": "只有在协调器设备中被标记为我的最爱的频道才会被导入。", + "ImportMissingEpisodesHelp": "缺少的剧集信息将被导入到你的数据库并分季分剧显示。可能会大大延长媒体库扫描时间。", "InstallingPackage": "正在安装 {0}(版本 {1})", "InstantMix": "即时混音", "ItemCount": "{0} 项", @@ -476,14 +476,14 @@ "LabelAppName": "APP名称", "LabelAppNameExample": "例如:Sickbeard, Sonarr", "LabelArtists": "艺术家:", - "LabelArtistsHelp": "独立多功能 ;", + "LabelArtistsHelp": "将多个艺术家用分号分隔", "LabelAudioLanguagePreference": "首选音频语言:", "LabelAutomaticallyRefreshInternetMetadataEvery": "自动从互联网获取元数据并刷新:", "LabelBindToLocalNetworkAddress": "监听的本地网络地址:", - "LabelBindToLocalNetworkAddressHelp": "(可选的)覆盖 HTTP 服务器绑定的本地 IP 地址。如果留空,服务器将会监听所有可用的地址。改变这个值需要重启 Jellyfin 服务器。", + "LabelBindToLocalNetworkAddressHelp": "覆盖 HTTP 服务器绑定的本地 IP 地址。如果留空,服务器将会监听所有可用的地址。改变这个值需要重启 Jellyfin 服务器。", "LabelBirthDate": "出生日期:", "LabelBirthYear": "出生年份:", - "LabelBlastMessageInterval": "活动信号的时间间隔(秒)", + "LabelBlastMessageInterval": "活动信号的时间间隔", "LabelBlastMessageIntervalHelp": "确定爆炸活动消息之间的持续时间(以秒为单位)。", "LabelBlockContentWithTags": "通过标签锁定内容:", "LabelBurnSubtitles": "烧录字幕:", @@ -541,7 +541,7 @@ "LabelEnableAutomaticPortMapHelp": "通过UPnP将路由器端口自动转发到服务器端口。这可能不适用于某些型号的路由器和网络配置。需要服务器重新启动后才会应用更改。", "LabelEnableBlastAliveMessages": "爆发活动信号", "LabelEnableBlastAliveMessagesHelp": "如果该服务器不能被网络中的其他UPnP设备检测到,请启用此选项。", - "LabelEnableDlnaClientDiscoveryInterval": "客户端搜寻时间间隔(秒)", + "LabelEnableDlnaClientDiscoveryInterval": "客户端搜寻时间间隔", "LabelEnableDlnaClientDiscoveryIntervalHelp": "确定由 Jellyfin 执行的 SSDP 搜索之间的持续时间 (以秒为单位)。", "LabelEnableDlnaDebugLogging": "启用 DLNA 调试日志", "LabelEnableDlnaDebugLoggingHelp": "创建一个很大的日志文件,仅应在排除故障时使用。", @@ -567,9 +567,9 @@ "LabelForgotPasswordUsernameHelp": "输入你的用户名,如果你还记得。", "LabelFormat": "格式:", "LabelFriendlyName": "好记的名称:", - "LabelServerNameHelp": "此名称将用做服务器名,如果留空,将使用计算机名。", + "LabelServerNameHelp": "此名称将用做服务器名,默认使用服务器的主机名。", "LabelGroupMoviesIntoCollections": "批量添加电影到收藏", - "LabelGroupMoviesIntoCollectionsHelp": "显示电影列表时,属于一个收藏的电影将显示为一个分组。", + "LabelGroupMoviesIntoCollectionsHelp": "显示电影列表时,同一收藏的电影将显示为一个分组。", "LabelH264Crf": "H264 CRF 编码质量等级:", "LabelEncoderPreset": "H264 和 H265 编码预设:", "LabelHardwareAccelerationType": "硬件加速:", @@ -577,7 +577,7 @@ "LabelHomeNetworkQuality": "家庭网络质量:", "LabelHomeScreenSectionValue": "主屏幕模块{0}:", "LabelHttpsPort": "本地 HTTPS 端口号:", - "LabelHttpsPortHelp": "Jellyfin HTTPS 服务器监听端口。", + "LabelHttpsPortHelp": "HTTPS 服务器监听的 TCP 端口号。", "LabelIconMaxHeight": "图标最大高度:", "LabelIconMaxHeightHelp": "通过UPnP显示的图标最大分辨率。", "LabelIconMaxWidth": "图标最大宽度:", @@ -604,7 +604,7 @@ "LabelLanguage": "语言:", "LabelLineup": "排队:", "LabelLocalHttpServerPortNumber": "本地 HTTP 端口号:", - "LabelLocalHttpServerPortNumberHelp": "Jellyfin HTTP 服务器监听的 TCP 端口。", + "LabelLocalHttpServerPortNumberHelp": "HTTP 服务器监听的 TCP 端口号。", "LabelLockItemToPreventChanges": "锁定此项目防止改动", "LabelLoginDisclaimer": "登录声明:", "LabelLoginDisclaimerHelp": "将在登录页面底部显示的信息。", @@ -646,9 +646,9 @@ "LabelMovieCategories": "电影分类:", "LabelMoviePrefix": "电影前缀:", "LabelMoviePrefixHelp": "如果将前缀应用于影片标题, 请在此处输入它, 以便服务器可以正确处理它。", - "LabelMovieRecordingPath": "电影录制路径 (可选的):", + "LabelMovieRecordingPath": "电影录制路径:", "LabelMusicStreamingTranscodingBitrate": "音乐转码的比特率:", - "LabelMusicStreamingTranscodingBitrateHelp": "请指定一个音乐媒体串流时的最大比特率。", + "LabelMusicStreamingTranscodingBitrateHelp": "请指定音乐媒体串流时的最大比特率。", "LabelName": "名字:", "LabelNewName": "新名字:", "LabelNewPassword": "新密码:", @@ -659,7 +659,7 @@ "LabelNumber": "编号:", "LabelNumberOfGuideDays": "下载几天的节目指南:", "LabelNumberOfGuideDaysHelp": "下载更多天的节目指南可以帮你进一步查看节目列表并做出提前安排,但下载过程也将耗时更久。它将基于频道数量自动选择。", - "LabelOptionalNetworkPath": "(可选的)共享的网络文件夹:", + "LabelOptionalNetworkPath": "共享的网络文件夹:", "LabelOptionalNetworkPathHelp": "如果这个文件夹在你的网络上是共享的,提供这个网络共享地址能够允许其他设备上的 Jellyfin 应用程序直接访问媒体文件,例如 {0} 或者 {1}。", "LabelOriginalAspectRatio": "原始长宽比:", "LabelOriginalTitle": "原标题:", @@ -704,7 +704,7 @@ "LabelReleaseDate": "发行日期:", "LabelRemoteClientBitrateLimit": "互联网流媒体传输比特率限制(Mbps):", "LabelRemoteClientBitrateLimitHelp": "所有网络设备都有一个可选的每流比特率限制。这对于防止设备请求比 internet 连接所能处理的更高的比特率非常有用。这可能会导致服务器上的 CPU 负载增加, 以便将视频转码到较低的比特率。", - "LabelRuntimeMinutes": "播放时长(分钟):", + "LabelRuntimeMinutes": "播放时长:", "LabelSaveLocalMetadata": "将媒体图像保存到媒体所在文件夹", "LabelSaveLocalMetadataHelp": "直接将媒体图像保存到媒体所在文件夹以方便编辑。", "LabelScheduledTaskLastRan": "最后运行 {0}, 花费时间 {1}.", @@ -714,7 +714,7 @@ "LabelSelectVersionToInstall": "选择安装版本:", "LabelSendNotificationToUsers": "发送通知至:", "LabelSerialNumber": "序列号", - "LabelSeriesRecordingPath": "电视剧录制路径 (可选的):", + "LabelSeriesRecordingPath": "电视剧录制路径:", "LabelServerHost": "主机:", "LabelServerHostHelp": "192.168.1.100:8096 或 https://myserver.com", "LabelSimultaneousConnectionLimit": "并发流限制:", @@ -786,7 +786,7 @@ "LabelYoureDone": "完成!", "LabelZipCode": "邮编:", "LabelffmpegPath": "FFmpeg 路径:", - "LabelffmpegPathHelp": "FFmpeg 应用程序的文件,或者包含了 FFmpeg 的文件夹的路径。", + "LabelffmpegPathHelp": "FFmpeg 应用文件或包含 FFmpeg 的文件夹的路径。", "LanNetworksHelp": "在强制带宽限制时,认作本地网络上的 IP 地址或 IP/网络掩码条目的逗号分隔列表。如果设置此项,所有其它 IP 地址将被视为在外部网络上,并且将受到外部带宽限制。如果保留为空,则只将服务器的子网视为本地网络。", "Large": "大", "LatestFromLibrary": "最新的{0}", @@ -918,7 +918,7 @@ "OptionAllowLinkSharingHelp": "只有网页包含的媒体信息会被共享。媒体文件不会被公开共享。共享是有时间限制的并且会在 {0} 天后到期。", "OptionAllowManageLiveTv": "允许电视直播录制管理", "OptionAllowMediaPlayback": "允许播放媒体", - "OptionAllowMediaPlaybackTranscodingHelp": "由于不支持的媒体格式, 限制对代码转换的访问可能会导致 Jellyfin 应用程序中的播放失败。", + "OptionAllowMediaPlaybackTranscodingHelp": "限制对转码的访问可能会由于不支持的媒体格式导致客户端中播放失败。", "OptionAllowRemoteControlOthers": "允许其他用户全程控制", "OptionAllowRemoteSharedDevices": "允许远程控制共享的设备", "OptionAllowRemoteSharedDevicesHelp": "DLNA 设备在用户对他们进行控制前都被视为是共享的。", @@ -931,7 +931,7 @@ "OptionAuto": "自动", "OptionAutomatic": "自动", "OptionAutomaticallyGroupSeries": "自动合并分布在不同文件夹的电视剧", - "OptionAutomaticallyGroupSeriesHelp": "如果启用,分布在这个媒体库的多个文件夹中的同一部电视剧将会自动整合成一部电视剧。", + "OptionAutomaticallyGroupSeriesHelp": "在这个媒体库的多个文件夹中的同一部电视剧将会自动整合成一部电视剧。", "OptionBlockBooks": "书籍", "OptionBlockChannelContent": "互联网频道内容", "OptionBlockLiveTvChannels": "电视直播频道", @@ -952,7 +952,7 @@ "OptionDatePlayed": "播放日期", "OptionDescending": "降序", "OptionDisableUser": "禁用此用户", - "OptionDisableUserHelp": "如果禁用该用户,服务器将不允许该用户连接。现有的连接将被终止。", + "OptionDisableUserHelp": "服务器将不允许来自该用户的任何连接。现有的连接将立即被终止。", "OptionDislikes": "不喜欢", "OptionDisplayFolderView": "显示一个“文件夹”类别用于按文件夹分类浏览你的媒体文件夹", "OptionDisplayFolderViewHelp": "在你的媒体库列表中显示文件夹。如果你有按文件夹分类进行浏览的需求,这会非常有用。", @@ -962,7 +962,7 @@ "OptionDownloadBoxImage": "包装", "OptionDownloadDiscImage": "光盘", "OptionDownloadImagesInAdvance": "提前下载图片", - "OptionDownloadImagesInAdvanceHelp": "默认下,大部分图片只有在 Jellyfin 应用程序请求时下载。开启此选项将随着媒体导入时下载所有图片。这可能需要更久媒体库扫描时间。", + "OptionDownloadImagesInAdvanceHelp": "默认大多数图片只在客户端请求时下载。开启此选项将在新媒体导入时预先下载所有图片。这可能大大延长媒体库扫描时间。", "OptionDownloadMenuImage": "菜单", "OptionDownloadPrimaryImage": "封面图", "OptionDownloadThumbImage": "缩略图", @@ -994,7 +994,7 @@ "OptionHlsSegmentedSubtitles": "HLS分段字幕", "OptionHomeVideos": "照片", "OptionIgnoreTranscodeByteRangeRequests": "忽略转码字节范围请求", - "OptionIgnoreTranscodeByteRangeRequestsHelp": "如果启用,这些请求会被兑现,但会忽略的字节范围标头。", + "OptionIgnoreTranscodeByteRangeRequestsHelp": "这些请求会被兑现,但会忽略的字节范围标头。", "OptionImdbRating": "IMDb 评分", "OptionIsHD": "HD高清", "OptionIsSD": "SD标清", @@ -1009,9 +1009,9 @@ "OptionOnInterval": "在一个期间", "OptionParentalRating": "家长分级", "OptionPlainStorageFolders": "显示所有文件夹作为一般存储文件夹", - "OptionPlainStorageFoldersHelp": "如果启用,所有文件夹在DIDL中显示为“ object.container.storageFolder ”,而不是一个更具体的类型,如“ object.container.person.musicArtist ” 。", + "OptionPlainStorageFoldersHelp": "所有文件夹在DIDL中显示为 \"object.container.storageFolder\" ,而不是一个更具体的类型,如 \"object.container.person.musicArtist\" 。", "OptionPlainVideoItems": "显示所有视频为一般视频项目", - "OptionPlainVideoItemsHelp": "如果启用,所有视频在DIDL中显示为“object.item.videoItem”,而不是一个更具体的类型,如“object.item.videoItem.movie ” 。", + "OptionPlainVideoItemsHelp": "所有视频在DIDL中显示为 \"object.item.videoItem\" ,而不是一个更具体的类型,如 \"object.item.videoItem.movie\" 。", "OptionPlayCount": "播放次数", "OptionPlayed": "已播放", "OptionPremiereDate": "首映日期", @@ -1316,7 +1316,7 @@ "ErrorDeletingItem": "从 Jellyfin Server 删除项目时出错。请确认 Jellyfin Server 是否拥有对媒体目录的写权限,然后重试。", "GroupBySeries": "按系列分组", "HeaderApp": "应用程序", - "DirectStreamHelp1": "该媒体文件的分辨率和编码(H.264、AC3 等)与您的设备兼容,但容器格式(.mkv、.avi、.wmv 等)不受支持。因此,视频在串流至您的设备之前将会被即时封装为另一种格式。", + "DirectStreamHelp1": "该媒体文件的分辨率和编码(H.264、AC3 等)与您的设备兼容,但文件格式(.mkv、.avi、.wmv 等)不受支持。因此,视频在串流至您的设备之前将会被即时封装为另一种格式。", "HeaderAppearsOn": "同时出现于", "HeaderCancelSeries": "取消系列", "HeaderFavoriteEpisodes": "最爱的剧集", @@ -1361,14 +1361,14 @@ "OptionDownloadLogoImage": "标志", "OptionLoginAttemptsBeforeLockout": "确定在锁定之前可以进行多少次不正确的登录尝试。", "OptionLoginAttemptsBeforeLockoutHelp": "如果值为0,则表示将允许普通用户尝试三次、管理员尝试五次的默认值。将此设置为-1将禁用此功能。", - "PasswordResetProviderHelp": "选择一个密码重置提供者用于密码重置", + "PasswordResetProviderHelp": "选择一个密码重置提供者用于此用户申请重置密码", "PlaceFavoriteChannelsAtBeginning": "将最喜爱的频道置顶", "PlayNext": "播放下一个", "PlayNextEpisodeAutomatically": "自动播放下一集", "Premieres": "首映", "Raised": "提高", "Recordings": "录音", - "RefreshDialogHelp": "元数据根据设置和Jellyfin服务器中启用的网络服务进行刷新。", + "RefreshDialogHelp": "元数据根据设置和仪表盘中启用的网络服务进行刷新。", "RepeatEpisodes": "重复剧集", "Schedule": "日程", "Screenshot": "屏幕截图", @@ -1421,7 +1421,7 @@ "ButtonAddImage": "添加图片", "LabelPlayer": "播放器:", "LabelBaseUrl": "基础 URL:", - "LabelBaseUrlHelp": "为服务器 URL添加自定义子目录,例如:http://example.com/<baseurl>。", + "LabelBaseUrlHelp": "为服务器 URL添加自定义子目录,例如:http://example.com/<baseurl>", "MusicLibraryHelp": "重播 {0}音乐命名指南{1}。", "HeaderFavoritePeople": "最喜欢的人物", "OptionRandom": "随机", @@ -1480,7 +1480,7 @@ "LabelRequireHttpsHelp": "开启后服务器将自动将所有 HTTP 请求重定向到 HTTPS。如果服务器没有启用 HTTPS 则不生效。", "LabelRequireHttps": "强制 HTTPS", "LabelStable": "稳定版", - "LabelEnableHttpsHelp": "开启服务器对所配置 HTTPS 端口的监听。必须配置有效的证书才会生效。", + "LabelEnableHttpsHelp": "监听已配置的 HTTPS 端口。必须配置有效的证书才会生效。", "LabelEnableHttps": "启用 HTTPS", "LabelChromecastVersion": "Chromecast版本", "HeaderDVR": "DVR", @@ -1539,5 +1539,13 @@ "ClearQueue": "清空队列", "StopPlayback": "停止播放", "Writers": "作者", - "ViewAlbumArtist": "查看专辑艺术家" + "ViewAlbumArtist": "查看专辑艺术家", + "Preview": "预览", + "SubtitleVerticalPositionHelp": "文字出现的行号。正数表示由上到下,负数表示由下到上。", + "LabelSubtitleVerticalPosition": "垂直位置:", + "PreviousTrack": "上一曲", + "MessageGetInstalledPluginsError": "获取已安装插件列表时出现错误。", + "MessagePluginInstallError": "安装插件时出现错误。", + "NextTrack": "下一曲", + "LabelUnstable": "不稳定" }