From e41436552e903febfdb6cfc05cba937af1dd0441 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 12 Jan 2024 21:08:06 +0300 Subject: [PATCH] Add livetv view --- .../components/library/GuideView.tsx | 51 ++ .../components/library/ItemsView.tsx | 56 +- .../components/library/PageTabContent.tsx | 31 +- .../components/library/PlayAllButton.tsx | 6 +- .../library/ProgramsSectionView.tsx | 98 ++++ .../components/library/QueueButton.tsx | 4 +- .../library/RecommendationContainer.tsx | 66 --- .../library/RecommendationItemsContainer.tsx | 46 -- .../components/library/SectionContainer.tsx | 10 +- .../components/library/ShuffleButton.tsx | 6 +- .../library/SuggestionsItemsContainer.tsx | 207 ------- .../library/SuggestionsSectionContainer.tsx | 50 -- .../library/SuggestionsSectionView.tsx | 134 +++++ .../components/library/SuggestionsView.tsx | 33 -- .../library/filter/FilterButton.tsx | 12 +- .../library/filter/FiltersGenres.tsx | 7 +- .../library/filter/FiltersOfficialRatings.tsx | 7 +- .../library/filter/FiltersStatus.tsx | 1 + .../library/filter/FiltersStudios.tsx | 8 +- .../components/library/filter/FiltersTags.tsx | 7 +- .../library/filter/FiltersYears.tsx | 7 +- .../experimental/components/tabs/AppTabs.tsx | 16 +- .../experimental/components/tabs/tabRoutes.ts | 4 +- .../experimental/routes/asyncRoutes/user.ts | 3 +- .../experimental/routes/legacyRoutes/user.ts | 6 - src/apps/experimental/routes/livetv/index.tsx | 71 +++ src/apps/experimental/routes/movies/index.tsx | 18 +- src/apps/experimental/routes/music/index.tsx | 18 +- src/apps/experimental/routes/shows/index.tsx | 18 +- .../homeScreenSettings/homeScreenSettings.js | 4 +- src/components/router/appRouter.js | 24 + src/controllers/livetv/livetvsuggested.js | 2 +- src/elements/ItemsContainerElement.tsx | 28 - .../emby-itemscontainer/ItemsContainer.tsx | 12 +- src/hooks/useCurrentTab.ts | 21 +- src/hooks/useFetchItems.ts | 520 +++++++++++++----- src/strings/en-us.json | 1 + src/types/cardOptions.ts | 2 +- src/types/libraryTab.ts | 2 +- src/types/libraryTabContent.ts | 12 +- src/types/listOptions.ts | 4 +- src/types/sections.ts | 109 ++++ src/types/suggestionsSections.ts | 36 -- src/utils/sections.ts | 367 ++++++++++++ 44 files changed, 1396 insertions(+), 749 deletions(-) create mode 100644 src/apps/experimental/components/library/GuideView.tsx create mode 100644 src/apps/experimental/components/library/ProgramsSectionView.tsx delete mode 100644 src/apps/experimental/components/library/RecommendationContainer.tsx delete mode 100644 src/apps/experimental/components/library/RecommendationItemsContainer.tsx delete mode 100644 src/apps/experimental/components/library/SuggestionsItemsContainer.tsx delete mode 100644 src/apps/experimental/components/library/SuggestionsSectionContainer.tsx create mode 100644 src/apps/experimental/components/library/SuggestionsSectionView.tsx delete mode 100644 src/apps/experimental/components/library/SuggestionsView.tsx create mode 100644 src/apps/experimental/routes/livetv/index.tsx delete mode 100644 src/elements/ItemsContainerElement.tsx create mode 100644 src/types/sections.ts delete mode 100644 src/types/suggestionsSections.ts create mode 100644 src/utils/sections.ts diff --git a/src/apps/experimental/components/library/GuideView.tsx b/src/apps/experimental/components/library/GuideView.tsx new file mode 100644 index 000000000..926c53e9e --- /dev/null +++ b/src/apps/experimental/components/library/GuideView.tsx @@ -0,0 +1,51 @@ +import React, { FC, useCallback, useEffect, useRef } from 'react'; +import Guide from 'components/guide/guide'; +import 'material-design-icons-iconfont'; +import 'elements/emby-programcell/emby-programcell'; +import 'elements/emby-button/emby-button'; +import 'elements/emby-button/paper-icon-button-light'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-scroller/emby-scroller'; +import 'components/guide/guide.scss'; +import 'components/guide/programs.scss'; +import 'styles/scrollstyles.scss'; +import 'styles/flexstyles.scss'; + +const GuideView: FC = () => { + const guideInstance = useRef(); + const tvGuideContainerRef = useRef(null); + + const initGuide = useCallback((element: HTMLDivElement) => { + guideInstance.current = new Guide({ + element: element, + serverId: window.ApiClient.serverId() + }); + }, []); + + useEffect(() => { + const element = tvGuideContainerRef.current; + if (!element) { + console.error('Unexpected null reference'); + return; + } + if (!guideInstance.current) { + initGuide(element); + } + }, [initGuide]); + + useEffect(() => { + if (guideInstance.current) { + guideInstance.current.resume(); + } + + return () => { + if (guideInstance.current) { + guideInstance.current.pause(); + } + }; + }, [initGuide]); + + return
; +}; + +export default GuideView; diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index 2770bf84f..c3789b07c 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -3,6 +3,7 @@ import { ImageType } from '@jellyfin/sdk/lib/generated-client'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import React, { FC, useCallback } from 'react'; import Box from '@mui/material/Box'; +import classNames from 'classnames'; import { useLocalStorage } from 'hooks/useLocalStorage'; import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems'; import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; @@ -33,6 +34,7 @@ interface ItemsViewProps { parentId: ParentId; itemType: BaseItemKind[]; collectionType?: CollectionType; + isPaginationEnabled?: boolean; isBtnPlayAllEnabled?: boolean; isBtnQueueEnabled?: boolean; isBtnShuffleEnabled?: boolean; @@ -48,6 +50,7 @@ const ItemsView: FC = ({ viewType, parentId, collectionType, + isPaginationEnabled = true, isBtnPlayAllEnabled = false, isBtnQueueEnabled = false, isBtnShuffleEnabled = false, @@ -145,6 +148,18 @@ const ItemsView: FC = ({ cardOptions.showParentTitle = libraryViewSettings.ShowTitle; } else if (viewType === LibraryTab.Artists) { cardOptions.lines = 1; + cardOptions.showYear = false; + } else if (viewType === LibraryTab.Channels) { + cardOptions.shape = 'square'; + cardOptions.showDetailsMenu = true; + cardOptions.showCurrentProgram = true; + cardOptions.showCurrentProgramTime = true; + } else if (viewType === LibraryTab.SeriesTimers) { + cardOptions.defaultShape = 'portrait'; + cardOptions.preferThumb = 'auto'; + cardOptions.showSeriesTimerTime = true; + cardOptions.showSeriesTimerChannel = true; + cardOptions.lines = 3; } return cardOptions; @@ -188,15 +203,23 @@ const ItemsView: FC = ({ ItemSortBy.SortName ); + const itemsContainerClass = classNames( + 'centered padded-left padded-right padded-right-withalphapicker', + libraryViewSettings.ViewMode === ViewMode.ListView ? + 'vertical-list' : + 'vertical-wrap' + ); return ( - + {isPaginationEnabled && ( + + )} {isBtnPlayAllEnabled && ( = ({ ) : ( )} - - - + {isPaginationEnabled && ( + + + + )} ); }; diff --git a/src/apps/experimental/components/library/PageTabContent.tsx b/src/apps/experimental/components/library/PageTabContent.tsx index 26c75de95..9726f2ffa 100644 --- a/src/apps/experimental/components/library/PageTabContent.tsx +++ b/src/apps/experimental/components/library/PageTabContent.tsx @@ -1,11 +1,13 @@ import React, { FC } from 'react'; -import SuggestionsView from './SuggestionsView'; +import SuggestionsSectionView from './SuggestionsSectionView'; 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'; +import GuideView from './GuideView'; +import ProgramsSectionView from './ProgramsSectionView'; interface PageTabContentProps { parentId: ParentId; @@ -15,18 +17,30 @@ interface PageTabContentProps { const PageTabContent: FC = ({ parentId, currentTab }) => { if (currentTab.viewType === LibraryTab.Suggestions) { return ( - ); } + if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) { + return ( + + ); + } + if (currentTab.viewType === LibraryTab.Upcoming) { return ; } @@ -41,11 +55,16 @@ const PageTabContent: FC = ({ parentId, currentTab }) => { ); } + if (currentTab.viewType === LibraryTab.Guide) { + return ; + } + return ( = ({ + parentId, + sectionType, + isUpcomingRecordingsEnabled = false +}) => { + const { isLoading, data: sectionsWithItems } = useGetProgramsSectionsWithItems(parentId, sectionType); + const { + isLoading: isUpcomingRecordingsLoading, + data: upcomingRecordings + } = useGetTimers(isUpcomingRecordingsEnabled); + + if (isLoading || isUpcomingRecordingsLoading) { + return ; + } + + if (!sectionsWithItems?.length && !upcomingRecordings?.length) { + return ( +
+

{globalize.translate('MessageNothingHere')}

+

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

+
+ ); + } + + const getRouteUrl = (section: Section) => { + return appRouter.getRouteUrl('list', { + serverId: window.ApiClient.serverId(), + itemTypes: section.itemTypes, + isAiring: section.parametersOptions?.isAiring, + isMovie: section.parametersOptions?.isMovie, + isSports: section.parametersOptions?.isSports, + isKids: section.parametersOptions?.isKids, + isNews: section.parametersOptions?.isNews, + isSeries: section.parametersOptions?.isSeries + }); + }; + + return ( + <> + {sectionsWithItems?.map(({ section, items }) => ( + + + ))} + + {upcomingRecordings?.map((group) => ( + + ))} + + ); +}; + +export default ProgramsSectionView; diff --git a/src/apps/experimental/components/library/QueueButton.tsx b/src/apps/experimental/components/library/QueueButton.tsx index fdc6a7666..6b8be9cb3 100644 --- a/src/apps/experimental/components/library/QueueButton.tsx +++ b/src/apps/experimental/components/library/QueueButton.tsx @@ -1,4 +1,4 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback } from 'react'; import { IconButton } from '@mui/material'; import QueueIcon from '@mui/icons-material/Queue'; @@ -8,7 +8,7 @@ import globalize from 'scripts/globalize'; interface QueueButtonProps { item: BaseItemDto | undefined - items: BaseItemDto[]; + items: BaseItemDto[] | SeriesTimerInfoDto[]; hasFilters: boolean; } diff --git a/src/apps/experimental/components/library/RecommendationContainer.tsx b/src/apps/experimental/components/library/RecommendationContainer.tsx deleted file mode 100644 index 6c8ab5cc5..000000000 --- a/src/apps/experimental/components/library/RecommendationContainer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC } from 'react'; - -import globalize from 'scripts/globalize'; -import escapeHTML from 'escape-html'; -import SectionContainer from './SectionContainer'; - -interface RecommendationContainerProps { - recommendation?: RecommendationDto; -} - -const RecommendationContainer: FC = ({ - recommendation = {} -}) => { - let title = ''; - - switch (recommendation.RecommendationType) { - case RecommendationType.SimilarToRecentlyPlayed: - title = globalize.translate( - 'RecommendationBecauseYouWatched', - recommendation.BaselineItemName - ); - break; - - case RecommendationType.SimilarToLikedItem: - title = globalize.translate( - 'RecommendationBecauseYouLike', - recommendation.BaselineItemName - ); - break; - - case RecommendationType.HasDirectorFromRecentlyPlayed: - case RecommendationType.HasLikedDirector: - title = globalize.translate( - 'RecommendationDirectedBy', - recommendation.BaselineItemName - ); - break; - - case RecommendationType.HasActorFromRecentlyPlayed: - case RecommendationType.HasLikedActor: - title = globalize.translate( - 'RecommendationStarring', - recommendation.BaselineItemName - ); - break; - } - - return ( - - ); -}; - -export default RecommendationContainer; diff --git a/src/apps/experimental/components/library/RecommendationItemsContainer.tsx b/src/apps/experimental/components/library/RecommendationItemsContainer.tsx deleted file mode 100644 index 1a1537a14..000000000 --- a/src/apps/experimental/components/library/RecommendationItemsContainer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -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 = ({ - parentId -}) => { - const { isLoading, data: movieRecommendationsItems } = - useGetMovieRecommendations(parentId); - - if (isLoading) return ; - - if (!movieRecommendationsItems?.length) { - return ( -
-

{globalize.translate('MessageNothingHere')}

-

- {globalize.translate('MessageNoMovieSuggestionsAvailable')} -

-
- ); - } - - return ( - <> - {movieRecommendationsItems.map((recommendation, index) => { - return ( - - ); - })} - - ); -}; - -export default RecommendationItemsContainer; diff --git a/src/apps/experimental/components/library/SectionContainer.tsx b/src/apps/experimental/components/library/SectionContainer.tsx index 325c950a0..18669452a 100644 --- a/src/apps/experimental/components/library/SectionContainer.tsx +++ b/src/apps/experimental/components/library/SectionContainer.tsx @@ -1,8 +1,8 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useEffect, useRef } from 'react'; import cardBuilder from 'components/cardbuilder/cardBuilder'; -import ItemsContainerElement from 'elements/ItemsContainerElement'; +import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; import Scroller from 'elements/emby-scroller/Scroller'; import LinkButton from 'elements/emby-button/LinkButton'; import imageLoader from 'components/images/imageLoader'; @@ -12,7 +12,7 @@ import { CardOptions } from 'types/cardOptions'; interface SectionContainerProps { url?: string; sectionTitle: string; - items: BaseItemDto[]; + items: BaseItemDto[] | TimerInfoDto[]; cardOptions: CardOptions; } @@ -64,7 +64,9 @@ const SectionContainer: FC = ({ isMouseWheelEnabled={false} isCenterFocusEnabled={true} > - +
); diff --git a/src/apps/experimental/components/library/ShuffleButton.tsx b/src/apps/experimental/components/library/ShuffleButton.tsx index c81ee4c4b..609c10272 100644 --- a/src/apps/experimental/components/library/ShuffleButton.tsx +++ b/src/apps/experimental/components/library/ShuffleButton.tsx @@ -1,4 +1,4 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import React, { FC, useCallback } from 'react'; import { IconButton } from '@mui/material'; @@ -11,8 +11,8 @@ import { LibraryViewSettings } from 'types/library'; import { LibraryTab } from 'types/libraryTab'; interface ShuffleButtonProps { - item: BaseItemDto | undefined; - items: BaseItemDto[]; + item: BaseItemDto | null | undefined; + items: BaseItemDto[] | SeriesTimerInfoDto[]; viewType: LibraryTab hasFilters: boolean; libraryViewSettings: LibraryViewSettings diff --git a/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx b/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx deleted file mode 100644 index dcd5ff918..000000000 --- a/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; -import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; -import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; -import React, { FC } from 'react'; -import * as userSettings from 'scripts/settings/userSettings'; -import SuggestionsSectionContainer from './SuggestionsSectionContainer'; -import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections'; -import { ParentId } from 'types/library'; - -const getSuggestionsSections = (): Sections[] => { - return [ - { - name: 'HeaderContinueWatching', - viewType: SectionsViewType.ResumeItems, - type: 'Movie', - view: SectionsView.ContinueWatchingMovies, - parametersOptions: { - includeItemTypes: [BaseItemKind.Movie] - }, - cardOptions: { - scalable: true, - overlayPlayButton: true, - showTitle: true, - centerText: true, - cardLayout: false, - preferThumb: true, - shape: 'overflowBackdrop', - showYear: true - } - }, - { - name: 'HeaderLatestMovies', - viewType: SectionsViewType.LatestMedia, - type: 'Movie', - view: SectionsView.LatestMovies, - parametersOptions: { - includeItemTypes: [BaseItemKind.Movie] - }, - cardOptions: { - scalable: true, - overlayPlayButton: true, - showTitle: true, - centerText: true, - cardLayout: false, - shape: 'overflowPortrait', - showYear: true - } - }, - { - name: 'HeaderContinueWatching', - viewType: SectionsViewType.ResumeItems, - type: 'Episode', - view: SectionsView.ContinueWatchingEpisode, - parametersOptions: { - includeItemTypes: [BaseItemKind.Episode] - }, - cardOptions: { - scalable: true, - overlayPlayButton: true, - showTitle: true, - centerText: true, - cardLayout: false, - shape: 'overflowBackdrop', - preferThumb: true, - inheritThumb: - !userSettings.useEpisodeImagesInNextUpAndResume(undefined), - showYear: true - } - }, - { - name: 'HeaderLatestEpisodes', - viewType: SectionsViewType.LatestMedia, - type: 'Episode', - view: SectionsView.LatestEpisode, - parametersOptions: { - includeItemTypes: [BaseItemKind.Episode] - }, - cardOptions: { - scalable: true, - overlayPlayButton: true, - showTitle: true, - centerText: true, - cardLayout: false, - shape: 'overflowBackdrop', - preferThumb: true, - showSeriesYear: true, - showParentTitle: true, - overlayText: false, - showUnplayedIndicator: false, - showChildCountIndicator: true, - lazy: true, - lines: 2 - } - }, - { - name: 'NextUp', - viewType: SectionsViewType.NextUp, - type: 'nextup', - view: SectionsView.NextUp, - cardOptions: { - scalable: true, - overlayPlayButton: true, - showTitle: true, - centerText: true, - cardLayout: false, - shape: 'overflowBackdrop', - preferThumb: true, - inheritThumb: - !userSettings.useEpisodeImagesInNextUpAndResume(undefined), - showParentTitle: true, - overlayText: false - } - }, - { - name: 'HeaderLatestMusic', - viewType: SectionsViewType.LatestMedia, - type: 'Audio', - view: SectionsView.LatestMusic, - parametersOptions: { - includeItemTypes: [BaseItemKind.Audio] - }, - cardOptions: { - showUnplayedIndicator: false, - shape: 'overflowSquare', - showTitle: true, - showParentTitle: true, - lazy: true, - centerText: true, - overlayPlayButton: true, - cardLayout: false, - coverImage: true - } - }, - { - name: 'HeaderRecentlyPlayed', - type: 'Audio', - view: SectionsView.RecentlyPlayedMusic, - parametersOptions: { - sortBy: [ItemSortBy.DatePlayed], - sortOrder: [SortOrder.Descending], - includeItemTypes: [BaseItemKind.Audio] - }, - cardOptions: { - showUnplayedIndicator: false, - shape: 'overflowSquare', - showTitle: true, - showParentTitle: true, - action: 'instantmix', - lazy: true, - centerText: true, - overlayMoreButton: true, - cardLayout: false, - coverImage: true - } - }, - { - name: 'HeaderFrequentlyPlayed', - type: 'Audio', - view: SectionsView.FrequentlyPlayedMusic, - parametersOptions: { - sortBy: [ItemSortBy.PlayCount], - sortOrder: [SortOrder.Descending], - includeItemTypes: [BaseItemKind.Audio] - }, - cardOptions: { - showUnplayedIndicator: false, - shape: 'overflowSquare', - showTitle: true, - showParentTitle: true, - action: 'instantmix', - lazy: true, - centerText: true, - overlayMoreButton: true, - cardLayout: false, - coverImage: true - } - } - ]; -}; - -interface SuggestionsItemsContainerProps { - parentId: ParentId; - sectionsView: SectionsView[]; -} - -const SuggestionsItemsContainer: FC = ({ - parentId, - sectionsView -}) => { - const suggestionsSections = getSuggestionsSections(); - - return ( - <> - {suggestionsSections - .filter((section) => sectionsView.includes(section.view)) - .map((section) => ( - - ))} - - ); -}; - -export default SuggestionsItemsContainer; diff --git a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx deleted file mode 100644 index 7c09bf489..000000000 --- a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { FC } from 'react'; -import { useGetItemsBySectionType } from 'hooks/useFetchItems'; -import globalize from 'scripts/globalize'; - -import Loading from 'components/loading/LoadingComponent'; -import { appRouter } from 'components/router/appRouter'; -import SectionContainer from './SectionContainer'; - -import { Sections } from 'types/suggestionsSections'; -import { ParentId } from 'types/library'; - -interface SuggestionsSectionContainerProps { - parentId: ParentId; - section: Sections; -} - -const SuggestionsSectionContainer: FC = ({ - parentId, - section -}) => { - const getRouteUrl = () => { - return appRouter.getRouteUrl('list', { - serverId: window.ApiClient.serverId(), - itemTypes: section.type, - parentId: parentId - }); - }; - - const { isLoading, data: items } = useGetItemsBySectionType( - section, - parentId - ); - - if (isLoading) { - return ; - } - - return ( - - ); -}; - -export default SuggestionsSectionContainer; diff --git a/src/apps/experimental/components/library/SuggestionsSectionView.tsx b/src/apps/experimental/components/library/SuggestionsSectionView.tsx new file mode 100644 index 000000000..039f49e4c --- /dev/null +++ b/src/apps/experimental/components/library/SuggestionsSectionView.tsx @@ -0,0 +1,134 @@ +import { + RecommendationDto, + RecommendationType +} from '@jellyfin/sdk/lib/generated-client'; +import React, { FC } from 'react'; +import escapeHTML from 'escape-html'; +import { + useGetMovieRecommendations, + useGetSuggestionSectionsWithItems +} from 'hooks/useFetchItems'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import Loading from 'components/loading/LoadingComponent'; +import SectionContainer from './SectionContainer'; +import { ParentId } from 'types/library'; +import { Section, SectionType } from 'types/sections'; + +interface SuggestionsSectionViewProps { + parentId: ParentId; + sectionType: SectionType[]; + isMovieRecommendationEnabled: boolean | undefined; +} + +const SuggestionsSectionView: FC = ({ + parentId, + sectionType, + isMovieRecommendationEnabled = false +}) => { + const { isLoading, data: sectionsWithItems } = + useGetSuggestionSectionsWithItems(parentId, sectionType); + + const { + isLoading: isRecommendationsLoading, + data: movieRecommendationsItems + } = useGetMovieRecommendations(isMovieRecommendationEnabled, parentId); + + if (isLoading || isRecommendationsLoading) { + return ; + } + + if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) { + return ( +
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoItemsAvailable')}

+
+ ); + } + + const getRouteUrl = (section: Section) => { + return appRouter.getRouteUrl('list', { + serverId: window.ApiClient.serverId(), + itemTypes: section.itemTypes, + parentId: parentId + }); + }; + + const getRecommendationTittle = (recommendation: RecommendationDto) => { + let title = ''; + + switch (recommendation.RecommendationType) { + case RecommendationType.SimilarToRecentlyPlayed: + title = globalize.translate( + 'RecommendationBecauseYouWatched', + recommendation.BaselineItemName + ); + break; + + case RecommendationType.SimilarToLikedItem: + title = globalize.translate( + 'RecommendationBecauseYouLike', + recommendation.BaselineItemName + ); + break; + + case RecommendationType.HasDirectorFromRecentlyPlayed: + case RecommendationType.HasLikedDirector: + title = globalize.translate( + 'RecommendationDirectedBy', + recommendation.BaselineItemName + ); + break; + + case RecommendationType.HasActorFromRecentlyPlayed: + case RecommendationType.HasLikedActor: + title = globalize.translate( + 'RecommendationStarring', + recommendation.BaselineItemName + ); + break; + } + return escapeHTML(title); + }; + + return ( + <> + {sectionsWithItems?.map(({ section, items }) => ( + + ))} + + {movieRecommendationsItems?.map((recommendation, index) => ( + + ))} + + ); +}; + +export default SuggestionsSectionView; diff --git a/src/apps/experimental/components/library/SuggestionsView.tsx b/src/apps/experimental/components/library/SuggestionsView.tsx deleted file mode 100644 index 41aed8291..000000000 --- a/src/apps/experimental/components/library/SuggestionsView.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 = ({ - parentId, - suggestionSectionViews = [], - isMovieRecommendations = false -}) => { - return ( - - - - {isMovieRecommendations && ( - - )} - - ); -}; - -export default SuggestionsView; diff --git a/src/apps/experimental/components/library/filter/FilterButton.tsx b/src/apps/experimental/components/library/filter/FilterButton.tsx index 31845803c..4fa7a9824 100644 --- a/src/apps/experimental/components/library/filter/FilterButton.tsx +++ b/src/apps/experimental/components/library/filter/FilterButton.tsx @@ -321,7 +321,7 @@ const FilterButton: FC = ({ = ({ = ({ = ({ = ({ )} )} - {isFiltersStudiosEnabled() && ( + {isFiltersStudiosEnabled() && studios && ( = ({ >; } const FiltersGenres: FC = ({ - filters, + genresOptions, libraryViewSettings, setLibraryViewSettings }) => { @@ -40,7 +39,7 @@ const FiltersGenres: FC = ({ return ( - {filters?.Genres?.map((filter) => ( + {genresOptions.map((filter) => ( >; } const FiltersOfficialRatings: FC = ({ - filters, + OfficialRatingsOptions, libraryViewSettings, setLibraryViewSettings }) => { @@ -40,7 +39,7 @@ const FiltersOfficialRatings: FC = ({ return ( - {filters?.OfficialRatings?.map((filter) => ( + {OfficialRatingsOptions.map((filter) => ( = ({ && viewType !== LibraryTab.Artists && viewType !== LibraryTab.AlbumArtists && viewType !== LibraryTab.Songs + && viewType !== LibraryTab.Channels ) { visibleFiltersStatus.push(ItemFilter.IsUnplayed); visibleFiltersStatus.push(ItemFilter.IsPlayed); diff --git a/src/apps/experimental/components/library/filter/FiltersStudios.tsx b/src/apps/experimental/components/library/filter/FiltersStudios.tsx index 306ab1a51..00aead503 100644 --- a/src/apps/experimental/components/library/filter/FiltersStudios.tsx +++ b/src/apps/experimental/components/library/filter/FiltersStudios.tsx @@ -1,4 +1,4 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback } from 'react'; import FormGroup from '@mui/material/FormGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; @@ -6,13 +6,13 @@ import Checkbox from '@mui/material/Checkbox'; import { LibraryViewSettings } from 'types/library'; interface FiltersStudiosProps { - filters?: BaseItemDtoQueryResult; + studiosOptions: BaseItemDto[]; libraryViewSettings: LibraryViewSettings; setLibraryViewSettings: React.Dispatch>; } const FiltersStudios: FC = ({ - filters, + studiosOptions, libraryViewSettings, setLibraryViewSettings }) => { @@ -40,7 +40,7 @@ const FiltersStudios: FC = ({ return ( - {filters?.Items?.map((filter) => ( + {studiosOptions?.map((filter) => ( >; } const FiltersTags: FC = ({ - filters, + tagsOptions, libraryViewSettings, setLibraryViewSettings }) => { @@ -40,7 +39,7 @@ const FiltersTags: FC = ({ return ( - {filters?.Tags?.map((filter) => ( + {tagsOptions.map((filter) => ( >; } const FiltersYears: FC = ({ - filters, + yearsOptions, libraryViewSettings, setLibraryViewSettings }) => { @@ -40,7 +39,7 @@ const FiltersYears: FC = ({ return ( - {filters?.Years?.map((filter) => ( + {yearsOptions.map((filter) => ( = ({ isDrawerOpen }) => { const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')); - const location = useLocation(); - const [ searchParams, setSearchParams ] = useSearchParams(); - const searchParamsTab = searchParams.get('tab'); - const libraryId = location.pathname === '/livetv.html' ? - 'livetv' : searchParams.get('topParentId'); - const activeTab = searchParamsTab !== null ? - parseInt(searchParamsTab, 10) : - getDefaultTabIndex(location.pathname, libraryId); + const { searchParams, setSearchParams, activeTab } = useCurrentTab(); // HACK: Force resizing to workaround upstream bug with tab resizing // https://github.com/mui/material-ui/issues/24011 @@ -71,7 +65,7 @@ const AppTabs: FC = ({ { route.tabs.map(({ index, label }) => ( { + const { libraryId, activeTab } = useCurrentTab(); + const currentTab = liveTvTabMapping[activeTab]; + + return ( + + + + ); +}; + +export default LiveTv; diff --git a/src/apps/experimental/routes/movies/index.tsx b/src/apps/experimental/routes/movies/index.tsx index b24f19b0d..995e66679 100644 --- a/src/apps/experimental/routes/movies/index.tsx +++ b/src/apps/experimental/routes/movies/index.tsx @@ -6,7 +6,7 @@ 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'; +import { MovieSuggestionsSectionsView } from 'types/sections'; const moviesTabContent: LibraryTabContent = { viewType: LibraryTab.Movies, @@ -40,13 +40,7 @@ const trailersTabContent: LibraryTabContent = { const suggestionsTabContent: LibraryTabContent = { viewType: LibraryTab.Suggestions, collectionType: CollectionType.Movies, - sectionsType: { - suggestionSectionsView: [ - SectionsView.ContinueWatchingMovies, - SectionsView.LatestMovies - ], - isMovieRecommendations: true - } + sectionsView: MovieSuggestionsSectionsView }; const genresTabContent: LibraryTabContent = { @@ -65,8 +59,8 @@ const moviesTabMapping: LibraryTabMapping = { }; const Movies: FC = () => { - const { searchParamsParentId, currentTabIndex } = useCurrentTab(); - const currentTab = moviesTabMapping[currentTabIndex]; + const { libraryId, activeTab } = useCurrentTab(); + const currentTab = moviesTabMapping[activeTab]; return ( { backDropType='movie' > ); diff --git a/src/apps/experimental/routes/music/index.tsx b/src/apps/experimental/routes/music/index.tsx index 6ab42749b..0bfdff50c 100644 --- a/src/apps/experimental/routes/music/index.tsx +++ b/src/apps/experimental/routes/music/index.tsx @@ -6,7 +6,7 @@ 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'; +import { MusicSuggestionsSectionsView } from 'types/sections'; const albumArtistsTabContent: LibraryTabContent = { viewType: LibraryTab.AlbumArtists, @@ -47,13 +47,7 @@ const songsTabContent: LibraryTabContent = { const suggestionsTabContent: LibraryTabContent = { viewType: LibraryTab.Suggestions, collectionType: CollectionType.Music, - sectionsType: { - suggestionSectionsView: [ - SectionsView.LatestMusic, - SectionsView.FrequentlyPlayedMusic, - SectionsView.RecentlyPlayedMusic - ] - } + sectionsView: MusicSuggestionsSectionsView }; const genresTabContent: LibraryTabContent = { @@ -73,8 +67,8 @@ const musicTabMapping: LibraryTabMapping = { }; const Music: FC = () => { - const { searchParamsParentId, currentTabIndex } = useCurrentTab(); - const currentTab = musicTabMapping[currentTabIndex]; + const { libraryId, activeTab } = useCurrentTab(); + const currentTab = musicTabMapping[activeTab]; return ( { backDropType='musicartist' > ); diff --git a/src/apps/experimental/routes/shows/index.tsx b/src/apps/experimental/routes/shows/index.tsx index 358162238..d19994425 100644 --- a/src/apps/experimental/routes/shows/index.tsx +++ b/src/apps/experimental/routes/shows/index.tsx @@ -5,8 +5,8 @@ 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'; +import { TvShowSuggestionsSectionsView } from 'types/sections'; const episodesTabContent: LibraryTabContent = { viewType: LibraryTab.Episodes, @@ -39,13 +39,7 @@ const upcomingTabContent: LibraryTabContent = { const suggestionsTabContent: LibraryTabContent = { viewType: LibraryTab.Suggestions, collectionType: CollectionType.TvShows, - sectionsType: { - suggestionSectionsView: [ - SectionsView.ContinueWatchingEpisode, - SectionsView.LatestEpisode, - SectionsView.NextUp - ] - } + sectionsView: TvShowSuggestionsSectionsView }; const genresTabContent: LibraryTabContent = { @@ -64,8 +58,8 @@ const tvShowsTabMapping: LibraryTabMapping = { }; const Shows: FC = () => { - const { searchParamsParentId, currentTabIndex } = useCurrentTab(); - const currentTab = tvShowsTabMapping[currentTabIndex]; + const { libraryId, activeTab } = useCurrentTab(); + const currentTab = tvShowsTabMapping[activeTab]; return ( { backDropType='series' > ); diff --git a/src/components/homeScreenSettings/homeScreenSettings.js b/src/components/homeScreenSettings/homeScreenSettings.js index 5a192905b..3a642a634 100644 --- a/src/components/homeScreenSettings/homeScreenSettings.js +++ b/src/components/homeScreenSettings/homeScreenSettings.js @@ -75,7 +75,7 @@ function getLandingScreenOptions(type) { } else if (type === 'tvshows') { list.push({ name: globalize.translate('Shows'), - value: LibraryTab.Shows, + value: LibraryTab.Series, isDefault: true }); list.push({ @@ -152,7 +152,7 @@ function getLandingScreenOptions(type) { }); list.push({ name: globalize.translate('Series'), - value: LibraryTab.Series + value: LibraryTab.SeriesTimers }); } diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index c56d7fc6b..e03fff3a6 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -545,6 +545,30 @@ class AppRouter { urlForList += '&IsFavorite=true'; } + if (options.isAiring) { + urlForList += '&IsAiring=true'; + } + + if (options.isMovie) { + urlForList += '&IsMovie=true'; + } + + if (options.isSeries) { + urlForList += '&IsSeries=true&IsMovie=false&IsNews=false'; + } + + if (options.isSports) { + urlForList += '&IsSports=true'; + } + + if (options.isKids) { + urlForList += '&IsKids=true'; + } + + if (options.isNews) { + urlForList += '&IsNews=true'; + } + return urlForList; } diff --git a/src/controllers/livetv/livetvsuggested.js b/src/controllers/livetv/livetvsuggested.js index b01e75429..aa40afc3d 100644 --- a/src/controllers/livetv/livetvsuggested.js +++ b/src/controllers/livetv/livetvsuggested.js @@ -199,7 +199,7 @@ function getDefaultTabIndex(folderId) { return 3; case LibraryTab.Schedule: return 4; - case LibraryTab.Series: + case LibraryTab.SeriesTimers: return 5; default: return 0; diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx deleted file mode 100644 index 0c4d55724..000000000 --- a/src/elements/ItemsContainerElement.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { FC } from 'react'; - -const createElement = ({ className, dataId }: IProps) => ({ - __html: `
-
` -}); - -interface IProps { - className?: string; - dataId?: string; -} - -const ItemsContainerElement: FC = ({ className, dataId }) => { - return ( -
- ); -}; - -export default ItemsContainerElement; diff --git a/src/elements/emby-itemscontainer/ItemsContainer.tsx b/src/elements/emby-itemscontainer/ItemsContainer.tsx index c4b25547a..bcbda5284 100644 --- a/src/elements/emby-itemscontainer/ItemsContainer.tsx +++ b/src/elements/emby-itemscontainer/ItemsContainer.tsx @@ -21,7 +21,7 @@ import itemShortcuts from 'components/shortcuts'; import MultiSelect from 'components/multiSelect/multiSelect'; import loading from 'components/loading/loading'; import focusManager from 'components/focusManager'; -import { LibraryViewSettings, ParentId, ViewMode } from 'types/library'; +import { ParentId } from 'types/library'; function disableEvent(e: MouseEvent) { e.preventDefault(); @@ -37,20 +37,18 @@ function getShortcutOptions() { interface ItemsContainerProps { className?: string; - libraryViewSettings: LibraryViewSettings; isContextMenuEnabled?: boolean; isMultiSelectEnabled?: boolean; isDragreOrderEnabled?: boolean; dataMonitor?: string; parentId?: ParentId; - reloadItems: () => void; + reloadItems?: () => void; getItemsHtml?: () => string; children?: React.ReactNode; } const ItemsContainer: FC = ({ className, - libraryViewSettings, isContextMenuEnabled, isMultiSelectEnabled, isDragreOrderEnabled, @@ -146,7 +144,9 @@ const ItemsContainer: FC = ({ }); loading.hide(); } catch (error) { + console.error('[Drag-Drop] error playlists Move Item: ' + error); loading.hide(); + if (!reloadItems) return; reloadItems(); } }, @@ -174,6 +174,7 @@ const ItemsContainer: FC = ({ const notifyRefreshNeeded = useCallback( (isInForeground: boolean) => { + if (!reloadItems) return; if (isInForeground === true) { reloadItems(); } else { @@ -506,9 +507,6 @@ const ItemsContainer: FC = ({ const itemsContainerClass = classNames( 'itemsContainer', { 'itemsContainer-tv': layoutManager.tv }, - libraryViewSettings.ViewMode === ViewMode.ListView ? - 'vertical-list' : - 'vertical-wrap', className ); diff --git a/src/hooks/useCurrentTab.ts b/src/hooks/useCurrentTab.ts index 33bc7400c..888be3c67 100644 --- a/src/hooks/useCurrentTab.ts +++ b/src/hooks/useCurrentTab.ts @@ -3,17 +3,22 @@ import { useLocation, useSearchParams } from 'react-router-dom'; const useCurrentTab = () => { const location = useLocation(); - const [searchParams] = useSearchParams(); - const searchParamsParentId = searchParams.get('topParentId'); + const [searchParams, setSearchParams] = useSearchParams(); const searchParamsTab = searchParams.get('tab'); - const currentTabIndex: number = - searchParamsTab !== null ? - parseInt(searchParamsTab, 10) : - getDefaultTabIndex(location.pathname, searchParamsParentId); + const libraryId = + location.pathname === '/livetv.html' ? + 'livetv' : + searchParams.get('topParentId'); + const activeTab: number = + searchParamsTab !== null ? + parseInt(searchParamsTab, 10) : + getDefaultTabIndex(location.pathname, libraryId); return { - searchParamsParentId, - currentTabIndex + searchParams, + setSearchParams, + libraryId, + activeTab }; }; diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index b3d40a287..978cf7a74 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -1,5 +1,5 @@ import { AxiosRequestConfig } from 'axios'; -import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest, TimerInfoDto } 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'; @@ -15,6 +15,7 @@ import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api'; 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 { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api'; import { useMutation, useQuery } from '@tanstack/react-query'; import datetime from 'scripts/datetime'; @@ -22,9 +23,10 @@ import globalize from 'scripts/globalize'; import { JellyfinApiContext, useApi } from './useApi'; import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items'; -import { Sections, SectionsViewType } from 'types/suggestionsSections'; +import { getProgramSections, getSuggestionSections } from 'utils/sections'; import { LibraryViewSettings, ParentId } from 'types/library'; import { LibraryTab } from 'types/libraryTab'; +import { Section, SectionApiMethod, SectionType } from 'types/sections'; const fetchGetItem = async ( currentApi: JellyfinApiContext, @@ -48,10 +50,11 @@ const fetchGetItem = async ( export const useGetItem = (parentId: ParentId) => { const currentApi = useApi(); + const isLivetv = parentId === 'livetv'; return useQuery({ queryKey: ['Item', parentId], queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }), - enabled: !!parentId + enabled: !!parentId && !isLivetv }); }; @@ -116,142 +119,12 @@ const fetchGetMovieRecommendations = async ( } }; -export const useGetMovieRecommendations = (parentId: ParentId) => { +export const useGetMovieRecommendations = (isMovieRecommendationEnabled: boolean, parentId: ParentId) => { const currentApi = useApi(); return useQuery({ - queryKey: ['MovieRecommendations', parentId], + queryKey: ['MovieRecommendations', isMovieRecommendationEnabled, parentId], queryFn: ({ signal }) => - fetchGetMovieRecommendations(currentApi, parentId, { signal }), - enabled: !!parentId - }); -}; - -const fetchGetItemsBySuggestionsType = async ( - currentApi: JellyfinApiContext, - sections: Sections, - parentId: ParentId, - options?: AxiosRequestConfig -) => { - const { api, user } = currentApi; - if (api && user?.Id) { - let response; - switch (sections.viewType) { - case SectionsViewType.NextUp: { - response = ( - await getTvShowsApi(api).getNextUp( - { - userId: user.Id, - limit: 25, - fields: [ - ItemFields.PrimaryImageAspectRatio, - ItemFields.MediaSourceCount - ], - parentId: parentId ?? undefined, - imageTypeLimit: 1, - enableImageTypes: [ - ImageType.Primary, - ImageType.Backdrop, - ImageType.Thumb - ], - enableTotalRecordCount: false, - ...sections.parametersOptions - }, - { - signal: options?.signal - } - ) - ).data.Items; - break; - } - case SectionsViewType.ResumeItems: { - response = ( - await getItemsApi(api).getResumeItems( - { - userId: user.Id, - parentId: parentId ?? undefined, - fields: [ - ItemFields.PrimaryImageAspectRatio, - ItemFields.MediaSourceCount - ], - imageTypeLimit: 1, - enableImageTypes: [ImageType.Thumb], - enableTotalRecordCount: false, - ...sections.parametersOptions - }, - { - signal: options?.signal - } - ) - ).data.Items; - break; - } - case SectionsViewType.LatestMedia: { - response = ( - await getUserLibraryApi(api).getLatestMedia( - { - userId: user.Id, - fields: [ - ItemFields.PrimaryImageAspectRatio, - ItemFields.MediaSourceCount - ], - parentId: parentId ?? undefined, - imageTypeLimit: 1, - enableImageTypes: [ ImageType.Primary, ImageType.Thumb ], - ...sections.parametersOptions - }, - { - signal: options?.signal - } - ) - ).data; - break; - } - default: { - response = ( - await getItemsApi(api).getItems( - { - userId: user.Id, - parentId: parentId ?? undefined, - recursive: true, - fields: [ItemFields.PrimaryImageAspectRatio], - filters: [ItemFilter.IsPlayed], - imageTypeLimit: 1, - enableImageTypes: [ - ImageType.Primary, - ImageType.Backdrop, - ImageType.Thumb - ], - limit: 25, - enableTotalRecordCount: false, - ...sections.parametersOptions - }, - { - signal: options?.signal - } - ) - ).data.Items; - break; - } - } - return response; - } -}; - -export const useGetItemsBySectionType = ( - sections: Sections, - parentId: ParentId -) => { - const currentApi = useApi(); - return useQuery({ - queryKey: ['ItemsBySuggestionsType', sections.view], - queryFn: ({ signal }) => - fetchGetItemsBySuggestionsType( - currentApi, - sections, - parentId, - { signal } - ), - enabled: !!sections.view + isMovieRecommendationEnabled ? fetchGetMovieRecommendations(currentApi, parentId, { signal }) : [] }); }; @@ -314,17 +187,18 @@ const fetchGetStudios = async ( signal: options?.signal } ); - return response.data; + return response.data.Items; } }; export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => { const currentApi = useApi(); + const isLivetv = parentId === 'livetv'; return useQuery({ queryKey: ['Studios', parentId, itemType], queryFn: ({ signal }) => fetchGetStudios(currentApi, parentId, itemType, { signal }), - enabled: !!parentId + enabled: !!parentId && !isLivetv }); }; @@ -355,13 +229,14 @@ export const useGetQueryFiltersLegacy = ( itemType: BaseItemKind[] ) => { const currentApi = useApi(); + const isLivetv = parentId === 'livetv'; return useQuery({ queryKey: ['QueryFiltersLegacy', parentId, itemType], queryFn: ({ signal }) => fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, { signal }), - enabled: !!parentId + enabled: !!parentId && !isLivetv }); }; @@ -434,6 +309,34 @@ const fetchGetItemsViewByType = async ( } ); break; + case LibraryTab.Channels: { + response = await getLiveTvApi(api).getLiveTvChannels( + { + userId: user.Id, + fields: [ItemFields.PrimaryImageAspectRatio], + startIndex: libraryViewSettings.StartIndex, + isFavorite: libraryViewSettings.Filters?.Status?.includes(ItemFilter.IsFavorite) ? + true : + undefined, + enableImageTypes: [ImageType.Primary] + }, + { + signal: options?.signal + } + ); + break; + } + case LibraryTab.SeriesTimers: + response = await getLiveTvApi(api).getSeriesTimers( + { + sortBy: 'SortName', + sortOrder: SortOrder.Ascending + }, + { + signal: options?.signal + } + ); + break; default: { response = await getItemsApi(api).getItems( { @@ -505,8 +408,10 @@ export const useGetItemsViewByType = ( LibraryTab.Songs, LibraryTab.Books, LibraryTab.Photos, - LibraryTab.Videos - ].includes(viewType) && !!parentId + LibraryTab.Videos, + LibraryTab.Channels, + LibraryTab.SeriesTimers + ].includes(viewType) }); }; @@ -690,3 +595,336 @@ export const useTogglePlayedMutation = () => { fetchUpdatePlayedState(currentApi, itemId, playedState ) }); }; + +export type GroupsTimers = { + name: string; + timerInfo: TimerInfoDto[]; +}; + +function groupsTimers(timers: TimerInfoDto[], indexByDate?: boolean) { + const items = timers.map(function (t) { + t.Type = 'Timer'; + return t; + }); + const groups: GroupsTimers[] = []; + let currentGroupName = ''; + let currentGroup: TimerInfoDto[] = []; + + for (const item of items) { + let dateText = ''; + + if (indexByDate !== false && item.StartDate) { + try { + const premiereDate = datetime.parseISO8601Date(item.StartDate, true); + dateText = datetime.toLocaleDateString(premiereDate, { + weekday: 'long', + month: 'short', + day: 'numeric' + }); + } catch (err) { + console.error('error parsing premiereDate:' + item.StartDate + '; error: ' + err); + } + } + + if (dateText != currentGroupName) { + if (currentGroup.length) { + groups.push({ + name: currentGroupName, + timerInfo: currentGroup + }); + } + + currentGroupName = dateText; + currentGroup = [item]; + } else { + currentGroup.push(item); + } + } + + if (currentGroup.length) { + groups.push({ + name: currentGroupName, + timerInfo: currentGroup + }); + } + return groups; +} + +const fetchGetTimers = async ( + currentApi: JellyfinApiContext, + indexByDate?: boolean, + options?: AxiosRequestConfig +) => { + const { api } = currentApi; + if (api) { + const response = await getLiveTvApi(api).getTimers( + { + isActive: false, + isScheduled: true + }, + { + signal: options?.signal + } + ); + + const timers = response.data.Items ?? []; + + return groupsTimers(timers, indexByDate); + } +}; + +export const useGetTimers = (isUpcomingRecordingsEnabled: boolean, indexByDate?: boolean) => { + const currentApi = useApi(); + return useQuery({ + queryKey: ['Timers', isUpcomingRecordingsEnabled, indexByDate], + queryFn: ({ signal }) => + isUpcomingRecordingsEnabled ? fetchGetTimers(currentApi, indexByDate, { signal }) : [] + }); +}; + +const fetchGetSectionItems = async ( + currentApi: JellyfinApiContext, + parentId: ParentId, + section: Section, + options?: AxiosRequestConfig +) => { + const { api, user } = currentApi; + if (api && user?.Id) { + let response; + switch (section.apiMethod) { + case SectionApiMethod.RecommendedPrograms: { + response = ( + await getLiveTvApi(api).getRecommendedPrograms( + { + userId: user.Id, + limit: 12, + imageTypeLimit: 1, + enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop], + enableTotalRecordCount: false, + fields: [ + ItemFields.ChannelInfo, + ItemFields.PrimaryImageAspectRatio, + ItemFields.MediaSourceCount + ], + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.LiveTvPrograms: { + response = ( + await getLiveTvApi(api).getLiveTvPrograms( + { + userId: user.Id, + limit: 12, + imageTypeLimit: 1, + enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop], + enableTotalRecordCount: false, + fields: [ + ItemFields.ChannelInfo, + ItemFields.PrimaryImageAspectRatio + ], + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.Recordings: { + response = ( + await getLiveTvApi(api).getRecordings( + { + userId: user.Id, + enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop], + enableTotalRecordCount: false, + fields: [ + ItemFields.CanDelete, + ItemFields.PrimaryImageAspectRatio + ], + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.RecordingFolders: { + response = ( + await getLiveTvApi(api).getRecordingFolders( + { + userId: user.Id + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.NextUp: { + response = ( + await getTvShowsApi(api).getNextUp( + { + userId: user.Id, + limit: 25, + fields: [ + ItemFields.PrimaryImageAspectRatio, + ItemFields.MediaSourceCount + ], + parentId: parentId ?? undefined, + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ], + enableTotalRecordCount: false, + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.ResumeItems: { + response = ( + await getItemsApi(api).getResumeItems( + { + userId: user.Id, + parentId: parentId ?? undefined, + fields: [ + ItemFields.PrimaryImageAspectRatio, + ItemFields.MediaSourceCount + ], + imageTypeLimit: 1, + enableImageTypes: [ImageType.Thumb], + enableTotalRecordCount: false, + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + case SectionApiMethod.LatestMedia: { + response = ( + await getUserLibraryApi(api).getLatestMedia( + { + userId: user.Id, + fields: [ + ItemFields.PrimaryImageAspectRatio, + ItemFields.MediaSourceCount + ], + parentId: parentId ?? undefined, + imageTypeLimit: 1, + enableImageTypes: [ImageType.Primary], + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data; + break; + } + default: { + response = ( + await getItemsApi(api).getItems( + { + userId: user.Id, + parentId: parentId ?? undefined, + recursive: true, + limit: 25, + enableTotalRecordCount: false, + ...section.parametersOptions + }, + { + signal: options?.signal + } + ) + ).data.Items; + break; + } + } + return response; + } +}; + +type SectionWithItems = { + section: Section; + items: BaseItemDto[]; +}; + +const getSectionsWithItems = async ( + currentApi: JellyfinApiContext, + parentId: ParentId, + sections: Section[], + sectionType?: SectionType[], + options?: AxiosRequestConfig +) => { + if (sectionType) { + sections = sections.filter((section) => sectionType.includes(section.type)); + } + + const updatedSectionWithItems: SectionWithItems[] = []; + + for (const section of sections) { + try { + const items = await fetchGetSectionItems( + currentApi, parentId, section, options + ); + + if (items && items.length > 0) { + updatedSectionWithItems.push({ + section, + items + }); + } + } catch (error) { + console.error(`Error occurred for section ${section.type}: ${error}`); + } + } + + return updatedSectionWithItems; +}; + +export const useGetSuggestionSectionsWithItems = ( + parentId: ParentId, + suggestionSectionType: SectionType[] +) => { + const currentApi = useApi(); + const sections = getSuggestionSections(); + return useQuery({ + queryKey: ['SuggestionSectionWithItems', suggestionSectionType], + queryFn: ({ signal }) => + getSectionsWithItems(currentApi, parentId, sections, suggestionSectionType, { signal }), + enabled: !!parentId + }); +}; + +export const useGetProgramsSectionsWithItems = ( + parentId: ParentId, + programSectionType: SectionType[] +) => { + const currentApi = useApi(); + const sections = getProgramSections(); + return useQuery({ + queryKey: ['ProgramSectionWithItems', programSectionType], + queryFn: ({ signal }) => + getSectionsWithItems(currentApi, parentId, sections, programSectionType, { signal }) + }); +}; + diff --git a/src/strings/en-us.json b/src/strings/en-us.json index ba5c8c033..1f282a211 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -323,6 +323,7 @@ "HeaderAdmin": "Administration", "HeaderAlbumArtists": "Album artists", "HeaderAlert": "Alert", + "HeaderAllRecordings": "All Recordings", "HeaderAllowMediaDeletionFrom": "Allow media deletion from", "HeaderApiKey": "API Key", "HeaderApiKeys": "API Keys", diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 658978293..075f983fd 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -12,7 +12,7 @@ export interface CardOptions { overlayMoreButton?: boolean; overlayPlayButton?: boolean; overlayText?: boolean; - preferThumb?: boolean; + preferThumb?: boolean | string | null; preferDisc?: boolean; preferLogo?: boolean; scalable?: boolean; diff --git a/src/types/libraryTab.ts b/src/types/libraryTab.ts index 1484ed964..1a7529f1a 100644 --- a/src/types/libraryTab.ts +++ b/src/types/libraryTab.ts @@ -15,7 +15,7 @@ export enum LibraryTab { Recordings = 'recordings', Schedule = 'schedule', Series = 'series', - Shows = 'shows', + SeriesTimers = 'seriestimers', Songs = 'songs', Suggestions = 'suggestions', Trailers = 'trailers', diff --git a/src/types/libraryTabContent.ts b/src/types/libraryTabContent.ts index f06896fdb..8fd5c8bf4 100644 --- a/src/types/libraryTabContent.ts +++ b/src/types/libraryTabContent.ts @@ -1,18 +1,22 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; import { LibraryTab } from './libraryTab'; import { CollectionType } from './collectionType'; -import { SectionsView } from './suggestionsSections'; +import { SectionType } from './sections'; -export interface SuggestionsSectionsType { - suggestionSectionsView: SectionsView[]; +export interface SectionsView { + suggestionSections?: SectionType[]; + favoriteSections?: SectionType[]; + programSections?: SectionType[]; isMovieRecommendations?: boolean; + isLiveTvUpcomingRecordings?: boolean; } export interface LibraryTabContent { viewType: LibraryTab; itemType?: BaseItemKind[]; collectionType?: CollectionType; - sectionsType?: SuggestionsSectionsType; + sectionsView?: SectionsView; + isPaginationEnabled?: boolean; isBtnPlayAllEnabled?: boolean; isBtnQueueEnabled?: boolean; isBtnShuffleEnabled?: boolean; diff --git a/src/types/listOptions.ts b/src/types/listOptions.ts index ad943e35b..25e14c49d 100644 --- a/src/types/listOptions.ts +++ b/src/types/listOptions.ts @@ -1,9 +1,9 @@ -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { CollectionType } from './collectionType'; export interface ListOptions { - items?: BaseItemDto[] | null; + items?: BaseItemDto[] | SeriesTimerInfoDto[] | null; index?: string; showIndex?: boolean; action?: string | null; diff --git a/src/types/sections.ts b/src/types/sections.ts new file mode 100644 index 000000000..6af500128 --- /dev/null +++ b/src/types/sections.ts @@ -0,0 +1,109 @@ +import { BaseItemKind, SortOrder } from '@jellyfin/sdk/lib/generated-client'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { CardOptions } from './cardOptions'; +import { SectionsView } from './libraryTabContent'; + +export interface ParametersOptions { + sortBy?: ItemSortBy[]; + sortOrder?: SortOrder[]; + includeItemTypes?: BaseItemKind[]; + isAiring?: boolean; + hasAired?: boolean; + isMovie?: boolean; + isSports?: boolean; + isKids?: boolean; + isNews?: boolean; + isSeries?: boolean; + isInProgress?: boolean; + IsActive?: boolean; + IsScheduled?: boolean; + limit?: number; + imageTypeLimit?: number; +} + +export enum SectionApiMethod { + ResumeItems = 'resumeItems', + LatestMedia = 'latestMedia', + NextUp = 'nextUp', + RecommendedPrograms = 'RecommendedPrograms', + LiveTvPrograms = 'liveTvPrograms', + Recordings = 'Recordings', + RecordingFolders = 'RecordingFolders', +} + +export enum SectionType { + ContinueWatchingMovies = 'continuewatchingmovies', + LatestMovies = 'latestmovies', + ContinueWatchingEpisode = 'continuewatchingepisode', + LatestEpisode = 'latestepisode', + NextUp = 'nextUp', + LatestMusic = 'latestmusic', + RecentlyPlayedMusic = 'recentlyplayedmusic', + FrequentlyPlayedMusic = 'frequentlyplayedmusic', + ActivePrograms = 'ActivePrograms', + UpcomingEpisodes = 'UpcomingEpisodes', + UpcomingMovies = 'UpcomingMovies', + UpcomingSports = 'UpcomingSports', + UpcomingKids = 'UpcomingKids', + UpcomingNews = 'UpcomingNews', + LatestRecordings = 'LatestRecordings', + RecordingFolders = 'RecordingFolders', + ActiveRecordings = 'ActiveRecordings', + UpcomingRecordings = 'UpcomingRecordings' +} + +export interface Section { + name: string; + type: SectionType; + apiMethod?: SectionApiMethod; + itemTypes: string; + parametersOptions?: ParametersOptions; + cardOptions: CardOptions; +} + +export const MovieSuggestionsSectionsView: SectionsView = { + suggestionSections: [ + SectionType.ContinueWatchingMovies, + SectionType.LatestMovies + ], + isMovieRecommendations: true +}; + +export const TvShowSuggestionsSectionsView: SectionsView = { + suggestionSections: [ + SectionType.ContinueWatchingEpisode, + SectionType.LatestEpisode, + SectionType.NextUp + ] +}; + +export const MusicSuggestionsSectionsView: SectionsView = { + suggestionSections: [ + SectionType.LatestMusic, + SectionType.FrequentlyPlayedMusic, + SectionType.RecentlyPlayedMusic + ] +}; + +export const ProgramSectionsView: SectionsView = { + programSections: [ + SectionType.ActivePrograms, + SectionType.UpcomingEpisodes, + SectionType.UpcomingMovies, + SectionType.UpcomingSports, + SectionType.UpcomingKids, + SectionType.UpcomingNews + ] +}; + +export const RecordingsSectionsView: SectionsView = { + programSections: [ + SectionType.LatestRecordings, + SectionType.RecordingFolders + ] +}; + +export const ScheduleSectionsView: SectionsView = { + programSections: [SectionType.ActiveRecordings], + isLiveTvUpcomingRecordings: true +}; diff --git a/src/types/suggestionsSections.ts b/src/types/suggestionsSections.ts deleted file mode 100644 index afb198a97..000000000 --- a/src/types/suggestionsSections.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; -import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; -import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; -import { CardOptions } from './cardOptions'; - -interface ParametersOptions { - sortBy?: ItemSortBy[]; - sortOrder?: SortOrder[]; - includeItemTypes?: BaseItemKind[]; -} - -export enum SectionsViewType { - ResumeItems = 'resumeItems', - LatestMedia = 'latestMedia', - NextUp = 'nextUp', -} - -export enum SectionsView { - ContinueWatchingMovies = 'continuewatchingmovies', - LatestMovies = 'latestmovies', - ContinueWatchingEpisode = 'continuewatchingepisode', - LatestEpisode = 'latestepisode', - NextUp = 'nextUp', - LatestMusic = 'latestmusic', - RecentlyPlayedMusic = 'recentlyplayedmusic', - FrequentlyPlayedMusic = 'frequentlyplayedmusic', -} - -export interface Sections { - name: string; - view: SectionsView; - type: string; - viewType?: SectionsViewType, - parametersOptions?: ParametersOptions; - cardOptions: CardOptions; -} diff --git a/src/utils/sections.ts b/src/utils/sections.ts new file mode 100644 index 000000000..4617ea7f1 --- /dev/null +++ b/src/utils/sections.ts @@ -0,0 +1,367 @@ +import { ImageType, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; +import * as userSettings from 'scripts/settings/userSettings'; +import { Section, SectionType, SectionApiMethod } from 'types/sections'; + +export const getSuggestionSections = (): Section[] => { + const parametersOptions = { + fields: [ItemFields.PrimaryImageAspectRatio], + filters: [ItemFilter.IsPlayed], + IsPlayed: true, + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ] + }; + return [ + { + name: 'HeaderContinueWatching', + apiMethod: SectionApiMethod.ResumeItems, + itemTypes: 'Movie', + type: SectionType.ContinueWatchingMovies, + parametersOptions: { + includeItemTypes: [BaseItemKind.Movie] + }, + cardOptions: { + overlayPlayButton: true, + preferThumb: true, + shape: 'overflowBackdrop', + showYear: true + } + }, + { + name: 'HeaderLatestMovies', + apiMethod: SectionApiMethod.LatestMedia, + itemTypes: 'Movie', + type: SectionType.LatestMovies, + parametersOptions: { + includeItemTypes: [BaseItemKind.Movie] + }, + cardOptions: { + overlayPlayButton: true, + shape: 'overflowPortrait', + showYear: true + } + }, + { + name: 'HeaderContinueWatching', + apiMethod: SectionApiMethod.ResumeItems, + itemTypes: 'Episode', + type: SectionType.ContinueWatchingEpisode, + parametersOptions: { + includeItemTypes: [BaseItemKind.Episode] + }, + cardOptions: { + overlayPlayButton: true, + shape: 'overflowBackdrop', + preferThumb: true, + inheritThumb: + !userSettings.useEpisodeImagesInNextUpAndResume(undefined), + showYear: true + } + }, + { + name: 'HeaderLatestEpisodes', + apiMethod: SectionApiMethod.LatestMedia, + itemTypes: 'Episode', + type: SectionType.LatestEpisode, + parametersOptions: { + includeItemTypes: [BaseItemKind.Episode] + }, + cardOptions: { + overlayPlayButton: true, + shape: 'overflowBackdrop', + preferThumb: true, + showSeriesYear: true, + showParentTitle: true, + showUnplayedIndicator: false, + showChildCountIndicator: true, + lines: 2 + } + }, + { + name: 'NextUp', + apiMethod: SectionApiMethod.NextUp, + itemTypes: 'nextup', + type: SectionType.NextUp, + cardOptions: { + overlayPlayButton: true, + shape: 'overflowBackdrop', + preferThumb: true, + inheritThumb: + !userSettings.useEpisodeImagesInNextUpAndResume(undefined), + showParentTitle: true + } + }, + { + name: 'HeaderLatestMusic', + apiMethod: SectionApiMethod.LatestMedia, + itemTypes: 'Audio', + type: SectionType.LatestMusic, + parametersOptions: { + includeItemTypes: [BaseItemKind.Audio] + }, + cardOptions: { + showUnplayedIndicator: false, + shape: 'overflowSquare', + showParentTitle: true, + overlayPlayButton: true, + coverImage: true + } + }, + { + name: 'HeaderRecentlyPlayed', + itemTypes: 'Audio', + type: SectionType.RecentlyPlayedMusic, + parametersOptions: { + sortBy: [ItemSortBy.DatePlayed], + sortOrder: [SortOrder.Descending], + includeItemTypes: [BaseItemKind.Audio], + ...parametersOptions + }, + cardOptions: { + showUnplayedIndicator: false, + shape: 'overflowSquare', + showParentTitle: true, + action: 'instantmix', + overlayMoreButton: true, + coverImage: true + } + }, + { + name: 'HeaderFrequentlyPlayed', + itemTypes: 'Audio', + type: SectionType.FrequentlyPlayedMusic, + parametersOptions: { + sortBy: [ItemSortBy.PlayCount], + sortOrder: [SortOrder.Descending], + includeItemTypes: [BaseItemKind.Audio], + ...parametersOptions + }, + cardOptions: { + showUnplayedIndicator: false, + shape: 'overflowSquare', + showParentTitle: true, + action: 'instantmix', + overlayMoreButton: true, + coverImage: true + } + } + ]; +}; + +export const getProgramSections = (): Section[] => { + const cardOptions = { + inheritThumb: false, + shape: 'autooverflow', + defaultShape: 'overflowBackdrop', + centerText: true, + coverImage: true, + overlayText: false, + lazy: true, + showAirTime: true + }; + + return [ + { + name: 'HeaderOnNow', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.RecommendedPrograms, + type: SectionType.ActivePrograms, + parametersOptions: { + isAiring: true + }, + cardOptions: { + showParentTitle: true, + showTitle: true, + showAirDateTime: false, + showAirEndTime: true, + overlayPlayButton: true, + overlayMoreButton: false, + overlayInfoButton: false, + preferThumb: 'auto', + ...cardOptions + } + }, + { + name: 'Shows', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.LiveTvPrograms, + type: SectionType.UpcomingEpisodes, + parametersOptions: { + isAiring: false, + hasAired: false, + isMovie: false, + isSports: false, + isKids: false, + isNews: false, + isSeries: true + }, + cardOptions: { + showParentTitle: true, + showTitle: true, + overlayPlayButton: false, + overlayMoreButton: false, + overlayInfoButton: false, + preferThumb: 'auto', + showAirDateTime: true, + ...cardOptions + } + }, + { + name: 'Movies', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.LiveTvPrograms, + type: SectionType.UpcomingMovies, + parametersOptions: { + isAiring: false, + hasAired: false, + isMovie: true + }, + cardOptions: { + preferThumb: null, + showParentTitle: false, + showTitle: true, + overlayPlayButton: false, + overlayMoreButton: false, + overlayInfoButton: false, + showAirDateTime: true, + ...cardOptions + } + }, + { + name: 'Sports', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.LiveTvPrograms, + type: SectionType.UpcomingSports, + parametersOptions: { + isAiring: false, + hasAired: false, + isSports: true + }, + cardOptions: { + showParentTitle: true, + showTitle: true, + overlayPlayButton: false, + overlayMoreButton: false, + overlayInfoButton: false, + preferThumb: 'auto', + showAirDateTime: true, + ...cardOptions + } + }, + { + name: 'HeaderForKids', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.LiveTvPrograms, + type: SectionType.UpcomingKids, + parametersOptions: { + isAiring: false, + hasAired: false, + isKids: true + }, + cardOptions: { + showParentTitle: true, + showTitle: true, + overlayPlayButton: false, + overlayMoreButton: false, + overlayInfoButton: false, + preferThumb: 'auto', + showAirDateTime: true, + ...cardOptions + } + }, + { + name: 'News', + itemTypes: 'Programs', + apiMethod: SectionApiMethod.LiveTvPrograms, + type: SectionType.UpcomingNews, + parametersOptions: { + isAiring: false, + hasAired: false, + isNews: true + }, + cardOptions: { + overlayPlayButton: false, + overlayMoreButton: false, + overlayInfoButton: false, + showParentTitleOrTitle: true, + showTitle: false, + showParentTitle: false, + preferThumb: 'auto', + showAirDateTime: true, + ...cardOptions + } + }, + { + name: 'HeaderLatestRecordings', + itemTypes: 'Recordings', + apiMethod: SectionApiMethod.Recordings, + type: SectionType.LatestRecordings, + parametersOptions: { + limit: 12, + imageTypeLimit: 1 + }, + cardOptions: { + showYear: true, + lines: 2, + shape: 'autooverflow', + defaultShape: 'overflowBackdrop', + showTitle: true, + showParentTitle: true, + coverImage: true, + cardLayout: false, + centerText: true, + preferThumb: 'auto', + overlayText: false + } + }, + { + name: 'HeaderAllRecordings', + itemTypes: 'Recordings', + apiMethod: SectionApiMethod.RecordingFolders, + type: SectionType.RecordingFolders, + cardOptions: { + showYear: false, + showParentTitle: false, + shape: 'autooverflow', + defaultShape: 'overflowBackdrop', + showTitle: true, + coverImage: true, + cardLayout: false, + centerText: true, + preferThumb: 'auto', + overlayText: false + } + }, + { + name: 'HeaderActiveRecordings', + itemTypes: 'Recordings', + apiMethod: SectionApiMethod.Recordings, + type: SectionType.ActiveRecordings, + parametersOptions: { + isInProgress: true + }, + cardOptions: { + shape: 'autooverflow', + defaultShape: 'backdrop', + showParentTitle: false, + showParentTitleOrTitle: true, + showTitle: true, + showAirTime: true, + showAirEndTime: true, + showChannelName: true, + coverImage: true, + overlayText: false, + overlayMoreButton: true, + cardLayout: false, + centerText: true, + preferThumb: 'auto' + } + } + ]; +};