';
- }
-
- 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 += '
';
- html += `
';
-
- elem.classList.add('hide');
- elem.innerHTML = html;
-
- const itemsContainer = elem.querySelector('.itemsContainer');
- itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId());
- itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType);
- itemsContainer.parentContainer = elem;
-}
-
-function getItemsToResumeFn(mediaType, serverId) {
- return function () {
- const apiClient = ServerConnections.getApiClient(serverId);
-
- const limit = enableScrollX() ? 12 : 5;
-
- const options = {
- Limit: limit,
- Recursive: true,
- Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
- ImageTypeLimit: 1,
- EnableImageTypes: 'Primary,Backdrop,Thumb',
- EnableTotalRecordCount: false,
- MediaTypes: mediaType
- };
-
- return apiClient.getResumableItems(apiClient.getCurrentUserId(), options);
- };
-}
-
-function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
- return function (items) {
- const cardLayout = false;
- return cardBuilder.getCardsHtml({
- items: items,
- preferThumb: true,
- inheritThumb: !useEpisodeImages,
- shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(),
- 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: getThumbShape(),
- lines: 3,
- overlayPlayButton: true
- });
-}
-
-function loadOnNow(elem, apiClient, user) {
- if (!user.Policy.EnableLiveTvAccess) {
- return Promise.resolve();
- }
-
- return apiClient.getLiveTvRecommendedPrograms({
- userId: apiClient.getCurrentUserId(),
- IsAiring: true,
- limit: 1,
- ImageTypeLimit: 1,
- EnableImageTypes: 'Primary,Thumb,Backdrop',
- EnableTotalRecordCount: false,
- Fields: 'ChannelInfo,PrimaryImageAspectRatio'
- }).then(function (result) {
- let html = '';
- if (result.Items.length) {
- elem.classList.remove('padded-left');
- elem.classList.remove('padded-right');
- elem.classList.remove('padded-bottom');
- elem.classList.remove('verticalSection');
-
- html += '
';
- html += '
';
- html += '
' + globalize.translate('LiveTV') + '
';
- html += '';
-
- if (enableScrollX()) {
- html += '
';
- html += '
';
- }
- html += '
';
- html += '
';
-
- html += '
';
- html += '
';
-
- if (enableScrollX()) {
- html += '
';
- html += '
';
- html += '
';
-
- elem.innerHTML = html;
-
- const itemsContainer = elem.querySelector('.itemsContainer');
- itemsContainer.parentContainer = elem;
- itemsContainer.fetchData = getOnNowFetchFn(apiClient.serverId());
- itemsContainer.getItemsHtml = getOnNowItemsHtml;
- }
- });
-}
-
-function getNextUpFetchFn(serverId, userSettings) {
- return function () {
- const apiClient = ServerConnections.getApiClient(serverId);
- const oldestDateForNextUp = new Date();
- oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
- return apiClient.getNextUpEpisodes({
- Limit: enableScrollX() ? 24 : 15,
- Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
- UserId: apiClient.getCurrentUserId(),
- ImageTypeLimit: 1,
- EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
- EnableTotalRecordCount: false,
- DisableFirstEpisode: false,
- NextUpDateCutoff: oldestDateForNextUp.toISOString(),
- EnableRewatching: userSettings.enableRewatchingInNextUp()
- });
- };
-}
-
-function getNextUpItemsHtmlFn(useEpisodeImages) {
- return function (items) {
- const cardLayout = false;
- return cardBuilder.getCardsHtml({
- items: items,
- preferThumb: true,
- inheritThumb: !useEpisodeImages,
- shape: getThumbShape(),
- overlayText: false,
- showTitle: true,
- showParentTitle: true,
- lazy: true,
- overlayPlayButton: true,
- context: 'home',
- centerText: !cardLayout,
- allowBottomPadding: !enableScrollX(),
- cardLayout: cardLayout
- });
- };
-}
-
-function loadNextUp(elem, apiClient, userSettings) {
- let html = '';
-
- html += '
';
-
- if (enableScrollX()) {
- html += '
';
- html += '
';
-
- elem.classList.add('hide');
- elem.innerHTML = html;
-
- const itemsContainer = elem.querySelector('.itemsContainer');
- itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings);
- itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
- itemsContainer.parentContainer = elem;
-}
-
-function getLatestRecordingsFetchFn(serverId, activeRecordingsOnly) {
- return function () {
- const apiClient = ServerConnections.getApiClient(serverId);
- return apiClient.getLiveTvRecordings({
- userId: apiClient.getCurrentUserId(),
- Limit: enableScrollX() ? 12 : 5,
- Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
- EnableTotalRecordCount: false,
- IsLibraryItem: activeRecordingsOnly ? null : false,
- IsInProgress: activeRecordingsOnly ? true : null
- });
- };
-}
-
-function getLatestRecordingItemsHtml(activeRecordingsOnly) {
- return function (items) {
- return cardBuilder.getCardsHtml({
- items: items,
- shape: enableScrollX() ? 'autooverflow' : 'auto',
- showTitle: true,
- showParentTitle: true,
- coverImage: true,
- lazy: true,
- showDetailsMenu: true,
- centerText: true,
- overlayText: false,
- showYear: true,
- lines: 2,
- overlayPlayButton: !activeRecordingsOnly,
- allowBottomPadding: !enableScrollX(),
- preferThumb: true,
- cardLayout: false,
- overlayMoreButton: activeRecordingsOnly,
- action: activeRecordingsOnly ? 'none' : null,
- centerPlayButton: activeRecordingsOnly
- });
- };
-}
-
-function loadLatestLiveTvRecordings(elem, activeRecordingsOnly, apiClient) {
- const title = activeRecordingsOnly ?
- globalize.translate('HeaderActiveRecordings') :
- globalize.translate('HeaderLatestRecordings');
-
- let html = '';
-
- html += '
';
- html += '
' + title + '
';
- html += '';
-
- if (enableScrollX()) {
- html += '
';
- html += '
';
-
- elem.classList.add('hide');
- elem.innerHTML = html;
-
- const itemsContainer = elem.querySelector('.itemsContainer');
- itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly);
- itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly);
- itemsContainer.parentContainer = elem;
-}
-
export default {
- loadLibraryTiles: loadLibraryTiles,
- getDefaultSection: getDefaultSection,
- loadSections: loadSections,
- destroySections: destroySections,
- pause: pause,
- resume: resume
+ getDefaultSection,
+ loadSections,
+ destroySections,
+ pause,
+ resume
};
diff --git a/src/components/homesections/sections/activeRecordings.ts b/src/components/homesections/sections/activeRecordings.ts
new file mode 100644
index 0000000000..8b8129f789
--- /dev/null
+++ b/src/components/homesections/sections/activeRecordings.ts
@@ -0,0 +1,92 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import type { ApiClient } from 'jellyfin-apiclient';
+
+import ServerConnections from 'components/ServerConnections';
+import cardBuilder from 'components/cardbuilder/cardBuilder';
+import globalize from 'scripts/globalize';
+
+import type { SectionContainerElement, SectionOptions } from './section';
+
+function getLatestRecordingsFetchFn(
+ serverId: string,
+ activeRecordingsOnly: boolean,
+ { enableOverflow }: SectionOptions
+) {
+ return function () {
+ const apiClient = ServerConnections.getApiClient(serverId);
+ return apiClient.getLiveTvRecordings({
+ userId: apiClient.getCurrentUserId(),
+ Limit: enableOverflow ? 12 : 5,
+ Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
+ EnableTotalRecordCount: false,
+ IsLibraryItem: activeRecordingsOnly ? null : false,
+ IsInProgress: activeRecordingsOnly ? true : null
+ });
+ };
+}
+
+function getLatestRecordingItemsHtml(
+ activeRecordingsOnly: boolean,
+ { enableOverflow }: SectionOptions
+) {
+ return function (items: BaseItemDto[]) {
+ return cardBuilder.getCardsHtml({
+ items: items,
+ shape: enableOverflow ? 'autooverflow' : 'auto',
+ showTitle: true,
+ showParentTitle: true,
+ coverImage: true,
+ lazy: true,
+ showDetailsMenu: true,
+ centerText: true,
+ overlayText: false,
+ showYear: true,
+ lines: 2,
+ overlayPlayButton: !activeRecordingsOnly,
+ allowBottomPadding: !enableOverflow,
+ preferThumb: true,
+ cardLayout: false,
+ overlayMoreButton: activeRecordingsOnly,
+ action: activeRecordingsOnly ? 'none' : null,
+ centerPlayButton: activeRecordingsOnly
+ });
+ };
+}
+
+export function loadRecordings(
+ elem: HTMLElement,
+ activeRecordingsOnly: boolean,
+ apiClient: ApiClient,
+ options: SectionOptions
+) {
+ const title = activeRecordingsOnly ?
+ globalize.translate('HeaderActiveRecordings') :
+ globalize.translate('HeaderLatestRecordings');
+
+ let html = '';
+
+ html += '
';
+ html += '
' + title + '
';
+ html += '';
+
+ if (options.enableOverflow) {
+ html += '
';
+ html += '
';
+
+ elem.classList.add('hide');
+ elem.innerHTML = html;
+
+ const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
+ if (!itemsContainer) return;
+ itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly, options);
+ itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly, options);
+ itemsContainer.parentContainer = elem;
+}
diff --git a/src/components/homesections/sections/libraryButtons.ts b/src/components/homesections/sections/libraryButtons.ts
new file mode 100644
index 0000000000..06656c343a
--- /dev/null
+++ b/src/components/homesections/sections/libraryButtons.ts
@@ -0,0 +1,36 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import escapeHtml from 'escape-html';
+
+import imageLoader from 'components/images/imageLoader';
+import { appRouter } from 'components/router/appRouter';
+import globalize from 'scripts/globalize';
+import imageHelper from 'scripts/imagehelper';
+
+function getLibraryButtonsHtml(items: BaseItemDto[]) {
+ let html = '';
+
+ html += '';
+
+ return html;
+}
+
+export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) {
+ elem.classList.remove('verticalSection');
+ const html = getLibraryButtonsHtml(userViews);
+
+ elem.innerHTML = html;
+ imageLoader.lazyChildren(elem);
+}
diff --git a/src/components/homesections/sections/libraryTiles.ts b/src/components/homesections/sections/libraryTiles.ts
new file mode 100644
index 0000000000..6ccfc528c5
--- /dev/null
+++ b/src/components/homesections/sections/libraryTiles.ts
@@ -0,0 +1,46 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+
+import cardBuilder from 'components/cardbuilder/cardBuilder';
+import imageLoader from 'components/images/imageLoader';
+import globalize from 'scripts/globalize';
+import { getBackdropShape } from 'utils/card';
+
+import type { SectionOptions } from './section';
+
+export function loadLibraryTiles(
+ elem: HTMLElement,
+ userViews: BaseItemDto[],
+ {
+ enableOverflow
+ }: SectionOptions
+) {
+ let html = '';
+ if (userViews.length) {
+ html += '
' + globalize.translate('HeaderMyMedia') + '
';
+ if (enableOverflow) {
+ html += '
';
+ html += '
';
+ }
+
+ elem.innerHTML = html;
+ imageLoader.lazyChildren(elem);
+}
diff --git a/src/components/homesections/sections/liveTv.ts b/src/components/homesections/sections/liveTv.ts
new file mode 100644
index 0000000000..7c8606c12b
--- /dev/null
+++ b/src/components/homesections/sections/liveTv.ts
@@ -0,0 +1,181 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
+import type { ApiClient } from 'jellyfin-apiclient';
+
+import { appRouter } from 'components/router/appRouter';
+import cardBuilder from 'components/cardbuilder/cardBuilder';
+import layoutManager from 'components/layoutManager';
+import ServerConnections from 'components/ServerConnections';
+import globalize from 'scripts/globalize';
+import { getBackdropShape } from 'utils/card';
+
+import type { SectionContainerElement, SectionOptions } from './section';
+
+function getOnNowFetchFn(
+ serverId: string
+) {
+ return function () {
+ const apiClient = ServerConnections.getApiClient(serverId);
+ return apiClient.getLiveTvRecommendedPrograms({
+ userId: apiClient.getCurrentUserId(),
+ IsAiring: true,
+ limit: 24,
+ ImageTypeLimit: 1,
+ EnableImageTypes: 'Primary,Thumb,Backdrop',
+ EnableTotalRecordCount: false,
+ Fields: 'ChannelInfo,PrimaryImageAspectRatio'
+ });
+ };
+}
+
+function getOnNowItemsHtmlFn(
+ { enableOverflow }: SectionOptions
+) {
+ return (items: BaseItemDto[]) => (
+ cardBuilder.getCardsHtml({
+ items: items,
+ preferThumb: 'auto',
+ inheritThumb: false,
+ shape: (enableOverflow ? 'autooverflow' : 'auto'),
+ showParentTitleOrTitle: true,
+ showTitle: true,
+ centerText: true,
+ coverImage: true,
+ overlayText: false,
+ allowBottomPadding: !enableOverflow,
+ showAirTime: true,
+ showChannelName: false,
+ showAirDateTime: false,
+ showAirEndTime: true,
+ defaultShape: getBackdropShape(enableOverflow),
+ lines: 3,
+ overlayPlayButton: true
+ })
+ );
+}
+
+function buildSection(
+ elem: HTMLElement,
+ serverId: string,
+ options: SectionOptions
+) {
+ let html = '';
+
+ elem.classList.remove('padded-left');
+ elem.classList.remove('padded-right');
+ elem.classList.remove('padded-bottom');
+ elem.classList.remove('verticalSection');
+
+ html += '
';
+ html += '
';
+ html += '
' + globalize.translate('LiveTV') + '
';
+ html += '';
+
+ if (options.enableOverflow) {
+ html += '
';
+ html += '
';
+ }
+ html += '
';
+ html += '
';
+
+ html += '
';
+ html += '
';
+
+ if (options.enableOverflow) {
+ html += '
';
+ html += '
';
+ html += '
';
+
+ elem.innerHTML = html;
+
+ const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
+ if (!itemsContainer) return;
+ itemsContainer.parentContainer = elem;
+ itemsContainer.fetchData = getOnNowFetchFn(serverId);
+ itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options);
+}
+
+export function loadLiveTV(
+ elem: HTMLElement,
+ apiClient: ApiClient,
+ user: UserDto,
+ options: SectionOptions
+) {
+ if (!user.Policy?.EnableLiveTvAccess) {
+ return Promise.resolve();
+ }
+
+ return apiClient.getLiveTvRecommendedPrograms({
+ userId: apiClient.getCurrentUserId(),
+ IsAiring: true,
+ limit: 1,
+ ImageTypeLimit: 1,
+ EnableImageTypes: 'Primary,Thumb,Backdrop',
+ EnableTotalRecordCount: false,
+ Fields: 'ChannelInfo,PrimaryImageAspectRatio'
+ }).then(function (result) {
+ if (result.Items?.length) {
+ buildSection(elem, apiClient.serverId(), options);
+ }
+ });
+}
diff --git a/src/components/homesections/sections/nextUp.ts b/src/components/homesections/sections/nextUp.ts
new file mode 100644
index 0000000000..4ab401ab86
--- /dev/null
+++ b/src/components/homesections/sections/nextUp.ts
@@ -0,0 +1,106 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import type { ApiClient } from 'jellyfin-apiclient';
+
+import cardBuilder from 'components/cardbuilder/cardBuilder';
+import layoutManager from 'components/layoutManager';
+import { appRouter } from 'components/router/appRouter';
+import ServerConnections from 'components/ServerConnections';
+import globalize from 'scripts/globalize';
+import type { UserSettings } from 'scripts/settings/userSettings';
+import { getBackdropShape } from 'utils/card';
+
+import type { SectionContainerElement, SectionOptions } from './section';
+
+function getNextUpFetchFn(
+ serverId: string,
+ userSettings: UserSettings,
+ { enableOverflow }: SectionOptions
+) {
+ return function () {
+ const apiClient = ServerConnections.getApiClient(serverId);
+ const oldestDateForNextUp = new Date();
+ oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
+ return apiClient.getNextUpEpisodes({
+ Limit: enableOverflow ? 24 : 15,
+ Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
+ UserId: apiClient.getCurrentUserId(),
+ ImageTypeLimit: 1,
+ EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
+ EnableTotalRecordCount: false,
+ DisableFirstEpisode: false,
+ NextUpDateCutoff: oldestDateForNextUp.toISOString(),
+ EnableRewatching: userSettings.enableRewatchingInNextUp()
+ });
+ };
+}
+
+function getNextUpItemsHtmlFn(
+ useEpisodeImages: boolean,
+ { enableOverflow }: SectionOptions
+) {
+ return function (items: BaseItemDto[]) {
+ const cardLayout = false;
+ return cardBuilder.getCardsHtml({
+ items: items,
+ preferThumb: true,
+ inheritThumb: !useEpisodeImages,
+ shape: getBackdropShape(enableOverflow),
+ overlayText: false,
+ showTitle: true,
+ showParentTitle: true,
+ lazy: true,
+ overlayPlayButton: true,
+ context: 'home',
+ centerText: !cardLayout,
+ allowBottomPadding: !enableOverflow,
+ cardLayout: cardLayout
+ });
+ };
+}
+
+export function loadNextUp(
+ elem: HTMLElement,
+ apiClient: ApiClient,
+ userSettings: UserSettings,
+ options: SectionOptions
+) {
+ let html = '';
+
+ html += '
';
+
+ if (options.enableOverflow) {
+ html += '
';
+ html += '
';
+
+ elem.classList.add('hide');
+ elem.innerHTML = html;
+
+ const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
+ if (!itemsContainer) return;
+ itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options);
+ itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options);
+ itemsContainer.parentContainer = elem;
+}
diff --git a/src/components/homesections/sections/recentlyAdded.ts b/src/components/homesections/sections/recentlyAdded.ts
new file mode 100644
index 0000000000..3c46ecbbe2
--- /dev/null
+++ b/src/components/homesections/sections/recentlyAdded.ts
@@ -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 += '
';
+
+ if (options.enableOverflow) {
+ html += '
';
+ html += '
';
+
+ elem.innerHTML = html;
+
+ const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
+ if (!itemsContainer) return;
+ itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options);
+ itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options);
+ itemsContainer.parentContainer = elem;
+}
+
+export function loadRecentlyAdded(
+ elem: HTMLElement,
+ apiClient: ApiClient,
+ user: UserDto,
+ userViews: BaseItemDto[],
+ options: SectionOptions
+) {
+ elem.classList.remove('verticalSection');
+ const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels'];
+ const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? [];
+
+ 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);
+ });
+}
diff --git a/src/components/homesections/sections/resume.ts b/src/components/homesections/sections/resume.ts
new file mode 100644
index 0000000000..e96dde3ee0
--- /dev/null
+++ b/src/components/homesections/sections/resume.ts
@@ -0,0 +1,105 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
+import type { ApiClient } from 'jellyfin-apiclient';
+
+import ServerConnections from 'components/ServerConnections';
+import cardBuilder from 'components/cardbuilder/cardBuilder';
+import globalize from 'scripts/globalize';
+import type { UserSettings } from 'scripts/settings/userSettings';
+import { getBackdropShape, getPortraitShape } from 'utils/card';
+
+import type { SectionContainerElement, SectionOptions } from './section';
+
+const dataMonitorHints: Record
= {
+ Audio: 'audioplayback,markplayed',
+ Video: 'videoplayback,markplayed'
+};
+
+function getItemsToResumeFn(
+ mediaType: BaseItemKind,
+ serverId: string,
+ { enableOverflow }: SectionOptions
+) {
+ return function () {
+ const apiClient = ServerConnections.getApiClient(serverId);
+
+ const limit = enableOverflow ? 12 : 5;
+
+ const options = {
+ Limit: limit,
+ Recursive: true,
+ Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
+ ImageTypeLimit: 1,
+ EnableImageTypes: 'Primary,Backdrop,Thumb',
+ EnableTotalRecordCount: false,
+ MediaTypes: mediaType
+ };
+
+ return apiClient.getResumableItems(apiClient.getCurrentUserId(), options);
+ };
+}
+
+function getItemsToResumeHtmlFn(
+ useEpisodeImages: boolean,
+ mediaType: BaseItemKind,
+ { enableOverflow }: SectionOptions
+) {
+ return function (items: BaseItemDto[]) {
+ const cardLayout = false;
+ return cardBuilder.getCardsHtml({
+ items: items,
+ preferThumb: true,
+ inheritThumb: !useEpisodeImages,
+ shape: (mediaType === 'Book') ?
+ getPortraitShape(enableOverflow) :
+ getBackdropShape(enableOverflow),
+ overlayText: false,
+ showTitle: true,
+ showParentTitle: true,
+ lazy: true,
+ showDetailsMenu: true,
+ overlayPlayButton: true,
+ context: 'home',
+ centerText: !cardLayout,
+ allowBottomPadding: false,
+ cardLayout: cardLayout,
+ showYear: true,
+ lines: 2
+ });
+ };
+}
+
+export function loadResume(
+ elem: HTMLElement,
+ apiClient: ApiClient,
+ titleLabel: string,
+ mediaType: BaseItemKind,
+ userSettings: UserSettings,
+ options: SectionOptions
+) {
+ let html = '';
+
+ const dataMonitor = dataMonitorHints[mediaType] ?? 'markplayed';
+
+ html += '' + globalize.translate(titleLabel) + '
';
+ if (options.enableOverflow) {
+ html += '';
+ html += `
';
+
+ elem.classList.add('hide');
+ elem.innerHTML = html;
+
+ const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
+ if (!itemsContainer) return;
+ itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options);
+ itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options);
+ itemsContainer.parentContainer = elem;
+}
diff --git a/src/components/homesections/sections/section.d.ts b/src/components/homesections/sections/section.d.ts
new file mode 100644
index 0000000000..9307ba7c13
--- /dev/null
+++ b/src/components/homesections/sections/section.d.ts
@@ -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
+ getItemsHtml: (items: BaseItemDto[]) => void
+ parentContainer: HTMLElement
+} & Element;
diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js
index 0ec982f15a..0416ce4716 100644
--- a/src/components/itemContextMenu.js
+++ b/src/components/itemContextMenu.js
@@ -444,7 +444,7 @@ function executeCommand(item, id, options) {
});
break;
case 'multiSelect':
- import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
+ import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
const card = dom.parentWithClass(options.positionTo, 'card');
startMultiSelect(card);
});
diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js
index 253fdee922..c56d7fc6be 100644
--- a/src/components/router/appRouter.js
+++ b/src/components/router/appRouter.js
@@ -527,7 +527,7 @@ class AppRouter {
}
if (item === 'manageserver') {
- return '#/dashboard.html';
+ return '#/dashboard';
}
if (item === 'recordedtv') {
diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx
index f8e5a12fbd..ebb5240b28 100644
--- a/src/components/search/SearchResults.tsx
+++ b/src/components/search/SearchResults.tsx
@@ -49,7 +49,7 @@ const SearchResults: FunctionComponent = ({ serverId = windo
const getDefaultParameters = useCallback(() => ({
ParentId: parentId,
searchTerm: query,
- Limit: 24,
+ Limit: 100,
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Recursive: true,
EnableTotalRecordCount: false,
diff --git a/src/components/toolbar/AppUserMenu.tsx b/src/components/toolbar/AppUserMenu.tsx
index 7119a4f504..634d02cf88 100644
--- a/src/components/toolbar/AppUserMenu.tsx
+++ b/src/components/toolbar/AppUserMenu.tsx
@@ -115,7 +115,7 @@ const AppUserMenu: FC = ({