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 {
|
interface GenresItemsContainerProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
collectionType: CollectionType;
|
collectionType: CollectionType | undefined;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
|
|
|
@ -16,8 +16,8 @@ import { ParentId } from 'types/library';
|
||||||
|
|
||||||
interface GenresSectionContainerProps {
|
interface GenresSectionContainerProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
collectionType: CollectionType;
|
collectionType: CollectionType | undefined;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind[];
|
||||||
genre: BaseItemDto;
|
genre: BaseItemDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
return {
|
return {
|
||||||
sortBy: [ItemSortBy.Random],
|
sortBy: [ItemSortBy.Random],
|
||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: itemType,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
fields: [
|
fields: [
|
||||||
ItemFields.PrimaryImageAspectRatio,
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
@ -70,9 +70,9 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
centerText: true,
|
centerText: true,
|
||||||
cardLayout: false,
|
cardLayout: false,
|
||||||
shape: itemType === BaseItemKind.MusicAlbum ? 'overflowSquare' : 'overflowPortrait',
|
shape: collectionType === CollectionType.Music ? 'overflowSquare' : 'overflowPortrait',
|
||||||
showParentTitle: itemType === BaseItemKind.MusicAlbum,
|
showParentTitle: collectionType === CollectionType.Music,
|
||||||
showYear: itemType !== BaseItemKind.MusicAlbum
|
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 React, { FC } from 'react';
|
||||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
import useCurrentTab from 'hooks/useCurrentTab';
|
||||||
|
|
||||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
|
||||||
import Page from 'components/Page';
|
import Page from 'components/Page';
|
||||||
import CollectionsView from './CollectionsView';
|
import PageTabContent from '../../components/library/PageTabContent';
|
||||||
import FavoritesView from './FavoritesView';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import GenresView from './GenresView';
|
import { CollectionType } from 'types/collectionType';
|
||||||
import MoviesView from './MoviesView';
|
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||||
import SuggestionsView from './SuggestionsView';
|
import { SectionsView } from 'types/suggestionsSections';
|
||||||
import TrailersView from './TrailersView';
|
|
||||||
|
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 Movies: FC = () => {
|
||||||
const location = useLocation();
|
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||||
const [ searchParams ] = useSearchParams();
|
const currentTab = moviesTabMapping[currentTabIndex];
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -57,8 +74,11 @@ const Movies: FC = () => {
|
||||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||||
backDropType='movie'
|
backDropType='movie'
|
||||||
>
|
>
|
||||||
{getTabComponent(currentTabIndex)}
|
<PageTabContent
|
||||||
|
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||||
|
currentTab={currentTab}
|
||||||
|
parentId={searchParamsParentId}
|
||||||
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
20
src/hooks/useCurrentTab.ts
Normal file
20
src/hooks/useCurrentTab.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { getDefaultTabIndex } from 'apps/experimental/components/tabs/tabRoutes';
|
||||||
|
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const useCurrentTab = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const searchParamsParentId = searchParams.get('topParentId');
|
||||||
|
const searchParamsTab = searchParams.get('tab');
|
||||||
|
const currentTabIndex: number =
|
||||||
|
searchParamsTab !== null ?
|
||||||
|
parseInt(searchParamsTab, 10) :
|
||||||
|
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParamsParentId,
|
||||||
|
currentTabIndex
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCurrentTab;
|
|
@ -1,5 +1,5 @@
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import { AxiosRequestConfig } from 'axios';
|
||||||
import type { ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
|
@ -16,6 +16,8 @@ import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import datetime from 'scripts/datetime';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import { JellyfinApiContext, useApi } from './useApi';
|
import { JellyfinApiContext, useApi } from './useApi';
|
||||||
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
||||||
|
@ -197,7 +199,7 @@ const fetchGetItemsBySuggestionsType = async (
|
||||||
],
|
],
|
||||||
parentId: parentId ?? undefined,
|
parentId: parentId ?? undefined,
|
||||||
imageTypeLimit: 1,
|
imageTypeLimit: 1,
|
||||||
enableImageTypes: [ImageType.Primary],
|
enableImageTypes: [ ImageType.Primary, ImageType.Thumb ],
|
||||||
...sections.parametersOptions
|
...sections.parametersOptions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -258,7 +260,7 @@ export const useGetItemsBySectionType = (
|
||||||
|
|
||||||
const fetchGetGenres = async (
|
const fetchGetGenres = async (
|
||||||
currentApi: JellyfinApiContext,
|
currentApi: JellyfinApiContext,
|
||||||
itemType: BaseItemKind,
|
itemType: BaseItemKind[],
|
||||||
parentId: ParentId,
|
parentId: ParentId,
|
||||||
options?: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
) => {
|
) => {
|
||||||
|
@ -269,7 +271,7 @@ const fetchGetGenres = async (
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
sortBy: [ItemSortBy.SortName],
|
sortBy: [ItemSortBy.SortName],
|
||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: itemType,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
parentId: parentId ?? undefined
|
parentId: parentId ?? undefined
|
||||||
},
|
},
|
||||||
|
@ -281,7 +283,7 @@ const fetchGetGenres = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetGenres = (itemType: BaseItemKind, parentId: ParentId) => {
|
export const useGetGenres = (itemType: BaseItemKind[], parentId: ParentId) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['Genres', parentId],
|
queryKey: ['Genres', parentId],
|
||||||
|
@ -531,3 +533,91 @@ export const usePlaylistsMoveItemMutation = () => {
|
||||||
fetchPlaylistsMoveItem(currentApi, requestParameters )
|
fetchPlaylistsMoveItem(currentApi, requestParameters )
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GroupsUpcomingEpisodes = {
|
||||||
|
name: string;
|
||||||
|
items: BaseItemDto[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function groupsUpcomingEpisodes(items: BaseItemDto[]) {
|
||||||
|
const groups: GroupsUpcomingEpisodes[] = [];
|
||||||
|
let currentGroupName = '';
|
||||||
|
let currentGroup: BaseItemDto[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
let dateText = '';
|
||||||
|
|
||||||
|
if (item.PremiereDate) {
|
||||||
|
try {
|
||||||
|
const premiereDate = datetime.parseISO8601Date(
|
||||||
|
item.PremiereDate,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
dateText = datetime.isRelativeDay(premiereDate, -1) ?
|
||||||
|
globalize.translate('Yesterday') :
|
||||||
|
datetime.toLocaleDateString(premiereDate, {
|
||||||
|
weekday: 'long',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error parsing timestamp for upcoming tv shows');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateText != currentGroupName) {
|
||||||
|
if (currentGroup.length) {
|
||||||
|
groups.push({
|
||||||
|
name: currentGroupName,
|
||||||
|
items: currentGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
currentGroupName = dateText;
|
||||||
|
currentGroup = [item];
|
||||||
|
} else {
|
||||||
|
currentGroup.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchGetGroupsUpcomingEpisodes = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
parentId: ParentId,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
const response = await getTvShowsApi(api).getUpcomingEpisodes(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
limit: 25,
|
||||||
|
fields: [ItemFields.AirTime],
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [
|
||||||
|
ImageType.Primary,
|
||||||
|
ImageType.Backdrop,
|
||||||
|
ImageType.Thumb
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const items = response.data.Items ?? [];
|
||||||
|
|
||||||
|
return groupsUpcomingEpisodes(items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetGroupsUpcomingEpisodes = (parentId: ParentId) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['GroupsUpcomingEpisodes', parentId],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetGroupsUpcomingEpisodes(currentApi, parentId, { signal }),
|
||||||
|
enabled: !!parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
29
src/types/libraryTabContent.ts
Normal file
29
src/types/libraryTabContent.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { LibraryTab } from './libraryTab';
|
||||||
|
import { CollectionType } from './collectionType';
|
||||||
|
import { SectionsView } from './suggestionsSections';
|
||||||
|
|
||||||
|
export interface SuggestionsSectionsType {
|
||||||
|
suggestionSectionsView: SectionsView[];
|
||||||
|
isMovieRecommendations?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryTabContent {
|
||||||
|
viewType: LibraryTab;
|
||||||
|
itemType?: BaseItemKind[];
|
||||||
|
collectionType?: CollectionType;
|
||||||
|
sectionsType?: SuggestionsSectionsType;
|
||||||
|
isBtnPlayAllEnabled?: boolean;
|
||||||
|
isBtnQueueEnabled?: boolean;
|
||||||
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
isBtnSortEnabled?: boolean;
|
||||||
|
isBtnFilterEnabled?: boolean;
|
||||||
|
isBtnNewCollectionEnabled?: boolean;
|
||||||
|
isBtnGridListEnabled?: boolean;
|
||||||
|
isAlphabetPickerEnabled?: boolean;
|
||||||
|
noItemsMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryTabMapping {
|
||||||
|
[index: number]: LibraryTabContent;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue