diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index d52713283b..ef44a1c33f 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -33,11 +33,11 @@ import '../guide/programs.scss'; const enableFocusTransform = !browser.slow && !browser.edge; /** - * Generate the HTML markup for cards for a set of items. - * @param items - The items used to generate cards. - * @param options - The options of the cards. - * @returns {string} The HTML markup for the cards. - */ + * Generate the HTML markup for cards for a set of items. + * @param items - The items used to generate cards. + * @param [options] - The options of the cards. + * @returns {string} The HTML markup for the cards. + */ export function getCardsHtml(items, options) { if (arguments.length === 1) { options = arguments[0]; @@ -48,10 +48,10 @@ export function getCardsHtml(items, options) { } /** - * Checks if the window is resizable. - * @param {number} windowWidth - Width of the device's screen. - * @returns {boolean} - Result of the check. - */ + * Checks if the window is resizable. + * @param {number} windowWidth - Width of the device's screen. + * @returns {boolean} - Result of the check. + */ function isResizable(windowWidth) { const screen = window.screen; if (screen) { @@ -66,22 +66,22 @@ function isResizable(windowWidth) { } /** - * Gets the width of a card's image according to the shape and amount of cards per row. - * @param {string} shape - Shape of the card. - * @param {number} screenWidth - Width of the screen. - * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. - * @returns {number} Width of the image for a card. - */ + * Gets the width of a card's image according to the shape and amount of cards per row. + * @param {string} shape - Shape of the card. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Width of the image for a card. + */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); return Math.round(screenWidth / imagesPerRow); } /** - * Normalizes the options for a card. - * @param {Object} items - A set of items. - * @param {Object} options - Options for handling the items. - */ + * Normalizes the options for a card. + * @param {Object} items - A set of items. + * @param {Object} options - Options for handling the items. + */ function setCardData(items, options) { options.shape = options.shape || 'auto'; @@ -138,11 +138,11 @@ function setCardData(items, options) { } /** - * Generates the internal HTML markup for cards. - * @param {Object} items - Items for which to generate the markup. - * @param {Object} options - Options for generating the markup. - * @returns {string} The internal HTML markup of the cards. - */ + * Generates the internal HTML markup for cards. + * @param {Object} items - Items for which to generate the markup. + * @param {Object} options - Options for generating the markup. + * @returns {string} The internal HTML markup of the cards. + */ function buildCardsHtmlInternal(items, options) { let isVertical = false; @@ -256,20 +256,20 @@ function buildCardsHtmlInternal(items, options) { } /** - * @typedef {Object} CardImageUrl - * @property {string} imgUrl - Image URL. - * @property {string} blurhash - Image blurhash. - * @property {boolean} forceName - Force name. - * @property {boolean} coverImage - Use cover style. - */ + * @typedef {Object} CardImageUrl + * @property {string} imgUrl - Image URL. + * @property {string} blurhash - Image blurhash. + * @property {boolean} forceName - Force name. + * @property {boolean} coverImage - Use cover style. + */ /** Get the URL of the card's image. - * @param {Object} item - Item for which to generate a card. - * @param {Object} apiClient - API client object. - * @param {Object} options - Options of the card. - * @param {string} shape - Shape of the desired image. - * @returns {CardImageUrl} Object representing the URL of the card's image. - */ + * @param {Object} item - Item for which to generate a card. + * @param {Object} apiClient - API client object. + * @param {Object} options - Options of the card. + * @param {string} shape - Shape of the desired image. + * @returns {CardImageUrl} Object representing the URL of the card's image. + */ function getCardImageUrl(item, apiClient, options, shape) { item = item.ProgramInfo || item; @@ -412,10 +412,10 @@ function getCardImageUrl(item, apiClient, options, shape) { } /** - * Generates an index used to select the default color of a card based on a string. - * @param {?string} [str] - String to use for generating the index. - * @returns {number} Index of the color. - */ + * Generates an index used to select the default color of a card based on a string. + * @param {?string} [str] - String to use for generating the index. + * @returns {number} Index of the color. + */ function getDefaultColorIndex(str) { const numRandomColors = 5; @@ -435,16 +435,16 @@ function getDefaultColorIndex(str) { } /** - * Generates the HTML markup for a card's text. - * @param {Array} lines - Array containing the text lines. - * @param {string} cssClass - Base CSS class to use for the lines. - * @param {boolean} forceLines - Flag to force the rendering of all lines. - * @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer. - * @param {string} cardLayout - DEPRECATED - * @param {boolean} addRightMargin - Flag to add a right margin to the text. - * @param {number} maxLines - Maximum number of lines to render. - * @returns {string} HTML markup for the card's text. - */ + * Generates the HTML markup for a card's text. + * @param {Array} lines - Array containing the text lines. + * @param {string} cssClass - Base CSS class to use for the lines. + * @param {boolean} forceLines - Flag to force the rendering of all lines. + * @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer. + * @param {string} cardLayout - DEPRECATED + * @param {boolean} addRightMargin - Flag to add a right margin to the text. + * @param {number} maxLines - Maximum number of lines to render. + * @returns {string} HTML markup for the card's text. + */ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) { let html = ''; @@ -488,21 +488,21 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout } /** - * Determines if the item is live TV. - * @param {Object} item - Item to use for the check. - * @returns {boolean} Flag showing if the item is live TV. - */ + * Determines if the item is live TV. + * @param {Object} item - Item to use for the check. + * @returns {boolean} Flag showing if the item is live TV. + */ function isUsingLiveTvNaming(item) { return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording'; } /** - * Returns the air time text for the item based on the given times. - * @param {object} item - Item used to generate the air time text. - * @param {boolean} showAirDateTime - ISO8601 date for the start of the show. - * @param {boolean} showAirEndTime - ISO8601 date for the end of the show. - * @returns {string} The air time text for the item based on the given dates. - */ + * Returns the air time text for the item based on the given times. + * @param {object} item - Item used to generate the air time text. + * @param {boolean} showAirDateTime - ISO8601 date for the start of the show. + * @param {boolean} showAirEndTime - ISO8601 date for the end of the show. + * @returns {string} The air time text for the item based on the given dates. + */ function getAirTimeText(item, showAirDateTime, showAirEndTime) { let airTimeText = ''; @@ -529,16 +529,16 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) { } /** - * Generates the HTML markup for the card's footer text. - * @param {Object} item - Item used to generate the footer text. - * @param {Object} apiClient - API client instance. - * @param {Object} options - Options used to generate the footer text. - * @param {string} footerClass - CSS classes of the footer element. - * @param {string} progressHtml - HTML markup of the progress bar element. - * @param {Object} flags - Various flags for the footer - * @param {Object} urls - Various urls for the footer - * @returns {string} HTML markup of the card's footer text element. - */ + * Generates the HTML markup for the card's footer text. + * @param {Object} item - Item used to generate the footer text. + * @param {Object} apiClient - API client instance. + * @param {Object} options - Options used to generate the footer text. + * @param {string} footerClass - CSS classes of the footer element. + * @param {string} progressHtml - HTML markup of the progress bar element. + * @param {Object} flags - Various flags for the footer + * @param {Object} urls - Various urls for the footer + * @returns {string} HTML markup of the card's footer text element. + */ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) { item = item.ProgramInfo || item; let html = ''; @@ -771,12 +771,12 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, } /** - * Generates the HTML markup for the action button. - * @param {Object} item - Item used to generate the action button. - * @param {string} text - Text of the action button. - * @param {string} serverId - ID of the server. - * @returns {string} HTML markup of the action button. - */ + * Generates the HTML markup for the action button. + * @param {Object} item - Item used to generate the action button. + * @param {string} text - Text of the action button. + * @param {string} serverId - ID of the server. + * @returns {string} HTML markup of the action button. + */ function getTextActionButton(item, text, serverId) { if (!text) { text = itemHelper.getDisplayName(item); @@ -797,11 +797,11 @@ function getTextActionButton(item, text, serverId) { } /** - * Generates HTML markup for the item count indicator. - * @param {Object} options - Options used to generate the item count. - * @param {Object} item - Item used to generate the item count. - * @returns {string} HTML markup for the item count indicator. - */ + * Generates HTML markup for the item count indicator. + * @param {Object} options - Options used to generate the item count. + * @param {Object} item - Item used to generate the item count. + * @returns {string} HTML markup for the item count indicator. + */ function getItemCountsHtml(options, item) { const counts = []; let childText; @@ -879,8 +879,8 @@ function getItemCountsHtml(options, item) { let refreshIndicatorLoaded; /** - * Imports the refresh indicator element. - */ + * Imports the refresh indicator element. + */ function importRefreshIndicator() { if (!refreshIndicatorLoaded) { refreshIndicatorLoaded = true; @@ -889,22 +889,22 @@ function importRefreshIndicator() { } /** - * Returns the default background class for a card based on a string. - * @param {?string} [str] - Text used to generate the background class. - * @returns {string} CSS classes for default card backgrounds. - */ + * Returns the default background class for a card based on a string. + * @param {?string} [str] - Text used to generate the background class. + * @returns {string} CSS classes for default card backgrounds. + */ export function getDefaultBackgroundClass(str) { return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str); } /** - * Builds the HTML markup for an individual card. - * @param {number} index - Index of the card - * @param {object} item - Item used to generate the card. - * @param {object} apiClient - API client instance. - * @param {object} options - Options used to generate the card. - * @returns {string} HTML markup for the generated card. - */ + * Builds the HTML markup for an individual card. + * @param {number} index - Index of the card + * @param {object} item - Item used to generate the card. + * @param {object} apiClient - API client instance. + * @param {object} options - Options used to generate the card. + * @returns {string} HTML markup for the generated card. + */ function buildCard(index, item, apiClient, options) { let action = options.action || 'link'; @@ -1211,11 +1211,11 @@ function buildCard(index, item, apiClient, options) { } /** - * Generates HTML markup for the card overlay. - * @param {object} item - Item used to generate the card overlay. - * @param {string} action - Action assigned to the overlay. - * @returns {string} HTML markup of the card overlay. - */ + * Generates HTML markup for the card overlay. + * @param {object} item - Item used to generate the card overlay. + * @param {string} action - Action assigned to the overlay. + * @returns {string} HTML markup of the card overlay. + */ function getHoverMenuHtml(item, action) { let html = ''; @@ -1253,11 +1253,11 @@ function getHoverMenuHtml(item, action) { } /** - * Generates the text or icon used for default card backgrounds. - * @param {object} item - Item used to generate the card overlay. - * @param {object} options - Options used to generate the card overlay. - * @returns {string} HTML markup of the card overlay. - */ + * Generates the text or icon used for default card backgrounds. + * @param {object} item - Item used to generate the card overlay. + * @param {object} options - Options used to generate the card overlay. + * @returns {string} HTML markup of the card overlay. + */ export function getDefaultText(item, options) { if (item.CollectionType) { return ''; @@ -1301,10 +1301,10 @@ export function getDefaultText(item, options) { } /** - * Builds a set of cards and inserts them into the page. - * @param {Array} items - Array of items used to build the cards. - * @param {options} options - Options of the cards to build. - */ + * Builds a set of cards and inserts them into the page. + * @param {Array} items - Array of items used to build the cards. + * @param {options} options - Options of the cards to build. + */ export function buildCards(items, options) { // Abort if the container has been disposed if (!document.body.contains(options.itemsContainer)) { @@ -1345,11 +1345,11 @@ export function buildCards(items, options) { } /** - * Ensures the indicators for a card exist and creates them if they don't exist. - * @param {HTMLDivElement} card - DOM element of the card. - * @param {HTMLDivElement} indicatorsElem - DOM element of the indicators. - * @returns {HTMLDivElement} - DOM element of the indicators. - */ + * Ensures the indicators for a card exist and creates them if they don't exist. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {HTMLDivElement} indicatorsElem - DOM element of the indicators. + * @returns {HTMLDivElement} - DOM element of the indicators. + */ function ensureIndicators(card, indicatorsElem) { if (indicatorsElem) { return indicatorsElem; @@ -1368,10 +1368,10 @@ function ensureIndicators(card, indicatorsElem) { } /** - * Adds user data to the card such as progress indicators and played status. - * @param {HTMLDivElement} card - DOM element of the card. - * @param {Object} userData - User data to apply to the card. - */ + * Adds user data to the card such as progress indicators and played status. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {Object} userData - User data to apply to the card. + */ function updateUserData(card, userData) { const type = card.getAttribute('data-type'); const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season'; @@ -1447,10 +1447,10 @@ function updateUserData(card, userData) { } /** - * Handles when user data has changed. - * @param {Object} userData - User data to apply to the card. - * @param {HTMLElement} scope - DOM element to use as a scope when selecting cards. - */ + * Handles when user data has changed. + * @param {Object} userData - User data to apply to the card. + * @param {HTMLElement} scope - DOM element to use as a scope when selecting cards. + */ export function onUserDataChanged(userData, scope) { const cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]'); @@ -1460,11 +1460,11 @@ export function onUserDataChanged(userData, scope) { } /** - * Handles when a timer has been created. - * @param {string} programId - ID of the program. - * @param {string} newTimerId - ID of the new timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a timer has been created. + * @param {string} programId - ID of the program. + * @param {string} newTimerId - ID of the new timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onTimerCreated(programId, newTimerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]'); @@ -1480,10 +1480,10 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) { } /** - * Handles when a timer has been cancelled. - * @param {string} timerId - ID of the cancelled timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a timer has been cancelled. + * @param {string} timerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onTimerCancelled(timerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]'); @@ -1497,10 +1497,10 @@ export function onTimerCancelled(timerId, itemsContainer) { } /** - * Handles when a series timer has been cancelled. - * @param {string} cancelledTimerId - ID of the cancelled timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a series timer has been cancelled. + * @param {string} cancelledTimerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]'); diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 27c48f0998..83b8ec551d 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -1,15 +1,14 @@ -import escapeHtml from 'escape-html'; - import globalize from 'scripts/globalize'; -import imageHelper from 'scripts/imagehelper'; -import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType'; import Dashboard from 'utils/dashboard'; -import cardBuilder from '../cardbuilder/cardBuilder'; -import imageLoader from '../images/imageLoader'; -import layoutManager from '../layoutManager'; -import { appRouter } from '../router/appRouter'; -import ServerConnections from '../ServerConnections'; +import { loadRecordings } from './sections/activeRecordings'; +import { loadLibraryButtons } from './sections/libraryButtons'; +import { loadLibraryTiles } from './sections/libraryTiles'; +import { loadLiveTV } from './sections/liveTv'; +import { loadNextUp } from './sections/nextUp'; +import { loadRecentlyAdded } from './sections/recentlyAdded'; +import { loadResume } from './sections/resume'; import 'elements/emby-button/paper-icon-button-light'; import 'elements/emby-itemscontainer/emby-itemscontainer'; @@ -19,26 +18,8 @@ import 'elements/emby-button/emby-button'; import './homesections.scss'; export function getDefaultSection(index) { - switch (index) { - case 0: - return 'smalllibrarytiles'; - case 1: - return 'resume'; - case 2: - return 'resumeaudio'; - case 3: - return 'resumebook'; - case 4: - return 'livetv'; - case 5: - return 'nextup'; - case 6: - return 'latestmedia'; - case 7: - return 'none'; - default: - return ''; - } + if (index < 0 || index > DEFAULT_SECTIONS.length) return ''; + return DEFAULT_SECTIONS[index]; } function getAllSectionsToShow(userSettings, sectionCount) { @@ -138,29 +119,36 @@ export function resume(elem, options) { function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) { const section = allSections[index]; const elem = page.querySelector('.section' + index); + const options = { enableOverflow: enableScrollX() }; - if (section === 'latestmedia') { - loadRecentlyAdded(elem, apiClient, user, userViews); - } else if (section === 'librarytiles' || section === 'smalllibrarytiles' || section === 'smalllibrarytiles-automobile' || section === 'librarytiles-automobile') { - loadLibraryTiles(elem, apiClient, user, userSettings, 'smallBackdrop', userViews); - } else if (section === 'librarybuttons') { - loadlibraryButtons(elem, apiClient, user, userSettings, userViews); - } else if (section === 'resume') { - return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings); - } else if (section === 'resumeaudio') { - return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings); - } else if (section === 'activerecordings') { - loadLatestLiveTvRecordings(elem, true, apiClient); - } else if (section === 'nextup') { - loadNextUp(elem, apiClient, userSettings); - } else if (section === 'onnow' || section === 'livetv') { - return loadOnNow(elem, apiClient, user); - } else if (section === 'resumebook') { - return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings); - } else { - elem.innerHTML = ''; - return Promise.resolve(); + switch (section) { + case HomeSectionType.ActiveRecordings: + loadRecordings(elem, true, apiClient, options); + break; + case HomeSectionType.LatestMedia: + loadRecentlyAdded(elem, apiClient, user, userViews, options); + break; + case HomeSectionType.LibraryButtons: + loadLibraryButtons(elem, userViews); + break; + case HomeSectionType.LiveTv: + return loadLiveTV(elem, apiClient, user, options); + case HomeSectionType.NextUp: + loadNextUp(elem, apiClient, userSettings, options); + break; + case HomeSectionType.Resume: + return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings, options); + case HomeSectionType.ResumeAudio: + return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings, options); + case HomeSectionType.ResumeBook: + return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings, options); + case HomeSectionType.SmallLibraryTiles: + loadLibraryTiles(elem, userViews, options); + break; + default: + elem.innerHTML = ''; } + return Promise.resolve(); } @@ -174,573 +162,11 @@ function enableScrollX() { return true; } -function getLibraryButtonsHtml(items) { - let html = ''; - - html += '
'; - html += '

' + globalize.translate('HeaderMyMedia') + '

'; - - html += '
'; - - // library card background images - for (let i = 0, length = items.length; i < length; i++) { - const item = items[i]; - const icon = imageHelper.getLibraryIcon(item.CollectionType); - html += '' + escapeHtml(item.Name) + ''; - } - - html += '
'; - html += '
'; - - return html; -} - -function loadlibraryButtons(elem, apiClient, user, userSettings, userViews) { - elem.classList.remove('verticalSection'); - const html = getLibraryButtonsHtml(userViews); - - elem.innerHTML = html; - imageLoader.lazyChildren(elem); -} - -function getFetchLatestItemsFn(serverId, parentId, collectionType) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - let limit = 16; - - if (enableScrollX()) { - if (collectionType === 'music') { - limit = 30; - } - } else if (collectionType === 'tvshows') { - limit = 5; - } else if (collectionType === 'music') { - limit = 9; - } else { - limit = 8; - } - - const options = { - Limit: limit, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - ParentId: parentId - }; - - return apiClient.getLatestItems(options); - }; -} - -function getLatestItemsHtmlFn(itemType, viewType) { - return function (items) { - const cardLayout = false; - let shape; - if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { - shape = getPortraitShape(enableScrollX()); - } else if (viewType === 'music' || viewType === 'homevideos') { - shape = getSquareShape(enableScrollX()); - } else { - shape = getBackdropShape(enableScrollX()); - } - - return cardBuilder.getCardsHtml({ - items: items, - shape: shape, - preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null, - showUnplayedIndicator: false, - showChildCountIndicator: true, - context: 'home', - overlayText: false, - centerText: !cardLayout, - overlayPlayButton: viewType !== 'photos', - allowBottomPadding: !enableScrollX() && !cardLayout, - cardLayout: cardLayout, - showTitle: viewType !== 'photos', - showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType, - showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')), - lines: 2 - }); - }; -} - -function renderLatestSection(elem, apiClient, user, parent) { - let html = ''; - - html += '
'; - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType); - itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType); - itemsContainer.parentContainer = elem; -} - -function loadRecentlyAdded(elem, apiClient, user, userViews) { - elem.classList.remove('verticalSection'); - const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels']; - - for (let i = 0, length = userViews.length; i < length; i++) { - const item = userViews[i]; - if (user.Configuration.LatestItemsExcludes.indexOf(item.Id) !== -1) { - continue; - } - - if (excludeViewTypes.indexOf(item.CollectionType || []) !== -1) { - continue; - } - - const frag = document.createElement('div'); - frag.classList.add('verticalSection'); - frag.classList.add('hide'); - elem.appendChild(frag); - - renderLatestSection(frag, apiClient, user, item); - } -} - -export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, userViews) { - let html = ''; - if (userViews.length) { - html += '

' + globalize.translate('HeaderMyMedia') + '

'; - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - html += cardBuilder.getCardsHtml({ - items: userViews, - shape: getBackdropShape(enableScrollX()), - showTitle: true, - centerText: true, - overlayText: false, - lazy: true, - transition: false, - allowBottomPadding: !enableScrollX() - }); - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - } - - elem.innerHTML = html; - imageLoader.lazyChildren(elem); -} - -const dataMonitorHints = { - 'Audio': 'audioplayback,markplayed', - 'Video': 'videoplayback,markplayed' -}; - -function loadResume(elem, apiClient, headerText, mediaType, userSettings) { - let html = ''; - - const dataMonitor = dataMonitorHints[mediaType] || 'markplayed'; - - html += '

' + globalize.translate(headerText) + '

'; - if (enableScrollX()) { - html += '
'; - html += `
`; - } else { - html += `
`; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId()); - itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType); - itemsContainer.parentContainer = elem; -} - -function getItemsToResumeFn(mediaType, serverId) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - - const limit = enableScrollX() ? 12 : 5; - - const options = { - Limit: limit, - Recursive: true, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - EnableTotalRecordCount: false, - MediaTypes: mediaType - }; - - return apiClient.getResumableItems(apiClient.getCurrentUserId(), options); - }; -} - -function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) { - return function (items) { - const cardLayout = false; - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: true, - inheritThumb: !useEpisodeImages, - shape: (mediaType === 'Book') ? - getPortraitShape(enableScrollX()) : - getBackdropShape(enableScrollX()), - overlayText: false, - showTitle: true, - showParentTitle: true, - lazy: true, - showDetailsMenu: true, - overlayPlayButton: true, - context: 'home', - centerText: !cardLayout, - allowBottomPadding: false, - cardLayout: cardLayout, - showYear: true, - lines: 2 - }); - }; -} - -function getOnNowFetchFn(serverId) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 24, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }); - }; -} - -function getOnNowItemsHtml(items) { - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: 'auto', - inheritThumb: false, - shape: (enableScrollX() ? 'autooverflow' : 'auto'), - showParentTitleOrTitle: true, - showTitle: true, - centerText: true, - coverImage: true, - overlayText: false, - allowBottomPadding: !enableScrollX(), - showAirTime: true, - showChannelName: false, - showAirDateTime: false, - showAirEndTime: true, - defaultShape: getBackdropShape(enableScrollX()), - lines: 3, - overlayPlayButton: true - }); -} - -function loadOnNow(elem, apiClient, user) { - if (!user.Policy.EnableLiveTvAccess) { - return Promise.resolve(); - } - - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 1, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }).then(function (result) { - let html = ''; - if (result.Items.length) { - elem.classList.remove('padded-left'); - elem.classList.remove('padded-right'); - elem.classList.remove('padded-bottom'); - elem.classList.remove('verticalSection'); - - html += '
'; - html += '
'; - html += '

' + globalize.translate('LiveTV') + '

'; - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += ''; - if (enableScrollX()) { - html += '
'; - } - html += '
'; - html += '
'; - - html += '
'; - html += '
'; - - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('HeaderOnNow'); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

' + globalize.translate('HeaderOnNow') + '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - - html += '
'; - html += '
'; - - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.parentContainer = elem; - itemsContainer.fetchData = getOnNowFetchFn(apiClient.serverId()); - itemsContainer.getItemsHtml = getOnNowItemsHtml; - } - }); -} - -function getNextUpFetchFn(serverId, userSettings) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - const oldestDateForNextUp = new Date(); - oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); - return apiClient.getNextUpEpisodes({ - Limit: enableScrollX() ? 24 : 15, - Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount', - UserId: apiClient.getCurrentUserId(), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - EnableTotalRecordCount: false, - DisableFirstEpisode: false, - NextUpDateCutoff: oldestDateForNextUp.toISOString(), - EnableRewatching: userSettings.enableRewatchingInNextUp() - }); - }; -} - -function getNextUpItemsHtmlFn(useEpisodeImages) { - return function (items) { - const cardLayout = false; - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: true, - inheritThumb: !useEpisodeImages, - shape: getBackdropShape(enableScrollX()), - overlayText: false, - showTitle: true, - showParentTitle: true, - lazy: true, - overlayPlayButton: true, - context: 'home', - centerText: !cardLayout, - allowBottomPadding: !enableScrollX(), - cardLayout: cardLayout - }); - }; -} - -function loadNextUp(elem, apiClient, userSettings) { - let html = ''; - - html += '
'; - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('NextUp'); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

'; - html += globalize.translate('NextUp'); - html += '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings); - itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume()); - itemsContainer.parentContainer = elem; -} - -function getLatestRecordingsFetchFn(serverId, activeRecordingsOnly) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecordings({ - userId: apiClient.getCurrentUserId(), - Limit: enableScrollX() ? 12 : 5, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', - EnableTotalRecordCount: false, - IsLibraryItem: activeRecordingsOnly ? null : false, - IsInProgress: activeRecordingsOnly ? true : null - }); - }; -} - -function getLatestRecordingItemsHtml(activeRecordingsOnly) { - return function (items) { - return cardBuilder.getCardsHtml({ - items: items, - shape: enableScrollX() ? 'autooverflow' : 'auto', - showTitle: true, - showParentTitle: true, - coverImage: true, - lazy: true, - showDetailsMenu: true, - centerText: true, - overlayText: false, - showYear: true, - lines: 2, - overlayPlayButton: !activeRecordingsOnly, - allowBottomPadding: !enableScrollX(), - preferThumb: true, - cardLayout: false, - overlayMoreButton: activeRecordingsOnly, - action: activeRecordingsOnly ? 'none' : null, - centerPlayButton: activeRecordingsOnly - }); - }; -} - -function loadLatestLiveTvRecordings(elem, activeRecordingsOnly, apiClient) { - const title = activeRecordingsOnly ? - globalize.translate('HeaderActiveRecordings') : - globalize.translate('HeaderLatestRecordings'); - - let html = ''; - - html += '
'; - html += '

' + title + '

'; - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly); - itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly); - itemsContainer.parentContainer = elem; -} - export default { - loadLibraryTiles: loadLibraryTiles, - getDefaultSection: getDefaultSection, - loadSections: loadSections, - destroySections: destroySections, - pause: pause, - resume: resume + getDefaultSection, + loadSections, + destroySections, + pause, + resume }; diff --git a/src/components/homesections/sections/activeRecordings.ts b/src/components/homesections/sections/activeRecordings.ts new file mode 100644 index 0000000000..8b8129f789 --- /dev/null +++ b/src/components/homesections/sections/activeRecordings.ts @@ -0,0 +1,92 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import globalize from 'scripts/globalize'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getLatestRecordingsFetchFn( + serverId: string, + activeRecordingsOnly: boolean, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + return apiClient.getLiveTvRecordings({ + userId: apiClient.getCurrentUserId(), + Limit: enableOverflow ? 12 : 5, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', + EnableTotalRecordCount: false, + IsLibraryItem: activeRecordingsOnly ? null : false, + IsInProgress: activeRecordingsOnly ? true : null + }); + }; +} + +function getLatestRecordingItemsHtml( + activeRecordingsOnly: boolean, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + return cardBuilder.getCardsHtml({ + items: items, + shape: enableOverflow ? 'autooverflow' : 'auto', + showTitle: true, + showParentTitle: true, + coverImage: true, + lazy: true, + showDetailsMenu: true, + centerText: true, + overlayText: false, + showYear: true, + lines: 2, + overlayPlayButton: !activeRecordingsOnly, + allowBottomPadding: !enableOverflow, + preferThumb: true, + cardLayout: false, + overlayMoreButton: activeRecordingsOnly, + action: activeRecordingsOnly ? 'none' : null, + centerPlayButton: activeRecordingsOnly + }); + }; +} + +export function loadRecordings( + elem: HTMLElement, + activeRecordingsOnly: boolean, + apiClient: ApiClient, + options: SectionOptions +) { + const title = activeRecordingsOnly ? + globalize.translate('HeaderActiveRecordings') : + globalize.translate('HeaderLatestRecordings'); + + let html = ''; + + html += '
'; + html += '

' + title + '

'; + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly, options); + itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly, options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/libraryButtons.ts b/src/components/homesections/sections/libraryButtons.ts new file mode 100644 index 0000000000..06656c343a --- /dev/null +++ b/src/components/homesections/sections/libraryButtons.ts @@ -0,0 +1,36 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import escapeHtml from 'escape-html'; + +import imageLoader from 'components/images/imageLoader'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import imageHelper from 'scripts/imagehelper'; + +function getLibraryButtonsHtml(items: BaseItemDto[]) { + let html = ''; + + html += '
'; + html += '

' + globalize.translate('HeaderMyMedia') + '

'; + + html += '
'; + + // library card background images + for (let i = 0, length = items.length; i < length; i++) { + const item = items[i]; + const icon = imageHelper.getLibraryIcon(item.CollectionType); + html += '' + escapeHtml(item.Name) + ''; + } + + html += '
'; + html += '
'; + + return html; +} + +export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) { + elem.classList.remove('verticalSection'); + const html = getLibraryButtonsHtml(userViews); + + elem.innerHTML = html; + imageLoader.lazyChildren(elem); +} diff --git a/src/components/homesections/sections/libraryTiles.ts b/src/components/homesections/sections/libraryTiles.ts new file mode 100644 index 0000000000..6ccfc528c5 --- /dev/null +++ b/src/components/homesections/sections/libraryTiles.ts @@ -0,0 +1,46 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionOptions } from './section'; + +export function loadLibraryTiles( + elem: HTMLElement, + userViews: BaseItemDto[], + { + enableOverflow + }: SectionOptions +) { + let html = ''; + if (userViews.length) { + html += '

' + globalize.translate('HeaderMyMedia') + '

'; + if (enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + html += cardBuilder.getCardsHtml({ + items: userViews, + shape: getBackdropShape(enableOverflow), + showTitle: true, + centerText: true, + overlayText: false, + lazy: true, + transition: false, + allowBottomPadding: !enableOverflow + }); + + if (enableOverflow) { + html += '
'; + } + html += '
'; + } + + elem.innerHTML = html; + imageLoader.lazyChildren(elem); +} diff --git a/src/components/homesections/sections/liveTv.ts b/src/components/homesections/sections/liveTv.ts new file mode 100644 index 0000000000..7c8606c12b --- /dev/null +++ b/src/components/homesections/sections/liveTv.ts @@ -0,0 +1,181 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import { appRouter } from 'components/router/appRouter'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import ServerConnections from 'components/ServerConnections'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getOnNowFetchFn( + serverId: string +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + return apiClient.getLiveTvRecommendedPrograms({ + userId: apiClient.getCurrentUserId(), + IsAiring: true, + limit: 24, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Thumb,Backdrop', + EnableTotalRecordCount: false, + Fields: 'ChannelInfo,PrimaryImageAspectRatio' + }); + }; +} + +function getOnNowItemsHtmlFn( + { enableOverflow }: SectionOptions +) { + return (items: BaseItemDto[]) => ( + cardBuilder.getCardsHtml({ + items: items, + preferThumb: 'auto', + inheritThumb: false, + shape: (enableOverflow ? 'autooverflow' : 'auto'), + showParentTitleOrTitle: true, + showTitle: true, + centerText: true, + coverImage: true, + overlayText: false, + allowBottomPadding: !enableOverflow, + showAirTime: true, + showChannelName: false, + showAirDateTime: false, + showAirEndTime: true, + defaultShape: getBackdropShape(enableOverflow), + lines: 3, + overlayPlayButton: true + }) + ); +} + +function buildSection( + elem: HTMLElement, + serverId: string, + options: SectionOptions +) { + let html = ''; + + elem.classList.remove('padded-left'); + elem.classList.remove('padded-right'); + elem.classList.remove('padded-bottom'); + elem.classList.remove('verticalSection'); + + html += '
'; + html += '
'; + html += '

' + globalize.translate('LiveTV') + '

'; + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += ''; + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + html += '
'; + + html += '
'; + html += '
'; + + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('HeaderOnNow'); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

' + globalize.translate('HeaderOnNow') + '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + + html += '
'; + html += '
'; + + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.parentContainer = elem; + itemsContainer.fetchData = getOnNowFetchFn(serverId); + itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options); +} + +export function loadLiveTV( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + options: SectionOptions +) { + if (!user.Policy?.EnableLiveTvAccess) { + return Promise.resolve(); + } + + return apiClient.getLiveTvRecommendedPrograms({ + userId: apiClient.getCurrentUserId(), + IsAiring: true, + limit: 1, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Thumb,Backdrop', + EnableTotalRecordCount: false, + Fields: 'ChannelInfo,PrimaryImageAspectRatio' + }).then(function (result) { + if (result.Items?.length) { + buildSection(elem, apiClient.serverId(), options); + } + }); +} diff --git a/src/components/homesections/sections/nextUp.ts b/src/components/homesections/sections/nextUp.ts new file mode 100644 index 0000000000..4ab401ab86 --- /dev/null +++ b/src/components/homesections/sections/nextUp.ts @@ -0,0 +1,106 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import ServerConnections from 'components/ServerConnections'; +import globalize from 'scripts/globalize'; +import type { UserSettings } from 'scripts/settings/userSettings'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getNextUpFetchFn( + serverId: string, + userSettings: UserSettings, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + const oldestDateForNextUp = new Date(); + oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); + return apiClient.getNextUpEpisodes({ + Limit: enableOverflow ? 24 : 15, + Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount', + UserId: apiClient.getCurrentUserId(), + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + EnableTotalRecordCount: false, + DisableFirstEpisode: false, + NextUpDateCutoff: oldestDateForNextUp.toISOString(), + EnableRewatching: userSettings.enableRewatchingInNextUp() + }); + }; +} + +function getNextUpItemsHtmlFn( + useEpisodeImages: boolean, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + return cardBuilder.getCardsHtml({ + items: items, + preferThumb: true, + inheritThumb: !useEpisodeImages, + shape: getBackdropShape(enableOverflow), + overlayText: false, + showTitle: true, + showParentTitle: true, + lazy: true, + overlayPlayButton: true, + context: 'home', + centerText: !cardLayout, + allowBottomPadding: !enableOverflow, + cardLayout: cardLayout + }); + }; +} + +export function loadNextUp( + elem: HTMLElement, + apiClient: ApiClient, + userSettings: UserSettings, + options: SectionOptions +) { + let html = ''; + + html += '
'; + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('NextUp'); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

'; + html += globalize.translate('NextUp'); + html += '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options); + itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/recentlyAdded.ts b/src/components/homesections/sections/recentlyAdded.ts new file mode 100644 index 0000000000..8f41cbb5ca --- /dev/null +++ b/src/components/homesections/sections/recentlyAdded.ts @@ -0,0 +1,165 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import escapeHtml from 'escape-html'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getFetchLatestItemsFn( + serverId: string, + parentId: string | undefined, + collectionType: string | null | undefined, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + let limit = 16; + + if (enableOverflow) { + if (collectionType === 'music') { + limit = 30; + } + } else if (collectionType === 'tvshows') { + limit = 5; + } else if (collectionType === 'music') { + limit = 9; + } else { + limit = 8; + } + + const options = { + Limit: limit, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + ParentId: parentId + }; + + return apiClient.getLatestItems(options); + }; +} + +function getLatestItemsHtmlFn( + itemType: BaseItemKind | undefined, + viewType: string | null | undefined, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + let shape; + if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { + shape = getPortraitShape(enableOverflow); + } else if (viewType === 'music' || viewType === 'homevideos') { + shape = getSquareShape(enableOverflow); + } else { + shape = getBackdropShape(enableOverflow); + } + + return cardBuilder.getCardsHtml({ + items: items, + shape: shape, + preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null, + showUnplayedIndicator: false, + showChildCountIndicator: true, + context: 'home', + overlayText: false, + centerText: !cardLayout, + overlayPlayButton: viewType !== 'photos', + allowBottomPadding: !enableOverflow && !cardLayout, + cardLayout: cardLayout, + showTitle: viewType !== 'photos', + showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType, + showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')), + lines: 2 + }); + }; +} + +function renderLatestSection( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + parent: BaseItemDto, + options: SectionOptions +) { + let html = ''; + + html += '
'; + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options); + itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options); + itemsContainer.parentContainer = elem; +} + +export function loadRecentlyAdded( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + userViews: BaseItemDto[], + options: SectionOptions +) { + elem.classList.remove('verticalSection'); + const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels']; + const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? []; + + for (let i = 0, length = userViews.length; i < length; i++) { + const item = userViews[i]; + if ( + !item.Id + || userExcludeItems.indexOf(item.Id) !== -1 + ) { + continue; + } + + if ( + !item.CollectionType + || excludeViewTypes.indexOf(item.CollectionType) !== -1 + ) { + continue; + } + + const frag = document.createElement('div'); + frag.classList.add('verticalSection'); + frag.classList.add('hide'); + elem.appendChild(frag); + + renderLatestSection(frag, apiClient, user, item, options); + } +} diff --git a/src/components/homesections/sections/resume.ts b/src/components/homesections/sections/resume.ts new file mode 100644 index 0000000000..e96dde3ee0 --- /dev/null +++ b/src/components/homesections/sections/resume.ts @@ -0,0 +1,105 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import globalize from 'scripts/globalize'; +import type { UserSettings } from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +const dataMonitorHints: Record = { + Audio: 'audioplayback,markplayed', + Video: 'videoplayback,markplayed' +}; + +function getItemsToResumeFn( + mediaType: BaseItemKind, + serverId: string, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + + const limit = enableOverflow ? 12 : 5; + + const options = { + Limit: limit, + Recursive: true, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + EnableTotalRecordCount: false, + MediaTypes: mediaType + }; + + return apiClient.getResumableItems(apiClient.getCurrentUserId(), options); + }; +} + +function getItemsToResumeHtmlFn( + useEpisodeImages: boolean, + mediaType: BaseItemKind, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + return cardBuilder.getCardsHtml({ + items: items, + preferThumb: true, + inheritThumb: !useEpisodeImages, + shape: (mediaType === 'Book') ? + getPortraitShape(enableOverflow) : + getBackdropShape(enableOverflow), + overlayText: false, + showTitle: true, + showParentTitle: true, + lazy: true, + showDetailsMenu: true, + overlayPlayButton: true, + context: 'home', + centerText: !cardLayout, + allowBottomPadding: false, + cardLayout: cardLayout, + showYear: true, + lines: 2 + }); + }; +} + +export function loadResume( + elem: HTMLElement, + apiClient: ApiClient, + titleLabel: string, + mediaType: BaseItemKind, + userSettings: UserSettings, + options: SectionOptions +) { + let html = ''; + + const dataMonitor = dataMonitorHints[mediaType] ?? 'markplayed'; + + html += '

' + globalize.translate(titleLabel) + '

'; + if (options.enableOverflow) { + html += '
'; + html += `
`; + } else { + html += `
`; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options); + itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/section.d.ts b/src/components/homesections/sections/section.d.ts new file mode 100644 index 0000000000..27f7c37708 --- /dev/null +++ b/src/components/homesections/sections/section.d.ts @@ -0,0 +1,11 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; + +export interface SectionOptions { + enableOverflow: boolean +} + +export type SectionContainerElement = { + fetchData: () => void + getItemsHtml: (items: BaseItemDto[]) => void + parentContainer: HTMLElement +} & Element; diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 691b07cb4d..9dc0656219 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -301,7 +301,7 @@ export class UserSettings { /** * Get or set 'Use Episode Images in Next Up and Continue Watching' state. - * @param {string|boolean|undefined} val - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined. + * @param {string|boolean|undefined} [val] - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined. * @return {boolean} 'Use Episode Images in Next Up' state. */ useEpisodeImagesInNextUpAndResume(val) { @@ -463,7 +463,7 @@ export class UserSettings { /** * Get or set max days for next up list. - * @param {number|undefined} val - Max days for next up. + * @param {number|undefined} [val] - Max days for next up. * @return {number} Max days for a show to stay in next up without being watched. */ maxDaysForNextUp(val) { @@ -482,7 +482,7 @@ export class UserSettings { /** * Get or set rewatching in next up. - * @param {boolean|undefined} val - If rewatching items should be included in next up. + * @param {boolean|undefined} [val] - If rewatching items should be included in next up. * @returns {boolean} Rewatching in next up state. */ enableRewatchingInNextUp(val) { diff --git a/src/types/homeSectionType.ts b/src/types/homeSectionType.ts new file mode 100644 index 0000000000..1a3e6eb876 --- /dev/null +++ b/src/types/homeSectionType.ts @@ -0,0 +1,27 @@ +// NOTE: This should be included in the OpenAPI spec ideally +// https://github.com/jellyfin/jellyfin/blob/1b4394199a2f9883cd601bdb8c9d66015397aa52/Jellyfin.Data/Enums/HomeSectionType.cs +export enum HomeSectionType { + None = 'none', + SmallLibraryTiles = 'smalllibrarytiles', + LibraryButtons = 'librarybuttons', + ActiveRecordings = 'activerecordings', + Resume = 'resume', + ResumeAudio = 'resumeaudio', + LatestMedia = 'latestmedia', + NextUp = 'nextup', + LiveTv = 'livetv', + ResumeBook = 'resumebook' +} + +// NOTE: This needs to match the server defaults +// https://github.com/jellyfin/jellyfin/blob/1b4394199a2f9883cd601bdb8c9d66015397aa52/Jellyfin.Api/Controllers/DisplayPreferencesController.cs#L120 +export const DEFAULT_SECTIONS: HomeSectionType[] = [ + HomeSectionType.SmallLibraryTiles, + HomeSectionType.Resume, + HomeSectionType.ResumeAudio, + HomeSectionType.ResumeBook, + HomeSectionType.LiveTv, + HomeSectionType.NextUp, + HomeSectionType.LatestMedia, + HomeSectionType.None +];