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 += '
';
-
- 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 (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 += '
';
- }
-
- 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 += `
';
-
- 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 += '
';
- }
- html += '
';
- html += '
';
-
- html += '
';
- 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 (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 += '
';
-
- 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 += '
';
+
+ 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 += '';
+
+ 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 += '
';
+ }
+
+ 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 += '
';
+ }
+ html += '
';
+ html += '
';
+
+ html += '
';
+ 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 (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 (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 += `
';
+
+ 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
+];