mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Refactor home screen
This commit is contained in:
parent
f7507fbeab
commit
f1afaa975e
12 changed files with 949 additions and 754 deletions
|
@ -33,11 +33,11 @@ import '../guide/programs.scss';
|
||||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the HTML markup for cards for a set of items.
|
* Generate the HTML markup for cards for a set of items.
|
||||||
* @param items - The items used to generate cards.
|
* @param items - The items used to generate cards.
|
||||||
* @param options - The options of the cards.
|
* @param [options] - The options of the cards.
|
||||||
* @returns {string} The HTML markup for the cards.
|
* @returns {string} The HTML markup for the cards.
|
||||||
*/
|
*/
|
||||||
export function getCardsHtml(items, options) {
|
export function getCardsHtml(items, options) {
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
options = arguments[0];
|
options = arguments[0];
|
||||||
|
@ -48,10 +48,10 @@ export function getCardsHtml(items, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the window is resizable.
|
* Checks if the window is resizable.
|
||||||
* @param {number} windowWidth - Width of the device's screen.
|
* @param {number} windowWidth - Width of the device's screen.
|
||||||
* @returns {boolean} - Result of the check.
|
* @returns {boolean} - Result of the check.
|
||||||
*/
|
*/
|
||||||
function isResizable(windowWidth) {
|
function isResizable(windowWidth) {
|
||||||
const screen = window.screen;
|
const screen = window.screen;
|
||||||
if (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.
|
* 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 {string} shape - Shape of the card.
|
||||||
* @param {number} screenWidth - Width of the screen.
|
* @param {number} screenWidth - Width of the screen.
|
||||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
||||||
* @returns {number} Width of the image for a card.
|
* @returns {number} Width of the image for a card.
|
||||||
*/
|
*/
|
||||||
function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
||||||
const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv);
|
const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv);
|
||||||
return Math.round(screenWidth / imagesPerRow);
|
return Math.round(screenWidth / imagesPerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes the options for a card.
|
* Normalizes the options for a card.
|
||||||
* @param {Object} items - A set of items.
|
* @param {Object} items - A set of items.
|
||||||
* @param {Object} options - Options for handling the items.
|
* @param {Object} options - Options for handling the items.
|
||||||
*/
|
*/
|
||||||
function setCardData(items, options) {
|
function setCardData(items, options) {
|
||||||
options.shape = options.shape || 'auto';
|
options.shape = options.shape || 'auto';
|
||||||
|
|
||||||
|
@ -138,11 +138,11 @@ function setCardData(items, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the internal HTML markup for cards.
|
* Generates the internal HTML markup for cards.
|
||||||
* @param {Object} items - Items for which to generate the markup.
|
* @param {Object} items - Items for which to generate the markup.
|
||||||
* @param {Object} options - Options for generating the markup.
|
* @param {Object} options - Options for generating the markup.
|
||||||
* @returns {string} The internal HTML markup of the cards.
|
* @returns {string} The internal HTML markup of the cards.
|
||||||
*/
|
*/
|
||||||
function buildCardsHtmlInternal(items, options) {
|
function buildCardsHtmlInternal(items, options) {
|
||||||
let isVertical = false;
|
let isVertical = false;
|
||||||
|
|
||||||
|
@ -256,20 +256,20 @@ function buildCardsHtmlInternal(items, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} CardImageUrl
|
* @typedef {Object} CardImageUrl
|
||||||
* @property {string} imgUrl - Image URL.
|
* @property {string} imgUrl - Image URL.
|
||||||
* @property {string} blurhash - Image blurhash.
|
* @property {string} blurhash - Image blurhash.
|
||||||
* @property {boolean} forceName - Force name.
|
* @property {boolean} forceName - Force name.
|
||||||
* @property {boolean} coverImage - Use cover style.
|
* @property {boolean} coverImage - Use cover style.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Get the URL of the card's image.
|
/** Get the URL of the card's image.
|
||||||
* @param {Object} item - Item for which to generate a card.
|
* @param {Object} item - Item for which to generate a card.
|
||||||
* @param {Object} apiClient - API client object.
|
* @param {Object} apiClient - API client object.
|
||||||
* @param {Object} options - Options of the card.
|
* @param {Object} options - Options of the card.
|
||||||
* @param {string} shape - Shape of the desired image.
|
* @param {string} shape - Shape of the desired image.
|
||||||
* @returns {CardImageUrl} Object representing the URL of the card's image.
|
* @returns {CardImageUrl} Object representing the URL of the card's image.
|
||||||
*/
|
*/
|
||||||
function getCardImageUrl(item, apiClient, options, shape) {
|
function getCardImageUrl(item, apiClient, options, shape) {
|
||||||
item = item.ProgramInfo || item;
|
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.
|
* 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.
|
* @param {?string} [str] - String to use for generating the index.
|
||||||
* @returns {number} Index of the color.
|
* @returns {number} Index of the color.
|
||||||
*/
|
*/
|
||||||
function getDefaultColorIndex(str) {
|
function getDefaultColorIndex(str) {
|
||||||
const numRandomColors = 5;
|
const numRandomColors = 5;
|
||||||
|
|
||||||
|
@ -435,16 +435,16 @@ function getDefaultColorIndex(str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the HTML markup for a card's text.
|
* Generates the HTML markup for a card's text.
|
||||||
* @param {Array} lines - Array containing the text lines.
|
* @param {Array} lines - Array containing the text lines.
|
||||||
* @param {string} cssClass - Base CSS class to use for the 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} forceLines - Flag to force the rendering of all lines.
|
||||||
* @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer.
|
* @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer.
|
||||||
* @param {string} cardLayout - DEPRECATED
|
* @param {string} cardLayout - DEPRECATED
|
||||||
* @param {boolean} addRightMargin - Flag to add a right margin to the text.
|
* @param {boolean} addRightMargin - Flag to add a right margin to the text.
|
||||||
* @param {number} maxLines - Maximum number of lines to render.
|
* @param {number} maxLines - Maximum number of lines to render.
|
||||||
* @returns {string} HTML markup for the card's text.
|
* @returns {string} HTML markup for the card's text.
|
||||||
*/
|
*/
|
||||||
function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) {
|
function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
@ -488,21 +488,21 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the item is live TV.
|
* Determines if the item is live TV.
|
||||||
* @param {Object} item - Item to use for the check.
|
* @param {Object} item - Item to use for the check.
|
||||||
* @returns {boolean} Flag showing if the item is live TV.
|
* @returns {boolean} Flag showing if the item is live TV.
|
||||||
*/
|
*/
|
||||||
function isUsingLiveTvNaming(item) {
|
function isUsingLiveTvNaming(item) {
|
||||||
return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording';
|
return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the air time text for the item based on the given times.
|
* 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 {object} item - Item used to generate the air time text.
|
||||||
* @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
|
* @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
|
||||||
* @param {boolean} showAirEndTime - ISO8601 date for the end 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 {string} The air time text for the item based on the given dates.
|
||||||
*/
|
*/
|
||||||
function getAirTimeText(item, showAirDateTime, showAirEndTime) {
|
function getAirTimeText(item, showAirDateTime, showAirEndTime) {
|
||||||
let airTimeText = '';
|
let airTimeText = '';
|
||||||
|
|
||||||
|
@ -529,16 +529,16 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the HTML markup for the card's footer text.
|
* Generates the HTML markup for the card's footer text.
|
||||||
* @param {Object} item - Item used to generate the footer text.
|
* @param {Object} item - Item used to generate the footer text.
|
||||||
* @param {Object} apiClient - API client instance.
|
* @param {Object} apiClient - API client instance.
|
||||||
* @param {Object} options - Options used to generate the footer text.
|
* @param {Object} options - Options used to generate the footer text.
|
||||||
* @param {string} footerClass - CSS classes of the footer element.
|
* @param {string} footerClass - CSS classes of the footer element.
|
||||||
* @param {string} progressHtml - HTML markup of the progress bar element.
|
* @param {string} progressHtml - HTML markup of the progress bar element.
|
||||||
* @param {Object} flags - Various flags for the footer
|
* @param {Object} flags - Various flags for the footer
|
||||||
* @param {Object} urls - Various urls for the footer
|
* @param {Object} urls - Various urls for the footer
|
||||||
* @returns {string} HTML markup of the card's footer text element.
|
* @returns {string} HTML markup of the card's footer text element.
|
||||||
*/
|
*/
|
||||||
function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) {
|
function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) {
|
||||||
item = item.ProgramInfo || item;
|
item = item.ProgramInfo || item;
|
||||||
let html = '';
|
let html = '';
|
||||||
|
@ -771,12 +771,12 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the HTML markup for the action button.
|
* Generates the HTML markup for the action button.
|
||||||
* @param {Object} item - Item used to generate the action button.
|
* @param {Object} item - Item used to generate the action button.
|
||||||
* @param {string} text - Text of the action button.
|
* @param {string} text - Text of the action button.
|
||||||
* @param {string} serverId - ID of the server.
|
* @param {string} serverId - ID of the server.
|
||||||
* @returns {string} HTML markup of the action button.
|
* @returns {string} HTML markup of the action button.
|
||||||
*/
|
*/
|
||||||
function getTextActionButton(item, text, serverId) {
|
function getTextActionButton(item, text, serverId) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
text = itemHelper.getDisplayName(item);
|
text = itemHelper.getDisplayName(item);
|
||||||
|
@ -797,11 +797,11 @@ function getTextActionButton(item, text, serverId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates 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} options - Options used to generate the item count.
|
||||||
* @param {Object} item - Item 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.
|
* @returns {string} HTML markup for the item count indicator.
|
||||||
*/
|
*/
|
||||||
function getItemCountsHtml(options, item) {
|
function getItemCountsHtml(options, item) {
|
||||||
const counts = [];
|
const counts = [];
|
||||||
let childText;
|
let childText;
|
||||||
|
@ -879,8 +879,8 @@ function getItemCountsHtml(options, item) {
|
||||||
let refreshIndicatorLoaded;
|
let refreshIndicatorLoaded;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports the refresh indicator element.
|
* Imports the refresh indicator element.
|
||||||
*/
|
*/
|
||||||
function importRefreshIndicator() {
|
function importRefreshIndicator() {
|
||||||
if (!refreshIndicatorLoaded) {
|
if (!refreshIndicatorLoaded) {
|
||||||
refreshIndicatorLoaded = true;
|
refreshIndicatorLoaded = true;
|
||||||
|
@ -889,22 +889,22 @@ function importRefreshIndicator() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default background class for a card based on a string.
|
* Returns the default background class for a card based on a string.
|
||||||
* @param {?string} [str] - Text used to generate the background class.
|
* @param {?string} [str] - Text used to generate the background class.
|
||||||
* @returns {string} CSS classes for default card backgrounds.
|
* @returns {string} CSS classes for default card backgrounds.
|
||||||
*/
|
*/
|
||||||
export function getDefaultBackgroundClass(str) {
|
export function getDefaultBackgroundClass(str) {
|
||||||
return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str);
|
return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the HTML markup for an individual card.
|
* Builds the HTML markup for an individual card.
|
||||||
* @param {number} index - Index of the card
|
* @param {number} index - Index of the card
|
||||||
* @param {object} item - Item used to generate the card.
|
* @param {object} item - Item used to generate the card.
|
||||||
* @param {object} apiClient - API client instance.
|
* @param {object} apiClient - API client instance.
|
||||||
* @param {object} options - Options used to generate the card.
|
* @param {object} options - Options used to generate the card.
|
||||||
* @returns {string} HTML markup for the generated card.
|
* @returns {string} HTML markup for the generated card.
|
||||||
*/
|
*/
|
||||||
function buildCard(index, item, apiClient, options) {
|
function buildCard(index, item, apiClient, options) {
|
||||||
let action = options.action || 'link';
|
let action = options.action || 'link';
|
||||||
|
|
||||||
|
@ -1211,11 +1211,11 @@ function buildCard(index, item, apiClient, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates HTML markup for the card overlay.
|
* Generates HTML markup for the card overlay.
|
||||||
* @param {object} item - Item used to generate the card overlay.
|
* @param {object} item - Item used to generate the card overlay.
|
||||||
* @param {string} action - Action assigned to the overlay.
|
* @param {string} action - Action assigned to the overlay.
|
||||||
* @returns {string} HTML markup of the card overlay.
|
* @returns {string} HTML markup of the card overlay.
|
||||||
*/
|
*/
|
||||||
function getHoverMenuHtml(item, action) {
|
function getHoverMenuHtml(item, action) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
@ -1253,11 +1253,11 @@ function getHoverMenuHtml(item, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the text or icon used for default card backgrounds.
|
* Generates the text or icon used for default card backgrounds.
|
||||||
* @param {object} item - Item used to generate the card overlay.
|
* @param {object} item - Item used to generate the card overlay.
|
||||||
* @param {object} options - Options used to generate the card overlay.
|
* @param {object} options - Options used to generate the card overlay.
|
||||||
* @returns {string} HTML markup of the card overlay.
|
* @returns {string} HTML markup of the card overlay.
|
||||||
*/
|
*/
|
||||||
export function getDefaultText(item, options) {
|
export function getDefaultText(item, options) {
|
||||||
if (item.CollectionType) {
|
if (item.CollectionType) {
|
||||||
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
|
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
|
||||||
|
@ -1301,10 +1301,10 @@ export function getDefaultText(item, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a set of cards and inserts them into the page.
|
* Builds a set of cards and inserts them into the page.
|
||||||
* @param {Array} items - Array of items used to build the cards.
|
* @param {Array} items - Array of items used to build the cards.
|
||||||
* @param {options} options - Options of the cards to build.
|
* @param {options} options - Options of the cards to build.
|
||||||
*/
|
*/
|
||||||
export function buildCards(items, options) {
|
export function buildCards(items, options) {
|
||||||
// Abort if the container has been disposed
|
// Abort if the container has been disposed
|
||||||
if (!document.body.contains(options.itemsContainer)) {
|
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.
|
* 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} card - DOM element of the card.
|
||||||
* @param {HTMLDivElement} indicatorsElem - DOM element of the indicators.
|
* @param {HTMLDivElement} indicatorsElem - DOM element of the indicators.
|
||||||
* @returns {HTMLDivElement} - DOM element of the indicators.
|
* @returns {HTMLDivElement} - DOM element of the indicators.
|
||||||
*/
|
*/
|
||||||
function ensureIndicators(card, indicatorsElem) {
|
function ensureIndicators(card, indicatorsElem) {
|
||||||
if (indicatorsElem) {
|
if (indicatorsElem) {
|
||||||
return indicatorsElem;
|
return indicatorsElem;
|
||||||
|
@ -1368,10 +1368,10 @@ function ensureIndicators(card, indicatorsElem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds user data to the card such as progress indicators and played status.
|
* Adds user data to the card such as progress indicators and played status.
|
||||||
* @param {HTMLDivElement} card - DOM element of the card.
|
* @param {HTMLDivElement} card - DOM element of the card.
|
||||||
* @param {Object} userData - User data to apply to the card.
|
* @param {Object} userData - User data to apply to the card.
|
||||||
*/
|
*/
|
||||||
function updateUserData(card, userData) {
|
function updateUserData(card, userData) {
|
||||||
const type = card.getAttribute('data-type');
|
const type = card.getAttribute('data-type');
|
||||||
const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season';
|
const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season';
|
||||||
|
@ -1447,10 +1447,10 @@ function updateUserData(card, userData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles when user data has changed.
|
* Handles when user data has changed.
|
||||||
* @param {Object} userData - User data to apply to the card.
|
* @param {Object} userData - User data to apply to the card.
|
||||||
* @param {HTMLElement} scope - DOM element to use as a scope when selecting cards.
|
* @param {HTMLElement} scope - DOM element to use as a scope when selecting cards.
|
||||||
*/
|
*/
|
||||||
export function onUserDataChanged(userData, scope) {
|
export function onUserDataChanged(userData, scope) {
|
||||||
const cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]');
|
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.
|
* Handles when a timer has been created.
|
||||||
* @param {string} programId - ID of the program.
|
* @param {string} programId - ID of the program.
|
||||||
* @param {string} newTimerId - ID of the new timer.
|
* @param {string} newTimerId - ID of the new timer.
|
||||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||||
*/
|
*/
|
||||||
export function onTimerCreated(programId, newTimerId, itemsContainer) {
|
export function onTimerCreated(programId, newTimerId, itemsContainer) {
|
||||||
const cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]');
|
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.
|
* Handles when a timer has been cancelled.
|
||||||
* @param {string} timerId - ID of the cancelled timer.
|
* @param {string} timerId - ID of the cancelled timer.
|
||||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||||
*/
|
*/
|
||||||
export function onTimerCancelled(timerId, itemsContainer) {
|
export function onTimerCancelled(timerId, itemsContainer) {
|
||||||
const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]');
|
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.
|
* Handles when a series timer has been cancelled.
|
||||||
* @param {string} cancelledTimerId - ID of the cancelled timer.
|
* @param {string} cancelledTimerId - ID of the cancelled timer.
|
||||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||||
*/
|
*/
|
||||||
export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
|
export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
|
||||||
const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]');
|
const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]');
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import escapeHtml from 'escape-html';
|
|
||||||
|
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import imageHelper from 'scripts/imagehelper';
|
import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType';
|
||||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
|
||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
import { loadRecordings } from './sections/activeRecordings';
|
||||||
import imageLoader from '../images/imageLoader';
|
import { loadLibraryButtons } from './sections/libraryButtons';
|
||||||
import layoutManager from '../layoutManager';
|
import { loadLibraryTiles } from './sections/libraryTiles';
|
||||||
import { appRouter } from '../router/appRouter';
|
import { loadLiveTV } from './sections/liveTv';
|
||||||
import ServerConnections from '../ServerConnections';
|
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-button/paper-icon-button-light';
|
||||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
@ -19,26 +18,8 @@ import 'elements/emby-button/emby-button';
|
||||||
import './homesections.scss';
|
import './homesections.scss';
|
||||||
|
|
||||||
export function getDefaultSection(index) {
|
export function getDefaultSection(index) {
|
||||||
switch (index) {
|
if (index < 0 || index > DEFAULT_SECTIONS.length) return '';
|
||||||
case 0:
|
return DEFAULT_SECTIONS[index];
|
||||||
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 '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSectionsToShow(userSettings, sectionCount) {
|
function getAllSectionsToShow(userSettings, sectionCount) {
|
||||||
|
@ -138,29 +119,36 @@ export function resume(elem, options) {
|
||||||
function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) {
|
function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) {
|
||||||
const section = allSections[index];
|
const section = allSections[index];
|
||||||
const elem = page.querySelector('.section' + index);
|
const elem = page.querySelector('.section' + index);
|
||||||
|
const options = { enableOverflow: enableScrollX() };
|
||||||
|
|
||||||
if (section === 'latestmedia') {
|
switch (section) {
|
||||||
loadRecentlyAdded(elem, apiClient, user, userViews);
|
case HomeSectionType.ActiveRecordings:
|
||||||
} else if (section === 'librarytiles' || section === 'smalllibrarytiles' || section === 'smalllibrarytiles-automobile' || section === 'librarytiles-automobile') {
|
loadRecordings(elem, true, apiClient, options);
|
||||||
loadLibraryTiles(elem, apiClient, user, userSettings, 'smallBackdrop', userViews);
|
break;
|
||||||
} else if (section === 'librarybuttons') {
|
case HomeSectionType.LatestMedia:
|
||||||
loadlibraryButtons(elem, apiClient, user, userSettings, userViews);
|
loadRecentlyAdded(elem, apiClient, user, userViews, options);
|
||||||
} else if (section === 'resume') {
|
break;
|
||||||
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings);
|
case HomeSectionType.LibraryButtons:
|
||||||
} else if (section === 'resumeaudio') {
|
loadLibraryButtons(elem, userViews);
|
||||||
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings);
|
break;
|
||||||
} else if (section === 'activerecordings') {
|
case HomeSectionType.LiveTv:
|
||||||
loadLatestLiveTvRecordings(elem, true, apiClient);
|
return loadLiveTV(elem, apiClient, user, options);
|
||||||
} else if (section === 'nextup') {
|
case HomeSectionType.NextUp:
|
||||||
loadNextUp(elem, apiClient, userSettings);
|
loadNextUp(elem, apiClient, userSettings, options);
|
||||||
} else if (section === 'onnow' || section === 'livetv') {
|
break;
|
||||||
return loadOnNow(elem, apiClient, user);
|
case HomeSectionType.Resume:
|
||||||
} else if (section === 'resumebook') {
|
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings, options);
|
||||||
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings);
|
case HomeSectionType.ResumeAudio:
|
||||||
} else {
|
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings, options);
|
||||||
elem.innerHTML = '';
|
case HomeSectionType.ResumeBook:
|
||||||
return Promise.resolve();
|
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings, options);
|
||||||
|
case HomeSectionType.SmallLibraryTiles:
|
||||||
|
loadLibraryTiles(elem, userViews, options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
elem.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,573 +162,11 @@ function enableScrollX() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLibraryButtonsHtml(items) {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
|
||||||
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-multiselect="false">';
|
|
||||||
|
|
||||||
// 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 += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + escapeHtml(item.Name) + '</span></a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
|
||||||
if (!layoutManager.tv) {
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(parent, {
|
|
||||||
section: 'latest'
|
|
||||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
|
||||||
html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name));
|
|
||||||
html += '</h2>';
|
|
||||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
|
||||||
html += '</a>';
|
|
||||||
} else {
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '</h2>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right focuscontainer-x vertical-wrap">';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += cardBuilder.getCardsHtml({
|
|
||||||
items: userViews,
|
|
||||||
shape: getBackdropShape(enableScrollX()),
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
overlayText: false,
|
|
||||||
lazy: true,
|
|
||||||
transition: false,
|
|
||||||
allowBottomPadding: !enableScrollX()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate(headerText) + '</h2>';
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += `<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="${dataMonitor}">`;
|
|
||||||
} else {
|
|
||||||
html += `<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="${dataMonitor}">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 += '<div class="verticalSection">';
|
|
||||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LiveTV') + '</h2>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true" data-scrollbuttons="false">';
|
|
||||||
html += '<div class="padded-top padded-bottom scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div class="padded-top padded-bottom focuscontainer-x">';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'programs'
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Programs') + '</span></a>';
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'guide'
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'channels'
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
|
||||||
serverId: apiClient.serverId()
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'dvrschedule'
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Schedule') + '</span></a>';
|
|
||||||
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'seriesrecording'
|
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Series') + '</span></a>';
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
html += '<div class="verticalSection">';
|
|
||||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
|
||||||
|
|
||||||
if (!layoutManager.tv) {
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
|
||||||
serverId: apiClient.serverId(),
|
|
||||||
section: 'onnow'
|
|
||||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
|
||||||
html += globalize.translate('HeaderOnNow');
|
|
||||||
html += '</h2>';
|
|
||||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
|
||||||
html += '</a>';
|
|
||||||
} else {
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
|
||||||
if (!layoutManager.tv) {
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
|
|
||||||
serverId: apiClient.serverId()
|
|
||||||
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
|
||||||
html += globalize.translate('NextUp');
|
|
||||||
html += '</h2>';
|
|
||||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
|
||||||
html += '</a>';
|
|
||||||
} else {
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
|
||||||
html += globalize.translate('NextUp');
|
|
||||||
html += '</h2>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 += '<div class="sectionTitleContainer sectionTitleContainer-cards">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + title + '</h2>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
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 {
|
export default {
|
||||||
loadLibraryTiles: loadLibraryTiles,
|
getDefaultSection,
|
||||||
getDefaultSection: getDefaultSection,
|
loadSections,
|
||||||
loadSections: loadSections,
|
destroySections,
|
||||||
destroySections: destroySections,
|
pause,
|
||||||
pause: pause,
|
resume
|
||||||
resume: resume
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
92
src/components/homesections/sections/activeRecordings.ts
Normal file
92
src/components/homesections/sections/activeRecordings.ts
Normal file
|
@ -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 += '<div class="sectionTitleContainer sectionTitleContainer-cards">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + title + '</h2>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||||
|
} else {
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
36
src/components/homesections/sections/libraryButtons.ts
Normal file
36
src/components/homesections/sections/libraryButtons.ts
Normal file
|
@ -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 += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||||
|
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-multiselect="false">';
|
||||||
|
|
||||||
|
// 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 += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + escapeHtml(item.Name) + '</span></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) {
|
||||||
|
elem.classList.remove('verticalSection');
|
||||||
|
const html = getLibraryButtonsHtml(userViews);
|
||||||
|
|
||||||
|
elem.innerHTML = html;
|
||||||
|
imageLoader.lazyChildren(elem);
|
||||||
|
}
|
46
src/components/homesections/sections/libraryTiles.ts
Normal file
46
src/components/homesections/sections/libraryTiles.ts
Normal file
|
@ -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 += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||||
|
if (enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||||
|
} else {
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right focuscontainer-x vertical-wrap">';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += cardBuilder.getCardsHtml({
|
||||||
|
items: userViews,
|
||||||
|
shape: getBackdropShape(enableOverflow),
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
overlayText: false,
|
||||||
|
lazy: true,
|
||||||
|
transition: false,
|
||||||
|
allowBottomPadding: !enableOverflow
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
elem.innerHTML = html;
|
||||||
|
imageLoader.lazyChildren(elem);
|
||||||
|
}
|
181
src/components/homesections/sections/liveTv.ts
Normal file
181
src/components/homesections/sections/liveTv.ts
Normal file
|
@ -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 += '<div class="verticalSection">';
|
||||||
|
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LiveTV') + '</h2>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true" data-scrollbuttons="false">';
|
||||||
|
html += '<div class="padded-top padded-bottom scrollSlider focuscontainer-x">';
|
||||||
|
} else {
|
||||||
|
html += '<div class="padded-top padded-bottom focuscontainer-x">';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'programs'
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Programs') + '</span></a>';
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'guide'
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'channels'
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||||
|
serverId
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'dvrschedule'
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Schedule') + '</span></a>';
|
||||||
|
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'seriesrecording'
|
||||||
|
}) + '" class="raised"><span>' + globalize.translate('Series') + '</span></a>';
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
html += '<div class="verticalSection">';
|
||||||
|
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||||
|
|
||||||
|
if (!layoutManager.tv) {
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||||
|
serverId,
|
||||||
|
section: 'onnow'
|
||||||
|
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||||
|
html += globalize.translate('HeaderOnNow');
|
||||||
|
html += '</h2>';
|
||||||
|
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||||
|
html += '</a>';
|
||||||
|
} else {
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||||
|
} else {
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
106
src/components/homesections/sections/nextUp.ts
Normal file
106
src/components/homesections/sections/nextUp.ts
Normal file
|
@ -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 += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||||
|
if (!layoutManager.tv) {
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
|
||||||
|
serverId: apiClient.serverId()
|
||||||
|
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||||
|
html += globalize.translate('NextUp');
|
||||||
|
html += '</h2>';
|
||||||
|
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||||
|
html += '</a>';
|
||||||
|
} else {
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||||
|
html += globalize.translate('NextUp');
|
||||||
|
html += '</h2>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||||
|
} else {
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
165
src/components/homesections/sections/recentlyAdded.ts
Normal file
165
src/components/homesections/sections/recentlyAdded.ts
Normal file
|
@ -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 += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||||
|
if (!layoutManager.tv) {
|
||||||
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(parent, {
|
||||||
|
section: 'latest'
|
||||||
|
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||||
|
html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name));
|
||||||
|
html += '</h2>';
|
||||||
|
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||||
|
html += '</a>';
|
||||||
|
} else {
|
||||||
|
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '</h2>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||||
|
} else {
|
||||||
|
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
105
src/components/homesections/sections/resume.ts
Normal file
105
src/components/homesections/sections/resume.ts
Normal file
|
@ -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<string, string> = {
|
||||||
|
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 += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate(titleLabel) + '</h2>';
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||||
|
html += `<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||||
|
} else {
|
||||||
|
html += `<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableOverflow) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
11
src/components/homesections/sections/section.d.ts
vendored
Normal file
11
src/components/homesections/sections/section.d.ts
vendored
Normal file
|
@ -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;
|
|
@ -301,7 +301,7 @@ export class UserSettings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set 'Use Episode Images in Next Up and Continue Watching' state.
|
* 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.
|
* @return {boolean} 'Use Episode Images in Next Up' state.
|
||||||
*/
|
*/
|
||||||
useEpisodeImagesInNextUpAndResume(val) {
|
useEpisodeImagesInNextUpAndResume(val) {
|
||||||
|
@ -463,7 +463,7 @@ export class UserSettings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set max days for next up list.
|
* 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.
|
* @return {number} Max days for a show to stay in next up without being watched.
|
||||||
*/
|
*/
|
||||||
maxDaysForNextUp(val) {
|
maxDaysForNextUp(val) {
|
||||||
|
@ -482,7 +482,7 @@ export class UserSettings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set rewatching in next up.
|
* 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.
|
* @returns {boolean} Rewatching in next up state.
|
||||||
*/
|
*/
|
||||||
enableRewatchingInNextUp(val) {
|
enableRewatchingInNextUp(val) {
|
||||||
|
|
27
src/types/homeSectionType.ts
Normal file
27
src/types/homeSectionType.ts
Normal file
|
@ -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
|
||||||
|
];
|
Loading…
Add table
Add a link
Reference in a new issue