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:
parent
c37783479e
commit
e41436552e
44 changed files with 1396 additions and 749 deletions
51
src/apps/experimental/components/library/GuideView.tsx
Normal file
51
src/apps/experimental/components/library/GuideView.tsx
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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={
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue