1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Add livetv view

This commit is contained in:
grafixeyehero 2024-01-12 21:08:06 +03:00
parent c37783479e
commit e41436552e
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={