1
0
Fork 0
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:
grafixeyehero 2022-08-07 02:33:25 +03:00
parent f4b878bea2
commit 7543e494c9
12 changed files with 311 additions and 187 deletions

View file

@ -1,9 +1,9 @@
import React, { FunctionComponent } from 'react';
const createButtonElement = ({ id, className }: IProps) => ({
const createElement = ({ id, className }: IProps) => ({
__html: `<div
is="emby-itemscontainer"
id="${id}"
${id}
class="${className}"
>
</div>`
@ -17,8 +17,8 @@ type IProps = {
const ItemsContainerElement: FunctionComponent<IProps> = ({ id, className }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
id: id,
dangerouslySetInnerHTML={createElement({
id: id ? `id='${id}'` : '',
className: className
})}
/>

View 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;

View 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;

View 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;

View 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;

View file

@ -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<SortProps> = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => {
const Sort: FunctionComponent<SortProps> = ({ sortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => {
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -20,7 +20,7 @@ const Sort: FunctionComponent<SortProps> = ({ 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<SortProps> = ({ SortMenuOptions, query, savedQuery
});
});
}
}, [SortMenuOptions, query, reloadItems, savedQueryKey]);
}, [sortMenuOptions, query, reloadItems, savedQueryKey]);
return (
<div ref={element}>

View file

@ -105,7 +105,7 @@ const CollectionsView: FunctionComponent<IProps> = ({ topParentId }: IProps) =>
<Pagination itemsResult= {itemsResult} query={query} 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 />
</div>

View file

@ -128,7 +128,7 @@ const FavoritesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
<Pagination itemsResult= {itemsResult} query={query} 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} />
</div>

View file

@ -134,7 +134,7 @@ const MoviesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
<Shuffle itemsResult= {itemsResult} topParentId={topParentId} />
<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} />
</div>

View file

@ -1,9 +0,0 @@
import React from 'react';
function ResumableItems() {
return (
<div>ResumableItems</div>
);
}
export default ResumableItems;

View file

@ -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<IProps> = (props: IProps) => {
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
const [ resumeItemsResult, setResumeItemsResult ] = useState<BaseItemDtoQueryResult>();
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
const element = useRef<HTMLDivElement>(null);
const enableScrollX = useCallback(() => {
@ -45,23 +48,12 @@ const SuggestionsView: FunctionComponent<IProps> = (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<IProps> = (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 += '<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]);
}, [autoFocus]);
const loadSuggestions = useCallback((page, userId) => {
const screenWidth: any = dom.getWindowSize();
@ -177,22 +98,12 @@ const SuggestionsView: FunctionComponent<IProps> = (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<IProps> = (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<IProps> = (props: IProps) => {
}
initSuggestedTab(page);
}, [initSuggestedTab]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadSuggestionsTab(page);
}, [loadSuggestionsTab]);
}, [initSuggestedTab, loadSuggestionsTab]);
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>
<ResumableItemsContainer getThumbShape={getThumbShape} enableScrollX={enableScrollX} itemsResult={resumeItemsResult} />
<ItemsContainerElement
id='resumableItems'
className='itemsContainer padded-left padded-right'
/>
<RecentlyAddedItemsContainer getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} items={latestItems} />
</div>
<div className='verticalSection'>
<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 className='recommendations'>
</div>
<div className='noItemsMessage hide padded-left padded-right'>
<br />
<p>
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
</p>
<div id='recommendations'>
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
<h1>{globalize.translate('MessageNothingHere')}</h1>
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
</div> : recommendations.map((recommendation, index) => {
return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
})}
{}
</div>
</div>
);

View file

@ -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'>
<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} />
</div>