mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4877 from grafixeyehero/Add-tv-show-view
Add tv show view
This commit is contained in:
commit
c455aab12e
21 changed files with 528 additions and 229 deletions
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -5890,9 +5890,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001480",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz",
|
||||
"integrity": "sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==",
|
||||
"version": "1.0.30001554",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
|
||||
"integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -25606,9 +25606,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001480",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz",
|
||||
"integrity": "sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==",
|
||||
"version": "1.0.30001554",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
|
||||
"integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"canvas": {
|
||||
|
|
|
@ -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;
|
|
@ -5,5 +5,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
|||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental }
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
||||
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental }
|
||||
];
|
||||
|
|
|
@ -61,12 +61,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'user/subtitles/index',
|
||||
view: 'user/subtitles/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'tv.html',
|
||||
pageProps: {
|
||||
controller: 'shows/tvrecommended',
|
||||
view: 'shows/tvrecommended.html'
|
||||
}
|
||||
}, {
|
||||
path: 'video',
|
||||
pageProps: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
85
src/apps/experimental/routes/shows/index.tsx
Normal file
85
src/apps/experimental/routes/shows/index.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
import Page from 'components/Page';
|
||||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
|
||||
const episodesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Episodes,
|
||||
itemType: [BaseItemKind.Episode],
|
||||
collectionType: CollectionType.TvShows,
|
||||
isAlphabetPickerEnabled: false,
|
||||
noItemsMessage: 'MessageNoEpisodesFound'
|
||||
};
|
||||
|
||||
const seriesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Series,
|
||||
itemType: [BaseItemKind.Series],
|
||||
collectionType: CollectionType.TvShows,
|
||||
isBtnShuffleEnabled: true
|
||||
};
|
||||
|
||||
const networksTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Networks,
|
||||
itemType: [BaseItemKind.Series],
|
||||
isBtnFilterEnabled: false,
|
||||
isBtnGridListEnabled: false,
|
||||
isBtnSortEnabled: false,
|
||||
isAlphabetPickerEnabled: false
|
||||
};
|
||||
|
||||
const upcomingTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Upcoming
|
||||
};
|
||||
|
||||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.TvShows,
|
||||
sectionsType: {
|
||||
suggestionSectionsView: [
|
||||
SectionsView.ContinueWatchingEpisode,
|
||||
SectionsView.LatestEpisode,
|
||||
SectionsView.NextUp
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Genres,
|
||||
itemType: [BaseItemKind.Series],
|
||||
collectionType: CollectionType.TvShows
|
||||
};
|
||||
|
||||
const tvShowsTabMapping: LibraryTabMapping = {
|
||||
0: seriesTabContent,
|
||||
1: suggestionsTabContent,
|
||||
2: upcomingTabContent,
|
||||
3: genresTabContent,
|
||||
4: networksTabContent,
|
||||
5: episodesTabContent
|
||||
};
|
||||
|
||||
const Shows: FC = () => {
|
||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||
const currentTab = tvShowsTabMapping[currentTabIndex];
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='tvshowsPage'
|
||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
backDropType='series'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={searchParamsParentId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shows;
|
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 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 { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
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 { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import datetime from 'scripts/datetime';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import { JellyfinApiContext, useApi } from './useApi';
|
||||
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
||||
|
@ -197,7 +199,7 @@ const fetchGetItemsBySuggestionsType = async (
|
|||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary],
|
||||
enableImageTypes: [ ImageType.Primary, ImageType.Thumb ],
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
|
@ -258,7 +260,7 @@ export const useGetItemsBySectionType = (
|
|||
|
||||
const fetchGetGenres = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
itemType: BaseItemKind,
|
||||
itemType: BaseItemKind[],
|
||||
parentId: ParentId,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
|
@ -269,7 +271,7 @@ const fetchGetGenres = async (
|
|||
userId: user.Id,
|
||||
sortBy: [ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
includeItemTypes: [itemType],
|
||||
includeItemTypes: itemType,
|
||||
enableTotalRecordCount: false,
|
||||
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();
|
||||
return useQuery({
|
||||
queryKey: ['Genres', parentId],
|
||||
|
@ -531,3 +533,91 @@ export const usePlaylistsMoveItemMutation = () => {
|
|||
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
|
||||
});
|
||||
};
|
||||
|
|
27
src/types/libraryTabContent.ts
Normal file
27
src/types/libraryTabContent.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
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 type LibraryTabMapping = Record<number, LibraryTabContent>;
|
Loading…
Add table
Add a link
Reference in a new issue