Skip to content

Commit

Permalink
Add tv show view
Browse files Browse the repository at this point in the history
  • Loading branch information
grafixeyehero committed Oct 25, 2023
1 parent 4882d9c commit 5dd4d4f
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/apps/experimental/routes/asyncRoutes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
];
6 changes: 0 additions & 6 deletions src/apps/experimental/routes/legacyRoutes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
22 changes: 22 additions & 0 deletions src/apps/experimental/routes/shows/EpisodesView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 EpisodesView: FC<LibraryViewProps> = ({ parentId }) => {
return (
<ItemsView
viewType={LibraryTab.Episodes}
parentId={parentId}
collectionType={CollectionType.TvShows}
isAlphabetPickerEnabled={false}
itemType={[BaseItemKind.Episode]}
noItemsMessage='MessageNoEpisodesFound'
/>
);
};

export default EpisodesView;
18 changes: 18 additions & 0 deletions src/apps/experimental/routes/shows/GenresView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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.TvShows}
itemType={BaseItemKind.Series}
/>
);
};

export default GenresView;
22 changes: 22 additions & 0 deletions src/apps/experimental/routes/shows/SeriesView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 SeriesView: FC<LibraryViewProps> = ({ parentId }) => {
return (
<ItemsView
viewType={LibraryTab.Series}
parentId={parentId}
collectionType={CollectionType.TvShows}
isBtnShuffleEnabled={true}
itemType={[BaseItemKind.Series]}
noItemsMessage='MessageNoItemsAvailable'
/>
);
};

export default SeriesView;
23 changes: 23 additions & 0 deletions src/apps/experimental/routes/shows/StudiosView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 StudiosView: FC<LibraryViewProps> = ({ parentId }) => {
return (
<ItemsView
viewType={LibraryTab.Networks}
parentId={parentId}
isBtnFilterEnabled={false}
isBtnGridListEnabled={false}
isBtnSortEnabled={false}
isAlphabetPickerEnabled={false}
itemType={[BaseItemKind.Series]}
noItemsMessage= 'MessageNoItemsAvailable'
/>
);
};

export default StudiosView;
20 changes: 20 additions & 0 deletions src/apps/experimental/routes/shows/SuggestionsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { FC } from 'react';

import SuggestionsItemsContainer from '../../components/library/SuggestionsItemsContainer';
import { LibraryViewProps } from 'types/library';
import { SectionsView } from 'types/suggestionsSections';

const SuggestionsView: FC<LibraryViewProps> = ({ parentId }) => {
return (
<SuggestionsItemsContainer
parentId={parentId}
sectionsView={[
SectionsView.ContinueWatchingEpisode,
SectionsView.LatestEpisode,
SectionsView.NextUp
]}
/>
);
};

export default SuggestionsView;
48 changes: 48 additions & 0 deletions src/apps/experimental/routes/shows/UpcomingView.tsx
Original file line number Diff line number Diff line change
@@ -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 '../../components/library/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;
67 changes: 67 additions & 0 deletions src/apps/experimental/routes/shows/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { FC } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';

import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
import Page from 'components/Page';
import GenresView from './GenresView';
import SuggestionsView from './SuggestionsView';
import StudiosView from './StudiosView';
import EpisodesView from './EpisodesView';
import SeriesView from './SeriesView';
import UpcomingView from './UpcomingView';

const Shows: 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 = <UpcomingView parentId={searchParamsParentId} />;
break;

case 3:
component = <GenresView parentId={searchParamsParentId} />;
break;

case 4:
component = <StudiosView parentId={searchParamsParentId} />;
break;

case 5:
component = <EpisodesView parentId={searchParamsParentId} />;
break;

default:
component = <SeriesView parentId={searchParamsParentId} />;
}

return component;
};

return (
<Page
id='tvshowsPage'
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
backDropType='series'
>
{getTabComponent(currentTabIndex)}

</Page>
);
};

export default Shows;
94 changes: 92 additions & 2 deletions src/hooks/useFetchItems.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -197,7 +199,7 @@ const fetchGetItemsBySuggestionsType = async (
],
parentId: parentId ?? undefined,
imageTypeLimit: 1,
enableImageTypes: [ImageType.Primary],
enableImageTypes: [ ImageType.Primary, ImageType.Thumb ],
...sections.parametersOptions
},
{
Expand Down Expand Up @@ -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
});
};

0 comments on commit 5dd4d4f

Please sign in to comment.