diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx new file mode 100644 index 0000000000..14f519590e --- /dev/null +++ b/src/elements/ItemsContainerElement.tsx @@ -0,0 +1,28 @@ +import React, { FunctionComponent } from 'react'; + +const createButtonElement = ({ id, className }: IProps) => ({ + __html: `
+
` +}); + +type IProps = { + id?: string; + className?: string; +} + +const ItemsContainerElement: FunctionComponent = ({ id, className }: IProps) => { + return ( +
+ ); +}; + +export default ItemsContainerElement; 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.tsx b/src/routes/movies.tsx new file mode 100644 index 0000000000..bf9686c0dd --- /dev/null +++ b/src/routes/movies.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, { FunctionComponent, 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 '../view/movies/CollectionsView'; +import FavoritesView from '../view/movies/FavoritesView'; +import GenresView from '../view/movies/GenresView'; +import MoviesView from '../view/movies/MoviesView'; +import SuggestionsView from '../view/movies/SuggestionsView'; +import TrailersView from '../view/movies/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 Movies: FunctionComponent = () => { + const [ searchParams ] = useSearchParams(); + const currentTabIndex = parseInt(searchParams.get('tab') || getDefaultTabIndex(searchParams.get('topParentId')).toString()); + const [ selectedIndex, setSelectedIndex ] = useState(currentTabIndex); + const element = useRef(null); + + 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 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(element.current, 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/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/view/components/AlphaPickerContainer.tsx b/src/view/components/AlphaPickerContainer.tsx new file mode 100644 index 0000000000..dd4cc36f2e --- /dev/null +++ b/src/view/components/AlphaPickerContainer.tsx @@ -0,0 +1,48 @@ +import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import AlphaPicker from '../../components/alphaPicker/alphaPicker'; +import { IQuery } from './type'; + +type AlphaPickerProps = { + query: IQuery; + reloadItems: () => void; +}; + +const AlphaPickerContainer: FunctionComponent = ({ query, reloadItems }: AlphaPickerProps) => { + const [ alphaPicker, setAlphaPicker ] = useState(); + const element = useRef(null); + + alphaPicker?.updateControls(query); + + useEffect(() => { + const alphaPickerElement = element.current?.querySelector('.alphaPicker'); + + if (alphaPickerElement) { + alphaPickerElement.addEventListener('alphavaluechanged', (e) => { + const newValue = (e as CustomEvent).detail.value; + if (newValue === '#') { + query.NameLessThan = 'A'; + delete query.NameStartsWith; + } else { + query.NameStartsWith = newValue; + delete query.NameLessThan; + } + query.StartIndex = 0; + reloadItems(); + }); + setAlphaPicker(new AlphaPicker({ + element: alphaPickerElement, + valueChangeEvent: 'click' + })); + + alphaPickerElement.classList.add('alphaPicker-fixed-right'); + } + }, [query, reloadItems, setAlphaPicker]); + + return ( +
+
+
+ ); +}; + +export default AlphaPickerContainer; diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx new file mode 100644 index 0000000000..d0fc0274f9 --- /dev/null +++ b/src/view/components/Filter.tsx @@ -0,0 +1,51 @@ +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import { Events } from 'jellyfin-apiclient'; +import IconButtonElement from '../../elements/IconButtonElement'; +import { IQuery } from './type'; + +type FilterProps = { + query: IQuery; + reloadItems: () => void; +} + +const Filter: FunctionComponent = ({ query, reloadItems }: FilterProps) => { + const element = useRef(null); + + const showFilterMenu = useCallback(() => { + import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { + const filterDialog = new filterDialogFactory({ + query: query, + mode: 'movies', + serverId: window.ApiClient.serverId() + }); + Events.on(filterDialog, 'filterchange', () => { + query.StartIndex = 0; + reloadItems(); + }); + filterDialog.show(); + }); + }, [query, reloadItems]); + + useEffect(() => { + const btnFilter = element.current?.querySelector('.btnFilter'); + + if (btnFilter) { + btnFilter.addEventListener('click', () => { + showFilterMenu(); + }); + } + }, [showFilterMenu]); + + return ( +
+ +
+ ); +}; + +export default Filter; diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx new file mode 100644 index 0000000000..65ca30448a --- /dev/null +++ b/src/view/components/GenresItemsContainer.tsx @@ -0,0 +1,165 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; +import layoutManager from '../../components/layoutManager'; +import { appRouter } from '../../components/appRouter'; +import escapeHTML from 'escape-html'; +import '../../elements/emby-button/emby-button'; +import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import { IQuery } from './type'; + +type GenresItemsContainerProps = { + topParentId?: string | null; + getCurrentViewStyle: () => string; + query: IQuery; + itemsResult?: BaseItemDtoQueryResult; +} + +const GenresItemsContainer: FunctionComponent = ({ topParentId, getCurrentViewStyle, query, itemsResult = {} }: GenresItemsContainerProps) => { + 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 fillItemsContainer = useCallback((entry) => { + const elem = entry.target; + const id = elem.getAttribute('data-id'); + const viewStyle = 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: topParentId + }; + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + const items = result.Items || []; + if (viewStyle == 'Thumb') { + cardBuilder.buildCards(items, { + itemsContainer: elem, + shape: getThumbShape(), + preferThumb: true, + showTitle: true, + scalable: true, + centerText: true, + overlayMoreButton: true, + allowBottomPadding: false + }); + } else if (viewStyle == 'ThumbCard') { + cardBuilder.buildCards(items, { + itemsContainer: elem, + shape: getThumbShape(), + preferThumb: true, + showTitle: true, + scalable: true, + centerText: false, + cardLayout: true, + showYear: true + }); + } else if (viewStyle == 'PosterCard') { + cardBuilder.buildCards(items, { + itemsContainer: elem, + shape: getPortraitShape(), + showTitle: true, + scalable: true, + centerText: false, + cardLayout: true, + showYear: true + }); + } else if (viewStyle == 'Poster') { + cardBuilder.buildCards(items, { + itemsContainer: elem, + shape: getPortraitShape(), + scalable: true, + overlayMoreButton: true, + allowBottomPadding: true, + showTitle: true, + centerText: true, + showYear: true + }); + } + }); + }, [enableScrollX, getCurrentViewStyle, getPortraitShape, getThumbShape, topParentId]); + + useEffect(() => { + const elem = element.current?.querySelector('#items') as HTMLDivElement; + let html = ''; + const items = itemsResult.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 (!itemsResult.Items?.length) { + html = ''; + + html += '
'; + html += '

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

'; + html += '

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

'; + html += '
'; + } + + elem.innerHTML = html; + lazyLoader.lazyChildren(elem, fillItemsContainer); + }, [getCurrentViewStyle, query.SortBy, itemsResult.Items, fillItemsContainer, topParentId, enableScrollX]); + + return ( +
+
+
+ ); +}; + +export default GenresItemsContainer; diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx new file mode 100644 index 0000000000..ef3ef01fd6 --- /dev/null +++ b/src/view/components/ItemsContainer.tsx @@ -0,0 +1,110 @@ +import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import listview from '../../components/listview/listview'; +import globalize from '../../scripts/globalize'; +import imageLoader from '../../components/images/imageLoader'; +import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import { IQuery } from './type'; + +type ItemsContainerProps = { + getCurrentViewStyle: () => string; + query: IQuery; + items?: BaseItemDto[] | null; + noItemsMessage?: string; +} + +const ItemsContainer: FunctionComponent = ({ getCurrentViewStyle, query, items = [], noItemsMessage }: ItemsContainerProps) => { + const element = useRef(null); + + useEffect(() => { + let html; + const viewStyle = getCurrentViewStyle(); + if (viewStyle == 'Thumb') { + html = cardBuilder.getCardsHtml(items, { + 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: 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: 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: items, + shape: 'portrait', + context: 'movies', + showTitle: true, + showYear: true, + centerText: true, + lazy: true, + cardLayout: true + }); + } else { + html = cardBuilder.getCardsHtml(items, { + items: items, + shape: 'portrait', + context: 'movies', + overlayPlayButton: true, + showTitle: true, + showYear: true, + centerText: true + }); + } + + if (!items?.length) { + html = ''; + + html += '
'; + html += '

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

'; + html += '

' + globalize.translate(noItemsMessage) + '

'; + html += '
'; + } + + const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; + itemsContainer.innerHTML = html; + imageLoader.lazyChildren(itemsContainer); + }, [getCurrentViewStyle, query.SortBy, items, noItemsMessage]); + + return ( +
+ +
+ ); +}; + +export default ItemsContainer; diff --git a/src/view/components/NewCollection.tsx b/src/view/components/NewCollection.tsx new file mode 100644 index 0000000000..a68fa2c85d --- /dev/null +++ b/src/view/components/NewCollection.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import IconButtonElement from '../../elements/IconButtonElement'; + +const NewCollection: FunctionComponent = () => { + const element = useRef(null); + + useEffect(() => { + const btnNewCollection = element.current?.querySelector('.btnNewCollection') as HTMLButtonElement; + if (btnNewCollection) { + btnNewCollection.addEventListener('click', () => { + import('../../components/collectionEditor/collectionEditor').then(({ default: collectionEditor }) => { + const serverId = window.ApiClient.serverId(); + new collectionEditor({ + items: [], + serverId: serverId + }); + }); + }); + } + }, []); + + return ( +
+ +
+ ); +}; + +export default NewCollection; diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx new file mode 100644 index 0000000000..e315e53f31 --- /dev/null +++ b/src/view/components/Pagination.tsx @@ -0,0 +1,65 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; +import libraryBrowser from '../../scripts/libraryBrowser'; + +import * as userSettings from '../../scripts/settings/userSettings'; +import { IQuery } from './type'; + +type PaginationProps = { + query: IQuery; + itemsResult?: BaseItemDtoQueryResult; + reloadItems: () => void; +} + +const Pagination: FunctionComponent = ({ query, itemsResult = {}, reloadItems }: PaginationProps) => { + const element = useRef(null); + useEffect(() => { + function onNextPageClick() { + if (userSettings.libraryPageSize(undefined) > 0) { + query.StartIndex += query.Limit; + } + reloadItems(); + } + + function onPreviousPageClick() { + if (userSettings.libraryPageSize(undefined) > 0) { + query.StartIndex = Math.max(0, query.StartIndex - query.Limit); + } + reloadItems(); + } + const pagingHtml = libraryBrowser.getQueryPagingHtml({ + startIndex: query.StartIndex, + limit: query.Limit, + totalRecordCount: itemsResult.TotalRecordCount, + showLimit: false, + updatePageSizeSetting: false, + addLayoutButton: false, + sortButton: false, + filterButton: false + }); + + const paging = element.current?.querySelector('.paging') as HTMLDivElement; + paging.innerHTML = pagingHtml; + + const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement; + if (btnNextPage) { + btnNextPage.addEventListener('click', onNextPageClick); + } + + const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement; + if (btnPreviousPage) { + btnPreviousPage.addEventListener('click', onPreviousPageClick); + } + }, [itemsResult, query, reloadItems]); + + return ( +
+
+
+ + ); +}; + +export default Pagination; diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx new file mode 100644 index 0000000000..8c74f13ab4 --- /dev/null +++ b/src/view/components/SelectView.tsx @@ -0,0 +1,45 @@ +import React, { FunctionComponent, useEffect, useRef } from 'react'; +import IconButtonElement from '../../elements/IconButtonElement'; + +import libraryBrowser from '../../scripts/libraryBrowser'; +import * as userSettings from '../../scripts/settings/userSettings'; +import { IQuery } from './type'; + +type SelectViewProps = { + getCurrentViewStyle: () => string; + query: IQuery; + savedViewKey: string; + onViewStyleChange: () => void; + reloadItems: () => void; +} + +const SelectView: FunctionComponent = ({ getCurrentViewStyle, savedViewKey, query, onViewStyleChange, reloadItems }: SelectViewProps) => { + const element = useRef(null); + + useEffect(() => { + const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement; + btnSelectView.addEventListener('click', (e) => { + libraryBrowser.showLayoutMenu(e.target, getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(',')); + }); + btnSelectView.addEventListener('layoutchange', (e) => { + const viewStyle = (e as CustomEvent).detail.viewStyle; + userSettings.set(savedViewKey, viewStyle, false); + query.StartIndex = 0; + onViewStyleChange(); + reloadItems(); + }); + }, [getCurrentViewStyle, onViewStyleChange, query, reloadItems, savedViewKey]); + + return ( +
+ +
+ ); +}; + +export default SelectView; diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx new file mode 100644 index 0000000000..fe8d92023f --- /dev/null +++ b/src/view/components/Shuffle.tsx @@ -0,0 +1,44 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; + +import { playbackManager } from '../../components/playback/playbackmanager'; +import IconButtonElement from '../../elements/IconButtonElement'; + +type ShuffleProps = { + itemsResult?: BaseItemDtoQueryResult; + topParentId: string | null; +} + +const Shuffle: FunctionComponent = ({ itemsResult = {}, topParentId }: ShuffleProps) => { + 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') as HTMLButtonElement; + btnShuffle.classList.toggle('hide', typeof itemsResult.TotalRecordCount === 'number' && itemsResult.TotalRecordCount < 1); + if (btnShuffle) { + btnShuffle.addEventListener('click', shuffle); + } + }, [itemsResult.TotalRecordCount, shuffle]); + + return ( +
+ +
+ ); +}; + +export default Shuffle; diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx new file mode 100644 index 0000000000..5b9a2e58c6 --- /dev/null +++ b/src/view/components/Sort.tsx @@ -0,0 +1,48 @@ +import React, { FunctionComponent, useEffect, useRef } from 'react'; +import IconButtonElement from '../../elements/IconButtonElement'; +import libraryBrowser from '../../scripts/libraryBrowser'; +import * as userSettings from '../../scripts/settings/userSettings'; +import { IQuery } from './type'; + +type SortProps = { + SortMenuOptions: () => { name: string; id: string}[]; + query: IQuery; + savedQueryKey: string; + reloadItems: () => void; +} + +const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { + const element = useRef(null); + + useEffect(() => { + const btnSort = element.current?.querySelector('.btnSort'); + + if (btnSort) { + btnSort.addEventListener('click', (e) => { + libraryBrowser.showSortMenu({ + items: SortMenuOptions(), + callback: () => { + query.StartIndex = 0; + userSettings.saveQuerySettings(savedQueryKey, query); + reloadItems(); + }, + query: query, + button: e.target + }); + }); + } + }, [SortMenuOptions, query, reloadItems, savedQueryKey]); + + return ( +
+ +
+ ); +}; + +export default Sort; diff --git a/src/view/components/type.ts b/src/view/components/type.ts new file mode 100644 index 0000000000..2dde6bb24f --- /dev/null +++ b/src/view/components/type.ts @@ -0,0 +1,15 @@ +export type IQuery = { + SortBy?: string; + SortOrder?: string; + IncludeItemTypes?: string; + Recursive?: boolean; + Fields?: string; + ImageTypeLimit?: number; + EnableImageTypes?: string; + StartIndex: number; + ParentId?: string | null; + IsFavorite?: boolean; + Limit:number; + NameLessThan?: string; + NameStartsWith?: string; +} diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx new file mode 100644 index 0000000000..46b030ac18 --- /dev/null +++ b/src/view/movies/CollectionsView.tsx @@ -0,0 +1,122 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import globalize from '../../scripts/globalize'; +import * as userSettings from '../../scripts/settings/userSettings'; +import ItemsContainer from '../components/ItemsContainer'; +import NewCollection from '../components/NewCollection'; +import Pagination from '../components/Pagination'; +import SelectView from '../components/SelectView'; +import Sort from '../components/Sort'; +import { IQuery } from '../components/type'; + +const SortMenuOptions = () => { + return [{ + name: globalize.translate('Name'), + id: 'SortName' + }, { + name: globalize.translate('OptionDateAdded'), + id: 'DateCreated,SortName' + }]; +}; + +type IProps = { + topParentId: string | null; +} + +const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-moviecollections'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'BoxSet', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,SortName', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId }), [topParentId]); + + userSettings.loadQuerySettings(savedQueryKey, query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(savedViewKey, false) || 'Poster'; + }, [savedViewKey]); + + const reloadItems = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + loading.show(); + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + setItemsResult(result); + + window.scrollTo(0, 0); + + loading.hide(); + + import('../../components/autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); + }); + }, [query]); + + const onViewStyleChange = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + const viewStyle = getCurrentViewStyle(); + const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement; + 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 = ''; + }, [getCurrentViewStyle]); + + useEffect(() => { + onViewStyleChange(); + reloadItems(); + }, [onViewStyleChange, reloadItems]); + + return ( +
+
+ + + + + + +
+ + + +
+ +
+
+ ); +}; + +export default CollectionsView; diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx new file mode 100644 index 0000000000..b94767b45f --- /dev/null +++ b/src/view/movies/FavoritesView.tsx @@ -0,0 +1,147 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import globalize from '../../scripts/globalize'; +import * as userSettings from '../../scripts/settings/userSettings'; +import AlphaPickerContainer from '../components/AlphaPickerContainer'; +import Filter from '../components/Filter'; +import ItemsContainer from '../components/ItemsContainer'; +import Pagination from '../components/Pagination'; +import SelectView from '../components/SelectView'; +import Sort from '../components/Sort'; +import { IQuery } from '../components/type'; + +type IProps = { + topParentId: string | null; +} + +const SortMenuOptions = () => { + return [{ + 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' + }]; +}; + +const FavoritesView: FunctionComponent = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-favorites'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + SortBy: 'SortName,ProductionYear', + SortOrder: 'Ascending', + IncludeItemTypes: 'Movie', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + IsFavorite: true, + StartIndex: 0, + ParentId: topParentId }), [topParentId]); + + userSettings.loadQuerySettings(savedQueryKey, query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(savedViewKey, false) || 'Poster'; + }, [savedViewKey]); + + const reloadItems = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + loading.show(); + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + setItemsResult(result); + window.scrollTo(0, 0); + loading.hide(); + + import('../../components/autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); + }); + }, [query]); + + const onViewStyleChange = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + const viewStyle = getCurrentViewStyle(); + const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement; + 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 = ''; + }, [getCurrentViewStyle]); + + useEffect(() => { + onViewStyleChange(); + reloadItems(); + }, [onViewStyleChange, query, reloadItems]); + + return ( +
+
+ + + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default FavoritesView; diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx new file mode 100644 index 0000000000..b37d5fbecf --- /dev/null +++ b/src/view/movies/GenresView.tsx @@ -0,0 +1,61 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import * as userSettings from '../../scripts/settings/userSettings'; +import GenresItemsContainer from '../components/GenresItemsContainer'; +import { IQuery } from '../components/type'; + +type IProps = { + topParentId: string | null; +} + +const GenresView: FunctionComponent = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-moviegenres'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'Movie', + Recursive: true, + EnableTotalRecordCount: false, + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId }), [topParentId]); + + userSettings.loadQuerySettings(savedQueryKey, query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(savedViewKey, false) || 'Poster'; + }, [savedViewKey]); + + const reloadItems = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + loading.show(); + window.ApiClient.getGenres(window.ApiClient.getCurrentUserId(), query).then((result) => { + setItemsResult(result); + loading.hide(); + }); + }, [query]); + + useEffect(() => { + reloadItems(); + }, [reloadItems]); + return ( +
+ +
+ ); +}; + +export default GenresView; diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx new file mode 100644 index 0000000000..74e72da160 --- /dev/null +++ b/src/view/movies/MoviesView.tsx @@ -0,0 +1,153 @@ +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import globalize from '../../scripts/globalize'; +import * as userSettings from '../../scripts/settings/userSettings'; +import AlphaPickerContainer from '../components/AlphaPickerContainer'; +import Filter from '../components/Filter'; +import ItemsContainer from '../components/ItemsContainer'; +import Pagination from '../components/Pagination'; +import SelectView from '../components/SelectView'; +import Shuffle from '../components/Shuffle'; +import Sort from '../components/Sort'; +import { IQuery } from '../components/type'; + +type IProps = { + topParentId: string | null; +} + +const SortMenuOptions = () => { + return [{ + 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' + }]; +}; + +const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-movies'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState(); + + const element = useRef(null); + + const query = useMemo(() => ({ + SortBy: 'SortName,ProductionYear', + SortOrder: 'Ascending', + IncludeItemTypes: 'Movie', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId }), [topParentId]); + + userSettings.loadQuerySettings(savedQueryKey, query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(savedViewKey, false) || 'Poster'; + }, [savedViewKey]); + + const reloadItems = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + loading.show(); + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + setItemsResult(result); + window.scrollTo(0, 0); + + loading.hide(); + + import('../../components/autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); + }); + }, [query]); + + const onViewStyleChange = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + const viewStyle = getCurrentViewStyle(); + const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement; + 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 = ''; + }, [getCurrentViewStyle]); + + useEffect(() => { + onViewStyleChange(); + }, [onViewStyleChange]); + + useEffect(() => { + reloadItems(); + }, [onViewStyleChange, query, reloadItems]); + + return ( +
+
+ + + + + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default MoviesView; diff --git a/src/view/movies/ResumableItems.tsx b/src/view/movies/ResumableItems.tsx new file mode 100644 index 0000000000..692c0fdd5c --- /dev/null +++ b/src/view/movies/ResumableItems.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +function ResumableItems() { + return ( +
ResumableItems
+ ); +} + +export default ResumableItems; diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx new file mode 100644 index 0000000000..1de301f2a9 --- /dev/null +++ b/src/view/movies/SuggestionsView.tsx @@ -0,0 +1,294 @@ +import escapeHtml from 'escape-html'; +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import imageLoader from '../../components/images/imageLoader'; +import layoutManager from '../../components/layoutManager'; +import loading from '../../components/loading/loading'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import dom from '../../scripts/dom'; +import globalize from '../../scripts/globalize'; + +type IProps = { + topParentId: string | null; +} + +const SuggestionsView: FunctionComponent = (props: IProps) => { + 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 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 => { + 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); + }); + }, [autoFocus, enableScrollX, getPortraitShape]); + + const loadResume = useCallback((page, userId, parentId) => { + loading.show(); + const screenWidth: any = dom.getWindowSize(); + const options = { + SortBy: 'DatePlayed', + SortOrder: 'Descending', + IncludeItemTypes: 'Movie', + Filters: 'IsResumable', + Limit: screenWidth.innerWidth >= 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 => { + 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 + }); + loading.hide(); + // FIXME: Wait for all sections to load + autoFocus(page); + }); + }, [autoFocus, enableScrollX, getThumbShape]); + + const getRecommendationHtml = useCallback((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; + }, [enableScrollX, getPortraitShape]); + + const loadSuggestions = useCallback((page, userId) => { + const screenWidth: any = dom.getWindowSize(); + let itemLimit = 5; + if (screenWidth.innerWidth >= 1600) { + itemLimit = 8; + } else if (screenWidth.innerWidth >= 1200) { + itemLimit = 6; + } + const url = window.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(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); + }); + }, [autoFocus, getRecommendationHtml]); + + const loadSuggestionsTab = useCallback((view) => { + const parentId = props.topParentId; + const userId = window.ApiClient.getCurrentUserId(); + loadResume(view, userId, parentId); + loadLatest(view, userId, parentId); + loadSuggestions(view, userId); + }, [loadLatest, loadResume, loadSuggestions, props.topParentId]); + + const initSuggestedTab = useCallback((tabContent) => { + function setScrollClasses(elem: { classList: { add: (arg0: string) => void; remove: (arg0: string) => void; }; }, scrollX: boolean) { + 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'); + } + } + const containers = tabContent.querySelectorAll('.itemsContainer'); + + for (const container of containers) { + setScrollClasses(container, enableScrollX()); + } + }, [enableScrollX]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + initSuggestedTab(page); + }, [initSuggestedTab]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + loadSuggestionsTab(page); + }, [loadSuggestionsTab]); + return ( +
+
+
+

+ {globalize.translate('HeaderContinueWatching')} +

+
+ + + +
+ +
+
+

+ {globalize.translate('HeaderLatestMovies')} +

+
+ + + +
+ +
+
+
+
+

+ {globalize.translate('MessageNoMovieSuggestionsAvailable')} +

+
+
+ ); +}; + +export default SuggestionsView; diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx new file mode 100644 index 0000000000..3eb6909b72 --- /dev/null +++ b/src/view/movies/TrailersView.tsx @@ -0,0 +1,132 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import loading from '../../components/loading/loading'; +import globalize from '../../scripts/globalize'; +import * as userSettings from '../../scripts/settings/userSettings'; +import AlphaPickerContainer from '../components/AlphaPickerContainer'; +import Filter from '../components/Filter'; +import ItemsContainer from '../components/ItemsContainer'; +import Pagination from '../components/Pagination'; +import Sort from '../components/Sort'; +import { IQuery } from '../components/type'; + +const SortMenuOptions = () => { + return [{ + 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' + }]; +}; + +type IProps = { + topParentId: string | null; +} + +const TrailersView: FunctionComponent = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-trailers'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState(); + const element = useRef(null); + + const query = useMemo(() => ({ + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'Trailer', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId }), [topParentId]); + + userSettings.loadQuerySettings(savedQueryKey, query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(savedViewKey, false) || 'Poster'; + }, [savedViewKey]); + + const reloadItems = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + loading.show(); + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + setItemsResult(result); + window.scrollTo(0, 0); + + loading.hide(); + }); + }, [query]); + + const onViewStyleChange = useCallback(() => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + const viewStyle = getCurrentViewStyle(); + const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement; + 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 = ''; + }, [getCurrentViewStyle]); + + useEffect(() => { + onViewStyleChange(); + reloadItems(); + }, [onViewStyleChange, query, reloadItems]); + + return ( +
+
+ + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default TrailersView;