1
0
Fork 0
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:
Bill Thornton 2023-10-28 01:43:58 -04:00 committed by GitHub
commit c455aab12e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 528 additions and 229 deletions

12
package-lock.json generated
View file

@ -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": {

View file

@ -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> = ({

View file

@ -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
}}
/>;
};

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

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

View file

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

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

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

View file

@ -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 }
];

View file

@ -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: {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
);
};

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

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

View file

@ -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
});
};

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