mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
refactor: suggestionview and genresview
This commit is contained in:
parent
13aa3c9efa
commit
17e8ccc93a
27 changed files with 1253 additions and 602 deletions
|
@ -0,0 +1,52 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import { useGetGenres } from 'hooks/useFetchItems';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import GenresSectionContainer from './GenresSectionContainer';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
interface GenresItemsContainerProps {
|
||||
parentId?: string | null;
|
||||
collectionType?: CollectionType;
|
||||
itemType: BaseItemKind;
|
||||
}
|
||||
|
||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||
parentId,
|
||||
collectionType,
|
||||
itemType
|
||||
}) => {
|
||||
const { isLoading, data: genresResult } = useGetGenres(
|
||||
parentId,
|
||||
itemType
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!genresResult?.Items?.length ? (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||
</div>
|
||||
) : (
|
||||
genresResult?.Items
|
||||
&& genresResult?.Items.map((genre) => (
|
||||
<GenresSectionContainer
|
||||
key={genre.Id}
|
||||
collectionType={collectionType}
|
||||
parentId={parentId}
|
||||
itemType={itemType}
|
||||
genre={genre}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenresItemsContainer;
|
|
@ -0,0 +1,79 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { useGetItems } from 'hooks/useFetchItems';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
interface GenresSectionContainerProps {
|
||||
parentId?: string | null;
|
||||
collectionType?: CollectionType;
|
||||
itemType: BaseItemKind;
|
||||
genre: BaseItemDto;
|
||||
}
|
||||
|
||||
const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||
parentId,
|
||||
collectionType,
|
||||
itemType,
|
||||
genre
|
||||
}) => {
|
||||
const getParametersOptions = () => {
|
||||
return {
|
||||
sortBy: [ItemSortBy.Random],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
includeItemTypes: [itemType],
|
||||
recursive: true,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount,
|
||||
ItemFields.BasicSyncInfo
|
||||
],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary],
|
||||
limit: 25,
|
||||
genreIds: genre.Id ? [genre.Id] : undefined,
|
||||
enableTotalRecordCount: false,
|
||||
parentId: parentId ?? undefined
|
||||
};
|
||||
};
|
||||
|
||||
const { isLoading, data: itemsResult } = useGetItems(getParametersOptions());
|
||||
|
||||
const getRouteUrl = (item: BaseItemDto) => {
|
||||
return appRouter.getRouteUrl(item, {
|
||||
context: collectionType,
|
||||
parentId: parentId
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <SectionContainer
|
||||
sectionTitle={escapeHTML(genre.Name)}
|
||||
items={itemsResult?.Items || []}
|
||||
url={getRouteUrl(genre)}
|
||||
cardOptions={{
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: itemType === BaseItemKind.MusicAlbum ? 'overflowSquare' : 'overflowPortrait',
|
||||
showParentTitle: itemType === BaseItemKind.MusicAlbum ? true : false,
|
||||
showYear: itemType === BaseItemKind.MusicAlbum ? false : true
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default GenresSectionContainer;
|
|
@ -0,0 +1,61 @@
|
|||
import { RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SectionContainer from './SectionContainer';
|
||||
|
||||
interface RecommendationContainerProps {
|
||||
recommendation?: RecommendationDto;
|
||||
}
|
||||
|
||||
const RecommendationContainer: FC<RecommendationContainerProps> = ({
|
||||
recommendation = {}
|
||||
}) => {
|
||||
let title = '';
|
||||
|
||||
switch (recommendation.RecommendationType) {
|
||||
case RecommendationType.SimilarToRecentlyPlayed:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouWatched',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.SimilarToLikedItem:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouLike',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedDirector:
|
||||
title = globalize.translate(
|
||||
'RecommendationDirectedBy',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasActorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedActor:
|
||||
title = globalize.translate(
|
||||
'RecommendationStarring',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
sectionTitle={escapeHTML(title)}
|
||||
items={recommendation.Items || []}
|
||||
cardOptions={{
|
||||
shape: 'overflowPortrait',
|
||||
showYear: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationContainer;
|
|
@ -0,0 +1,73 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
||||
import Scroller from 'elements/emby-scroller/Scroller';
|
||||
import LinkButton from 'elements/emby-button/LinkButton';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
|
||||
import { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface SectionContainerProps {
|
||||
url?: string;
|
||||
sectionTitle: string;
|
||||
items: BaseItemDto[];
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
const SectionContainer: FC<SectionContainerProps> = ({
|
||||
sectionTitle,
|
||||
url,
|
||||
items,
|
||||
cardOptions
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const itemsContainer = element.current?.querySelector('.itemsContainer');
|
||||
cardBuilder.buildCards(items, {
|
||||
itemsContainer: itemsContainer,
|
||||
parentContainer: element.current,
|
||||
|
||||
...cardOptions
|
||||
});
|
||||
|
||||
imageLoader.lazyChildren(itemsContainer);
|
||||
}, [cardOptions, items]);
|
||||
|
||||
return (
|
||||
<div ref={element} className='verticalSection hide'>
|
||||
<div className='sectionTitleContainer sectionTitleContainer-cards padded-left'>
|
||||
{url && items.length > 5 ? (
|
||||
<LinkButton
|
||||
className='more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre'
|
||||
href={url}
|
||||
>
|
||||
<h2 className='sectionTitle sectionTitle-cards'>
|
||||
{sectionTitle}
|
||||
</h2>
|
||||
<span
|
||||
className='material-icons chevron_right'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
</LinkButton>
|
||||
) : (
|
||||
<h2 className='sectionTitle sectionTitle-cards'>
|
||||
{sectionTitle}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Scroller
|
||||
className='padded-top-focusscale padded-bottom-focusscale'
|
||||
isMouseWheelEnabled={false}
|
||||
isCenterFocusEnabled={true}
|
||||
>
|
||||
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
|
||||
</Scroller>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionContainer;
|
|
@ -0,0 +1,206 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||
import React, { FC } from 'react';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
||||
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
||||
|
||||
const getSuggestionsSections = (): Sections[] => {
|
||||
return [
|
||||
{
|
||||
name: 'HeaderContinueWatching',
|
||||
viewType: SectionsViewType.ResumeItems,
|
||||
type: 'Movie',
|
||||
view: SectionsView.ContinueWatchingMovies,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Movie]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
preferThumb: true,
|
||||
shape: 'overflowBackdrop',
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestMovies',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Movie',
|
||||
view: SectionsView.LatestMovies,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Movie]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowPortrait',
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderContinueWatching',
|
||||
viewType: SectionsViewType.ResumeItems,
|
||||
type: 'Episode',
|
||||
view: SectionsView.ContinueWatchingEpisode,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Episode]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
inheritThumb:
|
||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestEpisodes',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Episode',
|
||||
view: SectionsView.LatestEpisode,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Episode]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
showSeriesYear: true,
|
||||
showParentTitle: true,
|
||||
overlayText: false,
|
||||
showUnplayedIndicator: false,
|
||||
showChildCountIndicator: true,
|
||||
lazy: true,
|
||||
lines: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'NextUp',
|
||||
viewType: SectionsViewType.NextUp,
|
||||
type: 'nextup',
|
||||
view: SectionsView.NextUp,
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
inheritThumb:
|
||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||
showParentTitle: true,
|
||||
overlayText: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestMusic',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Audio',
|
||||
view: SectionsView.LatestMusic,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderRecentlyPlayed',
|
||||
type: 'Audio',
|
||||
view: SectionsView.RecentlyPlayedMusic,
|
||||
parametersOptions: {
|
||||
sortBy: [ItemSortBy.DatePlayed],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
action: 'instantmix',
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderFrequentlyPlayed',
|
||||
type: 'Audio',
|
||||
view: SectionsView.FrequentlyPlayedMusic,
|
||||
parametersOptions: {
|
||||
sortBy: [ItemSortBy.PlayCount],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
action: 'instantmix',
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
interface SuggestionsItemsContainerProps {
|
||||
parentId?: string | null;
|
||||
sectionsView: SectionsView[];
|
||||
}
|
||||
|
||||
const SuggestionsItemsContainer: FC<SuggestionsItemsContainerProps> = ({
|
||||
parentId,
|
||||
sectionsView
|
||||
}) => {
|
||||
const suggestionsSections = getSuggestionsSections();
|
||||
|
||||
return (
|
||||
<>
|
||||
{suggestionsSections
|
||||
.filter((section) => sectionsView.includes(section.view))
|
||||
.map((section) => (
|
||||
<SuggestionsSectionContainer
|
||||
key={section.view}
|
||||
parentId={parentId}
|
||||
section={section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsItemsContainer;
|
|
@ -0,0 +1,49 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetItemsBySectionType } from 'hooks/useFetchItems';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import SectionContainer from './SectionContainer';
|
||||
|
||||
import { Sections } from 'types/suggestionsSections';
|
||||
|
||||
interface SuggestionsSectionContainerProps {
|
||||
parentId?: string | null;
|
||||
section: Sections;
|
||||
}
|
||||
|
||||
const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
|
||||
parentId,
|
||||
section
|
||||
}) => {
|
||||
const getRouteUrl = () => {
|
||||
return appRouter.getRouteUrl('list', {
|
||||
serverId: window.ApiClient.serverId(),
|
||||
itemTypes: section.type,
|
||||
parentId: parentId
|
||||
});
|
||||
};
|
||||
|
||||
const { isLoading, data: items } = useGetItemsBySectionType(
|
||||
section,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
sectionTitle={globalize.translate(section.name)}
|
||||
items={items || []}
|
||||
url={getRouteUrl()}
|
||||
cardOptions={{
|
||||
...section.cardOptions
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsSectionContainer;
|
Loading…
Add table
Add a link
Reference in a new issue