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;
|
||||
|
||||
/**
|
||||
* 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 '<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.
|
||||
* @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 + '"]');
|
||||
|
||||
|
|
|
@ -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 += '<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 {
|
||||
loadLibraryTiles: loadLibraryTiles,
|
||||
getDefaultSection: getDefaultSection,
|
||||
loadSections: loadSections,
|
||||
destroySections: destroySections,
|
||||
pause: pause,
|
||||
resume: resume
|
||||
getDefaultSection,
|
||||
loadSections,
|
||||
destroySections,
|
||||
pause,
|
||||
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.
|
||||
* @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) {
|
||||
|
|
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