mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
splitting SuggestionsView component
This commit is contained in:
parent
f4b878bea2
commit
7543e494c9
12 changed files with 311 additions and 187 deletions
|
@ -1,9 +1,9 @@
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
const createButtonElement = ({ id, className }: IProps) => ({
|
const createElement = ({ id, className }: IProps) => ({
|
||||||
__html: `<div
|
__html: `<div
|
||||||
is="emby-itemscontainer"
|
is="emby-itemscontainer"
|
||||||
id="${id}"
|
${id}
|
||||||
class="${className}"
|
class="${className}"
|
||||||
>
|
>
|
||||||
</div>`
|
</div>`
|
||||||
|
@ -17,8 +17,8 @@ type IProps = {
|
||||||
const ItemsContainerElement: FunctionComponent<IProps> = ({ id, className }: IProps) => {
|
const ItemsContainerElement: FunctionComponent<IProps> = ({ id, className }: IProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={createButtonElement({
|
dangerouslySetInnerHTML={createElement({
|
||||||
id: id,
|
id: id ? `id='${id}'` : '',
|
||||||
className: className
|
className: className
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
43
src/elements/ItemsScrollerContainerElement.tsx
Normal file
43
src/elements/ItemsScrollerContainerElement.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => ({
|
||||||
|
__html: `<div is="emby-scroller"
|
||||||
|
class="${scrollerclassName}"
|
||||||
|
${dataHorizontal}
|
||||||
|
${dataMousewheel}
|
||||||
|
${dataCenterfocus}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
is="emby-itemscontainer"
|
||||||
|
${id}
|
||||||
|
class="${className}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
scrollerclassName?: string;
|
||||||
|
dataHorizontal?: string;
|
||||||
|
dataMousewheel?: string;
|
||||||
|
dataCenterfocus?: string;
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsScrollerContainerElement: FunctionComponent<IProps> = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createScroller({
|
||||||
|
scrollerclassName: scrollerclassName,
|
||||||
|
dataHorizontal: dataHorizontal ? `data-horizontal="${dataHorizontal}"` : '',
|
||||||
|
dataMousewheel: dataMousewheel ? `data-mousewheel="${dataMousewheel}"` : '',
|
||||||
|
dataCenterfocus: dataCenterfocus ? `data-centerfocus="${dataCenterfocus}"` : '',
|
||||||
|
id: id ? `id='${id}'` : '',
|
||||||
|
className: className
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsScrollerContainerElement;
|
60
src/view/components/RecentlyAddedItemsContainer.tsx
Normal file
60
src/view/components/RecentlyAddedItemsContainer.tsx
Normal file
|
@ -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<RecentlyAddedItemsContainerProps> = ({ getPortraitShape, enableScrollX, items = [] }: RecentlyAddedItemsContainerProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={element}>
|
||||||
|
<div id='recentlyAddedItemsSection' className='verticalSection hide'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{globalize.translate('HeaderLatestMovies')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemsContainerElement
|
||||||
|
id='recentlyAddedItems'
|
||||||
|
className='itemsContainer padded-left padded-right'
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentlyAddedItemsContainer;
|
80
src/view/components/RecommendationContainer.tsx
Normal file
80
src/view/components/RecommendationContainer.tsx
Normal file
|
@ -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<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{escapeHTML(title)}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enableScrollX() ? <ItemsScrollerContainerElement
|
||||||
|
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
||||||
|
dataMousewheel='false'
|
||||||
|
dataCenterfocus='true'
|
||||||
|
className='itemsContainer scrollSlider focuscontainer-x'
|
||||||
|
/> : <ItemsContainerElement
|
||||||
|
className='itemsContainer focuscontainer-x padded-left padded-right vertical-wrap'
|
||||||
|
/>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendationContainer;
|
62
src/view/components/ResumableItemsContainer.tsx
Normal file
62
src/view/components/ResumableItemsContainer.tsx
Normal file
|
@ -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<ResumableItemsContainerProps> = ({ getThumbShape, enableScrollX, itemsResult = {} }: ResumableItemsContainerProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={element}>
|
||||||
|
<div id='resumableSection' className='verticalSection hide'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{globalize.translate('HeaderContinueWatching')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemsContainerElement
|
||||||
|
id='resumableItems'
|
||||||
|
className='itemsContainer padded-left padded-right'
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResumableItemsContainer;
|
|
@ -5,13 +5,13 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
import { IQuery } from './type';
|
import { IQuery } from './type';
|
||||||
|
|
||||||
type SortProps = {
|
type SortProps = {
|
||||||
SortMenuOptions: () => { name: string; id: string}[];
|
sortMenuOptions: () => { name: string; id: string}[];
|
||||||
query: IQuery;
|
query: IQuery;
|
||||||
savedQueryKey: string;
|
savedQueryKey: string;
|
||||||
reloadItems: () => void;
|
reloadItems: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sort: FunctionComponent<SortProps> = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => {
|
const Sort: FunctionComponent<SortProps> = ({ sortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -20,7 +20,7 @@ const Sort: FunctionComponent<SortProps> = ({ SortMenuOptions, query, savedQuery
|
||||||
if (btnSort) {
|
if (btnSort) {
|
||||||
btnSort.addEventListener('click', (e) => {
|
btnSort.addEventListener('click', (e) => {
|
||||||
libraryBrowser.showSortMenu({
|
libraryBrowser.showSortMenu({
|
||||||
items: SortMenuOptions(),
|
items: sortMenuOptions(),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
query.StartIndex = 0;
|
query.StartIndex = 0;
|
||||||
userSettings.saveQuerySettings(savedQueryKey, query);
|
userSettings.saveQuerySettings(savedQueryKey, query);
|
||||||
|
@ -31,7 +31,7 @@ const Sort: FunctionComponent<SortProps> = ({ SortMenuOptions, query, savedQuery
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [SortMenuOptions, query, reloadItems, savedQueryKey]);
|
}, [sortMenuOptions, query, reloadItems, savedQueryKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element}>
|
<div ref={element}>
|
||||||
|
|
|
@ -105,7 +105,7 @@ const CollectionsView: FunctionComponent<IProps> = ({ topParentId }: IProps) =>
|
||||||
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
<Sort sortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
<NewCollection />
|
<NewCollection />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -128,7 +128,7 @@ const FavoritesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
<Sort sortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
<Filter query={query} reloadItems={reloadItems} />
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -134,7 +134,7 @@ const MoviesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
<Shuffle itemsResult= {itemsResult} topParentId={topParentId} />
|
<Shuffle itemsResult= {itemsResult} topParentId={topParentId} />
|
||||||
|
|
||||||
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
<Sort sortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
<Filter query={query} reloadItems={reloadItems} />
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function ResumableItems() {
|
|
||||||
return (
|
|
||||||
<div>ResumableItems</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResumableItems;
|
|
|
@ -1,19 +1,22 @@
|
||||||
import escapeHtml from 'escape-html';
|
import { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
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 layoutManager from '../../components/layoutManager';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from '../../components/loading/loading';
|
||||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
|
||||||
import dom from '../../scripts/dom';
|
import dom from '../../scripts/dom';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
import RecentlyAddedItemsContainer from '../components/RecentlyAddedItemsContainer';
|
||||||
|
import RecommendationContainer from '../components/RecommendationContainer';
|
||||||
|
import ResumableItemsContainer from '../components/ResumableItemsContainer';
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
topParentId: string | null;
|
topParentId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
|
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
||||||
|
const [ resumeItemsResult, setResumeItemsResult ] = useState<BaseItemDtoQueryResult>();
|
||||||
|
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const enableScrollX = useCallback(() => {
|
const enableScrollX = useCallback(() => {
|
||||||
|
@ -45,23 +48,12 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
EnableTotalRecordCount: false
|
EnableTotalRecordCount: false
|
||||||
};
|
};
|
||||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => {
|
window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => {
|
||||||
const allowBottomPadding = !enableScrollX();
|
setLatestItems(items);
|
||||||
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
|
// FIXME: Wait for all sections to load
|
||||||
autoFocus(page);
|
autoFocus(page);
|
||||||
});
|
});
|
||||||
}, [autoFocus, enableScrollX, getPortraitShape]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
const loadResume = useCallback((page, userId, parentId) => {
|
const loadResume = useCallback((page, userId, parentId) => {
|
||||||
loading.show();
|
loading.show();
|
||||||
|
@ -81,84 +73,13 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
EnableTotalRecordCount: false
|
EnableTotalRecordCount: false
|
||||||
};
|
};
|
||||||
window.ApiClient.getItems(userId, options).then(result => {
|
window.ApiClient.getItems(userId, options).then(result => {
|
||||||
if (result.Items?.length) {
|
setResumeItemsResult(result);
|
||||||
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();
|
loading.hide();
|
||||||
// FIXME: Wait for all sections to load
|
// FIXME: Wait for all sections to load
|
||||||
autoFocus(page);
|
autoFocus(page);
|
||||||
});
|
});
|
||||||
}, [autoFocus, enableScrollX, getThumbShape]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
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 += '<div class="verticalSection">';
|
|
||||||
html += `<h2 class="sectionTitle sectionTitle-cards padded-left">${escapeHtml(title)}</h2>`;
|
|
||||||
const allowBottomPadding = true;
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-mousewheel="false" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += cardBuilder.getCardsHtml(recommendation.Items, {
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
allowBottomPadding: allowBottomPadding,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}, [enableScrollX, getPortraitShape]);
|
|
||||||
|
|
||||||
const loadSuggestions = useCallback((page, userId) => {
|
const loadSuggestions = useCallback((page, userId) => {
|
||||||
const screenWidth: any = dom.getWindowSize();
|
const screenWidth: any = dom.getWindowSize();
|
||||||
|
@ -177,22 +98,12 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
||||||
});
|
});
|
||||||
window.ApiClient.getJSON(url).then(recommendations => {
|
window.ApiClient.getJSON(url).then(recommendations => {
|
||||||
if (!recommendations.length) {
|
setRecommendations(recommendations);
|
||||||
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
|
// FIXME: Wait for all sections to load
|
||||||
autoFocus(page);
|
autoFocus(page);
|
||||||
});
|
});
|
||||||
}, [autoFocus, getRecommendationHtml]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
const loadSuggestionsTab = useCallback((view) => {
|
const loadSuggestionsTab = useCallback((view) => {
|
||||||
const parentId = props.topParentId;
|
const parentId = props.topParentId;
|
||||||
|
@ -202,32 +113,40 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
loadSuggestions(view, userId);
|
loadSuggestions(view, userId);
|
||||||
}, [loadLatest, loadResume, loadSuggestions, props.topParentId]);
|
}, [loadLatest, loadResume, loadSuggestions, props.topParentId]);
|
||||||
|
|
||||||
const initSuggestedTab = useCallback((tabContent) => {
|
const setScrollClasses = useCallback((elem, scrollX) => {
|
||||||
function setScrollClasses(elem: { classList: { add: (arg0: string) => void; remove: (arg0: string) => void; }; }, scrollX: boolean) {
|
const page = element.current;
|
||||||
if (scrollX) {
|
|
||||||
elem.classList.add('hiddenScrollX');
|
|
||||||
|
|
||||||
if (layoutManager.tv) {
|
if (!page) {
|
||||||
elem.classList.add('smoothScrollX');
|
console.error('Unexpected null reference');
|
||||||
elem.classList.add('padded-top-focusscale');
|
return;
|
||||||
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');
|
|
||||||
|
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) {
|
for (const container of containers) {
|
||||||
setScrollClasses(container, enableScrollX());
|
setScrollClasses(container, enableScrollX());
|
||||||
}
|
}
|
||||||
}, [enableScrollX]);
|
}, [enableScrollX, setScrollClasses]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const page = element.current;
|
const page = element.current;
|
||||||
|
@ -238,54 +157,23 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
initSuggestedTab(page);
|
initSuggestedTab(page);
|
||||||
}, [initSuggestedTab]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const page = element.current;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadSuggestionsTab(page);
|
loadSuggestionsTab(page);
|
||||||
}, [loadSuggestionsTab]);
|
}, [initSuggestedTab, loadSuggestionsTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element}>
|
<div ref={element}>
|
||||||
<div id='resumableSection' className='verticalSection hide'>
|
<ResumableItemsContainer getThumbShape={getThumbShape} enableScrollX={enableScrollX} itemsResult={resumeItemsResult} />
|
||||||
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
|
||||||
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
|
||||||
{globalize.translate('HeaderContinueWatching')}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ItemsContainerElement
|
<RecentlyAddedItemsContainer getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} items={latestItems} />
|
||||||
id='resumableItems'
|
|
||||||
className='itemsContainer padded-left padded-right'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
<div id='recommendations'>
|
||||||
|
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
|
||||||
<div className='verticalSection'>
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
|
||||||
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
</div> : recommendations.map((recommendation, index) => {
|
||||||
{globalize.translate('HeaderLatestMovies')}
|
return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
||||||
</h2>
|
})}
|
||||||
</div>
|
{}
|
||||||
|
|
||||||
<ItemsContainerElement
|
|
||||||
id='recentlyAddedItems'
|
|
||||||
className='itemsContainer padded-left padded-right'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='recommendations'>
|
|
||||||
</div>
|
|
||||||
<div className='noItemsMessage hide padded-left padded-right'>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -113,7 +113,7 @@ const TrailersView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
<Pagination itemsResult= {itemsResult} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
<Sort sortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
<Filter query={query} reloadItems={reloadItems} />
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue