1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'master' into Search

This commit is contained in:
Nathan G 2023-10-11 22:00:00 -07:00 committed by GitHub
commit 61e64cb530
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2097 additions and 1891 deletions

View file

@ -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 = '';
@ -669,7 +669,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
} else if (item.EndDate && item.ProductionYear) {
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
lines.push(productionYear + ((endYear === productionYear) ? '' : (' - ' + endYear)));
} else {
lines.push(productionYear || '');
}
@ -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 + '"]');

View file

@ -1,59 +0,0 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import AlphaPicker from '../alphaPicker/alphaPicker';
import { ViewQuerySettings } from '../../types/interface';
interface AlphaPickerContainerProps {
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
}
const AlphaPickerContainer: FC<AlphaPickerContainerProps> = ({ viewQuerySettings, setViewQuerySettings }) => {
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
const element = useRef<HTMLDivElement>(null);
alphaPicker?.updateControls(viewQuerySettings);
const onAlphaPickerChange = useCallback((e) => {
const newValue = (e as CustomEvent).detail.value;
let updatedValue: React.SetStateAction<ViewQuerySettings>;
if (newValue === '#') {
updatedValue = {
NameLessThan: 'A',
NameStartsWith: undefined
};
} else {
updatedValue = {
NameLessThan: undefined,
NameStartsWith: newValue
};
}
setViewQuerySettings((prevState) => ({
...prevState,
StartIndex: 0,
...updatedValue
}));
}, [setViewQuerySettings]);
useEffect(() => {
const alphaPickerElement = element.current;
setAlphaPicker(new AlphaPicker({
element: alphaPickerElement,
valueChangeEvent: 'click'
}));
if (alphaPickerElement) {
alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange);
}
return () => {
alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange);
};
}, [onAlphaPickerChange]);
return (
<div ref={element} className='alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical alphabetPicker-right' />
);
};
export default AlphaPickerContainer;

View file

@ -1,65 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
import { ViewQuerySettings } from '../../types/interface';
interface FilterProps {
topParentId?: string | null;
getItemTypes: () => string[];
getFilterMenuOptions: () => Record<string, never>;
getVisibleFilters: () => string[];
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
}
const Filter: FC<FilterProps> = ({
topParentId,
getItemTypes,
getVisibleFilters,
getFilterMenuOptions,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(null);
const showFilterMenu = useCallback(() => {
import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
const filterMenu = new FilterMenu();
filterMenu.show({
settings: viewQuerySettings,
visibleSettings: getVisibleFilters(),
parentId: topParentId,
itemTypes: getItemTypes(),
serverId: window.ApiClient.serverId(),
filterMenuOptions: getFilterMenuOptions(),
setfilters: setViewQuerySettings
}).catch(() => {
// filter menu closed
});
}).catch(err => {
console.error('[Filter] failed to load filter menu', err);
});
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);
useEffect(() => {
const btnFilter = element.current?.querySelector('.btnFilter');
btnFilter?.addEventListener('click', showFilterMenu);
return () => {
btnFilter?.removeEventListener('click', showFilterMenu);
};
}, [showFilterMenu]);
return (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnFilter autoSize'
title='Filter'
icon='material-icons filter_list'
/>
</div>
);
};
export default Filter;

View file

@ -1,33 +0,0 @@
import React, { FC, useEffect, useRef } from 'react';
import ItemsContainerElement from '../../elements/ItemsContainerElement';
import imageLoader from '../images/imageLoader';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import { ViewQuerySettings } from '../../types/interface';
interface ItemsContainerI {
viewQuerySettings: ViewQuerySettings;
getItemsHtml: () => string
}
const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }) => {
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
itemsContainer.innerHTML = getItemsHtml();
imageLoader.lazyChildren(itemsContainer);
}, [getItemsHtml]);
const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap';
return (
<div ref={element}>
<ItemsContainerElement
className={`itemsContainer ${cssClass} centered padded-left padded-right padded-right-withalphapicker`}
/>
</div>
);
};
export default ItemsContainer;

View file

