diff --git a/src/components/collectionEditor/collectionEditor.js b/src/components/collectionEditor/collectionEditor.js index 946ddfebbb..8af78fede2 100644 --- a/src/components/collectionEditor/collectionEditor.js +++ b/src/components/collectionEditor/collectionEditor.js @@ -206,8 +206,8 @@ import toast from '../toast/toast'; }); } - export class showEditor { - constructor(options) { + class CollectionEditor { + show(options) { const items = options.items || {}; currentServerId = options.serverId; @@ -266,4 +266,4 @@ import toast from '../toast/toast'; } /* eslint-enable indent */ -export default showEditor; +export default CollectionEditor; diff --git a/src/components/common/AlphaPickerContainer.tsx b/src/components/common/AlphaPickerContainer.tsx new file mode 100644 index 0000000000..a2d647a4d9 --- /dev/null +++ b/src/components/common/AlphaPickerContainer.tsx @@ -0,0 +1,53 @@ +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>; +} + +const AlphaPickerContainer: FC = ({ viewQuerySettings, setViewQuerySettings }) => { + const [ alphaPicker, setAlphaPicker ] = useState(); + const element = useRef(null); + + alphaPicker?.updateControls(viewQuerySettings); + + const onAlphaPickerChange = useCallback((e) => { + const newValue = (e as CustomEvent).detail.value; + let updatedValue: React.SetStateAction; + if (newValue === '#') { + updatedValue = {NameLessThan: 'A'}; + } else { + updatedValue = {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 ( +
+ ); +}; + +export default AlphaPickerContainer; diff --git a/src/components/common/Filter.tsx b/src/components/common/Filter.tsx new file mode 100644 index 0000000000..4db46e332b --- /dev/null +++ b/src/components/common/Filter.tsx @@ -0,0 +1,61 @@ +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; + getVisibleFilters: () => string[]; + viewQuerySettings: ViewQuerySettings; + setViewQuerySettings: React.Dispatch>; +} + +const Filter: FC = ({ + topParentId, + getItemTypes, + getVisibleFilters, + getFilterMenuOptions, + viewQuerySettings, + setViewQuerySettings +}) => { + const element = useRef(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 + }); + }); + }, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]); + + useEffect(() => { + const btnFilter = element.current?.querySelector('.btnFilter'); + + btnFilter?.addEventListener('click', showFilterMenu); + + return () => { + btnFilter?.removeEventListener('click', showFilterMenu); + }; + }, [showFilterMenu]); + + return ( +
+ +
+ ); +}; + +export default Filter; diff --git a/src/components/common/GenresItemsContainer.tsx b/src/components/common/GenresItemsContainer.tsx new file mode 100644 index 0000000000..e4098d3e77 --- /dev/null +++ b/src/components/common/GenresItemsContainer.tsx @@ -0,0 +1,126 @@ +import '../../elements/emby-button/emby-button'; +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; +import escapeHTML from 'escape-html'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; + +import { appRouter } from '../appRouter'; +import cardBuilder from '../cardbuilder/cardBuilder'; +import layoutManager from '../layoutManager'; +import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; +import globalize from '../../scripts/globalize'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; + +const createLinkElement = ({ className, title, href }: { className?: string, title?: string | null, href?: string }) => ({ + __html: ` +

+ ${title} +

+ +
` +}); + +interface GenresItemsContainerProps { + topParentId?: string | null; + itemsResult: BaseItemDtoQueryResult; +} + +const GenresItemsContainer: FC = ({ + topParentId, + itemsResult = {} +}) => { + const element = useRef(null); + + const enableScrollX = useCallback(() => { + return !layoutManager.desktop; + }, []); + + const getPortraitShape = useCallback(() => { + return enableScrollX() ? 'overflowPortrait' : 'portrait'; + }, [enableScrollX]); + + const fillItemsContainer = useCallback((entry) => { + const elem = entry.target; + const id = elem.getAttribute('data-id'); + + const query = { + SortBy: 'Random', + SortOrder: 'Ascending', + IncludeItemTypes: 'Movie', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary', + Limit: 12, + GenreIds: id, + EnableTotalRecordCount: false, + ParentId: topParentId + }; + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + cardBuilder.buildCards(result.Items || [], { + itemsContainer: elem, + shape: getPortraitShape(), + scalable: true, + overlayMoreButton: true, + allowBottomPadding: true, + showTitle: true, + centerText: true, + showYear: true + }); + }); + }, [getPortraitShape, topParentId]); + + useEffect(() => { + const elem = element.current; + lazyLoader.lazyChildren(elem, fillItemsContainer); + }, [itemsResult.Items, fillItemsContainer]); + + const items = itemsResult.Items || []; + return ( +
+ { + !items.length ? ( +
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoGenresAvailable')}

+
+ ) : items.map((item, index) => ( +
+
+ + {enableScrollX() ? + : + } +
+ )) + } +
+ ); +}; + +export default GenresItemsContainer; diff --git a/src/components/common/ItemsContainer.tsx b/src/components/common/ItemsContainer.tsx new file mode 100644 index 0000000000..6289c1d811 --- /dev/null +++ b/src/components/common/ItemsContainer.tsx @@ -0,0 +1,33 @@ +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 = ({ viewQuerySettings, getItemsHtml }) => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default ItemsContainer; diff --git a/src/components/common/NewCollection.tsx b/src/components/common/NewCollection.tsx new file mode 100644 index 0000000000..4cf6e07365 --- /dev/null +++ b/src/components/common/NewCollection.tsx @@ -0,0 +1,38 @@ +import React, { FC, useCallback, useEffect, useRef } from 'react'; + +import IconButtonElement from '../../elements/IconButtonElement'; + +const NewCollection: FC = () => { + const element = useRef(null); + + const showCollectionEditor = useCallback(() => { + import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { + const serverId = window.ApiClient.serverId(); + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ + items: [], + serverId: serverId + }); + }); + }, []); + + useEffect(() => { + const btnNewCollection = element.current?.querySelector('.btnNewCollection'); + if (btnNewCollection) { + btnNewCollection.addEventListener('click', showCollectionEditor); + } + }, [showCollectionEditor]); + + return ( +
+ +
+ ); +}; + +export default NewCollection; diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx new file mode 100644 index 0000000000..02775f44cc --- /dev/null +++ b/src/components/common/Pagination.tsx @@ -0,0 +1,96 @@ +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>; + itemsResult?: BaseItemDtoQueryResult; +} + +const Pagination: FC = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => { + const limit = userSettings.libraryPageSize(undefined); + const totalRecordCount = itemsResult.TotalRecordCount || 0; + const startIndex = viewQuerySettings.StartIndex || 0; + const recordsEnd = Math.min(startIndex + limit, totalRecordCount); + const showControls = limit < totalRecordCount; + const element = useRef(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 ( +
+
+ {showControls && ( +
+ + + {globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount)} + + + + +
+ )} +
+
+ ); +}; + +export default Pagination; diff --git a/src/components/common/RecommendationContainer.tsx b/src/components/common/RecommendationContainer.tsx new file mode 100644 index 0000000000..6c28272c9f --- /dev/null +++ b/src/components/common/RecommendationContainer.tsx @@ -0,0 +1,48 @@ +import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC } from 'react'; + +import globalize from '../../scripts/globalize'; +import escapeHTML from 'escape-html'; +import SectionContainer from './SectionContainer'; + +interface RecommendationContainerProps { + getPortraitShape: () => string; + enableScrollX: () => boolean; + recommendation?: RecommendationDto; +} + +const RecommendationContainer: FC = ({ getPortraitShape, enableScrollX, recommendation = {} }) => { + let title = ''; + + switch (recommendation.RecommendationType) { + case 'SimilarToRecentlyPlayed': + title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName); + break; + + case 'SimilarToLikedItem': + title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName); + break; + + case 'HasDirectorFromRecentlyPlayed': + case 'HasLikedDirector': + title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName); + break; + + case 'HasActorFromRecentlyPlayed': + case 'HasLikedActor': + title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName); + break; + } + + return ; +}; + +export default RecommendationContainer; diff --git a/src/components/common/SectionContainer.tsx b/src/components/common/SectionContainer.tsx new file mode 100644 index 0000000000..13c29ee61e --- /dev/null +++ b/src/components/common/SectionContainer.tsx @@ -0,0 +1,62 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useEffect, useRef } from 'react'; + +import cardBuilder from '../cardbuilder/cardBuilder'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; +import { CardOptions } from '../../types/interface'; + +interface SectionContainerProps { + sectionTitle: string; + enableScrollX: () => boolean; + items?: BaseItemDto[]; + cardOptions?: CardOptions; +} + +const SectionContainer: FC = ({ + sectionTitle, + enableScrollX, + items = [], + cardOptions = {} +}) => { + const element = useRef(null); + + useEffect(() => { + cardBuilder.buildCards(items, { + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current?.querySelector('.verticalSection'), + scalable: true, + overlayPlayButton: true, + showTitle: true, + centerText: true, + cardLayout: false, + ...cardOptions + }); + }, [cardOptions, enableScrollX, items]); + + return ( +
+
+
+

+ {sectionTitle} +

+
+ + {enableScrollX() ? : } + +
+
+ ); +}; + +export default SectionContainer; diff --git a/src/components/common/SelectView.tsx b/src/components/common/SelectView.tsx new file mode 100644 index 0000000000..7a65e48872 --- /dev/null +++ b/src/components/common/SelectView.tsx @@ -0,0 +1,50 @@ +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>; +} + +const SelectView: FC = ({ + getVisibleViewSettings, + viewQuerySettings, + setViewQuerySettings +}) => { + const element = useRef(null); + + const showViewSettingsMenu = useCallback(() => { + import('../viewSettings/viewSettings').then(({default: ViewSettings}) => { + const viewsettings = new ViewSettings(); + viewsettings.show({ + settings: viewQuerySettings, + visibleSettings: getVisibleViewSettings(), + setviewsettings: setViewQuerySettings + }); + }); + }, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]); + + useEffect(() => { + const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement; + btnSelectView?.addEventListener('click', showViewSettingsMenu); + + return () => { + btnSelectView?.removeEventListener('click', showViewSettingsMenu); + }; + }, [showViewSettingsMenu]); + + return ( +
+ +
+ ); +}; + +export default SelectView; diff --git a/src/components/common/Shuffle.tsx b/src/components/common/Shuffle.tsx new file mode 100644 index 0000000000..7d5b06be4b --- /dev/null +++ b/src/components/common/Shuffle.tsx @@ -0,0 +1,43 @@ +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 = ({ itemsResult = {}, topParentId }) => { + const element = useRef(null); + + const shuffle = useCallback(() => { + window.ApiClient.getItem( + window.ApiClient.getCurrentUserId(), + topParentId as string + ).then((item) => { + playbackManager.shuffle(item); + }); + }, [topParentId]); + + useEffect(() => { + const btnShuffle = element.current?.querySelector('.btnShuffle'); + if (btnShuffle) { + btnShuffle.addEventListener('click', shuffle); + } + }, [itemsResult.TotalRecordCount, shuffle]); + + return ( +
+ +
+ ); +}; + +export default Shuffle; diff --git a/src/components/common/Sort.tsx b/src/components/common/Sort.tsx new file mode 100644 index 0000000000..c99b332858 --- /dev/null +++ b/src/components/common/Sort.tsx @@ -0,0 +1,54 @@ +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>; +} + +const Sort: FC = ({ + getSortMenuOptions, + viewQuerySettings, + setViewQuerySettings +}) => { + const element = useRef(null); + + const showSortMenu = useCallback(() => { + import('../sortmenu/sortmenu').then(({default: SortMenu}) => { + const sortMenu = new SortMenu(); + sortMenu.show({ + settings: viewQuerySettings, + sortOptions: getSortMenuOptions(), + setSortValues: setViewQuerySettings + }); + }); + }, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]); + + useEffect(() => { + const btnSort = element.current?.querySelector('.btnSort'); + + btnSort?.addEventListener('click', showSortMenu); + + return () => { + btnSort?.removeEventListener('click', showSortMenu); + }; + }, [showSortMenu]); + + return ( +
+ +
+ ); +}; + +export default Sort; diff --git a/src/components/common/ViewItemsContainer.tsx b/src/components/common/ViewItemsContainer.tsx new file mode 100644 index 0000000000..f5e80b4b40 --- /dev/null +++ b/src/components/common/ViewItemsContainer.tsx @@ -0,0 +1,417 @@ +import type { BaseItemDtoQueryResult } 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 { CardOptions, ViewQuerySettings } from '../../types/interface'; +import ServerConnections from '../ServerConnections'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import listview from '../listview/listview'; +import cardBuilder from '../cardbuilder/cardBuilder'; + +interface ViewItemsContainerProps { + topParentId: string | null; + isBtnShuffleEnabled?: boolean; + isBtnFilterEnabled?: boolean; + isBtnNewCollectionEnabled?: boolean; + isAlphaPickerEnabled?: boolean; + getBasekey: () => string; + getItemTypes: () => string[]; + getNoItemsMessage: () => string; +} + +const getDefaultSortBy = () => { + return 'SortName'; +}; + +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 = ({ + 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 - ${getSettingsKey()}`, + defaultViewQuerySettings + ); + + const [ itemsResult, setItemsResult ] = useState({}); + + const element = useRef(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 += '
'; + html += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate(getNoItemsMessage()) + '

'; + html += '
'; + } + + return html; + }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); + + const getQuery = useCallback(() => { + let fields = 'BasicSyncInfo,MediaSourceCount'; + + if (viewQuerySettings.imageType === 'primary') { + fields += ',PrimaryImageAspectRatio'; + } + + if (viewQuerySettings.showYear) { + fields += ',ProductionYear'; + } + + const queryFilters: string[] = []; + + if (viewQuerySettings.IsPlayed) { + queryFilters.push('IsPlayed'); + } + + if (viewQuerySettings.IsUnplayed) { + queryFilters.push('IsUnplayed'); + } + + if (viewQuerySettings.IsFavorite) { + queryFilters.push('IsFavorite'); + } + + if (viewQuerySettings.IsResumable) { + queryFilters.push('IsResumable'); + } + + 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: fields, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', + Limit: userSettings.libraryPageSize(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.imageType, + viewQuerySettings.showYear, + viewQuerySettings.IsPlayed, + viewQuerySettings.IsUnplayed, + viewQuerySettings.IsFavorite, + viewQuerySettings.IsResumable, + viewQuerySettings.IsHD, + viewQuerySettings.IsSD, + viewQuerySettings.SortBy, + viewQuerySettings.SortOrder, + viewQuerySettings.VideoTypes, + viewQuerySettings.GenreIds, + viewQuerySettings.Is4K, + viewQuerySettings.Is3D, + viewQuerySettings.HasSubtitles, + viewQuerySettings.HasTrailer, + viewQuerySettings.HasSpecialFeature, + viewQuerySettings.HasThemeSong, + viewQuerySettings.HasThemeVideo, + viewQuerySettings.StartIndex, + viewQuerySettings.NameLessThan, + viewQuerySettings.NameStartsWith, + 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); + }); + loading.hide(); + setisLoading(true); + }); + }, [fetchData]); + + useEffect(() => { + reloadItems(); + }, [reloadItems]); + + return ( +
+
+ + + {isBtnShuffleEnabled && } + + + + + + {isBtnFilterEnabled && } + + {isBtnNewCollectionEnabled && } + +
+ + {isAlphaPickerEnabled && } + + {isLoading && } + +
+ +
+
+ ); +}; + +export default ViewItemsContainer; diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js index 12f535a18d..d6b85256a9 100644 --- a/src/components/filtermenu/filtermenu.js +++ b/src/components/filtermenu/filtermenu.js @@ -102,15 +102,8 @@ function onInputCommand(e) { break; } } -function saveValues(context, settings, settingsKey) { +function saveValues(context, settings, settingsKey, setfilters) { let elems = context.querySelectorAll('.simpleFilter'); - 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]); - } else { - setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input')); - } - } // Video type const videoTypes = []; @@ -121,7 +114,6 @@ function saveValues(context, settings, settingsKey) { videoTypes.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(settingsKey + '-filter-VideoTypes', videoTypes.join(',')); // Series status const seriesStatuses = []; @@ -132,7 +124,6 @@ function saveValues(context, settings, settingsKey) { seriesStatuses.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(`${settingsKey}-filter-SeriesStatus`, seriesStatuses.join(',')); // Genres const genres = []; @@ -143,7 +134,39 @@ function saveValues(context, settings, settingsKey) { genres.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(',')); + + if (setfilters) { + setfilters((prevState) => ({ + ...prevState, + StartIndex: 0, + IsPlayed: context.querySelector('.chkPlayed').checked, + IsUnplayed: context.querySelector('.chkUnplayed').checked, + IsFavorite: context.querySelector('.chkFavorite').checked, + IsResumable: context.querySelector('.chkResumable').checked, + Is4K: context.querySelector('.chk4KFilter').checked, + IsHD: context.querySelector('.chkHDFilter').checked, + IsSD: context.querySelector('.chkSDFilter').checked, + Is3D: context.querySelector('.chk3DFilter').checked, + VideoTypes: videoTypes.join(','), + SeriesStatus: seriesStatuses.join(','), + HasSubtitles: context.querySelector('.chkSubtitle').checked, + HasTrailer: context.querySelector('.chkTrailer').checked, + HasSpecialFeature: context.querySelector('.chkSpecialFeature').checked, + HasThemeSong: context.querySelector('.chkThemeSong').checked, + HasThemeVideo: context.querySelector('.chkThemeVideo').checked, + 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]); + } else { + setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input')); + } + } + + userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(',')); + } } function bindCheckboxInput(context, on) { const elems = context.querySelectorAll('.checkboxList-verticalwrap'); @@ -275,7 +298,7 @@ class FilterMenu { if (submitted) { //if (!options.onChange) { - saveValues(dlg, options.settings, options.settingsKey); + saveValues(dlg, options.settings, options.settingsKey, options.setfilters); return resolve(); //} } diff --git a/src/components/filtermenu/filtermenu.template.html b/src/components/filtermenu/filtermenu.template.html index 1a5545baa3..cacc69cf43 100644 --- a/src/components/filtermenu/filtermenu.template.html +++ b/src/components/filtermenu/filtermenu.template.html @@ -5,19 +5,19 @@
@@ -49,22 +49,22 @@
diff --git a/src/controllers/movies/moviecollections.js b/src/controllers/movies/moviecollections.js deleted file mode 100644 index d1b909511f..0000000000 --- a/src/controllers/movies/moviecollections.js +++ /dev/null @@ -1,267 +0,0 @@ -import loading from '../../components/loading/loading'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import imageLoader from '../../components/images/imageLoader'; -import listView from '../../components/listview/listview'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import * as userSettings from '../../scripts/settings/userSettings'; -import globalize from '../../scripts/globalize'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; - -/* eslint-disable indent */ - - export default function (view, params, tabContent) { - function getPageData(context) { - const key = getSavedQueryKey(context); - let pageData = data[key]; - - if (!pageData) { - pageData = data[key] = { - query: { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'BoxSet', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,SortName', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - StartIndex: 0 - }, - view: libraryBrowser.getSavedView(key) || 'Poster' - }; - - if (userSettings.libraryPageSize() > 0) { - pageData.query['Limit'] = userSettings.libraryPageSize(); - } - - pageData.query.ParentId = params.topParentId; - libraryBrowser.loadSavedQueryValues(key, pageData.query); - } - - return pageData; - } - - function getQuery(context) { - return getPageData(context).query; - } - - function getSavedQueryKey(context) { - if (!context.savedQueryKey) { - context.savedQueryKey = libraryBrowser.getSavedQueryKey('moviecollections'); - } - - return context.savedQueryKey; - } - - const onViewStyleChange = () => { - const viewStyle = this.getCurrentViewStyle(); - const itemsContainer = tabContent.querySelector('.itemsContainer'); - - if (viewStyle == 'List') { - itemsContainer.classList.add('vertical-list'); - itemsContainer.classList.remove('vertical-wrap'); - } else { - itemsContainer.classList.remove('vertical-list'); - itemsContainer.classList.add('vertical-wrap'); - } - - itemsContainer.innerHTML = ''; - }; - - const reloadItems = (page) => { - loading.show(); - isLoading = true; - const query = getQuery(page); - ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => { - function onNextPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex += query.Limit; - } - reloadItems(tabContent); - } - - function onPreviousPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex = Math.max(0, query.StartIndex - query.Limit); - } - reloadItems(tabContent); - } - - window.scrollTo(0, 0); - let html; - const pagingHtml = libraryBrowser.getQueryPagingHtml({ - startIndex: query.StartIndex, - limit: query.Limit, - totalRecordCount: result.TotalRecordCount, - showLimit: false, - updatePageSizeSetting: false, - addLayoutButton: false, - sortButton: false, - filterButton: false - }); - const viewStyle = this.getCurrentViewStyle(); - if (viewStyle == 'Thumb') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - overlayPlayButton: true, - centerText: true, - showTitle: true - }); - } else if (viewStyle == 'ThumbCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - lazy: true, - cardLayout: true, - showTitle: true - }); - } else if (viewStyle == 'Banner') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'banner', - preferBanner: true, - context: 'movies', - lazy: true - }); - } else if (viewStyle == 'List') { - html = listView.getListViewHtml({ - items: result.Items, - context: 'movies', - sortBy: query.SortBy - }); - } else if (viewStyle == 'PosterCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'auto', - context: 'movies', - showTitle: true, - centerText: false, - cardLayout: true - }); - } else { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'auto', - context: 'movies', - centerText: true, - overlayPlayButton: true, - showTitle: true - }); - } - - let elems = tabContent.querySelectorAll('.paging'); - - for (const elem of elems) { - elem.innerHTML = pagingHtml; - } - - elems = tabContent.querySelectorAll('.btnNextPage'); - for (const elem of elems) { - elem.addEventListener('click', onNextPageClick); - } - - elems = tabContent.querySelectorAll('.btnPreviousPage'); - for (const elem of elems) { - elem.addEventListener('click', onPreviousPageClick); - } - - if (!result.Items.length) { - html = ''; - - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoCollectionsAvailable') + '

'; - html += '
'; - } - - const itemsContainer = tabContent.querySelector('.itemsContainer'); - itemsContainer.innerHTML = html; - imageLoader.lazyChildren(itemsContainer); - libraryBrowser.saveQueryValues(getSavedQueryKey(page), query); - loading.hide(); - isLoading = false; - - import('../../components/autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); - }); - }; - - const data = {}; - let isLoading = false; - - this.getCurrentViewStyle = function () { - return getPageData(tabContent).view; - }; - - const initPage = (tabElement) => { - tabElement.querySelector('.btnSort').addEventListener('click', function (e) { - libraryBrowser.showSortMenu({ - items: [{ - name: globalize.translate('Name'), - id: 'SortName' - }, { - name: globalize.translate('OptionImdbRating'), - id: 'CommunityRating,SortName' - }, { - name: globalize.translate('OptionDateAdded'), - id: 'DateCreated,SortName' - }, { - name: globalize.translate('OptionParentalRating'), - id: 'OfficialRating,SortName' - }, { - name: globalize.translate('OptionReleaseDate'), - id: 'PremiereDate,SortName' - }], - callback: function () { - getQuery(tabElement).StartIndex = 0; - reloadItems(tabElement); - }, - query: getQuery(tabElement), - button: e.target - }); - }); - const btnSelectView = tabElement.querySelector('.btnSelectView'); - btnSelectView.addEventListener('click', (e) => { - libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'List,Poster,PosterCard,Thumb,ThumbCard'.split(',')); - }); - btnSelectView.addEventListener('layoutchange', function (e) { - const viewStyle = e.detail.viewStyle; - getPageData(tabElement).view = viewStyle; - libraryBrowser.saveViewSetting(getSavedQueryKey(tabElement), viewStyle); - getQuery(tabElement).StartIndex = 0; - onViewStyleChange(); - reloadItems(tabElement); - }); - tabElement.querySelector('.btnNewCollection').addEventListener('click', () => { - import('../../components/collectionEditor/collectionEditor').then(({default: collectionEditor}) => { - const serverId = ApiClient.serverInfo().Id; - new collectionEditor({ - items: [], - serverId: serverId - }); - }); - }); - }; - - initPage(tabContent); - onViewStyleChange(); - - this.renderTab = function () { - reloadItems(tabContent); - }; - } - -/* eslint-enable indent */ diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js deleted file mode 100644 index 62320fa3ab..0000000000 --- a/src/controllers/movies/moviegenres.js +++ /dev/null @@ -1,224 +0,0 @@ -import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; -import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/appRouter'; -import '../../elements/emby-button/emby-button'; - -/* eslint-disable indent */ - - export default function (view, params, tabContent) { - function getPageData() { - const key = getSavedQueryKey(); - let pageData = data[key]; - - if (!pageData) { - pageData = data[key] = { - query: { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'Movie', - Recursive: true, - EnableTotalRecordCount: false - }, - view: 'Poster' - }; - pageData.query.ParentId = params.topParentId; - libraryBrowser.loadSavedQueryValues(key, pageData.query); - } - - return pageData; - } - - function getQuery() { - return getPageData().query; - } - - function getSavedQueryKey() { - return libraryBrowser.getSavedQueryKey('moviegenres'); - } - - function getPromise() { - loading.show(); - const query = getQuery(); - return ApiClient.getGenres(ApiClient.getCurrentUserId(), query); - } - - function enableScrollX() { - return !layoutManager.desktop; - } - - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - - const fillItemsContainer = (entry) => { - const elem = entry.target; - const id = elem.getAttribute('data-id'); - const viewStyle = this.getCurrentViewStyle(); - let limit = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 5 : 9; - - if (enableScrollX()) { - limit = 10; - } - - const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary'; - const query = { - SortBy: 'Random', - SortOrder: 'Ascending', - IncludeItemTypes: 'Movie', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: enableImageTypes, - Limit: limit, - GenreIds: id, - EnableTotalRecordCount: false, - ParentId: params.topParentId - }; - ApiClient.getItems(ApiClient.getCurrentUserId(), query).then(function (result) { - if (viewStyle == 'Thumb') { - cardBuilder.buildCards(result.Items, { - itemsContainer: elem, - shape: getThumbShape(), - preferThumb: true, - showTitle: true, - scalable: true, - centerText: true, - overlayMoreButton: true, - allowBottomPadding: false - }); - } else if (viewStyle == 'ThumbCard') { - cardBuilder.buildCards(result.Items, { - itemsContainer: elem, - shape: getThumbShape(), - preferThumb: true, - showTitle: true, - scalable: true, - centerText: false, - cardLayout: true, - showYear: true - }); - } else if (viewStyle == 'PosterCard') { - cardBuilder.buildCards(result.Items, { - itemsContainer: elem, - shape: getPortraitShape(), - showTitle: true, - scalable: true, - centerText: false, - cardLayout: true, - showYear: true - }); - } else if (viewStyle == 'Poster') { - cardBuilder.buildCards(result.Items, { - itemsContainer: elem, - shape: getPortraitShape(), - scalable: true, - overlayMoreButton: true, - allowBottomPadding: true, - showTitle: true, - centerText: true, - showYear: true - }); - } - if (result.Items.length >= query.Limit) { - tabContent.querySelector('.btnMoreFromGenre' + id + ' .material-icons').classList.remove('hide'); - } - }); - }; - - function reloadItems(context, promise) { - const query = getQuery(); - promise.then(function (result) { - const elem = context.querySelector('#items'); - let html = ''; - const items = result.Items; - - for (let i = 0, length = items.length; i < length; i++) { - const item = items[i]; - - html += '
'; - html += ''; - if (enableScrollX()) { - let scrollXClass = 'scrollX hiddenScrollX'; - - if (layoutManager.tv) { - scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale'; - } - - html += '
'; - } else { - html += '
'; - } - - html += '
'; - html += '
'; - } - - if (!result.Items.length) { - html = ''; - - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoGenresAvailable') + '

'; - html += '
'; - } - - elem.innerHTML = html; - lazyLoader.lazyChildren(elem, fillItemsContainer); - libraryBrowser.saveQueryValues(getSavedQueryKey(), query); - loading.hide(); - }); - } - - const fullyReload = () => { - this.preRender(); - this.renderTab(); - }; - - const data = {}; - - this.getViewStyles = function () { - return 'Poster,PosterCard,Thumb,ThumbCard'.split(','); - }; - - this.getCurrentViewStyle = function () { - return getPageData().view; - }; - - this.setCurrentViewStyle = function (viewStyle) { - getPageData().view = viewStyle; - libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle); - fullyReload(); - }; - - this.enableViewSelection = true; - let promise; - - this.preRender = function () { - promise = getPromise(); - }; - - this.renderTab = function () { - reloadItems(tabContent, promise); - }; - } - -/* eslint-enable indent */ diff --git a/src/controllers/movies/movies.html b/src/controllers/movies/movies.html deleted file mode 100644 index 7a08694b2a..0000000000 --- a/src/controllers/movies/movies.html +++ /dev/null @@ -1,92 +0,0 @@ -
- -
-
-
- - - - -
- -
-
- -
-
-
-
-
-
-
-
-
-

${HeaderContinueWatching}

-
- -
-
-
- -
-
-

${HeaderLatestMovies}

-
- -
-
-
- -
-
-
-
-

${MessageNoMovieSuggestionsAvailable}

-
-
-
-
-
- - -
- -
-
- -
-
-
-
-
-
-
-
-
- -
- -
-
-
-
-
-
-
-
-
- - - -
- -
-
-
-
-
-
-
-
-
-
diff --git a/src/controllers/movies/movies.js b/src/controllers/movies/movies.js deleted file mode 100644 index 89e086ce7e..0000000000 --- a/src/controllers/movies/movies.js +++ /dev/null @@ -1,327 +0,0 @@ -import loading from '../../components/loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import { AlphaPicker } from '../../components/alphaPicker/alphaPicker'; -import listView from '../../components/listview/listview'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import globalize from '../../scripts/globalize'; -import Events from '../../utils/events.ts'; -import { playbackManager } from '../../components/playback/playbackmanager'; - -import '../../elements/emby-itemscontainer/emby-itemscontainer'; - -/* eslint-disable indent */ - - export default function (view, params, tabContent, options) { - const onViewStyleChange = () => { - if (this.getCurrentViewStyle() == 'List') { - itemsContainer.classList.add('vertical-list'); - itemsContainer.classList.remove('vertical-wrap'); - } else { - itemsContainer.classList.remove('vertical-list'); - itemsContainer.classList.add('vertical-wrap'); - } - - itemsContainer.innerHTML = ''; - }; - - function fetchData() { - isLoading = true; - loading.show(); - return ApiClient.getItems(ApiClient.getCurrentUserId(), query); - } - - function shuffle() { - ApiClient.getItem( - ApiClient.getCurrentUserId(), - params.topParentId - ).then((item) => { - playbackManager.shuffle(item); - }); - } - - const afterRefresh = (result) => { - function onNextPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex += query.Limit; - } - itemsContainer.refreshItems(); - } - - function onPreviousPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex = Math.max(0, query.StartIndex - query.Limit); - } - itemsContainer.refreshItems(); - } - - window.scrollTo(0, 0); - this.alphaPicker?.updateControls(query); - const pagingHtml = libraryBrowser.getQueryPagingHtml({ - startIndex: query.StartIndex, - limit: query.Limit, - totalRecordCount: result.TotalRecordCount, - showLimit: false, - updatePageSizeSetting: false, - addLayoutButton: false, - sortButton: false, - filterButton: false - }); - - for (const elem of tabContent.querySelectorAll('.paging')) { - elem.innerHTML = pagingHtml; - } - - for (const elem of tabContent.querySelectorAll('.btnNextPage')) { - elem.addEventListener('click', onNextPageClick); - } - - for (const elem of tabContent.querySelectorAll('.btnPreviousPage')) { - elem.addEventListener('click', onPreviousPageClick); - } - - tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1); - - isLoading = false; - loading.hide(); - - import('../../components/autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(tabContent); - }); - }; - - const getItemsHtml = (items) => { - let html; - const viewStyle = this.getCurrentViewStyle(); - - if (viewStyle == 'Thumb') { - html = cardBuilder.getCardsHtml({ - items: items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - lazy: true, - overlayPlayButton: true, - showTitle: true, - showYear: true, - centerText: true - }); - } else if (viewStyle == 'ThumbCard') { - html = cardBuilder.getCardsHtml({ - items: items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - lazy: true, - cardLayout: true, - showTitle: true, - showYear: true, - centerText: true - }); - } else if (viewStyle == 'Banner') { - html = cardBuilder.getCardsHtml({ - items: items, - shape: 'banner', - preferBanner: true, - context: 'movies', - lazy: true - }); - } else if (viewStyle == 'List') { - html = listView.getListViewHtml({ - items: items, - context: 'movies', - sortBy: query.SortBy - }); - } else if (viewStyle == 'PosterCard') { - html = cardBuilder.getCardsHtml({ - items: items, - shape: 'portrait', - context: 'movies', - showTitle: true, - showYear: true, - centerText: true, - lazy: true, - cardLayout: true - }); - } else { - html = cardBuilder.getCardsHtml({ - items: items, - shape: 'portrait', - context: 'movies', - overlayPlayButton: true, - showTitle: true, - showYear: true, - centerText: true - }); - } - - return html; - }; - - const initPage = (tabElement) => { - itemsContainer.fetchData = fetchData; - itemsContainer.getItemsHtml = getItemsHtml; - itemsContainer.afterRefresh = afterRefresh; - const alphaPickerElement = tabElement.querySelector('.alphaPicker'); - - if (alphaPickerElement) { - alphaPickerElement.addEventListener('alphavaluechanged', function (e) { - const newValue = e.detail.value; - if (newValue === '#') { - query.NameLessThan = 'A'; - delete query.NameStartsWith; - } else { - query.NameStartsWith = newValue; - delete query.NameLessThan; - } - query.StartIndex = 0; - itemsContainer.refreshItems(); - }); - this.alphaPicker = new AlphaPicker({ - element: alphaPickerElement, - valueChangeEvent: 'click' - }); - - tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right'); - alphaPickerElement.classList.add('alphaPicker-fixed-right'); - itemsContainer.classList.add('padded-right-withalphapicker'); - } - - const btnFilter = tabElement.querySelector('.btnFilter'); - - if (btnFilter) { - btnFilter.addEventListener('click', () => { - this.showFilterMenu(); - }); - } - const btnSort = tabElement.querySelector('.btnSort'); - - if (btnSort) { - btnSort.addEventListener('click', function (e) { - libraryBrowser.showSortMenu({ - items: [{ - name: globalize.translate('Name'), - id: 'SortName,ProductionYear' - }, { - name: globalize.translate('OptionRandom'), - id: 'Random' - }, { - name: globalize.translate('OptionImdbRating'), - id: 'CommunityRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionCriticRating'), - id: 'CriticRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDateAdded'), - id: 'DateCreated,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDatePlayed'), - id: 'DatePlayed,SortName,ProductionYear' - }, { - name: globalize.translate('OptionParentalRating'), - id: 'OfficialRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionPlayCount'), - id: 'PlayCount,SortName,ProductionYear' - }, { - name: globalize.translate('OptionReleaseDate'), - id: 'PremiereDate,SortName,ProductionYear' - }, { - name: globalize.translate('Runtime'), - id: 'Runtime,SortName,ProductionYear' - }], - callback: function () { - query.StartIndex = 0; - userSettings.saveQuerySettings(savedQueryKey, query); - itemsContainer.refreshItems(); - }, - query: query, - button: e.target - }); - }); - } - const btnSelectView = tabElement.querySelector('.btnSelectView'); - btnSelectView.addEventListener('click', (e) => { - libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(',')); - }); - btnSelectView.addEventListener('layoutchange', function (e) { - const viewStyle = e.detail.viewStyle; - userSettings.set(savedViewKey, viewStyle); - query.StartIndex = 0; - onViewStyleChange(); - itemsContainer.refreshItems(); - }); - - tabElement.querySelector('.btnShuffle').addEventListener('click', shuffle); - }; - - let itemsContainer = tabContent.querySelector('.itemsContainer'); - const savedQueryKey = params.topParentId + '-' + options.mode; - const savedViewKey = savedQueryKey + '-view'; - let query = { - SortBy: 'SortName,ProductionYear', - SortOrder: 'Ascending', - IncludeItemTypes: 'Movie', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - StartIndex: 0, - ParentId: params.topParentId - }; - - if (userSettings.libraryPageSize() > 0) { - query['Limit'] = userSettings.libraryPageSize(); - } - - let isLoading = false; - - if (options.mode === 'favorites') { - query.IsFavorite = true; - } - - query = userSettings.loadQuerySettings(savedQueryKey, query); - - this.showFilterMenu = function () { - import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { - const filterDialog = new filterDialogFactory({ - query: query, - mode: 'movies', - serverId: ApiClient.serverId() - }); - Events.on(filterDialog, 'filterchange', () => { - query.StartIndex = 0; - itemsContainer.refreshItems(); - }); - filterDialog.show(); - }); - }; - - this.getCurrentViewStyle = function () { - return userSettings.get(savedViewKey) || 'Poster'; - }; - - this.initTab = function () { - initPage(tabContent); - onViewStyleChange(); - }; - - this.renderTab = () => { - itemsContainer.refreshItems(); - this.alphaPicker?.updateControls(query); - }; - - this.destroy = function () { - itemsContainer = null; - }; - } - -/* eslint-enable indent */ diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js deleted file mode 100644 index b1b0103311..0000000000 --- a/src/controllers/movies/moviesrecommended.js +++ /dev/null @@ -1,428 +0,0 @@ -import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import inputManager from '../../scripts/inputManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import globalize from '../../scripts/globalize'; -import Dashboard from '../../utils/dashboard'; -import Events from '../../utils/events.ts'; - -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; - -/* eslint-disable indent */ - - function enableScrollX() { - return !layoutManager.desktop; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function loadLatest(page, userId, parentId) { - const options = { - IncludeItemTypes: 'Movie', - Limit: 18, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - ParentId: parentId, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - EnableTotalRecordCount: false - }; - ApiClient.getJSON(ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) { - const allowBottomPadding = !enableScrollX(); - const container = page.querySelector('#recentlyAddedItems'); - cardBuilder.buildCards(items, { - itemsContainer: container, - shape: getPortraitShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - showTitle: true, - showYear: true, - centerText: true - }); - - // FIXME: Wait for all sections to load - autoFocus(page); - }); - } - - function loadResume(page, userId, parentId) { - const screenWidth = dom.getWindowSize().innerWidth; - const options = { - SortBy: 'DatePlayed', - SortOrder: 'Descending', - IncludeItemTypes: 'Movie', - Filters: 'IsResumable', - Limit: screenWidth >= 1600 ? 5 : 3, - Recursive: true, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - CollapseBoxSetItems: false, - ParentId: parentId, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - EnableTotalRecordCount: false - }; - ApiClient.getItems(userId, options).then(function (result) { - if (result.Items.length) { - page.querySelector('#resumableSection').classList.remove('hide'); - } else { - page.querySelector('#resumableSection').classList.add('hide'); - } - - const allowBottomPadding = !enableScrollX(); - const container = page.querySelector('#resumableItems'); - cardBuilder.buildCards(result.Items, { - itemsContainer: container, - preferThumb: true, - shape: getThumbShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - cardLayout: false, - showTitle: true, - showYear: true, - centerText: true - }); - - // FIXME: Wait for all sections to load - autoFocus(page); - }); - } - - function getRecommendationHtml(recommendation) { - let html = ''; - let title = ''; - - switch (recommendation.RecommendationType) { - case 'SimilarToRecentlyPlayed': - title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName); - break; - - case 'SimilarToLikedItem': - title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName); - break; - - case 'HasDirectorFromRecentlyPlayed': - case 'HasLikedDirector': - title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName); - break; - - case 'HasActorFromRecentlyPlayed': - case 'HasLikedActor': - title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName); - break; - } - - html += '
'; - html += '

' + escapeHtml(title) + '

'; - const allowBottomPadding = true; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - html += cardBuilder.getCardsHtml(recommendation.Items, { - shape: getPortraitShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - showTitle: true, - showYear: true, - centerText: true - }); - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - html += '
'; - return html; - } - - function loadSuggestions(page, userId) { - const screenWidth = dom.getWindowSize().innerWidth; - let itemLimit = 5; - if (screenWidth >= 1600) { - itemLimit = 8; - } else if (screenWidth >= 1200) { - itemLimit = 6; - } - - const url = ApiClient.getUrl('Movies/Recommendations', { - userId: userId, - categoryLimit: 6, - ItemLimit: itemLimit, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb' - }); - ApiClient.getJSON(url).then(function (recommendations) { - if (!recommendations.length) { - page.querySelector('.noItemsMessage').classList.remove('hide'); - page.querySelector('.recommendations').innerHTML = ''; - return; - } - - const html = recommendations.map(getRecommendationHtml).join(''); - page.querySelector('.noItemsMessage').classList.add('hide'); - const recs = page.querySelector('.recommendations'); - recs.innerHTML = html; - imageLoader.lazyChildren(recs); - - // FIXME: Wait for all sections to load - autoFocus(page); - }); - } - - function autoFocus(page) { - import('../../components/autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); - } - - function setScrollClasses(elem, scrollX) { - if (scrollX) { - elem.classList.add('hiddenScrollX'); - - if (layoutManager.tv) { - elem.classList.add('smoothScrollX'); - elem.classList.add('padded-top-focusscale'); - elem.classList.add('padded-bottom-focusscale'); - } - - elem.classList.add('scrollX'); - elem.classList.remove('vertical-wrap'); - } else { - elem.classList.remove('hiddenScrollX'); - elem.classList.remove('smoothScrollX'); - elem.classList.remove('scrollX'); - elem.classList.add('vertical-wrap'); - } - } - - function initSuggestedTab(page, tabContent) { - const containers = tabContent.querySelectorAll('.itemsContainer'); - - for (const container of containers) { - setScrollClasses(container, enableScrollX()); - } - } - - function loadSuggestionsTab(view, params, tabContent) { - const parentId = params.topParentId; - const userId = ApiClient.getCurrentUserId(); - loadResume(tabContent, userId, parentId); - loadLatest(tabContent, userId, parentId); - loadSuggestions(tabContent, userId); - } - - function getTabs() { - return [{ - name: globalize.translate('Movies') - }, { - name: globalize.translate('Suggestions') - }, { - name: globalize.translate('Trailers') - }, { - name: globalize.translate('Favorites') - }, { - name: globalize.translate('Collections') - }, { - name: globalize.translate('Genres') - }]; - } - - function getDefaultTabIndex(folderId) { - switch (userSettings.get('landing-' + folderId)) { - case 'suggestions': - return 1; - - case 'favorites': - return 3; - - case 'collections': - return 4; - - case 'genres': - return 5; - - default: - return 0; - } - } - - export default function (view, params) { - function onBeforeTabChange(e) { - preLoadTab(view, parseInt(e.detail.selectedTabIndex)); - } - - function onTabChange(e) { - const newIndex = parseInt(e.detail.selectedTabIndex); - loadTab(view, newIndex); - } - - function getTabContainers() { - return view.querySelectorAll('.pageTabContent'); - } - - function initTabs() { - mainTabsManager.setTabs(view, currentTabIndex, getTabs, getTabContainers, onBeforeTabChange, onTabChange); - } - - const getTabController = (page, index, callback) => { - let depends = ''; - - switch (index) { - case 0: - depends = 'movies'; - break; - - case 1: - depends = 'moviesrecommended.js'; - break; - - case 2: - depends = 'movietrailers'; - break; - - case 3: - depends = 'movies'; - break; - - case 4: - depends = 'moviecollections'; - break; - - case 5: - depends = 'moviegenres'; - break; - } - - import(`../movies/${depends}`).then(({default: controllerFactory}) => { - let tabContent; - - if (index === suggestionsTabIndex) { - tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']"); - this.tabContent = tabContent; - } - - let controller = tabControllers[index]; - - if (!controller) { - tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']"); - - if (index === suggestionsTabIndex) { - controller = this; - } else if (index == 0 || index == 3) { - controller = new controllerFactory(view, params, tabContent, { - mode: index ? 'favorites' : 'movies' - }); - } else { - controller = new controllerFactory(view, params, tabContent); - } - - tabControllers[index] = controller; - - if (controller.initTab) { - controller.initTab(); - } - } - - callback(controller); - }); - }; - - function preLoadTab(page, index) { - getTabController(page, index, function (controller) { - if (renderedTabs.indexOf(index) == -1 && controller.preRender) { - controller.preRender(); - } - }); - } - - function loadTab(page, index) { - currentTabIndex = index; - getTabController(page, index, ((controller) => { - if (renderedTabs.indexOf(index) == -1) { - renderedTabs.push(index); - controller.renderTab(); - } - })); - } - - function onPlaybackStop(e, state) { - if (state.NowPlayingItem && state.NowPlayingItem.MediaType == 'Video') { - renderedTabs = []; - mainTabsManager.getTabsElement().triggerTabChange(); - } - } - - function onInputCommand(e) { - if (e.detail.command === 'search') { - e.preventDefault(); - Dashboard.navigate('search.html?collectionType=movies&parentId=' + params.topParentId); - } - } - - let currentTabIndex = parseInt(params.tab || getDefaultTabIndex(params.topParentId)); - const suggestionsTabIndex = 1; - - this.initTab = function () { - const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']"); - initSuggestedTab(view, tabContent); - }; - - this.renderTab = function () { - const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']"); - loadSuggestionsTab(view, params, tabContent); - }; - - const tabControllers = []; - let renderedTabs = []; - view.addEventListener('viewshow', function () { - initTabs(); - if (!view.getAttribute('data-title')) { - const parentId = params.topParentId; - - if (parentId) { - ApiClient.getItem(ApiClient.getCurrentUserId(), parentId).then(function (item) { - view.setAttribute('data-title', item.Name); - libraryMenu.setTitle(item.Name); - }); - } else { - view.setAttribute('data-title', globalize.translate('Movies')); - libraryMenu.setTitle(globalize.translate('Movies')); - } - } - - Events.on(playbackManager, 'playbackstop', onPlaybackStop); - inputManager.on(window, onInputCommand); - }); - view.addEventListener('viewbeforehide', function () { - inputManager.off(window, onInputCommand); - }); - for (const tabController of tabControllers) { - if (tabController.destroy) { - tabController.destroy(); - } - } - } - -/* eslint-enable indent */ diff --git a/src/controllers/movies/movietrailers.js b/src/controllers/movies/movietrailers.js deleted file mode 100644 index 2e55e6eea8..0000000000 --- a/src/controllers/movies/movietrailers.js +++ /dev/null @@ -1,279 +0,0 @@ -import loading from '../../components/loading/loading'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import imageLoader from '../../components/images/imageLoader'; -import { AlphaPicker } from '../../components/alphaPicker/alphaPicker'; -import listView from '../../components/listview/listview'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import * as userSettings from '../../scripts/settings/userSettings'; -import globalize from '../../scripts/globalize'; -import Events from '../../utils/events.ts'; - -import '../../elements/emby-itemscontainer/emby-itemscontainer'; - -/* eslint-disable indent */ - - export default function (view, params, tabContent) { - function getPageData(context) { - const key = getSavedQueryKey(context); - let pageData = data[key]; - - if (!pageData) { - pageData = data[key] = { - query: { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'Trailer', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - StartIndex: 0 - }, - view: libraryBrowser.getSavedView(key) || 'Poster' - }; - - if (userSettings.libraryPageSize() > 0) { - pageData.query['Limit'] = userSettings.libraryPageSize(); - } - - libraryBrowser.loadSavedQueryValues(key, pageData.query); - } - - return pageData; - } - - function getQuery(context) { - return getPageData(context).query; - } - - function getSavedQueryKey(context) { - if (!context.savedQueryKey) { - context.savedQueryKey = libraryBrowser.getSavedQueryKey('trailers'); - } - - return context.savedQueryKey; - } - - const reloadItems = () => { - loading.show(); - isLoading = true; - const query = getQuery(tabContent); - ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => { - function onNextPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex += query.Limit; - } - reloadItems(); - } - - function onPreviousPageClick() { - if (isLoading) { - return; - } - - if (userSettings.libraryPageSize() > 0) { - query.StartIndex = Math.max(0, query.StartIndex - query.Limit); - } - reloadItems(); - } - - window.scrollTo(0, 0); - this.alphaPicker?.updateControls(query); - const pagingHtml = libraryBrowser.getQueryPagingHtml({ - startIndex: query.StartIndex, - limit: query.Limit, - totalRecordCount: result.TotalRecordCount, - showLimit: false, - updatePageSizeSetting: false, - addLayoutButton: false, - sortButton: false, - filterButton: false - }); - let html; - const viewStyle = this.getCurrentViewStyle(); - - if (viewStyle == 'Thumb') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - overlayPlayButton: true - }); - } else if (viewStyle == 'ThumbCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - preferThumb: true, - context: 'movies', - cardLayout: true, - showTitle: true, - showYear: true, - centerText: true - }); - } else if (viewStyle == 'Banner') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'banner', - preferBanner: true, - context: 'movies' - }); - } else if (viewStyle == 'List') { - html = listView.getListViewHtml({ - items: result.Items, - context: 'movies', - sortBy: query.SortBy - }); - } else if (viewStyle == 'PosterCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'portrait', - context: 'movies', - showTitle: true, - showYear: true, - cardLayout: true, - centerText: true - }); - } else { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'portrait', - context: 'movies', - centerText: true, - overlayPlayButton: true, - showTitle: true, - showYear: true - }); - } - - let elems = tabContent.querySelectorAll('.paging'); - - for (const elem of elems) { - elem.innerHTML = pagingHtml; - } - - elems = tabContent.querySelectorAll('.btnNextPage'); - for (const elem of elems) { - elem.addEventListener('click', onNextPageClick); - } - - elems = tabContent.querySelectorAll('.btnPreviousPage'); - for (const elem of elems) { - elem.addEventListener('click', onPreviousPageClick); - } - - if (!result.Items.length) { - html = ''; - - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoTrailersFound') + '

'; - html += '
'; - } - - const itemsContainer = tabContent.querySelector('.itemsContainer'); - itemsContainer.innerHTML = html; - imageLoader.lazyChildren(itemsContainer); - libraryBrowser.saveQueryValues(getSavedQueryKey(tabContent), query); - loading.hide(); - isLoading = false; - }); - }; - - const data = {}; - let isLoading = false; - - this.showFilterMenu = function () { - import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { - const filterDialog = new filterDialogFactory({ - query: getQuery(tabContent), - mode: 'movies', - serverId: ApiClient.serverId() - }); - Events.on(filterDialog, 'filterchange', function () { - getQuery(tabContent).StartIndex = 0; - reloadItems(); - }); - filterDialog.show(); - }); - }; - - this.getCurrentViewStyle = function () { - return getPageData(tabContent).view; - }; - - const initPage = (tabElement) => { - const alphaPickerElement = tabElement.querySelector('.alphaPicker'); - const itemsContainer = tabElement.querySelector('.itemsContainer'); - alphaPickerElement.addEventListener('alphavaluechanged', function (e) { - const newValue = e.detail.value; - const query = getQuery(tabElement); - if (newValue === '#') { - query.NameLessThan = 'A'; - delete query.NameStartsWith; - } else { - query.NameStartsWith = newValue; - delete query.NameLessThan; - } - query.StartIndex = 0; - reloadItems(); - }); - this.alphaPicker = new AlphaPicker({ - element: alphaPickerElement, - valueChangeEvent: 'click' - }); - - tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right'); - alphaPickerElement.classList.add('alphaPicker-fixed-right'); - itemsContainer.classList.add('padded-right-withalphapicker'); - - tabElement.querySelector('.btnFilter').addEventListener('click', () => { - this.showFilterMenu(); - }); - tabElement.querySelector('.btnSort').addEventListener('click', function (e) { - libraryBrowser.showSortMenu({ - items: [{ - name: globalize.translate('Name'), - id: 'SortName' - }, { - name: globalize.translate('OptionImdbRating'), - id: 'CommunityRating,SortName' - }, { - name: globalize.translate('OptionDateAdded'), - id: 'DateCreated,SortName' - }, { - name: globalize.translate('OptionDatePlayed'), - id: 'DatePlayed,SortName' - }, { - name: globalize.translate('OptionParentalRating'), - id: 'OfficialRating,SortName' - }, { - name: globalize.translate('OptionPlayCount'), - id: 'PlayCount,SortName' - }, { - name: globalize.translate('OptionReleaseDate'), - id: 'PremiereDate,SortName' - }], - callback: function () { - getQuery(tabElement).StartIndex = 0; - reloadItems(); - }, - query: getQuery(tabElement), - button: e.target - }); - }); - }; - - initPage(tabContent); - - this.renderTab = () => { - reloadItems(); - this.alphaPicker?.updateControls(getQuery(tabContent)); - }; - } - -/* eslint-enable indent */ diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx new file mode 100644 index 0000000000..0c4d55724d --- /dev/null +++ b/src/elements/ItemsContainerElement.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; + +const createElement = ({ className, dataId }: IProps) => ({ + __html: `
+
` +}); + +interface IProps { + className?: string; + dataId?: string; +} + +const ItemsContainerElement: FC = ({ className, dataId }) => { + return ( +
+ ); +}; + +export default ItemsContainerElement; diff --git a/src/elements/ItemsScrollerContainerElement.tsx b/src/elements/ItemsScrollerContainerElement.tsx new file mode 100644 index 0000000000..6e16807b85 --- /dev/null +++ b/src/elements/ItemsScrollerContainerElement.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; + +const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, dataId, className }: IProps) => ({ + __html: `
+
+
+
` +}); + +interface IProps { + scrollerclassName?: string; + dataHorizontal?: string; + dataMousewheel?: string; + dataCenterfocus?: string; + dataId?: string; + className?: string; +} + +const ItemsScrollerContainerElement: FC = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, dataId, className }) => { + return ( +
+ ); +}; + +export default ItemsScrollerContainerElement; diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx new file mode 100644 index 0000000000..779147039c --- /dev/null +++ b/src/hooks/useLocalStorage.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export function useLocalStorage(key: string, initialValue: T | (() => T)) { + const [value, setValue] = useState(() => { + const storedValues = localStorage.getItem(key); + if (storedValues != null) return JSON.parse(storedValues); + + if (typeof initialValue === 'function') { + return (initialValue as () => T)(); + } else { + return initialValue; + } + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue] as [typeof value, typeof setValue]; +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d37fa9a7af..d18ce561e1 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,6 +11,7 @@ import UserPassword from './user/userpassword'; import UserProfile from './user/userprofile'; import UserProfiles from './user/userprofiles'; import Home from './home'; +import Movies from './movies'; const AppRoutes = () => ( @@ -20,6 +21,7 @@ const AppRoutes = () => ( } /> } /> } /> + } /> {/* Admin routes */} diff --git a/src/routes/movies/CollectionsView.tsx b/src/routes/movies/CollectionsView.tsx new file mode 100644 index 0000000000..6e94ad2b96 --- /dev/null +++ b/src/routes/movies/CollectionsView.tsx @@ -0,0 +1,32 @@ +import React, { FC, useCallback } from 'react'; + +import ViewItemsContainer from '../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const CollectionsView: FC = ({ topParentId }) => { + const getBasekey = useCallback(() => { + return 'collections'; + }, []); + + const getItemTypes = useCallback(() => { + return ['BoxSet']; + }, []); + + const getNoItemsMessage = useCallback(() => { + return 'MessageNoCollectionsAvailable'; + }, []); + + return ( + + ); +}; + +export default CollectionsView; diff --git a/src/routes/movies/FavoritesView.tsx b/src/routes/movies/FavoritesView.tsx new file mode 100644 index 0000000000..cf6969bbb2 --- /dev/null +++ b/src/routes/movies/FavoritesView.tsx @@ -0,0 +1,29 @@ +import React, { FC, useCallback } from 'react'; + +import ViewItemsContainer from '../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const FavoritesView: FC = ({ topParentId }) => { + const getBasekey = useCallback(() => { + return 'favorites'; + }, []); + + const getItemTypes = useCallback(() => { + return ['Movie']; + }, []); + + const getNoItemsMessage = useCallback(() => { + return 'MessageNoFavoritesAvailable'; + }, []); + + return ( + + ); +}; + +export default FavoritesView; diff --git a/src/routes/movies/GenresView.tsx b/src/routes/movies/GenresView.tsx new file mode 100644 index 0000000000..24ae5f79c0 --- /dev/null +++ b/src/routes/movies/GenresView.tsx @@ -0,0 +1,41 @@ +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback, useEffect, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import GenresItemsContainer from '../../components/common/GenresItemsContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const GenresView: FC = ({ topParentId }) => { + const [ itemsResult, setItemsResult ] = useState({}); + + const reloadItems = useCallback(() => { + loading.show(); + window.ApiClient.getGenres( + window.ApiClient.getCurrentUserId(), + { + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'Movie', + Recursive: true, + EnableTotalRecordCount: false, + ParentId: topParentId + } + ).then((result) => { + setItemsResult(result); + loading.hide(); + }); + }, [topParentId]); + + useEffect(() => { + reloadItems(); + }, [reloadItems]); + + return ( + + ); +}; + +export default GenresView; diff --git a/src/routes/movies/MoviesView.tsx b/src/routes/movies/MoviesView.tsx new file mode 100644 index 0000000000..4103eb908c --- /dev/null +++ b/src/routes/movies/MoviesView.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; + +import ViewItemsContainer from '../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const MoviesView: FC = ({ topParentId }) => { + const getBasekey = useCallback(() => { + return 'movies'; + }, []); + + const getItemTypes = useCallback(() => { + return ['Movie']; + }, []); + + const getNoItemsMessage = useCallback(() => { + return 'MessageNoItemsAvailable'; + }, []); + + return ( + + ); +}; + +export default MoviesView; diff --git a/src/routes/movies/SuggestionsView.tsx b/src/routes/movies/SuggestionsView.tsx new file mode 100644 index 0000000000..605d40062c --- /dev/null +++ b/src/routes/movies/SuggestionsView.tsx @@ -0,0 +1,153 @@ +import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; + +import layoutManager from '../../components/layoutManager'; +import loading from '../../components/loading/loading'; +import dom from '../../scripts/dom'; +import globalize from '../../scripts/globalize'; +import RecommendationContainer from '../../components/common/RecommendationContainer'; +import SectionContainer from '../../components/common/SectionContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const SuggestionsView: FC = ({topParentId}) => { + const [ latestItems, setLatestItems ] = useState([]); + const [ resumeResult, setResumeResult ] = useState({}); + const [ recommendations, setRecommendations ] = useState([]); + const element = useRef(null); + + const enableScrollX = useCallback(() => { + return !layoutManager.desktop; + }, []); + + const getPortraitShape = useCallback(() => { + return enableScrollX() ? 'overflowPortrait' : 'portrait'; + }, [enableScrollX]); + + const getThumbShape = useCallback(() => { + return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; + }, [enableScrollX]); + + const autoFocus = useCallback((page) => { + import('../../components/autoFocuser').then(({default: autoFocuser}) => { + autoFocuser.autoFocus(page); + }); + }, []); + + const loadResume = useCallback((page, userId, parentId) => { + loading.show(); + const screenWidth = dom.getWindowSize().innerWidth; + const options = { + SortBy: 'DatePlayed', + SortOrder: 'Descending', + IncludeItemTypes: 'Movie', + Filters: 'IsResumable', + Limit: screenWidth >= 1600 ? 5 : 3, + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + CollapseBoxSetItems: false, + ParentId: parentId, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + EnableTotalRecordCount: false + }; + window.ApiClient.getItems(userId, options).then(result => { + setResumeResult(result); + + loading.hide(); + autoFocus(page); + }); + }, [autoFocus]); + + const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => { + const options = { + IncludeItemTypes: 'Movie', + Limit: 18, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ParentId: parentId, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + EnableTotalRecordCount: false + }; + window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => { + setLatestItems(items); + + autoFocus(page); + }); + }, [autoFocus]); + + const loadSuggestions = useCallback((page, userId) => { + const screenWidth = dom.getWindowSize().innerWidth; + let itemLimit = 5; + if (screenWidth >= 1600) { + itemLimit = 8; + } else if (screenWidth >= 1200) { + itemLimit = 6; + } + const url = window.ApiClient.getUrl('Movies/Recommendations', { + userId: userId, + categoryLimit: 6, + ItemLimit: itemLimit, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb' + }); + window.ApiClient.getJSON(url).then(result => { + setRecommendations(result); + + autoFocus(page); + }); + }, [autoFocus]); + + const loadSuggestionsTab = useCallback((view) => { + const parentId = topParentId; + const userId = window.ApiClient.getCurrentUserId(); + loadResume(view, userId, parentId); + loadLatest(view, userId, parentId); + loadSuggestions(view, userId); + }, [loadLatest, loadResume, loadSuggestions, topParentId]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + loadSuggestionsTab(page); + }, [loadSuggestionsTab]); + + return ( +
+ + + + + {!recommendations.length ?
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoMovieSuggestionsAvailable')}

+
: recommendations.map((recommendation, index) => { + return ; + })} +
+ ); +}; + +export default SuggestionsView; diff --git a/src/routes/movies/TrailersView.tsx b/src/routes/movies/TrailersView.tsx new file mode 100644 index 0000000000..9e11d41b94 --- /dev/null +++ b/src/routes/movies/TrailersView.tsx @@ -0,0 +1,30 @@ + +import React, { FC, useCallback } from 'react'; + +import ViewItemsContainer from '../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../types/interface'; + +const TrailersView: FC = ({ topParentId }) => { + const getBasekey = useCallback(() => { + return 'trailers'; + }, []); + + const getItemTypes = useCallback(() => { + return ['Trailer']; + }, []); + + const getNoItemsMessage = useCallback(() => { + return 'MessageNoTrailersFound'; + }, []); + + return ( + + ); +}; + +export default TrailersView; diff --git a/src/routes/movies/index.tsx b/src/routes/movies/index.tsx new file mode 100644 index 0000000000..3b74b464d5 --- /dev/null +++ b/src/routes/movies/index.tsx @@ -0,0 +1,139 @@ +import '../../elements/emby-scroller/emby-scroller'; +import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import '../../elements/emby-tabs/emby-tabs'; +import '../../elements/emby-button/emby-button'; + +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import * as mainTabsManager from '../../components/maintabsmanager'; +import Page from '../../components/Page'; +import globalize from '../../scripts/globalize'; +import libraryMenu from '../../scripts/libraryMenu'; +import * as userSettings from '../../scripts/settings/userSettings'; +import CollectionsView from './CollectionsView'; +import FavoritesView from './FavoritesView'; +import GenresView from './GenresView'; +import MoviesView from './MoviesView'; +import SuggestionsView from './SuggestionsView'; +import TrailersView from './TrailersView'; + +const getDefaultTabIndex = (folderId: string | null) => { + switch (userSettings.get('landing-' + folderId, false)) { + case 'suggestions': + return 1; + + case 'favorites': + return 3; + + case 'collections': + return 4; + + case 'genres': + return 5; + + default: + return 0; + } +}; + +const getTabs = () => { + return [{ + name: globalize.translate('Movies') + }, { + name: globalize.translate('Suggestions') + }, { + name: globalize.translate('Trailers') + }, { + name: globalize.translate('Favorites') + }, { + name: globalize.translate('Collections') + }, { + name: globalize.translate('Genres') + }]; +}; + +const Movies: FC = () => { + const [ searchParams ] = useSearchParams(); + const currentTabIndex = parseInt(searchParams.get('tab') || getDefaultTabIndex(searchParams.get('topParentId')).toString()); + const [ selectedIndex, setSelectedIndex ] = useState(currentTabIndex); + const element = useRef(null); + + const getTabComponent = (index: number) => { + if (index == null) { + throw new Error('index cannot be null'); + } + + let component; + switch (index) { + case 0: + component = ; + break; + + case 1: + component = ; + break; + + case 2: + component = ; + break; + + case 3: + component = ; + break; + + case 4: + component = ; + break; + + case 5: + component = ; + break; + } + + return component; + }; + + const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; }; }) => { + const newIndex = parseInt(e.detail.selectedTabIndex); + setSelectedIndex(newIndex); + }, []); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + mainTabsManager.setTabs(page, selectedIndex, getTabs, undefined, undefined, onTabChange); + if (!page.getAttribute('data-title')) { + const parentId = searchParams.get('topParentId'); + + if (parentId) { + window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => { + page.setAttribute('data-title', item.Name as string); + libraryMenu.setTitle(item.Name); + }); + } else { + page.setAttribute('data-title', globalize.translate('Movies')); + libraryMenu.setTitle(globalize.translate('Movies')); + } + } + }, [onTabChange, searchParams, selectedIndex]); + + return ( +
+ + {getTabComponent(selectedIndex)} + + +
+ ); +}; + +export default Movies; diff --git a/src/scripts/dom.js b/src/scripts/dom.js index e336088c87..06d30235b2 100644 --- a/src/scripts/dom.js +++ b/src/scripts/dom.js @@ -145,9 +145,15 @@ windowSize = null; } + /** + * @typedef {Object} windowSize + * @property {number} innerHeight - window innerHeight. + * @property {number} innerWidth - window innerWidth. + */ + /** * Returns window size. - * @returns {Object} Window size. + * @returns {windowSize} Window size. */ export function getWindowSize() { if (!windowSize) { diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 0e82513765..a92ef28e1a 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -345,13 +345,6 @@ import { appRouter } from '../components/appRouter'; controller: 'livetvtuner' }); - defineRoute({ - alias: '/movies.html', - path: 'movies/movies.html', - autoFocus: false, - controller: 'movies/moviesrecommended' - }); - defineRoute({ alias: '/music.html', path: 'music/music.html', diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 062749020c..d670bb0102 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -467,11 +467,17 @@ export class UserSettings { return this.get('soundeffects', false); } + /** + * @typedef {Object} Query + * @property {number} StartIndex - query StartIndex. + * @property {number} Limit - query Limit. + */ + /** * Load query settings. * @param {string} key - Query key. * @param {Object} query - Query base. - * @return {Object} Query. + * @return {Query} Query. */ loadQuerySettings(key, query) { let values = this.get(key); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 4fa437b462..1487f8bf29 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -236,6 +236,7 @@ "EnableNextVideoInfoOverlayHelp": "At the end of a video, display info about the next video coming up in the current playlist.", "EnablePhotos": "Display the photos", "EnablePhotosHelp": "Images will be detected and displayed alongside other media files.", + "EnableCardLayout": "Display visual CardBox", "EnableRewatchingNextUp": "Enable Rewatching in Next Up", "EnableRewatchingNextUpHelp": "Enable showing already watched episodes in 'Next Up' sections.", "EnableQuickConnect": "Enable Quick Connect on this server", @@ -1065,6 +1066,8 @@ "MessageItemsAdded": "Items added.", "MessageItemSaved": "Item saved.", "MessageLeaveEmptyToInherit": "Leave empty to inherit settings from a parent item or the global default value.", + "MessageNoItemsAvailable": "No Items are currently available.", + "MessageNoFavoritesAvailable": "No favorites are currently available.", "MessageNoAvailablePlugins": "No available plugins.", "MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, and Albums. Click the '+' button to start creating collections.", "MessageNoGenresAvailable": "Enable some metadata providers to pull genres from the internet.", diff --git a/src/types/interface.ts b/src/types/interface.ts new file mode 100644 index 0000000000..67c6565e27 --- /dev/null +++ b/src/types/interface.ts @@ -0,0 +1,122 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; + +export interface Query extends ViewQuerySettings { + IncludeItemTypes?: string; + Recursive?: boolean; + Fields?: string | null; + ImageTypeLimit?: number; + EnableTotalRecordCount?: boolean; + EnableImageTypes?: string; + StartIndex?: number; + ParentId?: string | null; + IsMissing?: boolean | null; + Limit?:number; + Filters?: string | null; +} + +export interface ViewQuerySettings { + showTitle?: boolean; + showYear?: boolean; + imageType?: string; + viewType?: string; + cardLayout?: boolean; + SortBy?: string | null; + SortOrder?: string | null; + IsPlayed?: boolean | null; + IsUnplayed?: boolean | null; + IsFavorite?: boolean | null; + IsResumable?: boolean | null; + Is4K?: boolean | null; + IsHD?: boolean | null; + IsSD?: boolean | null; + Is3D?: boolean | null; + VideoTypes?: string | null; + SeriesStatus?: string | null; + HasSubtitles?: boolean | null; + HasTrailer?: boolean | null; + HasSpecialFeature?: boolean | null; + ParentIndexNumber?: boolean | null; + HasThemeSong?: boolean | null; + HasThemeVideo?: boolean | null; + GenreIds?: string | null; + NameLessThan?: string | null; + NameStartsWith?: string | null; + StartIndex?: number; +} + +export interface CardOptions { + itemsContainer?: HTMLElement | null; + parentContainer?: HTMLElement | null; + items?: BaseItemDto[] | null; + allowBottomPadding?: boolean; + centerText?: boolean; + coverImage?: boolean; + inheritThumb?: boolean; + overlayMoreButton?: boolean; + overlayPlayButton?: boolean; + overlayText?: boolean; + preferThumb?: boolean; + preferDisc?: boolean; + preferLogo?: boolean; + scalable?: boolean; + shape?: string | null; + lazy?: boolean; + cardLayout?: boolean | string; + showParentTitle?: boolean; + showParentTitleOrTitle?: boolean; + showAirTime?: boolean; + showAirDateTime?: boolean; + showChannelName?: boolean; + showTitle?: boolean | string; + showYear?: boolean | string; + showDetailsMenu?: boolean; + missingIndicator?: boolean; + showLocationTypeIndicator?: boolean; + showSeriesYear?: boolean; + showUnplayedIndicator?: boolean; + showChildCountIndicator?: boolean; + lines?: number; + context?: string | null; + action?: string | null; + defaultShape?: string; + indexBy?: string; + parentId?: string | null; + showMenu?: boolean; + cardCssClass?: string | null; + cardClass?: string | null; + centerPlayButton?: boolean; + overlayInfoButton?: boolean; + autoUpdate?: boolean; + cardFooterAside?: string; + includeParentInfoInTitle?: boolean; + maxLines?: number; + overlayMarkPlayedButton?: boolean; + overlayRateButton?: boolean; + showAirEndTime?: boolean; + showCurrentProgram?: boolean; + showCurrentProgramTime?: boolean; + showItemCounts?: boolean; + showPersonRoleOrType?: boolean; + showProgressBar?: boolean; + showPremiereDate?: boolean; + showRuntime?: boolean; + showSeriesTimerTime?: boolean; + showSeriesTimerChannel?: boolean; + showSongCount?: boolean; + width?: number; + showChannelLogo?: boolean; + showLogo?: boolean; + serverId?: string; + collectionId?: string | null; + playlistId?: string | null; + defaultCardImageIcon?: string; + disableHoverMenu?: boolean; + disableIndicators?: boolean; + showGroupCount?: boolean; + containerClass?: string; + noItemsMessage?: string; +} + +export interface LibraryViewProps { + topParentId: string | null; +}