Merge pull request #5055 from grafixeyehero/Add-livetv-view
Add livetv view
This commit is contained in:
commit
5ea61f7559
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 { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
||||||
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
||||||
|
@ -33,6 +34,7 @@ interface ItemsViewProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
itemType: BaseItemKind[];
|
itemType: BaseItemKind[];
|
||||||
collectionType?: CollectionType;
|
collectionType?: CollectionType;
|
||||||
|
isPaginationEnabled?: boolean;
|
||||||
isBtnPlayAllEnabled?: boolean;
|
isBtnPlayAllEnabled?: boolean;
|
||||||
isBtnQueueEnabled?: boolean;
|
isBtnQueueEnabled?: boolean;
|
||||||
isBtnShuffleEnabled?: boolean;
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
@ -48,6 +50,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
viewType,
|
viewType,
|
||||||
parentId,
|
parentId,
|
||||||
collectionType,
|
collectionType,
|
||||||
|
isPaginationEnabled = true,
|
||||||
isBtnPlayAllEnabled = false,
|
isBtnPlayAllEnabled = false,
|
||||||
isBtnQueueEnabled = false,
|
isBtnQueueEnabled = false,
|
||||||
isBtnShuffleEnabled = false,
|
isBtnShuffleEnabled = false,
|
||||||
|
@ -145,6 +148,18 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
||||||
} else if (viewType === LibraryTab.Artists) {
|
} else if (viewType === LibraryTab.Artists) {
|
||||||
cardOptions.lines = 1;
|
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;
|
return cardOptions;
|
||||||
|
@ -188,15 +203,23 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
ItemSortBy.SortName
|
ItemSortBy.SortName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const itemsContainerClass = classNames(
|
||||||
|
'centered padded-left padded-right padded-right-withalphapicker',
|
||||||
|
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
||||||
|
'vertical-list' :
|
||||||
|
'vertical-wrap'
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
<Pagination
|
{isPaginationEnabled && (
|
||||||
totalRecordCount={totalRecordCount}
|
<Pagination
|
||||||
libraryViewSettings={libraryViewSettings}
|
totalRecordCount={totalRecordCount}
|
||||||
isPreviousData={isPreviousData}
|
libraryViewSettings={libraryViewSettings}
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
isPreviousData={isPreviousData}
|
||||||
/>
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBtnPlayAllEnabled && (
|
{isBtnPlayAllEnabled && (
|
||||||
<PlayAllButton
|
<PlayAllButton
|
||||||
|
@ -263,22 +286,23 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<ItemsContainer
|
<ItemsContainer
|
||||||
className='centered padded-left padded-right padded-right-withalphapicker'
|
className={itemsContainerClass}
|
||||||
libraryViewSettings={libraryViewSettings}
|
|
||||||
parentId={parentId}
|
parentId={parentId}
|
||||||
reloadItems={refetch}
|
reloadItems={refetch}
|
||||||
getItemsHtml={getItemsHtml}
|
getItemsHtml={getItemsHtml}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
{isPaginationEnabled && (
|
||||||
<Pagination
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
totalRecordCount={totalRecordCount}
|
<Pagination
|
||||||
libraryViewSettings={libraryViewSettings}
|
totalRecordCount={totalRecordCount}
|
||||||
isPreviousData={isPreviousData}
|
libraryViewSettings={libraryViewSettings}
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
isPreviousData={isPreviousData}
|
||||||
/>
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import SuggestionsView from './SuggestionsView';
|
import SuggestionsSectionView from './SuggestionsSectionView';
|
||||||
import UpcomingView from './UpcomingView';
|
import UpcomingView from './UpcomingView';
|
||||||
import GenresView from './GenresView';
|
import GenresView from './GenresView';
|
||||||
import ItemsView from './ItemsView';
|
import ItemsView from './ItemsView';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import { ParentId } from 'types/library';
|
import { ParentId } from 'types/library';
|
||||||
import { LibraryTabContent } from 'types/libraryTabContent';
|
import { LibraryTabContent } from 'types/libraryTabContent';
|
||||||
|
import GuideView from './GuideView';
|
||||||
|
import ProgramsSectionView from './ProgramsSectionView';
|
||||||
|
|
||||||
interface PageTabContentProps {
|
interface PageTabContentProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
|
@ -15,18 +17,30 @@ interface PageTabContentProps {
|
||||||
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||||
if (currentTab.viewType === LibraryTab.Suggestions) {
|
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||||
return (
|
return (
|
||||||
<SuggestionsView
|
<SuggestionsSectionView
|
||||||
parentId={parentId}
|
parentId={parentId}
|
||||||
suggestionSectionViews={
|
sectionType={
|
||||||
currentTab.sectionsType?.suggestionSectionsView
|
currentTab.sectionsView?.suggestionSections ?? []
|
||||||
}
|
}
|
||||||
isMovieRecommendations={
|
isMovieRecommendationEnabled={
|
||||||
currentTab.sectionsType?.isMovieRecommendations
|
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) {
|
if (currentTab.viewType === LibraryTab.Upcoming) {
|
||||||
return <UpcomingView parentId={parentId} />;
|
return <UpcomingView parentId={parentId} />;
|
||||||
}
|
}
|
||||||
|
@ -41,11 +55,16 @@ const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentTab.viewType === LibraryTab.Guide) {
|
||||||
|
return <GuideView />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemsView
|
<ItemsView
|
||||||
viewType={currentTab.viewType}
|
viewType={currentTab.viewType}
|
||||||
parentId={parentId}
|
parentId={parentId}
|
||||||
collectionType={currentTab.collectionType}
|
collectionType={currentTab.collectionType}
|
||||||
|
isPaginationEnabled={currentTab.isPaginationEnabled}
|
||||||
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
||||||
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
||||||
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
|
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 React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
|
@ -10,8 +10,8 @@ import { LibraryViewSettings } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
interface PlayAllButtonProps {
|
interface PlayAllButtonProps {
|
||||||
item: BaseItemDto | undefined;
|
item: BaseItemDto | null | undefined;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
hasFilters: boolean;
|
hasFilters: boolean;
|
||||||
libraryViewSettings: LibraryViewSettings
|
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 React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import QueueIcon from '@mui/icons-material/Queue';
|
import QueueIcon from '@mui/icons-material/Queue';
|
||||||
|
@ -8,7 +8,7 @@ import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
interface QueueButtonProps {
|
interface QueueButtonProps {
|
||||||
item: BaseItemDto | undefined
|
item: BaseItemDto | undefined
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
hasFilters: boolean;
|
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 React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
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 Scroller from 'elements/emby-scroller/Scroller';
|
||||||
import LinkButton from 'elements/emby-button/LinkButton';
|
import LinkButton from 'elements/emby-button/LinkButton';
|
||||||
import imageLoader from 'components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
|
@ -12,7 +12,7 @@ import { CardOptions } from 'types/cardOptions';
|
||||||
interface SectionContainerProps {
|
interface SectionContainerProps {
|
||||||
url?: string;
|
url?: string;
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | TimerInfoDto[];
|
||||||
cardOptions: CardOptions;
|
cardOptions: CardOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,9 @@ const SectionContainer: FC<SectionContainerProps> = ({
|
||||||
isMouseWheelEnabled={false}
|
isMouseWheelEnabled={false}
|
||||||
isCenterFocusEnabled={true}
|
isCenterFocusEnabled={true}
|
||||||
>
|
>
|
||||||
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
|
<ItemsContainer
|
||||||
|
className='itemsContainer scrollSlider focuscontainer-x'
|
||||||
|
/>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</div>
|
</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 { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
|
@ -11,8 +11,8 @@ import { LibraryViewSettings } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
interface ShuffleButtonProps {
|
interface ShuffleButtonProps {
|
||||||
item: BaseItemDto | undefined;
|
item: BaseItemDto | null | undefined;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
viewType: LibraryTab
|
viewType: LibraryTab
|
||||||
hasFilters: boolean;
|
hasFilters: boolean;
|
||||||
libraryViewSettings: LibraryViewSettings
|
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>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersGenres
|
<FiltersGenres
|
||||||
filters={data}
|
genresOptions={data.Genres}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -355,7 +355,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersOfficialRatings
|
<FiltersOfficialRatings
|
||||||
filters={data}
|
OfficialRatingsOptions={data.OfficialRatings}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -382,7 +382,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersTags
|
<FiltersTags
|
||||||
filters={data}
|
tagsOptions={data.Tags}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -409,7 +409,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersYears
|
<FiltersYears
|
||||||
filters={data}
|
yearsOptions={data.Years}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isFiltersStudiosEnabled() && (
|
{isFiltersStudiosEnabled() && studios && (
|
||||||
<Accordion
|
<Accordion
|
||||||
expanded={expanded === 'filtersStudios'}
|
expanded={expanded === 'filtersStudios'}
|
||||||
onChange={handleChange('filtersStudios')}
|
onChange={handleChange('filtersStudios')}
|
||||||
|
@ -437,7 +437,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersStudios
|
<FiltersStudios
|
||||||
filters={studios}
|
studiosOptions={studios}
|
||||||
libraryViewSettings={libraryViewSettings}
|
libraryViewSettings={libraryViewSettings}
|
||||||
setLibraryViewSettings={
|
setLibraryViewSettings={
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersGenresProps {
|
interface FiltersGenresProps {
|
||||||
filters?: QueryFiltersLegacy;
|
genresOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersGenres: FC<FiltersGenresProps> = ({
|
const FiltersGenres: FC<FiltersGenresProps> = ({
|
||||||
filters,
|
genresOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersGenres: FC<FiltersGenresProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Genres?.map((filter) => (
|
{genresOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersOfficialRatingsProps {
|
interface FiltersOfficialRatingsProps {
|
||||||
filters?: QueryFiltersLegacy;
|
OfficialRatingsOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
||||||
filters,
|
OfficialRatingsOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.OfficialRatings?.map((filter) => (
|
{OfficialRatingsOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -55,6 +55,7 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
|
||||||
&& viewType !== LibraryTab.Artists
|
&& viewType !== LibraryTab.Artists
|
||||||
&& viewType !== LibraryTab.AlbumArtists
|
&& viewType !== LibraryTab.AlbumArtists
|
||||||
&& viewType !== LibraryTab.Songs
|
&& viewType !== LibraryTab.Songs
|
||||||
|
&& viewType !== LibraryTab.Channels
|
||||||
) {
|
) {
|
||||||
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
||||||
visibleFiltersStatus.push(ItemFilter.IsPlayed);
|
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 React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +6,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersStudiosProps {
|
interface FiltersStudiosProps {
|
||||||
filters?: BaseItemDtoQueryResult;
|
studiosOptions: BaseItemDto[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersStudios: FC<FiltersStudiosProps> = ({
|
const FiltersStudios: FC<FiltersStudiosProps> = ({
|
||||||
filters,
|
studiosOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +40,7 @@ const FiltersStudios: FC<FiltersStudiosProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Items?.map((filter) => (
|
{studiosOptions?.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter.Id}
|
key={filter.Id}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersTagsProps {
|
interface FiltersTagsProps {
|
||||||
filters?: QueryFiltersLegacy;
|
tagsOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersTags: FC<FiltersTagsProps> = ({
|
const FiltersTags: FC<FiltersTagsProps> = ({
|
||||||
filters,
|
tagsOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersTags: FC<FiltersTagsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Tags?.map((filter) => (
|
{tagsOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersYearsProps {
|
interface FiltersYearsProps {
|
||||||
filters?: QueryFiltersLegacy;
|
yearsOptions: number[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersYears: FC<FiltersYearsProps> = ({
|
const FiltersYears: FC<FiltersYearsProps> = ({
|
||||||
filters,
|
yearsOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersYears: FC<FiltersYearsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Years?.map((filter) => (
|
{yearsOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -4,9 +4,10 @@ import Tabs from '@mui/material/Tabs';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import React, { FC, useCallback, useEffect } from 'react';
|
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 {
|
interface AppTabsParams {
|
||||||
isDrawerOpen: boolean
|
isDrawerOpen: boolean
|
||||||
|
@ -18,14 +19,7 @@ const AppTabs: FC<AppTabsParams> = ({
|
||||||
isDrawerOpen
|
isDrawerOpen
|
||||||
}) => {
|
}) => {
|
||||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||||
const location = useLocation();
|
const { searchParams, setSearchParams, activeTab } = useCurrentTab();
|
||||||
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);
|
|
||||||
|
|
||||||
// HACK: Force resizing to workaround upstream bug with tab resizing
|
// HACK: Force resizing to workaround upstream bug with tab resizing
|
||||||
// https://github.com/mui/material-ui/issues/24011
|
// https://github.com/mui/material-ui/issues/24011
|
||||||
|
@ -71,7 +65,7 @@ const AppTabs: FC<AppTabsParams> = ({
|
||||||
{
|
{
|
||||||
route.tabs.map(({ index, label }) => (
|
route.tabs.map(({ index, label }) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={`${route}-tab-${index}`}
|
key={`${route.path}-tab-${index}`}
|
||||||
label={label}
|
label={label}
|
||||||
data-tab-index={`${index}`}
|
data-tab-index={`${index}`}
|
||||||
onClick={onTabClick}
|
onClick={onTabClick}
|
||||||
|
|
|
@ -68,7 +68,7 @@ const TabRoutes: TabRoute[] = [
|
||||||
{
|
{
|
||||||
index: 5,
|
index: 5,
|
||||||
label: globalize.translate('Series'),
|
label: globalize.translate('Series'),
|
||||||
value: LibraryTab.Series
|
value: LibraryTab.SeriesTimers
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -155,7 +155,7 @@ const TabRoutes: TabRoute[] = [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
label: globalize.translate('Shows'),
|
label: globalize.translate('Shows'),
|
||||||
value: LibraryTab.Shows,
|
value: LibraryTab.Series,
|
||||||
isDefault: true
|
isDefault: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,5 +7,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
|
{ 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 }
|
||||||
];
|
];
|
||||||
|
|
|
@ -13,12 +13,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'list',
|
controller: 'list',
|
||||||
view: 'list.html'
|
view: 'list.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'livetv.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'livetv/livetvsuggested',
|
|
||||||
view: 'livetv.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'mypreferencesmenu.html',
|
path: 'mypreferencesmenu.html',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
71
src/apps/experimental/routes/livetv/index.tsx
Normal file
71
src/apps/experimental/routes/livetv/index.tsx
Normal 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;
|
|
@ -6,7 +6,7 @@ import PageTabContent from '../../components/library/PageTabContent';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from 'types/collectionType';
|
||||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||||
import { SectionsView } from 'types/suggestionsSections';
|
import { MovieSuggestionsSectionsView } from 'types/sections';
|
||||||
|
|
||||||
const moviesTabContent: LibraryTabContent = {
|
const moviesTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.Movies,
|
viewType: LibraryTab.Movies,
|
||||||
|
@ -40,13 +40,7 @@ const trailersTabContent: LibraryTabContent = {
|
||||||
const suggestionsTabContent: LibraryTabContent = {
|
const suggestionsTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.Suggestions,
|
viewType: LibraryTab.Suggestions,
|
||||||
collectionType: CollectionType.Movies,
|
collectionType: CollectionType.Movies,
|
||||||
sectionsType: {
|
sectionsView: MovieSuggestionsSectionsView
|
||||||
suggestionSectionsView: [
|
|
||||||
SectionsView.ContinueWatchingMovies,
|
|
||||||
SectionsView.LatestMovies
|
|
||||||
],
|
|
||||||
isMovieRecommendations: true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const genresTabContent: LibraryTabContent = {
|
const genresTabContent: LibraryTabContent = {
|
||||||
|
@ -65,8 +59,8 @@ const moviesTabMapping: LibraryTabMapping = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Movies: FC = () => {
|
const Movies: FC = () => {
|
||||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
const { libraryId, activeTab } = useCurrentTab();
|
||||||
const currentTab = moviesTabMapping[currentTabIndex];
|
const currentTab = moviesTabMapping[activeTab];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -75,9 +69,9 @@ const Movies: FC = () => {
|
||||||
backDropType='movie'
|
backDropType='movie'
|
||||||
>
|
>
|
||||||
<PageTabContent
|
<PageTabContent
|
||||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
key={`${currentTab.viewType} - ${libraryId}`}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
parentId={searchParamsParentId}
|
parentId={libraryId}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import PageTabContent from '../../components/library/PageTabContent';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from 'types/collectionType';
|
||||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||||
import { SectionsView } from 'types/suggestionsSections';
|
import { MusicSuggestionsSectionsView } from 'types/sections';
|
||||||
|
|
||||||
const albumArtistsTabContent: LibraryTabContent = {
|
const albumArtistsTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.AlbumArtists,
|
viewType: LibraryTab.AlbumArtists,
|
||||||
|
@ -47,13 +47,7 @@ const songsTabContent: LibraryTabContent = {
|
||||||
const suggestionsTabContent: LibraryTabContent = {
|
const suggestionsTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.Suggestions,
|
viewType: LibraryTab.Suggestions,
|
||||||
collectionType: CollectionType.Music,
|
collectionType: CollectionType.Music,
|
||||||
sectionsType: {
|
sectionsView: MusicSuggestionsSectionsView
|
||||||
suggestionSectionsView: [
|
|
||||||
SectionsView.LatestMusic,
|
|
||||||
SectionsView.FrequentlyPlayedMusic,
|
|
||||||
SectionsView.RecentlyPlayedMusic
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const genresTabContent: LibraryTabContent = {
|
const genresTabContent: LibraryTabContent = {
|
||||||
|
@ -73,8 +67,8 @@ const musicTabMapping: LibraryTabMapping = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Music: FC = () => {
|
const Music: FC = () => {
|
||||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
const { libraryId, activeTab } = useCurrentTab();
|
||||||
const currentTab = musicTabMapping[currentTabIndex];
|
const currentTab = musicTabMapping[activeTab];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -83,9 +77,9 @@ const Music: FC = () => {
|
||||||
backDropType='musicartist'
|
backDropType='musicartist'
|
||||||
>
|
>
|
||||||
<PageTabContent
|
<PageTabContent
|
||||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
key={`${currentTab.viewType} - ${libraryId}`}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
parentId={searchParamsParentId}
|
parentId={libraryId}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,8 +5,8 @@ import Page from 'components/Page';
|
||||||
import PageTabContent from '../../components/library/PageTabContent';
|
import PageTabContent from '../../components/library/PageTabContent';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from 'types/collectionType';
|
||||||
import { SectionsView } from 'types/suggestionsSections';
|
|
||||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||||
|
import { TvShowSuggestionsSectionsView } from 'types/sections';
|
||||||
|
|
||||||
const episodesTabContent: LibraryTabContent = {
|
const episodesTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.Episodes,
|
viewType: LibraryTab.Episodes,
|
||||||
|
@ -39,13 +39,7 @@ const upcomingTabContent: LibraryTabContent = {
|
||||||
const suggestionsTabContent: LibraryTabContent = {
|
const suggestionsTabContent: LibraryTabContent = {
|
||||||
viewType: LibraryTab.Suggestions,
|
viewType: LibraryTab.Suggestions,
|
||||||
collectionType: CollectionType.TvShows,
|
collectionType: CollectionType.TvShows,
|
||||||
sectionsType: {
|
sectionsView: TvShowSuggestionsSectionsView
|
||||||
suggestionSectionsView: [
|
|
||||||
SectionsView.ContinueWatchingEpisode,
|
|
||||||
SectionsView.LatestEpisode,
|
|
||||||
SectionsView.NextUp
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const genresTabContent: LibraryTabContent = {
|
const genresTabContent: LibraryTabContent = {
|
||||||
|
@ -64,8 +58,8 @@ const tvShowsTabMapping: LibraryTabMapping = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Shows: FC = () => {
|
const Shows: FC = () => {
|
||||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
const { libraryId, activeTab } = useCurrentTab();
|
||||||
const currentTab = tvShowsTabMapping[currentTabIndex];
|
const currentTab = tvShowsTabMapping[activeTab];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -74,9 +68,9 @@ const Shows: FC = () => {
|
||||||
backDropType='series'
|
backDropType='series'
|
||||||
>
|
>
|
||||||
<PageTabContent
|
<PageTabContent
|
||||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
key={`${currentTab.viewType} - ${libraryId}`}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
parentId={searchParamsParentId}
|
parentId={libraryId}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -75,7 +75,7 @@ function getLandingScreenOptions(type) {
|
||||||
} else if (type === 'tvshows') {
|
} else if (type === 'tvshows') {
|
||||||
list.push({
|
list.push({
|
||||||
name: globalize.translate('Shows'),
|
name: globalize.translate('Shows'),
|
||||||
value: LibraryTab.Shows,
|
value: LibraryTab.Series,
|
||||||
isDefault: true
|
isDefault: true
|
||||||
});
|
});
|
||||||
list.push({
|
list.push({
|
||||||
|
@ -152,7 +152,7 @@ function getLandingScreenOptions(type) {
|
||||||
});
|
});
|
||||||
list.push({
|
list.push({
|
||||||
name: globalize.translate('Series'),
|
name: globalize.translate('Series'),
|
||||||
value: LibraryTab.Series
|
value: LibraryTab.SeriesTimers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -545,6 +545,30 @@ class AppRouter {
|
||||||
urlForList += '&IsFavorite=true';
|
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;
|
return urlForList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,7 +199,7 @@ function getDefaultTabIndex(folderId) {
|
||||||
return 3;
|
return 3;
|
||||||
case LibraryTab.Schedule:
|
case LibraryTab.Schedule:
|
||||||
return 4;
|
return 4;
|
||||||
case LibraryTab.Series:
|
case LibraryTab.SeriesTimers:
|
||||||
return 5;
|
return 5;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -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;
|
|
|
@ -21,7 +21,7 @@ import itemShortcuts from 'components/shortcuts';
|
||||||
import MultiSelect from 'components/multiSelect/multiSelect';
|
import MultiSelect from 'components/multiSelect/multiSelect';
|
||||||
import loading from 'components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import focusManager from 'components/focusManager';
|
import focusManager from 'components/focusManager';
|
||||||
import { LibraryViewSettings, ParentId, ViewMode } from 'types/library';
|
import { ParentId } from 'types/library';
|
||||||
|
|
||||||
function disableEvent(e: MouseEvent) {
|
function disableEvent(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -37,20 +37,18 @@ function getShortcutOptions() {
|
||||||
|
|
||||||
interface ItemsContainerProps {
|
interface ItemsContainerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
libraryViewSettings: LibraryViewSettings;
|
|
||||||
isContextMenuEnabled?: boolean;
|
isContextMenuEnabled?: boolean;
|
||||||
isMultiSelectEnabled?: boolean;
|
isMultiSelectEnabled?: boolean;
|
||||||
isDragreOrderEnabled?: boolean;
|
isDragreOrderEnabled?: boolean;
|
||||||
dataMonitor?: string;
|
dataMonitor?: string;
|
||||||
parentId?: ParentId;
|
parentId?: ParentId;
|
||||||
reloadItems: () => void;
|
reloadItems?: () => void;
|
||||||
getItemsHtml?: () => string;
|
getItemsHtml?: () => string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemsContainer: FC<ItemsContainerProps> = ({
|
const ItemsContainer: FC<ItemsContainerProps> = ({
|
||||||
className,
|
className,
|
||||||
libraryViewSettings,
|
|
||||||
isContextMenuEnabled,
|
isContextMenuEnabled,
|
||||||
isMultiSelectEnabled,
|
isMultiSelectEnabled,
|
||||||
isDragreOrderEnabled,
|
isDragreOrderEnabled,
|
||||||
|
@ -146,7 +144,9 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
||||||
});
|
});
|
||||||
loading.hide();
|
loading.hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[Drag-Drop] error playlists Move Item: ' + error);
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
if (!reloadItems) return;
|
||||||
reloadItems();
|
reloadItems();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -174,6 +174,7 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
||||||
|
|
||||||
const notifyRefreshNeeded = useCallback(
|
const notifyRefreshNeeded = useCallback(
|
||||||
(isInForeground: boolean) => {
|
(isInForeground: boolean) => {
|
||||||
|
if (!reloadItems) return;
|
||||||
if (isInForeground === true) {
|
if (isInForeground === true) {
|
||||||
reloadItems();
|
reloadItems();
|
||||||
} else {
|
} else {
|
||||||
|
@ -506,9 +507,6 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
||||||
const itemsContainerClass = classNames(
|
const itemsContainerClass = classNames(
|
||||||
'itemsContainer',
|
'itemsContainer',
|
||||||
{ 'itemsContainer-tv': layoutManager.tv },
|
{ 'itemsContainer-tv': layoutManager.tv },
|
||||||
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
|
||||||
'vertical-list' :
|
|
||||||
'vertical-wrap',
|
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,22 @@ import { useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const useCurrentTab = () => {
|
const useCurrentTab = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const searchParamsParentId = searchParams.get('topParentId');
|
|
||||||
const searchParamsTab = searchParams.get('tab');
|
const searchParamsTab = searchParams.get('tab');
|
||||||
const currentTabIndex: number =
|
const libraryId =
|
||||||
searchParamsTab !== null ?
|
location.pathname === '/livetv.html' ?
|
||||||
parseInt(searchParamsTab, 10) :
|
'livetv' :
|
||||||
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
searchParams.get('topParentId');
|
||||||
|
const activeTab: number =
|
||||||
|
searchParamsTab !== null ?
|
||||||
|
parseInt(searchParamsTab, 10) :
|
||||||
|
getDefaultTabIndex(location.pathname, libraryId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchParamsParentId,
|
searchParams,
|
||||||
currentTabIndex
|
setSearchParams,
|
||||||
|
libraryId,
|
||||||
|
activeTab
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AxiosRequestConfig } from 'axios';
|
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 type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
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 { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-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 { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import datetime from 'scripts/datetime';
|
import datetime from 'scripts/datetime';
|
||||||
|
@ -22,9 +23,10 @@ import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import { JellyfinApiContext, useApi } from './useApi';
|
import { JellyfinApiContext, useApi } from './useApi';
|
||||||
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
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 { LibraryViewSettings, ParentId } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
import { Section, SectionApiMethod, SectionType } from 'types/sections';
|
||||||
|
|
||||||
const fetchGetItem = async (
|
const fetchGetItem = async (
|
||||||
currentApi: JellyfinApiContext,
|
currentApi: JellyfinApiContext,
|
||||||
|
@ -48,10 +50,11 @@ const fetchGetItem = async (
|
||||||
|
|
||||||
export const useGetItem = (parentId: ParentId) => {
|
export const useGetItem = (parentId: ParentId) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
|
const isLivetv = parentId === 'livetv';
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['Item', parentId],
|
queryKey: ['Item', parentId],
|
||||||
queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }),
|
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();
|
const currentApi = useApi();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['MovieRecommendations', parentId],
|
queryKey: ['MovieRecommendations', isMovieRecommendationEnabled, parentId],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchGetMovieRecommendations(currentApi, parentId, { signal }),
|
isMovieRecommendationEnabled ? 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
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -314,17 +187,18 @@ const fetchGetStudios = async (
|
||||||
signal: options?.signal
|
signal: options?.signal
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data.Items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => {
|
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
|
const isLivetv = parentId === 'livetv';
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['Studios', parentId, itemType],
|
queryKey: ['Studios', parentId, itemType],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchGetStudios(currentApi, parentId, itemType, { signal }),
|
fetchGetStudios(currentApi, parentId, itemType, { signal }),
|
||||||
enabled: !!parentId
|
enabled: !!parentId && !isLivetv
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -355,13 +229,14 @@ export const useGetQueryFiltersLegacy = (
|
||||||
itemType: BaseItemKind[]
|
itemType: BaseItemKind[]
|
||||||
) => {
|
) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
|
const isLivetv = parentId === 'livetv';
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['QueryFiltersLegacy', parentId, itemType],
|
queryKey: ['QueryFiltersLegacy', parentId, itemType],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, {
|
fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, {
|
||||||
signal
|
signal
|
||||||
}),
|
}),
|
||||||
enabled: !!parentId
|
enabled: !!parentId && !isLivetv
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -434,6 +309,34 @@ const fetchGetItemsViewByType = async (
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
break;
|
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: {
|
default: {
|
||||||
response = await getItemsApi(api).getItems(
|
response = await getItemsApi(api).getItems(
|
||||||
{
|
{
|
||||||
|
@ -505,8 +408,10 @@ export const useGetItemsViewByType = (
|
||||||
LibraryTab.Songs,
|
LibraryTab.Songs,
|
||||||
LibraryTab.Books,
|
LibraryTab.Books,
|
||||||
LibraryTab.Photos,
|
LibraryTab.Photos,
|
||||||
LibraryTab.Videos
|
LibraryTab.Videos,
|
||||||
].includes(viewType) && !!parentId
|
LibraryTab.Channels,
|
||||||
|
LibraryTab.SeriesTimers
|
||||||
|
].includes(viewType)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -690,3 +595,336 @@ export const useTogglePlayedMutation = () => {
|
||||||
fetchUpdatePlayedState(currentApi, itemId, playedState )
|
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 })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -323,6 +323,7 @@
|
||||||
"HeaderAdmin": "Administration",
|
"HeaderAdmin": "Administration",
|
||||||
"HeaderAlbumArtists": "Album artists",
|
"HeaderAlbumArtists": "Album artists",
|
||||||
"HeaderAlert": "Alert",
|
"HeaderAlert": "Alert",
|
||||||
|
"HeaderAllRecordings": "All Recordings",
|
||||||
"HeaderAllowMediaDeletionFrom": "Allow media deletion from",
|
"HeaderAllowMediaDeletionFrom": "Allow media deletion from",
|
||||||
"HeaderApiKey": "API Key",
|
"HeaderApiKey": "API Key",
|
||||||
"HeaderApiKeys": "API Keys",
|
"HeaderApiKeys": "API Keys",
|
||||||
|
|
|
@ -12,7 +12,7 @@ export interface CardOptions {
|
||||||
overlayMoreButton?: boolean;
|
overlayMoreButton?: boolean;
|
||||||
overlayPlayButton?: boolean;
|
overlayPlayButton?: boolean;
|
||||||
overlayText?: boolean;
|
overlayText?: boolean;
|
||||||
preferThumb?: boolean;
|
preferThumb?: boolean | string | null;
|
||||||
preferDisc?: boolean;
|
preferDisc?: boolean;
|
||||||
preferLogo?: boolean;
|
preferLogo?: boolean;
|
||||||
scalable?: boolean;
|
scalable?: boolean;
|
||||||
|
|
|
@ -15,7 +15,7 @@ export enum LibraryTab {
|
||||||
Recordings = 'recordings',
|
Recordings = 'recordings',
|
||||||
Schedule = 'schedule',
|
Schedule = 'schedule',
|
||||||
Series = 'series',
|
Series = 'series',
|
||||||
Shows = 'shows',
|
SeriesTimers = 'seriestimers',
|
||||||
Songs = 'songs',
|
Songs = 'songs',
|
||||||
Suggestions = 'suggestions',
|
Suggestions = 'suggestions',
|
||||||
Trailers = 'trailers',
|
Trailers = 'trailers',
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { LibraryTab } from './libraryTab';
|
import { LibraryTab } from './libraryTab';
|
||||||
import { CollectionType } from './collectionType';
|
import { CollectionType } from './collectionType';
|
||||||
import { SectionsView } from './suggestionsSections';
|
import { SectionType } from './sections';
|
||||||
|
|
||||||
export interface SuggestionsSectionsType {
|
export interface SectionsView {
|
||||||
suggestionSectionsView: SectionsView[];
|
suggestionSections?: SectionType[];
|
||||||
|
favoriteSections?: SectionType[];
|
||||||
|
programSections?: SectionType[];
|
||||||
isMovieRecommendations?: boolean;
|
isMovieRecommendations?: boolean;
|
||||||
|
isLiveTvUpcomingRecordings?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibraryTabContent {
|
export interface LibraryTabContent {
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
itemType?: BaseItemKind[];
|
itemType?: BaseItemKind[];
|
||||||
collectionType?: CollectionType;
|
collectionType?: CollectionType;
|
||||||
sectionsType?: SuggestionsSectionsType;
|
sectionsView?: SectionsView;
|
||||||
|
isPaginationEnabled?: boolean;
|
||||||
isBtnPlayAllEnabled?: boolean;
|
isBtnPlayAllEnabled?: boolean;
|
||||||
isBtnQueueEnabled?: boolean;
|
isBtnQueueEnabled?: boolean;
|
||||||
isBtnShuffleEnabled?: boolean;
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
|
|
@ -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 { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import { CollectionType } from './collectionType';
|
import { CollectionType } from './collectionType';
|
||||||
|
|
||||||
export interface ListOptions {
|
export interface ListOptions {
|
||||||
items?: BaseItemDto[] | null;
|
items?: BaseItemDto[] | SeriesTimerInfoDto[] | null;
|
||||||
index?: string;
|
index?: string;
|
||||||
showIndex?: boolean;
|
showIndex?: boolean;
|
||||||
action?: string | null;
|
action?: string | null;
|
||||||
|
|
109
src/types/sections.ts
Normal file
109
src/types/sections.ts
Normal 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
|
||||||
|
};
|
|
@ -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
367
src/utils/sections.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue