diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx index 14f519590e..44ea3de2df 100644 --- a/src/elements/ItemsContainerElement.tsx +++ b/src/elements/ItemsContainerElement.tsx @@ -1,9 +1,9 @@ import React, { FunctionComponent } from 'react'; -const createButtonElement = ({ id, className }: IProps) => ({ +const createElement = ({ id, className }: IProps) => ({ __html: `
` @@ -17,8 +17,8 @@ type IProps = { const ItemsContainerElement: FunctionComponent = ({ id, className }: IProps) => { return (
diff --git a/src/elements/ItemsScrollerContainerElement.tsx b/src/elements/ItemsScrollerContainerElement.tsx new file mode 100644 index 0000000000..438944c26c --- /dev/null +++ b/src/elements/ItemsScrollerContainerElement.tsx @@ -0,0 +1,43 @@ +import React, { FunctionComponent } from 'react'; + +const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => ({ + __html: `
+
+
+
` +}); + +type IProps = { + scrollerclassName?: string; + dataHorizontal?: string; + dataMousewheel?: string; + dataCenterfocus?: string; + id?: string; + className?: string; +} + +const ItemsScrollerContainerElement: FunctionComponent = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => { + return ( +
+ ); +}; + +export default ItemsScrollerContainerElement; diff --git a/src/view/components/RecentlyAddedItemsContainer.tsx b/src/view/components/RecentlyAddedItemsContainer.tsx new file mode 100644 index 0000000000..c252b917c0 --- /dev/null +++ b/src/view/components/RecentlyAddedItemsContainer.tsx @@ -0,0 +1,60 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; + +type RecentlyAddedItemsContainerProps = { + getPortraitShape: () => string; + enableScrollX: () => boolean; + items?: BaseItemDto[]; +} + +const RecentlyAddedItemsContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, items = [] }: RecentlyAddedItemsContainerProps) => { + const element = useRef(null); + + useEffect(() => { + const section = element.current?.querySelector('#recentlyAddedItemsSection') as HTMLDivElement; + if (items?.length) { + section.classList.remove('hide'); + } else { + section.classList.add('hide'); + } + + const allowBottomPadding = !enableScrollX(); + const container = element.current?.querySelector('#recentlyAddedItems'); + cardBuilder.buildCards(items, { + itemsContainer: container, + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getPortraitShape, items]); + + return ( +
+
+
+

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

+
+ + + +
+
+ ); +}; + +export default RecentlyAddedItemsContainer; diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx new file mode 100644 index 0000000000..2fbef7b20e --- /dev/null +++ b/src/view/components/RecommendationContainer.tsx @@ -0,0 +1,80 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; +import escapeHTML from 'escape-html'; + +type RecommendationContainerProps = { + getPortraitShape: () => string; + enableScrollX: () => boolean; + recommendation?: RecommendationDto; +} + +const RecommendationContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => { + const element = useRef(null); + + 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; + } + + useEffect(() => { + cardBuilder.buildCards(recommendation.Items || [], { + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current, + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: true, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getPortraitShape, recommendation]); + + return ( +
+
+
+

+ {escapeHTML(title)} +

+
+ + {enableScrollX() ? : } + +
+
+ ); +}; + +export default RecommendationContainer; diff --git a/src/view/components/ResumableItemsContainer.tsx b/src/view/components/ResumableItemsContainer.tsx new file mode 100644 index 0000000000..cb11ea7d45 --- /dev/null +++ b/src/view/components/ResumableItemsContainer.tsx @@ -0,0 +1,62 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; + +type ResumableItemsContainerProps = { + getThumbShape: () => string; + enableScrollX: () => boolean; + itemsResult?: BaseItemDtoQueryResult; +} + +const ResumableItemsContainer: FunctionComponent = ({ getThumbShape, enableScrollX, itemsResult = {} }: ResumableItemsContainerProps) => { + const element = useRef(null); + + useEffect(() => { + const section = element.current?.querySelector('#resumableSection') as HTMLDivElement; + if (itemsResult.Items?.length) { + section.classList.remove('hide'); + } else { + section.classList.add('hide'); + } + + const allowBottomPadding = !enableScrollX(); + const container = element.current?.querySelector('#resumableItems') as HTMLDivElement; + cardBuilder.buildCards(itemsResult.Items || [], { + itemsContainer: container, + preferThumb: true, + shape: getThumbShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + cardLayout: false, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getThumbShape, itemsResult.Items]); + + return ( +
+
+
+

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

+
+ + + +
+
+ ); +}; + +export default ResumableItemsContainer; diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 5b9a2e58c6..9cdb127777 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -5,13 +5,13 @@ import * as userSettings from '../../scripts/settings/userSettings'; import { IQuery } from './type'; type SortProps = { - SortMenuOptions: () => { name: string; id: string}[]; + sortMenuOptions: () => { name: string; id: string}[]; query: IQuery; savedQueryKey: string; reloadItems: () => void; } -const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { +const Sort: FunctionComponent = ({ sortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { const element = useRef(null); useEffect(() => { @@ -20,7 +20,7 @@ const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQuery if (btnSort) { btnSort.addEventListener('click', (e) => { libraryBrowser.showSortMenu({ - items: SortMenuOptions(), + items: sortMenuOptions(), callback: () => { query.StartIndex = 0; userSettings.saveQuerySettings(savedQueryKey, query); @@ -31,7 +31,7 @@ const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQuery }); }); } - }, [SortMenuOptions, query, reloadItems, savedQueryKey]); + }, [sortMenuOptions, query, reloadItems, savedQueryKey]); return (
diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 46b030ac18..17ddbe4d17 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -105,7 +105,7 @@ const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => - +
diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index b94767b45f..81388f2c1c 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -128,7 +128,7 @@ const FavoritesView: FunctionComponent = ({ topParentId }: IProps) => { - +
diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index 4a6d19be6d..0db029c6e3 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -134,7 +134,7 @@ const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { - +
diff --git a/src/view/movies/ResumableItems.tsx b/src/view/movies/ResumableItems.tsx deleted file mode 100644 index 692c0fdd5c..0000000000 --- a/src/view/movies/ResumableItems.tsx +++ /dev/null @@ -1,9 +0,0 @@ -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 index 1de301f2a9..10d965570b 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -1,19 +1,22 @@ -import escapeHtml from 'escape-html'; -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useRef, useState } 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'; +import RecentlyAddedItemsContainer from '../components/RecentlyAddedItemsContainer'; +import RecommendationContainer from '../components/RecommendationContainer'; +import ResumableItemsContainer from '../components/ResumableItemsContainer'; type IProps = { topParentId: string | null; } const SuggestionsView: FunctionComponent = (props: IProps) => { + const [ latestItems, setLatestItems ] = useState([]); + const [ resumeItemsResult, setResumeItemsResult ] = useState(); + const [ recommendations, setRecommendations ] = useState([]); const element = useRef(null); const enableScrollX = useCallback(() => { @@ -45,23 +48,12 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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 - }); + setLatestItems(items); // FIXME: Wait for all sections to load autoFocus(page); }); - }, [autoFocus, enableScrollX, getPortraitShape]); + }, [autoFocus]); const loadResume = useCallback((page, userId, parentId) => { loading.show(); @@ -81,84 +73,13 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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'); - } + setResumeItemsResult(result); - 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]); + }, [autoFocus]); const loadSuggestions = useCallback((page, userId) => { const screenWidth: any = dom.getWindowSize(); @@ -177,22 +98,12 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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); + setRecommendations(recommendations); // FIXME: Wait for all sections to load autoFocus(page); }); - }, [autoFocus, getRecommendationHtml]); + }, [autoFocus]); const loadSuggestionsTab = useCallback((view) => { const parentId = props.topParentId; @@ -202,32 +113,40 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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'); + const setScrollClasses = useCallback((elem, scrollX) => { + const page = element.current; - 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'); - } + if (!page) { + console.error('Unexpected null reference'); + return; } - const containers = tabContent.querySelectorAll('.itemsContainer'); + + 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 initSuggestedTab = useCallback((view) => { + const containers = view.querySelectorAll('.itemsContainer'); for (const container of containers) { setScrollClasses(container, enableScrollX()); } - }, [enableScrollX]); + }, [enableScrollX, setScrollClasses]); useEffect(() => { const page = element.current; @@ -238,54 +157,23 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { } initSuggestedTab(page); - }, [initSuggestedTab]); - - useEffect(() => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } loadSuggestionsTab(page); - }, [loadSuggestionsTab]); + }, [initSuggestedTab, loadSuggestionsTab]); + return (
-
-
-

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

-
+ - + -
- -
-
-

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

-
- - - -
- -
-
-
-
-

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

+
+ {!recommendations.length ?
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoMovieSuggestionsAvailable')}

+
: recommendations.map((recommendation, index) => { + return ; + })} + {}
); diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 3eb6909b72..f856b142e2 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -113,7 +113,7 @@ const TrailersView: FunctionComponent = ({ topParentId }: IProps) => {
- +