From 5dd4d4fbc1b3d65d470c216625c335cd0d52ee27 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 20 Oct 2023 19:16:08 +0300 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFAdd=20tv=20show=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../experimental/routes/asyncRoutes/user.ts | 3 +- .../experimental/routes/legacyRoutes/user.ts | 6 -- .../routes/shows/EpisodesView.tsx | 22 +++++ .../experimental/routes/shows/GenresView.tsx | 18 ++++ .../experimental/routes/shows/SeriesView.tsx | 22 +++++ .../experimental/routes/shows/StudiosView.tsx | 23 +++++ .../routes/shows/SuggestionsView.tsx | 20 ++++ .../routes/shows/UpcomingView.tsx | 48 ++++++++++ src/apps/experimental/routes/shows/index.tsx | 67 +++++++++++++ src/hooks/useFetchItems.ts | 94 ++++++++++++++++++- 10 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 src/apps/experimental/routes/shows/EpisodesView.tsx create mode 100644 src/apps/experimental/routes/shows/GenresView.tsx create mode 100644 src/apps/experimental/routes/shows/SeriesView.tsx create mode 100644 src/apps/experimental/routes/shows/StudiosView.tsx create mode 100644 src/apps/experimental/routes/shows/SuggestionsView.tsx create mode 100644 src/apps/experimental/routes/shows/UpcomingView.tsx create mode 100644 src/apps/experimental/routes/shows/index.tsx diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index 023e292edbe4..eb30c49b635b 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -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 } ]; diff --git a/src/apps/experimental/routes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts index 965767d915cc..b17e62a155e2 100644 --- a/src/apps/experimental/routes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -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: { diff --git a/src/apps/experimental/routes/shows/EpisodesView.tsx b/src/apps/experimental/routes/shows/EpisodesView.tsx new file mode 100644 index 000000000000..ecf1e09938dd --- /dev/null +++ b/src/apps/experimental/routes/shows/EpisodesView.tsx @@ -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 = ({ parentId }) => { + return ( + + ); +}; + +export default EpisodesView; diff --git a/src/apps/experimental/routes/shows/GenresView.tsx b/src/apps/experimental/routes/shows/GenresView.tsx new file mode 100644 index 000000000000..0f034f697010 --- /dev/null +++ b/src/apps/experimental/routes/shows/GenresView.tsx @@ -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 = ({ parentId }) => { + return ( + + ); +}; + +export default GenresView; diff --git a/src/apps/experimental/routes/shows/SeriesView.tsx b/src/apps/experimental/routes/shows/SeriesView.tsx new file mode 100644 index 000000000000..fcd78ce7a234 --- /dev/null +++ b/src/apps/experimental/routes/shows/SeriesView.tsx @@ -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 = ({ parentId }) => { + return ( + + ); +}; + +export default SeriesView; diff --git a/src/apps/experimental/routes/shows/StudiosView.tsx b/src/apps/experimental/routes/shows/StudiosView.tsx new file mode 100644 index 000000000000..591408723b5a --- /dev/null +++ b/src/apps/experimental/routes/shows/StudiosView.tsx @@ -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 = ({ parentId }) => { + return ( + + ); +}; + +export default StudiosView; diff --git a/src/apps/experimental/routes/shows/SuggestionsView.tsx b/src/apps/experimental/routes/shows/SuggestionsView.tsx new file mode 100644 index 000000000000..68f5393f6b6b --- /dev/null +++ b/src/apps/experimental/routes/shows/SuggestionsView.tsx @@ -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 = ({ parentId }) => { + return ( + + ); +}; + +export default SuggestionsView; diff --git a/src/apps/experimental/routes/shows/UpcomingView.tsx b/src/apps/experimental/routes/shows/UpcomingView.tsx new file mode 100644 index 000000000000..3e9885c39ed8 --- /dev/null +++ b/src/apps/experimental/routes/shows/UpcomingView.tsx @@ -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 = ({ parentId }) => { + const { isLoading, data: groupsUpcomingEpisodes } = useGetGroupsUpcomingEpisodes(parentId); + + if (isLoading) return ; + + return ( + + {!groupsUpcomingEpisodes?.length ? ( +
+

{globalize.translate('MessageNothingHere')}

+

+ {globalize.translate( + 'MessagePleaseEnsureInternetMetadata' + )} +

+
+ ) : ( + groupsUpcomingEpisodes?.map((group) => ( + + )) + )} +
+ ); +}; + +export default UpcomingView; diff --git a/src/apps/experimental/routes/shows/index.tsx b/src/apps/experimental/routes/shows/index.tsx new file mode 100644 index 000000000000..1fe63045e0a3 --- /dev/null +++ b/src/apps/experimental/routes/shows/index.tsx @@ -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 = ; + break; + + case 2: + component = ; + break; + + case 3: + component = ; + break; + + case 4: + component = ; + break; + + case 5: + component = ; + break; + + default: + component = ; + } + + return component; + }; + + return ( + + {getTabComponent(currentTabIndex)} + + + ); +}; + +export default Shows; diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index 89c7498eff46..e2fb9e703d21 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -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 }, { @@ -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 + }); +};