mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
refactor: extract reusable component
This commit is contained in:
parent
4882d9c8cc
commit
d370afd0b2
17 changed files with 437 additions and 216 deletions
|
@ -9,8 +9,8 @@ import { ParentId } from 'types/library';
|
|||
|
||||
interface GenresItemsContainerProps {
|
||||
parentId: ParentId;
|
||||
collectionType: CollectionType;
|
||||
itemType: BaseItemKind;
|
||||
collectionType: CollectionType | undefined;
|
||||
itemType: BaseItemKind[];
|
||||
}
|
||||
|
||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||
|
|
|
@ -16,8 +16,8 @@ import { ParentId } from 'types/library';
|
|||
|
||||
interface GenresSectionContainerProps {
|
||||
parentId: ParentId;
|
||||
collectionType: CollectionType;
|
||||
itemType: BaseItemKind;
|
||||
collectionType: CollectionType | undefined;
|
||||
itemType: BaseItemKind[];
|
||||
genre: BaseItemDto;
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
|||
return {
|
||||
sortBy: [ItemSortBy.Random],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
includeItemTypes: [itemType],
|
||||
includeItemTypes: itemType,
|
||||
recursive: true,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
|
@ -70,9 +70,9 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
|||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: itemType === BaseItemKind.MusicAlbum ? 'overflowSquare' : 'overflowPortrait',
|
||||
showParentTitle: itemType === BaseItemKind.MusicAlbum,
|
||||
showYear: itemType !== BaseItemKind.MusicAlbum
|
||||
shape: collectionType === CollectionType.Music ? 'overflowSquare' : 'overflowPortrait',
|
||||
showParentTitle: collectionType === CollectionType.Music,
|
||||
showYear: collectionType !== CollectionType.Music
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
|
23
src/apps/experimental/components/library/GenresView.tsx
Normal file
23
src/apps/experimental/components/library/GenresView.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import GenresItemsContainer from './GenresItemsContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
interface GenresViewProps {
|
||||
parentId: ParentId;
|
||||
collectionType: CollectionType | undefined;
|
||||
itemType: BaseItemKind[];
|
||||
}
|
||||
|
||||
const GenresView: FC<GenresViewProps> = ({ parentId, collectionType, itemType }) => {
|
||||
return (
|
||||
<GenresItemsContainer
|
||||
parentId={parentId}
|
||||
collectionType={collectionType}
|
||||
itemType={itemType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenresView;
|
65
src/apps/experimental/components/library/PageTabContent.tsx
Normal file
65
src/apps/experimental/components/library/PageTabContent.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { FC } from 'react';
|
||||
import SuggestionsView from './SuggestionsView';
|
||||
import UpcomingView from './UpcomingView';
|
||||
import GenresView from './GenresView';
|
||||
import ItemsView from './ItemsView';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { ParentId } from 'types/library';
|
||||
import { LibraryTabContent } from 'types/libraryTabContent';
|
||||
|
||||
interface PageTabContentProps {
|
||||
parentId: ParentId;
|
||||
currentTab: LibraryTabContent;
|
||||
}
|
||||
|
||||
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||
return (
|
||||
<SuggestionsView
|
||||
parentId={parentId}
|
||||
suggestionSectionViews={
|
||||
currentTab.sectionsType?.suggestionSectionsView
|
||||
}
|
||||
isMovieRecommendations={
|
||||
currentTab.sectionsType?.isMovieRecommendations
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Upcoming) {
|
||||
return <UpcomingView parentId={parentId} />;
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Genres) {
|
||||
return (
|
||||
<GenresView
|
||||
parentId={parentId}
|
||||
collectionType={currentTab.collectionType}
|
||||
itemType={currentTab.itemType || []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={currentTab.viewType}
|
||||
parentId={parentId}
|
||||
collectionType={currentTab.collectionType}
|
||||
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
||||
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
||||
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
|
||||
isBtnNewCollectionEnabled={currentTab.isBtnNewCollectionEnabled}
|
||||
isBtnFilterEnabled={currentTab.isBtnFilterEnabled}
|
||||
isBtnGridListEnabled={currentTab.isBtnGridListEnabled}
|
||||
isBtnSortEnabled={currentTab.isBtnSortEnabled}
|
||||
isAlphabetPickerEnabled={currentTab.isAlphabetPickerEnabled}
|
||||
itemType={currentTab.itemType || []}
|
||||
noItemsMessage={
|
||||
currentTab.noItemsMessage || 'MessageNoItemsAvailable'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageTabContent;
|
|
@ -0,0 +1,46 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetMovieRecommendations } from 'hooks/useFetchItems';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import globalize from 'scripts/globalize';
|
||||
import RecommendationContainer from './RecommendationContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
|
||||
interface RecommendationItemsContainerProps {
|
||||
parentId?: ParentId;
|
||||
}
|
||||
|
||||
const RecommendationItemsContainer: FC<RecommendationItemsContainerProps> = ({
|
||||
parentId
|
||||
}) => {
|
||||
const { isLoading, data: movieRecommendationsItems } =
|
||||
useGetMovieRecommendations(parentId);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
if (!movieRecommendationsItems?.length) {
|
||||
return (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>
|
||||
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{movieRecommendationsItems.map((recommendation, index) => {
|
||||
return (
|
||||
<RecommendationContainer
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||
recommendation={recommendation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationItemsContainer;
|
33
src/apps/experimental/components/library/SuggestionsView.tsx
Normal file
33
src/apps/experimental/components/library/SuggestionsView.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import SuggestionsItemsContainer from './SuggestionsItemsContainer';
|
||||
import RecommendationItemsContainer from './RecommendationItemsContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
|
||||
interface SuggestionsViewProps {
|
||||
parentId: ParentId;
|
||||
suggestionSectionViews: SectionsView[] | undefined;
|
||||
isMovieRecommendations: boolean | undefined;
|
||||
}
|
||||
|
||||
const SuggestionsView: FC<SuggestionsViewProps> = ({
|
||||
parentId,
|
||||
suggestionSectionViews = [],
|
||||
isMovieRecommendations = false
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
<SuggestionsItemsContainer
|
||||
parentId={parentId}
|
||||
sectionsView={suggestionSectionViews}
|
||||
/>
|
||||
|
||||
{isMovieRecommendations && (
|
||||
<RecommendationItemsContainer parentId={parentId} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsView;
|
48
src/apps/experimental/components/library/UpcomingView.tsx
Normal file
48
src/apps/experimental/components/library/UpcomingView.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import React, { FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import globalize from 'scripts/globalize';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
|
||||
const UpcomingView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const { isLoading, data: groupsUpcomingEpisodes } = useGetGroupsUpcomingEpisodes(parentId);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!groupsUpcomingEpisodes?.length ? (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>
|
||||
{globalize.translate(
|
||||
'MessagePleaseEnsureInternetMetadata'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
groupsUpcomingEpisodes?.map((group) => (
|
||||
<SectionContainer
|
||||
key={group.name}
|
||||
sectionTitle={group.name}
|
||||
items={group.items ?? []}
|
||||
cardOptions={{
|
||||
shape: 'overflowBackdrop',
|
||||
showLocationTypeIndicator: false,
|
||||
showParentTitle: true,
|
||||
preferThumb: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
missingIndicator: false,
|
||||
cardLayout: false
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpcomingView;
|
|
@ -1,24 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ItemsView from '../../components/library/ItemsView';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={LibraryTab.Collections}
|
||||
parentId={parentId}
|
||||
collectionType={CollectionType.Movies}
|
||||
isBtnFilterEnabled={false}
|
||||
isBtnNewCollectionEnabled={true}
|
||||
isAlphabetPickerEnabled={false}
|
||||
itemType={[BaseItemKind.BoxSet]}
|
||||
noItemsMessage='MessageNoCollectionsAvailable'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionsView;
|
|
@ -1,19 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ItemsView from '../../components/library/ItemsView';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={LibraryTab.Favorites}
|
||||
parentId={parentId}
|
||||
itemType={[BaseItemKind.Movie]}
|
||||
noItemsMessage='MessageNoFavoritesAvailable'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoritesView;
|
|
@ -1,17 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import GenresItemsContainer from '../../components/library/GenresItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
const GenresView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<GenresItemsContainer
|
||||
parentId={parentId}
|
||||
collectionType={CollectionType.Movies}
|
||||
itemType={BaseItemKind.Movie}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenresView;
|
|
@ -1,22 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ItemsView from '../../components/library/ItemsView';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={LibraryTab.Movies}
|
||||
parentId={parentId}
|
||||
collectionType={CollectionType.Movies}
|
||||
isBtnShuffleEnabled={true}
|
||||
itemType={[BaseItemKind.Movie]}
|
||||
noItemsMessage='MessageNoItemsAvailable'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoviesView;
|
|
@ -1,52 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetMovieRecommendations } from 'hooks/useFetchItems';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import RecommendationContainer from '../../components/library/RecommendationContainer';
|
||||
import SuggestionsItemsContainer from '../../components/library/SuggestionsItemsContainer';
|
||||
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const {
|
||||
isLoading,
|
||||
data: movieRecommendationsItems
|
||||
} = useGetMovieRecommendations(parentId);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SuggestionsItemsContainer
|
||||
parentId={parentId}
|
||||
sectionsView={[SectionsView.ContinueWatchingMovies, SectionsView.LatestMovies]}
|
||||
/>
|
||||
|
||||
{!movieRecommendationsItems?.length ? (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>
|
||||
{globalize.translate(
|
||||
'MessageNoMovieSuggestionsAvailable'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
movieRecommendationsItems.map((recommendation, index) => {
|
||||
return (
|
||||
<RecommendationContainer
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||
recommendation={recommendation}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsView;
|
|
@ -1,19 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ItemsView from '../../components/library/ItemsView';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={LibraryTab.Trailers}
|
||||
parentId={parentId}
|
||||
itemType={[BaseItemKind.Trailer]}
|
||||
noItemsMessage='MessageNoTrailersFound'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrailersView;
|
|
@ -1,55 +1,72 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
import Page from 'components/Page';
|
||||
import CollectionsView from './CollectionsView';
|
||||
import FavoritesView from './FavoritesView';
|
||||
import GenresView from './GenresView';
|
||||
import MoviesView from './MoviesView';
|
||||
import SuggestionsView from './SuggestionsView';
|
||||
import TrailersView from './TrailersView';
|
||||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
|
||||
const moviesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Movies,
|
||||
collectionType: CollectionType.Movies,
|
||||
isBtnShuffleEnabled: true,
|
||||
itemType: [BaseItemKind.Movie]
|
||||
};
|
||||
|
||||
const collectionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Collections,
|
||||
collectionType: CollectionType.Movies,
|
||||
isBtnFilterEnabled: false,
|
||||
isBtnNewCollectionEnabled: true,
|
||||
isAlphabetPickerEnabled: false,
|
||||
itemType: [BaseItemKind.BoxSet],
|
||||
noItemsMessage: 'MessageNoCollectionsAvailable'
|
||||
};
|
||||
|
||||
const favoritesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Favorites,
|
||||
collectionType: CollectionType.Movies,
|
||||
itemType: [BaseItemKind.Movie]
|
||||
};
|
||||
|
||||
const trailersTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Trailers,
|
||||
itemType: [BaseItemKind.Trailer],
|
||||
noItemsMessage: 'MessageNoTrailersFound'
|
||||
};
|
||||
|
||||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.Movies,
|
||||
sectionsType: {
|
||||
suggestionSectionsView: [
|
||||
SectionsView.ContinueWatchingMovies,
|
||||
SectionsView.LatestMovies
|
||||
],
|
||||
isMovieRecommendations: true
|
||||
}
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Genres,
|
||||
collectionType: CollectionType.Movies,
|
||||
itemType: [BaseItemKind.Movie]
|
||||
};
|
||||
|
||||
const moviesTabMapping: LibraryTabMapping = {
|
||||
0: moviesTabContent,
|
||||
1: suggestionsTabContent,
|
||||
2: trailersTabContent,
|
||||
3: favoritesTabContent,
|
||||
4: collectionsTabContent,
|
||||
5: genresTabContent
|
||||
};
|
||||
|
||||
const Movies: FC = () => {
|
||||
const location = useLocation();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const searchParamsParentId = searchParams.get('topParentId');
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const currentTabIndex = searchParamsTab !== null ? parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
||||
|
||||
const getTabComponent = (index: number) => {
|
||||
if (index == null) {
|
||||
throw new Error('index cannot be null');
|
||||
}
|
||||
|
||||
let component;
|
||||
switch (index) {
|
||||
case 1:
|
||||
component = <SuggestionsView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
component = <TrailersView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
component = <FavoritesView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
component = <CollectionsView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
component = <GenresView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
default:
|
||||
component = <MoviesView parentId={searchParamsParentId} />;
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||
const currentTab = moviesTabMapping[currentTabIndex];
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
@ -57,8 +74,11 @@ const Movies: FC = () => {
|
|||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
backDropType='movie'
|
||||
>
|
||||
{getTabComponent(currentTabIndex)}
|
||||
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={searchParamsParentId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue