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:
parent
cf137497a0
commit
9d88af3dfe
5 changed files with 116 additions and 158 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue