Merge pull request #5055 from grafixeyehero/Add-livetv-view

Add livetv view
This commit is contained in:
Bill Thornton 2024-01-12 16:36:04 -05:00 committed by GitHub
commit 5ea61f7559
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1396 additions and 749 deletions

View file

@ -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<Guide | null>();
const tvGuideContainerRef = useRef<HTMLDivElement>(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 <div ref={tvGuideContainerRef} />;
};
export default GuideView;

View file

@ -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<ItemsViewProps> = ({
viewType,
parentId,
collectionType,
isPaginationEnabled = true,
isBtnPlayAllEnabled = false,
isBtnQueueEnabled = false,
isBtnShuffleEnabled = false,
@ -145,6 +148,18 @@ const ItemsView: FC<ItemsViewProps> = ({
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<ItemsViewProps> = ({
ItemSortBy.SortName
);
const itemsContainerClass = classNames(
'centered padded-left padded-right padded-right-withalphapicker',
libraryViewSettings.ViewMode === ViewMode.ListView ?
'vertical-list' :
'vertical-wrap'
);
return (
<Box>
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
{isPaginationEnabled && (
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnPlayAllEnabled && (
<PlayAllButton
@ -263,22 +286,23 @@ const ItemsView: FC<ItemsViewProps> = ({
<Loading />
) : (
<ItemsContainer
className='centered padded-left padded-right padded-right-withalphapicker'
libraryViewSettings={libraryViewSettings}
className={itemsContainerClass}
parentId={parentId}
reloadItems={refetch}
getItemsHtml={getItemsHtml}
/>
)}
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
</Box>
{isPaginationEnabled && (
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
</Box>
)}
</Box>
);
};

View file

@ -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<PageTabContentProps> = ({ parentId, currentTab }) => {
if (currentTab.viewType === LibraryTab.Suggestions) {
return (
<SuggestionsView
<SuggestionsSectionView
parentId={parentId}
suggestionSectionViews={
currentTab.sectionsType?.suggestionSectionsView
sectionType={
currentTab.sectionsView?.suggestionSections ?? []
}
isMovieRecommendations={
currentTab.sectionsType?.isMovieRecommendations
isMovieRecommendationEnabled={
currentTab.sectionsView?.isMovieRecommendations
}
/>
);
}
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
return (
<ProgramsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.programSections ?? []
}
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
/>
);
}
if (currentTab.viewType === LibraryTab.Upcoming) {
return <UpcomingView parentId={parentId} />;
}
@ -41,11 +55,16 @@ const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
);
}
if (currentTab.viewType === LibraryTab.Guide) {
return <GuideView />;
}
return (
<ItemsView
viewType={currentTab.viewType}
parentId={parentId}
collectionType={currentTab.collectionType}
isPaginationEnabled={currentTab.isPaginationEnabled}
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}

View file

@ -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 PlayArrowIcon from '@mui/icons-material/PlayArrow';
@ -10,8 +10,8 @@ import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
interface PlayAllButtonProps {
item: BaseItemDto | undefined;
items: BaseItemDto[];
item: BaseItemDto | null | undefined;
items: BaseItemDto[] | SeriesTimerInfoDto[];
viewType: LibraryTab;
hasFilters: boolean;
libraryViewSettings: LibraryViewSettings

View file

@ -0,0 +1,98 @@
import React, { FC } from 'react';
import { useGetProgramsSectionsWithItems, useGetTimers } 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 ProgramsSectionViewProps {
parentId: ParentId;
sectionType: SectionType[];
isUpcomingRecordingsEnabled: boolean | undefined
}
const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
parentId,
sectionType,
isUpcomingRecordingsEnabled = false
}) => {
const { isLoading, data: sectionsWithItems } = useGetProgramsSectionsWithItems(parentId, sectionType);
const {
isLoading: isUpcomingRecordingsLoading,
data: upcomingRecordings
} = useGetTimers(isUpcomingRecordingsEnabled);
if (isLoading || isUpcomingRecordingsLoading) {
return <Loading />;
}
if (!sectionsWithItems?.length && !upcomingRecordings?.length) {
return (
<div className='noItemsMessage centerMessage'>
<h1>{globalize.translate('MessageNothingHere')}</h1>
<p>
{globalize.translate('MessageNoItemsAvailable')}
</p>
</div>
);
}
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 }) => (
<SectionContainer
key={section.type}
sectionTitle={globalize.translate(section.name)}
items={items ?? []}
url={getRouteUrl(section)}
cardOptions={{
...section.cardOptions
}}
/>
))}
{upcomingRecordings?.map((group) => (
<SectionContainer
key={group.name}
sectionTitle={group.name}
items={group.timerInfo ?? []}
cardOptions={{
shape: 'overflowBackdrop',
showTitle: true,
showParentTitleOrTitle: true,
showAirTime: true,
showAirEndTime: true,
showChannelName: false,
cardLayout: true,
centerText: false,
action: 'edit',
cardFooterAside: 'none',
preferThumb: true,
coverImage: true,
allowBottomPadding: false,
overlayText: false,
showChannelLogo: true
}}
/>
))}
</>
);
};
export default ProgramsSectionView;

View file

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

View file

@ -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<RecommendationContainerProps> = ({
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 (
<SectionContainer
sectionTitle={escapeHTML(title)}
items={recommendation.Items ?? []}
cardOptions={{
shape: 'overflowPortrait',
showYear: true,
scalable: true,
overlayPlayButton: true,
showTitle: true,
centerText: true,
cardLayout: false
}}
/>
);
};
export default RecommendationContainer;

View file

@ -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<RecommendationItemsContainerProps> = ({
parentId
}) => {
const { isLoading, data: movieRecommendationsItems } =
useGetMovieRecommendations(parentId);
if (isLoading) return <Loading />;
if (!movieRecommendationsItems?.length) {
return (
<div className='noItemsMessage centerMessage'>
<h1>{globalize.translate('MessageNothingHere')}</h1>
<p>
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
</p>
</div>
);
}
return (
<>
{movieRecommendationsItems.map((recommendation, index) => {
return (
<RecommendationContainer
// eslint-disable-next-line react/no-array-index-key
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
recommendation={recommendation}
/>
);
})}
</>
);
};
export default RecommendationItemsContainer;

View file

@ -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<SectionContainerProps> = ({
isMouseWheelEnabled={false}
isCenterFocusEnabled={true}
>
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
<ItemsContainer
className='itemsContainer scrollSlider focuscontainer-x'
/>
</Scroller>
</div>
);

View file

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

View file

@ -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<SuggestionsItemsContainerProps> = ({
parentId,
sectionsView
}) => {
const suggestionsSections = getSuggestionsSections();
return (
<>
{suggestionsSections
.filter((section) => sectionsView.includes(section.view))
.map((section) => (
<SuggestionsSectionContainer
key={section.view}
parentId={parentId}
section={section}
/>
))}
</>
);
};
export default SuggestionsItemsContainer;

View file

@ -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<SuggestionsSectionContainerProps> = ({
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 <Loading />;
}
return (
<SectionContainer
sectionTitle={globalize.translate(section.name)}
items={items ?? []}
url={getRouteUrl()}
cardOptions={{
...section.cardOptions
}}
/>
);
};
export default SuggestionsSectionContainer;

View file

@ -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<SuggestionsSectionViewProps> = ({
parentId,
sectionType,
isMovieRecommendationEnabled = false
}) => {
const { isLoading, data: sectionsWithItems } =
useGetSuggestionSectionsWithItems(parentId, sectionType);
const {
isLoading: isRecommendationsLoading,
data: movieRecommendationsItems
} = useGetMovieRecommendations(isMovieRecommendationEnabled, parentId);
if (isLoading || isRecommendationsLoading) {
return <Loading />;
}
if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) {
return (
<div className='noItemsMessage centerMessage'>
<h1>{globalize.translate('MessageNothingHere')}</h1>
<p>{globalize.translate('MessageNoItemsAvailable')}</p>
</div>
);
}
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 }) => (
<SectionContainer
key={section.type}
sectionTitle={globalize.translate(section.name)}
items={items ?? []}
url={getRouteUrl(section)}
cardOptions={{
...section.cardOptions,
showTitle: true,
centerText: true,
cardLayout: false,
overlayText: false
}}
/>
))}
{movieRecommendationsItems?.map((recommendation, index) => (
<SectionContainer
// eslint-disable-next-line react/no-array-index-key
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
sectionTitle={getRecommendationTittle(recommendation)}
items={recommendation.Items ?? []}
cardOptions={{
shape: 'overflowPortrait',
showYear: true,
scalable: true,
overlayPlayButton: true,
showTitle: true,
centerText: true,
cardLayout: false
}}
/>
))}
</>
);
};
export default SuggestionsSectionView;

View file

@ -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<SuggestionsViewProps> = ({
parentId,
suggestionSectionViews = [],
isMovieRecommendations = false
}) => {
return (
<Box>
<SuggestionsItemsContainer
parentId={parentId}
sectionsView={suggestionSectionViews}
/>
{isMovieRecommendations && (
<RecommendationItemsContainer parentId={parentId} />
)}
</Box>
);
};
export default SuggestionsView;

View file

@ -321,7 +321,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary>
<AccordionDetails>
<FiltersGenres
filters={data}
genresOptions={data.Genres}
libraryViewSettings={
libraryViewSettings
}
@ -355,7 +355,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary>
<AccordionDetails>
<FiltersOfficialRatings
filters={data}
OfficialRatingsOptions={data.OfficialRatings}
libraryViewSettings={
libraryViewSettings
}
@ -382,7 +382,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary>
<AccordionDetails>
<FiltersTags
filters={data}
tagsOptions={data.Tags}
libraryViewSettings={
libraryViewSettings
}
@ -409,7 +409,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary>
<AccordionDetails>
<FiltersYears
filters={data}
yearsOptions={data.Years}
libraryViewSettings={
libraryViewSettings
}
@ -422,7 +422,7 @@ const FilterButton: FC<FilterButtonProps> = ({
)}
</>
)}
{isFiltersStudiosEnabled() && (
{isFiltersStudiosEnabled() && studios && (
<Accordion
expanded={expanded === 'filtersStudios'}
onChange={handleChange('filtersStudios')}
@ -437,7 +437,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary>
<AccordionDetails>
<FiltersStudios
filters={studios}
studiosOptions={studios}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings

View file

@ -1,4 +1,3 @@
import type { QueryFiltersLegacy } 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 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersGenresProps {
filters?: QueryFiltersLegacy;
genresOptions: string[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersGenres: FC<FiltersGenresProps> = ({
filters,
genresOptions,
libraryViewSettings,
setLibraryViewSettings
}) => {
@ -40,7 +39,7 @@ const FiltersGenres: FC<FiltersGenresProps> = ({
return (
<FormGroup>
{filters?.Genres?.map((filter) => (
{genresOptions.map((filter) => (
<FormControlLabel
key={filter}
control={

View file

@ -1,4 +1,3 @@
import type { QueryFiltersLegacy } 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 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersOfficialRatingsProps {
filters?: QueryFiltersLegacy;
OfficialRatingsOptions: string[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
filters,
OfficialRatingsOptions,
libraryViewSettings,
setLibraryViewSettings
}) => {
@ -40,7 +39,7 @@ const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
return (
<FormGroup>
{filters?.OfficialRatings?.map((filter) => (
{OfficialRatingsOptions.map((filter) => (
<FormControlLabel
key={filter}
control={

View file

@ -55,6 +55,7 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
&& viewType !== LibraryTab.Artists
&& viewType !== LibraryTab.AlbumArtists
&& viewType !== LibraryTab.Songs
&& viewType !== LibraryTab.Channels
) {
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
visibleFiltersStatus.push(ItemFilter.IsPlayed);

View file

@ -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<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersStudios: FC<FiltersStudiosProps> = ({
filters,
studiosOptions,
libraryViewSettings,
setLibraryViewSettings
}) => {
@ -40,7 +40,7 @@ const FiltersStudios: FC<FiltersStudiosProps> = ({
return (
<FormGroup>
{filters?.Items?.map((filter) => (
{studiosOptions?.map((filter) => (
<FormControlLabel
key={filter.Id}
control={

View file

@ -1,4 +1,3 @@
import type { QueryFiltersLegacy } 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 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersTagsProps {
filters?: QueryFiltersLegacy;
tagsOptions: string[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersTags: FC<FiltersTagsProps> = ({
filters,
tagsOptions,
libraryViewSettings,
setLibraryViewSettings
}) => {
@ -40,7 +39,7 @@ const FiltersTags: FC<FiltersTagsProps> = ({
return (
<FormGroup>
{filters?.Tags?.map((filter) => (
{tagsOptions.map((filter) => (
<FormControlLabel
key={filter}
control={

View file

@ -1,4 +1,3 @@
import type { QueryFiltersLegacy } 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 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersYearsProps {
filters?: QueryFiltersLegacy;
yearsOptions: number[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersYears: FC<FiltersYearsProps> = ({
filters,
yearsOptions,
libraryViewSettings,
setLibraryViewSettings
}) => {
@ -40,7 +39,7 @@ const FiltersYears: FC<FiltersYearsProps> = ({
return (
<FormGroup>
{filters?.Years?.map((filter) => (
{yearsOptions.map((filter) => (
<FormControlLabel
key={filter}
control={

View file

@ -4,9 +4,10 @@ import Tabs from '@mui/material/Tabs';
import useMediaQuery from '@mui/material/useMediaQuery';
import { debounce } from 'lodash-es';
import React, { FC, useCallback, useEffect } from 'react';
import { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
import TabRoutes from './tabRoutes';
import useCurrentTab from 'hooks/useCurrentTab';
interface AppTabsParams {
isDrawerOpen: boolean
@ -18,14 +19,7 @@ const AppTabs: FC<AppTabsParams> = ({
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<AppTabsParams> = ({
{
route.tabs.map(({ index, label }) => (
<Tab
key={`${route}-tab-${index}`}
key={`${route.path}-tab-${index}`}
label={label}
data-tab-index={`${index}`}
onClick={onTabClick}

View file

@ -68,7 +68,7 @@ const TabRoutes: TabRoute[] = [
{
index: 5,
label: globalize.translate('Series'),
value: LibraryTab.Series
value: LibraryTab.SeriesTimers
}
]
},
@ -155,7 +155,7 @@ const TabRoutes: TabRoute[] = [
{
index: 0,
label: globalize.translate('Shows'),
value: LibraryTab.Shows,
value: LibraryTab.Series,
isDefault: true
},
{

View file

@ -7,5 +7,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental }
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental }
];

View file

@ -13,12 +13,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'list',
view: 'list.html'
}
}, {
path: 'livetv.html',
pageProps: {
controller: 'livetv/livetvsuggested',
view: 'livetv.html'
}
}, {
path: 'mypreferencesmenu.html',
pageProps: {

View file

@ -0,0 +1,71 @@
import React, { FC } from 'react';
import useCurrentTab from 'hooks/useCurrentTab';
import Page from 'components/Page';
import PageTabContent from '../../components/library/PageTabContent';
import { LibraryTab } from 'types/libraryTab';
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
import { ProgramSectionsView, RecordingsSectionsView, ScheduleSectionsView } from 'types/sections';
const seriestimersTabContent: LibraryTabContent = {
viewType: LibraryTab.SeriesTimers,
isPaginationEnabled: false,
isBtnFilterEnabled: false,
isBtnGridListEnabled: false,
isBtnSortEnabled: false,
isAlphabetPickerEnabled: false
};
const scheduleTabContent: LibraryTabContent = {
viewType: LibraryTab.Schedule,
sectionsView: ScheduleSectionsView
};
const recordingsTabContent: LibraryTabContent = {
viewType: LibraryTab.Recordings,
sectionsView: RecordingsSectionsView
};
const channelsTabContent: LibraryTabContent = {
viewType: LibraryTab.Channels,
isBtnGridListEnabled: false,
isBtnSortEnabled: false,
isAlphabetPickerEnabled: false
};
const programsTabContent: LibraryTabContent = {
viewType: LibraryTab.Programs,
sectionsView: ProgramSectionsView
};
const guideTabContent: LibraryTabContent = {
viewType: LibraryTab.Guide
};
const liveTvTabMapping: LibraryTabMapping = {
0: programsTabContent,
1: guideTabContent,
2: channelsTabContent,
3: recordingsTabContent,
4: scheduleTabContent,
5: seriestimersTabContent
};
const LiveTv: FC = () => {
const { libraryId, activeTab } = useCurrentTab();
const currentTab = liveTvTabMapping[activeTab];
return (
<Page
id='liveTvPage'
className='mainAnimatedPage libraryPage collectionEditorPage pageWithAbsoluteTabs withTabs'
>
<PageTabContent
key={`${currentTab.viewType} - ${libraryId}`}
currentTab={currentTab}
parentId={libraryId}
/>
</Page>
);
};
export default LiveTv;

View file

@ -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 (
<Page
@ -75,9 +69,9 @@ const Movies: FC = () => {
backDropType='movie'
>
<PageTabContent
key={`${currentTab.viewType} - ${searchParamsParentId}`}
key={`${currentTab.viewType} - ${libraryId}`}
currentTab={currentTab}
parentId={searchParamsParentId}
parentId={libraryId}
/>
</Page>
);

View file

@ -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 (
<Page
@ -83,9 +77,9 @@ const Music: FC = () => {
backDropType='musicartist'
>
<PageTabContent
key={`${currentTab.viewType} - ${searchParamsParentId}`}
key={`${currentTab.viewType} - ${libraryId}`}
currentTab={currentTab}
parentId={searchParamsParentId}
parentId={libraryId}
/>
</Page>
);

View file

@ -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 (
<Page
@ -74,9 +68,9 @@ const Shows: FC = () => {
backDropType='series'
>
<PageTabContent
key={`${currentTab.viewType} - ${searchParamsParentId}`}
key={`${currentTab.viewType} - ${libraryId}`}
currentTab={currentTab}
parentId={searchParamsParentId}
parentId={libraryId}
/>
</Page>
);

View file

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

View file

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

View file

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

View file

@ -1,28 +0,0 @@
import React, { FC } from 'react';
const createElement = ({ className, dataId }: IProps) => ({
__html: `<div
is="emby-itemscontainer"
class="${className}"
${dataId}
>
</div>`
});
interface IProps {
className?: string;
dataId?: string;
}
const ItemsContainerElement: FC<IProps> = ({ className, dataId }) => {
return (
<div
dangerouslySetInnerHTML={createElement({
className: className,
dataId: dataId ? `data-id="${dataId}"` : ''
})}
/>
);
};
export default ItemsContainerElement;

View file

@ -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<ItemsContainerProps> = ({
className,
libraryViewSettings,
isContextMenuEnabled,
isMultiSelectEnabled,
isDragreOrderEnabled,
@ -146,7 +144,9 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
});
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<ItemsContainerProps> = ({
const notifyRefreshNeeded = useCallback(
(isInForeground: boolean) => {
if (!reloadItems) return;
if (isInForeground === true) {
reloadItems();
} else {
@ -506,9 +507,6 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
const itemsContainerClass = classNames(
'itemsContainer',
{ 'itemsContainer-tv': layoutManager.tv },
libraryViewSettings.ViewMode === ViewMode.ListView ?
'vertical-list' :
'vertical-wrap',
className
);

View file

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

View file

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

View file

@ -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",

View file

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

View file

@ -15,7 +15,7 @@ export enum LibraryTab {
Recordings = 'recordings',
Schedule = 'schedule',
Series = 'series',
Shows = 'shows',
SeriesTimers = 'seriestimers',
Songs = 'songs',
Suggestions = 'suggestions',
Trailers = 'trailers',

View file

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

View file

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

109
src/types/sections.ts Normal file
View file

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

View file

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

367
src/utils/sections.ts Normal file
View file

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