@ -1,42 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
const NewCollection: FC = () => {
const element = useRef<HTMLDivElement>(null);
const showCollectionEditor = useCallback(() => {
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const serverId = window.ApiClient.serverId();
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [],
serverId: serverId
}).catch(() => {
// closed collection editor
});
}).catch(err => {
console.error('[NewCollection] failed to load collection editor', err);
});
}, []);
useEffect(() => {
const btnNewCollection = element.current?.querySelector('.btnNewCollection');
if (btnNewCollection) {
btnNewCollection.addEventListener('click', showCollectionEditor);
}
}, [showCollectionEditor]);
return (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnNewCollection autoSize'
title='Add'
icon='material-icons add'
/>
</div>
);
};
export default NewCollection;

View file

@ -1,97 +0,0 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
import globalize from '../../scripts/globalize';
import * as userSettings from '../../scripts/settings/userSettings';
import { ViewQuerySettings } from '../../types/interface';
interface PaginationProps {
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
itemsResult?: BaseItemDtoQueryResult;
}
const Pagination: FC<PaginationProps> = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => {
const limit = userSettings.libraryPageSize(undefined);
const totalRecordCount = itemsResult.TotalRecordCount || 0;
const startIndex = viewQuerySettings.StartIndex || 0;
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : totalRecordCount;
const showControls = limit > 0 && limit < totalRecordCount;
const element = useRef<HTMLDivElement>(null);
const onNextPageClick = useCallback(() => {
if (limit > 0) {
const newIndex = startIndex + limit;
setViewQuerySettings((prevState) => ({
...prevState,
StartIndex: newIndex
}));
}
}, [limit, setViewQuerySettings, startIndex]);
const onPreviousPageClick = useCallback(() => {
if (limit > 0) {
const newIndex = Math.max(0, startIndex - limit);
setViewQuerySettings((prevState) => ({
...prevState,
StartIndex: newIndex
}));
}
}, [limit, setViewQuerySettings, startIndex]);
useEffect(() => {
const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement;
if (btnNextPage) {
if (startIndex + limit >= totalRecordCount) {
btnNextPage.disabled = true;
} else {
btnNextPage.disabled = false;
}
btnNextPage.addEventListener('click', onNextPageClick);
}
const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement;
if (btnPreviousPage) {
if (startIndex) {
btnPreviousPage.disabled = false;
} else {
btnPreviousPage.disabled = true;
}
btnPreviousPage.addEventListener('click', onPreviousPageClick);
}
return () => {
btnNextPage?.removeEventListener('click', onNextPageClick);
btnPreviousPage?.removeEventListener('click', onPreviousPageClick);
};
}, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]);
return (
<div ref={element}>
<div className='paging'>
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
<span>
{globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)}
</span>
{showControls && (
<>
<IconButtonElement
is='paper-icon-button-light'
className='btnPreviousPage autoSize'
icon='material-icons arrow_back'
/>
<IconButtonElement
is='paper-icon-button-light'
className='btnNextPage autoSize'
icon='material-icons arrow_forward'
/>
</>
)}
</div>
</div>
</div>
);
};
export default Pagination;

View file

@ -1,54 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
import { ViewQuerySettings } from '../../types/interface';
interface SelectViewProps {
getVisibleViewSettings: () => string[];
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
}
const SelectView: FC<SelectViewProps> = ({
getVisibleViewSettings,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(null);
const showViewSettingsMenu = useCallback(() => {
import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
const viewsettings = new ViewSettings();
viewsettings.show({
settings: viewQuerySettings,
visibleSettings: getVisibleViewSettings(),
setviewsettings: setViewQuerySettings
}).catch(() => {
// view settings closed
});
}).catch(err => {
console.error('[SelectView] failed to load view settings', err);
});
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);
useEffect(() => {
const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement;
btnSelectView?.addEventListener('click', showViewSettingsMenu);
return () => {
btnSelectView?.removeEventListener('click', showViewSettingsMenu);
};
}, [showViewSettingsMenu]);
return (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnSelectView autoSize'
title='ButtonSelectView'
icon='material-icons view_comfy'
/>
</div>
);
};
export default SelectView;

View file

@ -1,45 +0,0 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef } from 'react';
import { playbackManager } from '../playback/playbackmanager';
import IconButtonElement from '../../elements/IconButtonElement';
interface ShuffleProps {
itemsResult?: BaseItemDtoQueryResult;
topParentId: string | null;
}
const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
const element = useRef<HTMLDivElement>(null);
const shuffle = useCallback(() => {
window.ApiClient.getItem(
window.ApiClient.getCurrentUserId(),
topParentId as string
).then((item) => {
playbackManager.shuffle(item);
}).catch(err => {
console.error('[Shuffle] failed to fetch items', err);
});
}, [topParentId]);
useEffect(() => {
const btnShuffle = element.current?.querySelector('.btnShuffle');
if (btnShuffle) {
btnShuffle.addEventListener('click', shuffle);
}
}, [itemsResult.TotalRecordCount, shuffle]);
return (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnShuffle autoSize'
title='Shuffle'
icon='material-icons shuffle'
/>
</div>
);
};
export default Shuffle;

View file

@ -1,58 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
import { ViewQuerySettings } from '../../types/interface';
interface SortProps {
getSortMenuOptions: () => {
name: string;
value: string;
}[];
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
}
const Sort: FC<SortProps> = ({
getSortMenuOptions,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(null);
const showSortMenu = useCallback(() => {
import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
const sortMenu = new SortMenu();
sortMenu.show({
settings: viewQuerySettings,
sortOptions: getSortMenuOptions(),
setSortValues: setViewQuerySettings
}).catch(() => {
// sort menu closed
});
}).catch(err => {
console.error('[Sort] failed to load sort menu', err);
});
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);
useEffect(() => {
const btnSort = element.current?.querySelector('.btnSort');
btnSort?.addEventListener('click', showSortMenu);
return () => {
btnSort?.removeEventListener('click', showSortMenu);
};
}, [showSortMenu]);
return (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnSort autoSize'
title='Sort'
icon='material-icons sort_by_alpha'
/>
</div>
);
};
export default Sort;

View file

@ -1,411 +0,0 @@
import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import loading from '../loading/loading';
import * as userSettings from '../../scripts/settings/userSettings';
import AlphaPickerContainer from './AlphaPickerContainer';
import Filter from './Filter';
import ItemsContainer from './ItemsContainer';
import Pagination from './Pagination';
import SelectView from './SelectView';
import Shuffle from './Shuffle';
import Sort from './Sort';
import NewCollection from './NewCollection';
import globalize from '../../scripts/globalize';
import ServerConnections from '../ServerConnections';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import listview from '../listview/listview';
import cardBuilder from '../cardbuilder/cardBuilder';
import { ViewQuerySettings } from '../../types/interface';
import { CardOptions } from '../../types/cardOptions';
interface ViewItemsContainerProps {
topParentId: string | null;
isBtnShuffleEnabled?: boolean;
isBtnFilterEnabled?: boolean;
isBtnNewCollectionEnabled?: boolean;
isAlphaPickerEnabled?: boolean;
getBasekey: () => string;
getItemTypes: () => string[];
getNoItemsMessage: () => string;
}
const getDefaultSortBy = () => {
return 'SortName';
};
const getFields = (viewQuerySettings: ViewQuerySettings) => {
const fields: ItemFields[] = [
ItemFields.BasicSyncInfo,
ItemFields.MediaSourceCount
];
if (viewQuerySettings.imageType === 'primary') {
fields.push(ItemFields.PrimaryImageAspectRatio);
}
return fields.join(',');
};
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
const filters: ItemFilter[] = [];
if (viewQuerySettings.IsPlayed) {
filters.push(ItemFilter.IsPlayed);
}
if (viewQuerySettings.IsUnplayed) {
filters.push(ItemFilter.IsUnplayed);
}
if (viewQuerySettings.IsFavorite) {
filters.push(ItemFilter.IsFavorite);
}
if (viewQuerySettings.IsResumable) {
filters.push(ItemFilter.IsResumable);
}
return filters;
};
const getVisibleViewSettings = () => {
return [
'showTitle',
'showYear',
'imageType',
'cardLayout'
];
};
const getFilterMenuOptions = () => {
return {};
};
const getVisibleFilters = () => {
return [
'IsUnplayed',
'IsPlayed',
'IsFavorite',
'IsResumable',
'VideoType',
'HasSubtitles',
'HasTrailer',
'HasSpecialFeature',
'HasThemeSong',
'HasThemeVideo'
];
};
const getSortMenuOptions = () => {
return [{
name: globalize.translate('Name'),
value: 'SortName,ProductionYear'
}, {
name: globalize.translate('OptionRandom'),
value: 'Random'
}, {
name: globalize.translate('OptionImdbRating'),
value: 'CommunityRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionCriticRating'),
value: 'CriticRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDateAdded'),
value: 'DateCreated,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDatePlayed'),
value: 'DatePlayed,SortName,ProductionYear'
}, {
name: globalize.translate('OptionParentalRating'),
value: 'OfficialRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionPlayCount'),
value: 'PlayCount,SortName,ProductionYear'
}, {
name: globalize.translate('OptionReleaseDate'),
value: 'PremiereDate,SortName,ProductionYear'
}, {
name: globalize.translate('Runtime'),
value: 'Runtime,SortName,ProductionYear'
}];
};
const defaultViewQuerySettings: ViewQuerySettings = {
showTitle: true,
showYear: true,
imageType: 'primary',
viewType: '',
cardLayout: false,
SortBy: getDefaultSortBy(),
SortOrder: 'Ascending',
IsPlayed: false,
IsUnplayed: false,
IsFavorite: false,
IsResumable: false,
Is4K: null,
IsHD: null,
IsSD: null,
Is3D: null,
VideoTypes: '',
SeriesStatus: '',
HasSubtitles: null,
HasTrailer: null,
HasSpecialFeature: null,
HasThemeSong: null,
HasThemeVideo: null,
GenreIds: '',
StartIndex: 0
};
const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
topParentId,
isBtnShuffleEnabled = false,
isBtnFilterEnabled = true,
isBtnNewCollectionEnabled = false,
isAlphaPickerEnabled = true,
getBasekey,
getItemTypes,
getNoItemsMessage
}) => {
const getSettingsKey = useCallback(() => {
return `${topParentId} - ${getBasekey()}`;
}, [getBasekey, topParentId]);
const [isLoading, setisLoading] = useState(false);
const [viewQuerySettings, setViewQuerySettings] = useLocalStorage<ViewQuerySettings>(
`viewQuerySettings - ${getSettingsKey()}`,
defaultViewQuerySettings
);
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
const element = useRef<HTMLDivElement>(null);
const getContext = useCallback(() => {
const itemType = getItemTypes().join(',');
if (itemType === 'Movie' || itemType === 'BoxSet') {
return 'movies';
}
return null;
}, [getItemTypes]);
const getCardOptions = useCallback(() => {
let shape;
let preferThumb;
let preferDisc;
let preferLogo;
if (viewQuerySettings.imageType === 'banner') {
shape = 'banner';
} else if (viewQuerySettings.imageType === 'disc') {
shape = 'square';
preferDisc = true;
} else if (viewQuerySettings.imageType === 'logo') {
shape = 'backdrop';
preferLogo = true;
} else if (viewQuerySettings.imageType === 'thumb') {
shape = 'backdrop';
preferThumb = true;
} else {
shape = 'autoVertical';
}
const cardOptions: CardOptions = {
shape: shape,
showTitle: viewQuerySettings.showTitle,
showYear: viewQuerySettings.showYear,
cardLayout: viewQuerySettings.cardLayout,
centerText: true,
context: getContext(),
coverImage: true,
preferThumb: preferThumb,
preferDisc: preferDisc,
preferLogo: preferLogo,
overlayPlayButton: false,
overlayMoreButton: true,
overlayText: !viewQuerySettings.showTitle
};
cardOptions.items = itemsResult.Items || [];
return cardOptions;
}, [
getContext,
itemsResult.Items,
viewQuerySettings.cardLayout,
viewQuerySettings.imageType,
viewQuerySettings.showTitle,
viewQuerySettings.showYear
]);
const getItemsHtml = useCallback(() => {
let html = '';
if (viewQuerySettings.imageType === 'list') {
html = listview.getListViewHtml({
items: itemsResult.Items || [],
context: getContext()
});
} else {
html = cardBuilder.getCardsHtml(itemsResult.Items || [], getCardOptions());
}
if (!itemsResult.Items?.length) {
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate(getNoItemsMessage()) + '</p>';
html += '</div>';
}
return html;
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
const getQuery = useCallback(() => {
const queryFilters = getFilters(viewQuerySettings);
let queryIsHD;
if (viewQuerySettings.IsHD) {
queryIsHD = true;
}
if (viewQuerySettings.IsSD) {
queryIsHD = false;
}
return {
SortBy: viewQuerySettings.SortBy,
SortOrder: viewQuerySettings.SortOrder,
IncludeItemTypes: getItemTypes().join(','),
Recursive: true,
Fields: getFields(viewQuerySettings),
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
Limit: userSettings.libraryPageSize(undefined) || undefined,
IsFavorite: getBasekey() === 'favorites' ? true : null,
VideoTypes: viewQuerySettings.VideoTypes,
GenreIds: viewQuerySettings.GenreIds,
Is4K: viewQuerySettings.Is4K ? true : null,
IsHD: queryIsHD,
Is3D: viewQuerySettings.Is3D ? true : null,
HasSubtitles: viewQuerySettings.HasSubtitles ? true : null,
HasTrailer: viewQuerySettings.HasTrailer ? true : null,
HasSpecialFeature: viewQuerySettings.HasSpecialFeature ? true : null,
HasThemeSong: viewQuerySettings.HasThemeSong ? true : null,
HasThemeVideo: viewQuerySettings.HasThemeVideo ? true : null,
Filters: queryFilters.length ? queryFilters.join(',') : null,
StartIndex: viewQuerySettings.StartIndex,
NameLessThan: viewQuerySettings.NameLessThan,
NameStartsWith: viewQuerySettings.NameStartsWith,
ParentId: topParentId
};
}, [
viewQuerySettings,
getItemTypes,
getBasekey,
topParentId
]);
const fetchData = useCallback(() => {
loading.show();
const apiClient = ServerConnections.getApiClient(window.ApiClient.serverId());
return apiClient.getItems(
apiClient.getCurrentUserId(),
{
...getQuery()
}
);
}, [getQuery]);
const reloadItems = useCallback(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setisLoading(false);
fetchData().then((result) => {
setItemsResult(result);
window.scrollTo(0, 0);
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
}).catch(err => {
console.error('[ViewItemsContainer] failed to load autofocuser', err);
});
loading.hide();
setisLoading(true);
}).catch(err => {
console.error('[ViewItemsContainer] failed to fetch data', err);
});
}, [fetchData]);
useEffect(() => {
reloadItems();
}, [reloadItems]);
return (
<div ref={element}>
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
itemsResult= {itemsResult}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
{isBtnShuffleEnabled && <Shuffle itemsResult={itemsResult} topParentId={topParentId} />}
<SelectView
getVisibleViewSettings={getVisibleViewSettings}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
<Sort
getSortMenuOptions={getSortMenuOptions}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
{isBtnFilterEnabled && <Filter
topParentId={topParentId}
getItemTypes={getItemTypes}
getVisibleFilters={getVisibleFilters}
getFilterMenuOptions={getFilterMenuOptions}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>}
{isBtnNewCollectionEnabled && <NewCollection />}
</div>
{isAlphaPickerEnabled && <AlphaPickerContainer
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>}
{isLoading && <ItemsContainer
viewQuerySettings={viewQuerySettings}
getItemsHtml={getItemsHtml}
/>}
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
itemsResult= {itemsResult}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
</div>
</div>
);
};
export default ViewItemsContainer;

