1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Refactoring Suggestions View

This commit is contained in:
grafixeyehero 2022-09-02 02:46:05 +03:00
parent cf137497a0
commit 9d88af3dfe
5 changed files with 116 additions and 158 deletions

View file

@ -1,13 +1,9 @@
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react'; import React, { FunctionComponent } from 'react';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import ItemsContainerElement from '../../elements/ItemsContainerElement';
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
import escapeHTML from 'escape-html'; import escapeHTML from 'escape-html';
import SectionContainer from './SectionContainer';
type RecommendationContainerProps = { type RecommendationContainerProps = {
getPortraitShape: () => string; getPortraitShape: () => string;
@ -16,8 +12,6 @@ type RecommendationContainerProps = {
} }
const RecommendationContainer: FunctionComponent<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => { const RecommendationContainer: FunctionComponent<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => {
const element = useRef<HTMLDivElement>(null);
let title = ''; let title = '';
switch (recommendation.RecommendationType) { switch (recommendation.RecommendationType) {
@ -40,40 +34,15 @@ const RecommendationContainer: FunctionComponent<RecommendationContainerProps> =
break; break;
} }
useEffect(() => { return <SectionContainer
cardBuilder.buildCards(recommendation.Items || [], { sectionTitle={escapeHTML(title)}
itemsContainer: element.current?.querySelector('.itemsContainer'), enableScrollX={enableScrollX}
items={recommendation.Items || []}
cardOptions={{
shape: getPortraitShape(), shape: getPortraitShape(),
scalable: true, showYear: 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; export default RecommendationContainer;

View file

@ -1,60 +0,0 @@
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';
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
type ResumableItemsContainerProps = {
getThumbShape: () => string;
enableScrollX: () => boolean;
itemsResult?: BaseItemDtoQueryResult;
}
const ResumableItemsContainer: FunctionComponent<ResumableItemsContainerProps> = ({ getThumbShape, enableScrollX, itemsResult = {} }: ResumableItemsContainerProps) => {
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
const allowBottomPadding = !enableScrollX();
cardBuilder.buildCards(itemsResult.Items || [], {
itemsContainer: element.current?.querySelector('.itemsContainer'),
parentContainer: element.current?.querySelector('#resumableSection'),
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>
{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 ResumableItemsContainer;

View file

@ -4,39 +4,44 @@ import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react'; import React, { FunctionComponent, useEffect, useRef } from 'react';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import cardBuilder from '../../components/cardbuilder/cardBuilder';
import globalize from '../../scripts/globalize';
import ItemsContainerElement from '../../elements/ItemsContainerElement'; import ItemsContainerElement from '../../elements/ItemsContainerElement';
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
import { ICardOptions } from './type';
type RecentlyAddedItemsContainerProps = { type SectionContainerProps = {
getPortraitShape: () => string; sectionTitle: string;
enableScrollX: () => boolean; enableScrollX: () => boolean;
items?: BaseItemDto[]; items?: BaseItemDto[];
cardOptions?: ICardOptions;
} }
const RecentlyAddedItemsContainer: FunctionComponent<RecentlyAddedItemsContainerProps> = ({ getPortraitShape, enableScrollX, items = [] }: RecentlyAddedItemsContainerProps) => { const SectionContainer: FunctionComponent<SectionContainerProps> = ({
sectionTitle,
enableScrollX,
items = [],
cardOptions = {}
}: SectionContainerProps) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
cardBuilder.buildCards(items, { cardBuilder.buildCards(items, {
itemsContainer: element.current?.querySelector('.itemsContainer'), itemsContainer: element.current?.querySelector('.itemsContainer'),
parentContainer: element.current?.querySelector('#recentlyAddedItemsSection'), parentContainer: element.current?.querySelector('.verticalSection'),
shape: getPortraitShape(),
scalable: true, scalable: true,
overlayPlayButton: true, overlayPlayButton: true,
allowBottomPadding: true,
showTitle: true, showTitle: true,
showYear: true, centerText: true,
centerText: true cardLayout: false,
...cardOptions
}); });
}, [enableScrollX, getPortraitShape, items]); }, [cardOptions, enableScrollX, items]);
return ( return (
<div ref={element}> <div ref={element}>
<div id='recentlyAddedItemsSection' className='verticalSection hide'> <div className='verticalSection hide'>
<div className='sectionTitleContainer sectionTitleContainer-cards'> <div className='sectionTitleContainer sectionTitleContainer-cards'>
<h2 className='sectionTitle sectionTitle-cards padded-left'> <h2 className='sectionTitle sectionTitle-cards padded-left'>
{globalize.translate('HeaderLatestMovies')} {sectionTitle}
</h2> </h2>
</div> </div>
@ -54,4 +59,4 @@ const RecentlyAddedItemsContainer: FunctionComponent<RecentlyAddedItemsContainer
); );
}; };
export default RecentlyAddedItemsContainer; export default SectionContainer;

View file

@ -10,7 +10,40 @@ export type IQuery = {
StartIndex: number; StartIndex: number;
ParentId?: string | null; ParentId?: string | null;
IsFavorite?: boolean; IsFavorite?: boolean;
IsMissing?: boolean;
Limit:number; Limit:number;
NameLessThan?: string; NameLessThan?: string;
NameStartsWith?: string; NameStartsWith?: string;
} }
export type ICardOptions = {
itemsContainer?: HTMLElement;
parentContainer?: HTMLElement;
allowBottomPadding?: boolean;
centerText?: boolean;
coverImage?: boolean;
inheritThumb?: boolean;
overlayMoreButton?: boolean;
overlayPlayButton?: boolean;
overlayText?: boolean;
preferThumb?: boolean;
scalable?: boolean;
shape?: string;
lazy?: boolean;
cardLayout?: boolean;
showParentTitle?: boolean;
showParentTitleOrTitle?: boolean;
showAirTime?: boolean;
showAirDateTime?: boolean;
showChannelName?: boolean;
showTitle?: boolean;
showYear?: boolean;
showDetailsMenu?: boolean;
missingIndicator?: boolean;
showLocationTypeIndicator?: boolean;
showSeriesYear?: boolean;
showUnplayedIndicator?: boolean;
showChildCountIndicator?: boolean;
lines?: number;
context?: string;
}

View file

@ -5,9 +5,8 @@ import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading'; import loading from '../../components/loading/loading';
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 RecommendationContainer from '../components/RecommendationContainer';
import ResumableItemsContainer from '../components/ResumableItemsContainer'; import SectionContainer from '../components/SectionContainer';
type IProps = { type IProps = {
topParentId: string | null; topParentId: string | null;
@ -15,7 +14,7 @@ type IProps = {
const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => { const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]); const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
const [ resumeItemsResult, setResumeItemsResult ] = useState<BaseItemDtoQueryResult>(); const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]); const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
@ -37,6 +36,31 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
}); });
}, []); }, []);
const loadResume = useCallback((page, userId, parentId) => {
loading.show();
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'DatePlayed',
SortOrder: 'Descending',
IncludeItemTypes: 'Movie',
Filters: 'IsResumable',
Limit: screenWidth >= 1600 ? 5 : 3,
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
CollapseBoxSetItems: false,
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
window.ApiClient.getItems(userId, options).then(result => {
setResumeResult(result);
loading.hide();
autoFocus(page);
});
}, [autoFocus]);
const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => { const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => {
const options = { const options = {
IncludeItemTypes: 'Movie', IncludeItemTypes: 'Movie',
@ -50,43 +74,16 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
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 => {
setLatestItems(items); setLatestItems(items);
// FIXME: Wait for all sections to load
autoFocus(page);
});
}, [autoFocus]);
const loadResume = useCallback((page, userId, parentId) => {
loading.show();
const screenWidth = 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 => {
setResumeItemsResult(result);
loading.hide();
// FIXME: Wait for all sections to load
autoFocus(page); autoFocus(page);
}); });
}, [autoFocus]); }, [autoFocus]);
const loadSuggestions = useCallback((page, userId) => { const loadSuggestions = useCallback((page, userId) => {
const screenWidth = dom.getWindowSize(); const screenWidth = dom.getWindowSize().innerWidth;
let itemLimit = 5; let itemLimit = 5;
if (screenWidth.innerWidth >= 1600) { if (screenWidth >= 1600) {
itemLimit = 8; itemLimit = 8;
} else if (screenWidth.innerWidth >= 1200) { } else if (screenWidth >= 1200) {
itemLimit = 6; itemLimit = 6;
} }
const url = window.window.ApiClient.getUrl('Movies/Recommendations', { const url = window.window.ApiClient.getUrl('Movies/Recommendations', {
@ -100,7 +97,6 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
window.ApiClient.getJSON(url).then(result => { window.ApiClient.getJSON(url).then(result => {
setRecommendations(result); setRecommendations(result);
// FIXME: Wait for all sections to load
autoFocus(page); autoFocus(page);
}); });
}, [autoFocus]); }, [autoFocus]);
@ -126,18 +122,33 @@ const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
return ( return (
<div ref={element}> <div ref={element}>
<ResumableItemsContainer getThumbShape={getThumbShape} enableScrollX={enableScrollX} itemsResult={resumeItemsResult} /> <SectionContainer
sectionTitle={globalize.translate('HeaderContinueWatching')}
enableScrollX={enableScrollX}
items={resumeResult.Items || []}
cardOptions={{
preferThumb: true,
shape: getThumbShape(),
showYear: true
}}
/>
<RecentlyAddedItemsContainer getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} items={latestItems} /> <SectionContainer
sectionTitle={globalize.translate('HeaderLatestMovies')}
enableScrollX={enableScrollX}
items={latestItems}
cardOptions={{
shape: getPortraitShape(),
showYear: true
}}
/>
<div id='recommendations'> {!recommendations.length ? <div className='noItemsMessage centerMessage'>
{!recommendations.length ? <div className='noItemsMessage centerMessage'> <h1>{globalize.translate('MessageNothingHere')}</h1>
<h1>{globalize.translate('MessageNothingHere')}</h1> <p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p> </div> : recommendations.map((recommendation, index) => {
</div> : recommendations.map((recommendation, index) => { return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />; })}
})}
</div>
</div> </div>
); );
}; };