View file

@ -103,37 +103,29 @@ function onInputCommand(e) {
}
}
function saveValues(context, settings, settingsKey, setfilters) {
let elems = context.querySelectorAll('.simpleFilter');
// Video type
const videoTypes = [];
elems = context.querySelectorAll('.chkVideoTypeFilter');
for (let i = 0, length = elems.length; i < length; i++) {
if (elems[i].checked) {
videoTypes.push(elems[i].getAttribute('data-filter'));
context.querySelectorAll('.chkVideoTypeFilter').forEach(elem => {
if (elem.checked) {
videoTypes.push(elem.getAttribute('data-filter'));
}
}
});
// Series status
const seriesStatuses = [];
elems = context.querySelectorAll('.chkSeriesStatus');
for (let i = 0, length = elems.length; i < length; i++) {
if (elems[i].checked) {
seriesStatuses.push(elems[i].getAttribute('data-filter'));
context.querySelectorAll('.chkSeriesStatus').forEach(elem => {
if (elem.checked) {
seriesStatuses.push(elem.getAttribute('data-filter'));
}
}
});
// Genres
const genres = [];
elems = context.querySelectorAll('.chkGenreFilter');
for (let i = 0, length = elems.length; i < length; i++) {
if (elems[i].checked) {
genres.push(elems[i].getAttribute('data-filter'));
context.querySelectorAll('.chkGenreFilter').forEach(elem => {
if (elem.checked) {
genres.push(elem.getAttribute('data-filter'));
}
}
});
if (setfilters) {
setfilters((prevState) => ({
@ -157,13 +149,13 @@ function saveValues(context, settings, settingsKey, setfilters) {
GenreIds: genres.join(',')
}));
} else {
for (let i = 0, length = elems.length; i < length; i++) {
if (elems[i].tagName === 'INPUT') {
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i]);
context.querySelectorAll('.simpleFilter').forEach(elem => {
if (elem.tagName === 'INPUT') {
setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem);
} else {
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input'));
setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem.querySelector('input'));
}
}
});
userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(','));
}

View file

@ -1,15 +1,15 @@
import escapeHtml from 'escape-html';
import layoutManager from 'components/layoutManager';
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 +19,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) {
@ -52,6 +34,18 @@ function getAllSectionsToShow(userSettings, sectionCount) {
sections.push(section);
}
// Ensure libraries are visible in TV layout
if (
layoutManager.tv
&& !sections.includes(HomeSectionType.SmallLibraryTiles)
&& !sections.includes(HomeSectionType.LibraryButtons)
) {
return [
HomeSectionType.SmallLibraryTiles,
...sections
];
}
return sections;
}
@ -60,8 +54,10 @@ export function loadSections(elem, apiClient, user, userSettings) {
let html = '';
if (userViews.length) {
const sectionCount = 7;
for (let i = 0; i < sectionCount; i++) {
const userSectionCount = 7;
// TV layout can have an extra section to ensure libraries are visible
const totalSectionCount = layoutManager.tv ? userSectionCount + 1 : userSectionCount;
for (let i = 0; i < totalSectionCount; i++) {
html += '<div class="verticalSection section' + i + '"></div>';
}
@ -69,7 +65,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
elem.classList.add('homeSectionsContainer');
const promises = [];
const sections = getAllSectionsToShow(userSettings, sectionCount);
const sections = getAllSectionsToShow(userSettings, userSectionCount);
for (let i = 0; i < sections.length; i++) {
promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i));
}
@ -138,29 +134,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 +177,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
};

View 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;
}

View 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);
}

View 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);
}

View 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);
}
});
}

View 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;
}

View file

@ -0,0 +1,158 @@
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 ?? [];
userViews.forEach(item => {
if (!item.Id || userExcludeItems.indexOf(item.Id) !== -1) {
return;
}
if (!item.CollectionType || excludeViewTypes.indexOf(item.CollectionType) !== -1) {
return;
}
const frag = document.createElement('div');
frag.classList.add('verticalSection');
frag.classList.add('hide');
elem.appendChild(frag);
renderLatestSection(frag, apiClient, user, item, options);
});
}

View 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;
}

View file

@ -0,0 +1,12 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-query-result';
export interface SectionOptions {
enableOverflow: boolean
}
export type SectionContainerElement = {
fetchData: () => Promise<BaseItemDtoQueryResult | BaseItemDto[]>
getItemsHtml: (items: BaseItemDto[]) => void
parentContainer: HTMLElement
} & Element;

View file

@ -1848,6 +1848,56 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Audio'
});
} else if (firstItem.Type === 'Series' || firstItem.Type === 'Season') {
const apiClient = ServerConnections.getApiClient(firstItem.ServerId);
promise = apiClient.getEpisodes(firstItem.SeriesId || firstItem.Id, {
IsVirtualUnaired: false,
IsMissing: false,
UserId: apiClient.getCurrentUserId(),
Fields: 'Chapters'
}).then(function (episodesResult) {
const originalResults = episodesResult.Items;
const isSeries = firstItem.Type === 'Series';
let foundItem = false;
episodesResult.Items = episodesResult.Items.filter(function (e) {
if (foundItem) {
return true;
}
if (!e.UserData.Played && (isSeries || e.SeasonId === firstItem.Id)) {
foundItem = true;
return true;
}
return false;
});
if (episodesResult.Items.length === 0) {
if (isSeries) {
episodesResult.Items = originalResults;
} else {
episodesResult.Items = originalResults.filter(function (e) {
if (foundItem) {
return true;
}
if (e.SeasonId === firstItem.Id) {
foundItem = true;
return true;
}
return false;
});
}
}
episodesResult.TotalRecordCount = episodesResult.Items.length;
return episodesResult;
});
} else if (firstItem.IsFolder && firstItem.CollectionType === 'homevideos') {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
ParentId: firstItem.Id,

View file

@ -51,7 +51,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const getDefaultParameters = useCallback(() => ({
ParentId: parentId,
searchTerm: query,
Limit: 24,
Limit: 100,
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Recursive: true,
EnableTotalRecordCount: false,